@hira-core/sdk 1.0.6 → 1.0.8
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.
- package/README.md +32 -11
- package/dist/index.d.ts +471 -30
- package/dist/index.js +1349 -128
- package/package.json +2 -5
package/dist/index.js
CHANGED
|
@@ -12453,6 +12453,7 @@ var BaseFlow = class {
|
|
|
12453
12453
|
page,
|
|
12454
12454
|
profile,
|
|
12455
12455
|
index,
|
|
12456
|
+
execution: params.execution,
|
|
12456
12457
|
globalInput: params.globalInput || {},
|
|
12457
12458
|
profileInput: profileData || {},
|
|
12458
12459
|
output: outputObj,
|
|
@@ -16654,7 +16655,10 @@ var HidemiumStandaloneAdapter = class {
|
|
|
16654
16655
|
const command = [
|
|
16655
16656
|
`--window-position=${x},${y}`,
|
|
16656
16657
|
`--window-size=${windowConfig.width},${windowConfig.height}`,
|
|
16657
|
-
`--force-device-scale-factor=${windowConfig.scale}
|
|
16658
|
+
`--force-device-scale-factor=${windowConfig.scale}`,
|
|
16659
|
+
`--disable-background-timer-throttling`,
|
|
16660
|
+
`--disable-backgrounding-occluded-windows`,
|
|
16661
|
+
`--disable-renderer-backgrounding`
|
|
16658
16662
|
].join(" ");
|
|
16659
16663
|
try {
|
|
16660
16664
|
await this.service.stopProfile(targetProfile.uuid);
|
|
@@ -16800,6 +16804,9 @@ var AntidetectBaseFlow = class extends BaseFlow {
|
|
|
16800
16804
|
}
|
|
16801
16805
|
};
|
|
16802
16806
|
|
|
16807
|
+
// src/utils/browser.utils.ts
|
|
16808
|
+
var import_puppeteer_core3 = require("puppeteer-core");
|
|
16809
|
+
|
|
16803
16810
|
// src/sdk-config.ts
|
|
16804
16811
|
var SDK_CONFIG = {
|
|
16805
16812
|
actionDelayMs: 1e3,
|
|
@@ -16809,25 +16816,537 @@ function getSdkConfig() {
|
|
|
16809
16816
|
return SDK_CONFIG;
|
|
16810
16817
|
}
|
|
16811
16818
|
|
|
16819
|
+
// src/utils/hira-cursor.ts
|
|
16820
|
+
var import_ghost_cursor = require("ghost-cursor");
|
|
16821
|
+
var CURSOR_SVG_NORMAL = `
|
|
16822
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
16823
|
+
<defs>
|
|
16824
|
+
<linearGradient id="hiraGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
16825
|
+
<stop offset="0%" stop-color="#FF3333"/>
|
|
16826
|
+
<stop offset="100%" stop-color="#FF6B35"/>
|
|
16827
|
+
</linearGradient>
|
|
16828
|
+
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
16829
|
+
<feDropShadow dx="1" dy="1" stdDeviation="0.8" flood-color="#000" flood-opacity="0.3"/>
|
|
16830
|
+
</filter>
|
|
16831
|
+
</defs>
|
|
16832
|
+
<path d="M 4 2 L 4 20 L 9 15 L 14 22 L 16 21 L 11 14 L 18 14 Z"
|
|
16833
|
+
fill="url(#hiraGrad)" stroke="#FFFFFF" stroke-width="1.2" stroke-linejoin="round"
|
|
16834
|
+
filter="url(#shadow)"/>
|
|
16835
|
+
</svg>`;
|
|
16836
|
+
var CURSOR_SVG_CLICK = `
|
|
16837
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
16838
|
+
<defs>
|
|
16839
|
+
<linearGradient id="hiraClickGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
16840
|
+
<stop offset="0%" stop-color="#FFD700"/>
|
|
16841
|
+
<stop offset="100%" stop-color="#FFA500"/>
|
|
16842
|
+
</linearGradient>
|
|
16843
|
+
<filter id="shadowClick" x="-20%" y="-20%" width="140%" height="140%">
|
|
16844
|
+
<feDropShadow dx="1" dy="1" stdDeviation="0.8" flood-color="#000" flood-opacity="0.3"/>
|
|
16845
|
+
</filter>
|
|
16846
|
+
</defs>
|
|
16847
|
+
<path d="M 4 2 L 4 20 L 9 15 L 14 22 L 16 21 L 11 14 L 18 14 Z"
|
|
16848
|
+
fill="url(#hiraClickGrad)" stroke="#FFFFFF" stroke-width="1.2" stroke-linejoin="round"
|
|
16849
|
+
filter="url(#shadowClick)"/>
|
|
16850
|
+
</svg>`;
|
|
16851
|
+
var _HiraCursor = class _HiraCursor {
|
|
16852
|
+
constructor(enabled) {
|
|
16853
|
+
/** ghost-cursor instances keyed by page — weak so GC cleans up when page closes */
|
|
16854
|
+
this.cursorMap = /* @__PURE__ */ new WeakMap();
|
|
16855
|
+
/** Track which pages already have SVG injected */
|
|
16856
|
+
this.injectedPages = /* @__PURE__ */ new WeakSet();
|
|
16857
|
+
this.enabled = enabled;
|
|
16858
|
+
}
|
|
16859
|
+
/** Whether virtual cursor is enabled */
|
|
16860
|
+
get isEnabled() {
|
|
16861
|
+
return this.enabled;
|
|
16862
|
+
}
|
|
16863
|
+
/**
|
|
16864
|
+
* Inject SVG cursor + mousemove listener onto the page.
|
|
16865
|
+
* Idempotent — calling multiple times on the same page will skip.
|
|
16866
|
+
* Must be called after each page.goto() since navigation clears the DOM.
|
|
16867
|
+
*/
|
|
16868
|
+
async inject(page) {
|
|
16869
|
+
if (!this.enabled) return;
|
|
16870
|
+
if (this.injectedPages.has(page)) return;
|
|
16871
|
+
try {
|
|
16872
|
+
if (page.isClosed()) return;
|
|
16873
|
+
const svgNormal = `data:image/svg+xml;base64,${Buffer.from(CURSOR_SVG_NORMAL).toString("base64")}`;
|
|
16874
|
+
const svgClick = `data:image/svg+xml;base64,${Buffer.from(CURSOR_SVG_CLICK).toString("base64")}`;
|
|
16875
|
+
await page.evaluate(
|
|
16876
|
+
(normalUri, clickUri) => {
|
|
16877
|
+
const old = document.getElementById("hira-cursor");
|
|
16878
|
+
if (old) old.remove();
|
|
16879
|
+
const el = document.createElement("div");
|
|
16880
|
+
el.id = "hira-cursor";
|
|
16881
|
+
const img = document.createElement("img");
|
|
16882
|
+
img.src = normalUri;
|
|
16883
|
+
img.style.cssText = "width:24px;height:24px;pointer-events:none;";
|
|
16884
|
+
el.appendChild(img);
|
|
16885
|
+
el.style.cssText = `
|
|
16886
|
+
position: fixed;
|
|
16887
|
+
left: -30px; top: -30px;
|
|
16888
|
+
z-index: 2147483647;
|
|
16889
|
+
pointer-events: none;
|
|
16890
|
+
transform: translate(-2px, -2px);
|
|
16891
|
+
`;
|
|
16892
|
+
document.body.appendChild(el);
|
|
16893
|
+
document.documentElement.style.cursor = "none";
|
|
16894
|
+
document.body.style.cursor = "none";
|
|
16895
|
+
document.addEventListener(
|
|
16896
|
+
"mousemove",
|
|
16897
|
+
(e) => {
|
|
16898
|
+
el.style.left = e.clientX + "px";
|
|
16899
|
+
el.style.top = e.clientY + "px";
|
|
16900
|
+
},
|
|
16901
|
+
true
|
|
16902
|
+
);
|
|
16903
|
+
document.addEventListener(
|
|
16904
|
+
"mousedown",
|
|
16905
|
+
() => {
|
|
16906
|
+
img.src = clickUri;
|
|
16907
|
+
el.style.transform = "translate(-2px, -2px) scale(0.88)";
|
|
16908
|
+
},
|
|
16909
|
+
true
|
|
16910
|
+
);
|
|
16911
|
+
document.addEventListener(
|
|
16912
|
+
"mouseup",
|
|
16913
|
+
() => {
|
|
16914
|
+
img.src = normalUri;
|
|
16915
|
+
el.style.transform = "translate(-2px, -2px) scale(1)";
|
|
16916
|
+
},
|
|
16917
|
+
true
|
|
16918
|
+
);
|
|
16919
|
+
},
|
|
16920
|
+
svgNormal,
|
|
16921
|
+
svgClick
|
|
16922
|
+
);
|
|
16923
|
+
this.injectedPages.add(page);
|
|
16924
|
+
this.getOrCreateCursor(page);
|
|
16925
|
+
const viewport = await page.evaluate(() => ({
|
|
16926
|
+
w: window.innerWidth,
|
|
16927
|
+
h: window.innerHeight
|
|
16928
|
+
}));
|
|
16929
|
+
const startX = viewport.w * (0.3 + Math.random() * 0.4);
|
|
16930
|
+
const startY = viewport.h * (0.3 + Math.random() * 0.4);
|
|
16931
|
+
await page.mouse.move(startX, startY);
|
|
16932
|
+
} catch {
|
|
16933
|
+
}
|
|
16934
|
+
}
|
|
16935
|
+
/**
|
|
16936
|
+
* Mark page as needing re-injection (after navigate).
|
|
16937
|
+
* Next inject() call will re-create the SVG.
|
|
16938
|
+
*/
|
|
16939
|
+
markDirty(page) {
|
|
16940
|
+
this.injectedPages.delete(page);
|
|
16941
|
+
this.cursorMap.delete(page);
|
|
16942
|
+
}
|
|
16943
|
+
/**
|
|
16944
|
+
* Bézier move + click element — uses ghost-cursor click() native.
|
|
16945
|
+
*
|
|
16946
|
+
* ghost-cursor click() flow:
|
|
16947
|
+
* 1. move(element) — Bézier curve + overshoot + scrollIntoView
|
|
16948
|
+
* 2. delay(hesitate) — pause before pressing (simulates human thinking)
|
|
16949
|
+
* 3. mouseDown via CDP
|
|
16950
|
+
* 4. delay(waitForClick) — hold mouse button (simulates real press-and-hold)
|
|
16951
|
+
* 5. mouseUp via CDP
|
|
16952
|
+
*
|
|
16953
|
+
* ⚠️ Default ghost-cursor: hesitate=0, waitForClick=0 (instant click = bot-like)
|
|
16954
|
+
* → We override with random hesitate 80-250ms, waitForClick 30-120ms
|
|
16955
|
+
*
|
|
16956
|
+
* For long distances, segmentedMove runs first to create visible cursor travel,
|
|
16957
|
+
* then ghost-cursor click handles the final short-range approach.
|
|
16958
|
+
*
|
|
16959
|
+
* @param options.clickCount - Number of clicks (e.g. 3 = select all text)
|
|
16960
|
+
* @param options.hesitate - Override hesitate (ms). Default: random 80-250ms
|
|
16961
|
+
* @param options.waitForClick - Override mousedown hold duration (ms). Default: random 30-120ms
|
|
16962
|
+
*/
|
|
16963
|
+
async clickElement(page, element, options) {
|
|
16964
|
+
var _a, _b, _c;
|
|
16965
|
+
if (!this.enabled) return false;
|
|
16966
|
+
try {
|
|
16967
|
+
if (page.isClosed()) return false;
|
|
16968
|
+
const cursor = this.getOrCreateCursor(page);
|
|
16969
|
+
const box = await element.boundingBox();
|
|
16970
|
+
if (box) {
|
|
16971
|
+
const targetX = box.x + box.width / 2;
|
|
16972
|
+
const targetY = box.y + box.height / 2;
|
|
16973
|
+
await this.segmentedMove(page, cursor, targetX, targetY);
|
|
16974
|
+
}
|
|
16975
|
+
const hesitate = (_a = options == null ? void 0 : options.hesitate) != null ? _a : 80 + Math.random() * 170;
|
|
16976
|
+
const waitForClick = (_b = options == null ? void 0 : options.waitForClick) != null ? _b : 30 + Math.random() * 90;
|
|
16977
|
+
await cursor.click(element, {
|
|
16978
|
+
hesitate,
|
|
16979
|
+
waitForClick,
|
|
16980
|
+
moveDelay: 0,
|
|
16981
|
+
// SDK handles delay via actionDelay() after click
|
|
16982
|
+
clickCount: (_c = options == null ? void 0 : options.clickCount) != null ? _c : 1
|
|
16983
|
+
});
|
|
16984
|
+
return true;
|
|
16985
|
+
} catch {
|
|
16986
|
+
return false;
|
|
16987
|
+
}
|
|
16988
|
+
}
|
|
16989
|
+
/**
|
|
16990
|
+
* Move cursor to coordinates via Bézier curve.
|
|
16991
|
+
* For long distances (> SEGMENT_THRESHOLD), splits into segments with delays.
|
|
16992
|
+
*/
|
|
16993
|
+
async moveTo(page, x, y) {
|
|
16994
|
+
if (!this.enabled) return;
|
|
16995
|
+
try {
|
|
16996
|
+
if (page.isClosed()) return;
|
|
16997
|
+
const cursor = this.getOrCreateCursor(page);
|
|
16998
|
+
await this.segmentedMove(page, cursor, x, y);
|
|
16999
|
+
} catch {
|
|
17000
|
+
}
|
|
17001
|
+
}
|
|
17002
|
+
/**
|
|
17003
|
+
* Move cursor through multiple small segments for long distances.
|
|
17004
|
+
*
|
|
17005
|
+
* Ghost-cursor dispatches all CDP mouseMoved events in a tight loop
|
|
17006
|
+
* with NO delay between steps → cursor "teleports" on long moves.
|
|
17007
|
+
*
|
|
17008
|
+
* Fix: split long paths into ~SEGMENT_SIZE px segments, each segment
|
|
17009
|
+
* is a single ghost-cursor moveTo (short Bézier curve), with
|
|
17010
|
+
* SEGMENT_DELAY ms pause between each segment.
|
|
17011
|
+
* → Cursor movement is visible and natural.
|
|
17012
|
+
*
|
|
17013
|
+
* The final segment always uses ghost-cursor moveTo directly for precision.
|
|
17014
|
+
*/
|
|
17015
|
+
async segmentedMove(page, cursor, targetX, targetY) {
|
|
17016
|
+
const currentPos = cursor.getLocation();
|
|
17017
|
+
const dx = targetX - currentPos.x;
|
|
17018
|
+
const dy = targetY - currentPos.y;
|
|
17019
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
17020
|
+
if (distance <= _HiraCursor.SEGMENT_THRESHOLD) {
|
|
17021
|
+
await cursor.moveTo({ x: targetX, y: targetY });
|
|
17022
|
+
return;
|
|
17023
|
+
}
|
|
17024
|
+
const segCount = Math.ceil(distance / _HiraCursor.SEGMENT_SIZE);
|
|
17025
|
+
const stepX = dx / segCount;
|
|
17026
|
+
const stepY = dy / segCount;
|
|
17027
|
+
for (let i = 1; i < segCount; i++) {
|
|
17028
|
+
if (page.isClosed()) return;
|
|
17029
|
+
const wx = currentPos.x + stepX * i + (Math.random() - 0.5) * 10;
|
|
17030
|
+
const wy = currentPos.y + stepY * i + (Math.random() - 0.5) * 10;
|
|
17031
|
+
await cursor.moveTo({ x: Math.max(0, wx), y: Math.max(0, wy) });
|
|
17032
|
+
const [minDelay, maxDelay] = _HiraCursor.SEGMENT_DELAY;
|
|
17033
|
+
const segDelay = minDelay + Math.random() * (maxDelay - minDelay);
|
|
17034
|
+
await new Promise((r) => setTimeout(r, segDelay));
|
|
17035
|
+
}
|
|
17036
|
+
await cursor.moveTo({ x: targetX, y: targetY });
|
|
17037
|
+
}
|
|
17038
|
+
/**
|
|
17039
|
+
* Smooth 60fps scroll — animation runs INSIDE browser via requestAnimationFrame.
|
|
17040
|
+
*
|
|
17041
|
+
* Behaves like native `scrollIntoView({ behavior: 'smooth' })`:
|
|
17042
|
+
* - True 60fps (requestAnimationFrame, no Node.js setTimeout jitter)
|
|
17043
|
+
* - Ease-in-out sine curve (accelerate → decelerate → smooth stop)
|
|
17044
|
+
* - Browser generates natural scroll events
|
|
17045
|
+
*
|
|
17046
|
+
* @param deltaY - Positive = scroll down, Negative = scroll up
|
|
17047
|
+
* @param speed - 1-100 (100 = instant, default: 50)
|
|
17048
|
+
*/
|
|
17049
|
+
async scrollDelta(page, deltaY, speed = 50) {
|
|
17050
|
+
try {
|
|
17051
|
+
if (page.isClosed()) return;
|
|
17052
|
+
const absDelta = Math.abs(deltaY);
|
|
17053
|
+
if (speed >= 100 || absDelta <= 5) {
|
|
17054
|
+
await page.evaluate((dy) => window.scrollBy(0, dy), deltaY);
|
|
17055
|
+
return;
|
|
17056
|
+
}
|
|
17057
|
+
const durationMs = Math.max(150, 1e3 - speed * 10);
|
|
17058
|
+
await page.evaluate((totalDelta, duration) => {
|
|
17059
|
+
return new Promise((resolve) => {
|
|
17060
|
+
let scrolled = 0;
|
|
17061
|
+
const start = performance.now();
|
|
17062
|
+
function frame(now) {
|
|
17063
|
+
const elapsed = now - start;
|
|
17064
|
+
const t = Math.min(elapsed / duration, 1);
|
|
17065
|
+
const eased = -(Math.cos(Math.PI * t) - 1) / 2;
|
|
17066
|
+
const target = eased * totalDelta;
|
|
17067
|
+
const delta = target - scrolled;
|
|
17068
|
+
scrolled = target;
|
|
17069
|
+
if (Math.abs(delta) > 0.5) {
|
|
17070
|
+
window.scrollBy(0, delta);
|
|
17071
|
+
}
|
|
17072
|
+
if (t < 1) {
|
|
17073
|
+
requestAnimationFrame(frame);
|
|
17074
|
+
} else {
|
|
17075
|
+
resolve();
|
|
17076
|
+
}
|
|
17077
|
+
}
|
|
17078
|
+
requestAnimationFrame(frame);
|
|
17079
|
+
});
|
|
17080
|
+
}, deltaY, durationMs);
|
|
17081
|
+
} catch {
|
|
17082
|
+
}
|
|
17083
|
+
}
|
|
17084
|
+
/**
|
|
17085
|
+
* Scroll a container element (with overflow) — smooth 60fps inside browser.
|
|
17086
|
+
* Requires a container selector instead of scrolling the window.
|
|
17087
|
+
*/
|
|
17088
|
+
async scrollContainerDelta(page, containerSelector, deltaY, speed = 50) {
|
|
17089
|
+
try {
|
|
17090
|
+
if (page.isClosed()) return;
|
|
17091
|
+
const durationMs = Math.max(150, 1e3 - speed * 10);
|
|
17092
|
+
await page.evaluate((selector, totalDelta, duration) => {
|
|
17093
|
+
return new Promise((resolve) => {
|
|
17094
|
+
const el = document.querySelector(selector);
|
|
17095
|
+
if (!el) {
|
|
17096
|
+
resolve();
|
|
17097
|
+
return;
|
|
17098
|
+
}
|
|
17099
|
+
let scrolled = 0;
|
|
17100
|
+
const start = performance.now();
|
|
17101
|
+
function frame(now) {
|
|
17102
|
+
const elapsed = now - start;
|
|
17103
|
+
const t = Math.min(elapsed / duration, 1);
|
|
17104
|
+
const eased = -(Math.cos(Math.PI * t) - 1) / 2;
|
|
17105
|
+
const target = eased * totalDelta;
|
|
17106
|
+
const delta = target - scrolled;
|
|
17107
|
+
scrolled = target;
|
|
17108
|
+
if (Math.abs(delta) > 0.5) {
|
|
17109
|
+
el.scrollBy(0, delta);
|
|
17110
|
+
}
|
|
17111
|
+
if (t < 1) {
|
|
17112
|
+
requestAnimationFrame(frame);
|
|
17113
|
+
} else {
|
|
17114
|
+
resolve();
|
|
17115
|
+
}
|
|
17116
|
+
}
|
|
17117
|
+
requestAnimationFrame(frame);
|
|
17118
|
+
});
|
|
17119
|
+
}, containerSelector, deltaY, durationMs);
|
|
17120
|
+
} catch {
|
|
17121
|
+
}
|
|
17122
|
+
}
|
|
17123
|
+
/**
|
|
17124
|
+
* Scroll to top or bottom — calculates distance then uses scrollDelta (smooth).
|
|
17125
|
+
*/
|
|
17126
|
+
async scrollToPosition(page, position, speed = 50) {
|
|
17127
|
+
try {
|
|
17128
|
+
if (page.isClosed()) return;
|
|
17129
|
+
const { scrollY, scrollHeight, innerHeight } = await page.evaluate(() => ({
|
|
17130
|
+
scrollY: window.scrollY,
|
|
17131
|
+
scrollHeight: document.body.scrollHeight,
|
|
17132
|
+
innerHeight: window.innerHeight
|
|
17133
|
+
}));
|
|
17134
|
+
let deltaY;
|
|
17135
|
+
if (position === "top") {
|
|
17136
|
+
deltaY = -scrollY;
|
|
17137
|
+
} else {
|
|
17138
|
+
deltaY = scrollHeight - innerHeight - scrollY;
|
|
17139
|
+
}
|
|
17140
|
+
if (Math.abs(deltaY) < 5) return;
|
|
17141
|
+
await this.scrollDelta(page, deltaY, speed);
|
|
17142
|
+
} catch {
|
|
17143
|
+
}
|
|
17144
|
+
}
|
|
17145
|
+
/**
|
|
17146
|
+
* Scroll element into viewport — calculates offset then uses scrollDelta (smooth).
|
|
17147
|
+
*/
|
|
17148
|
+
async scrollElementIntoView(page, element, speed = 50) {
|
|
17149
|
+
try {
|
|
17150
|
+
if (page.isClosed()) return;
|
|
17151
|
+
const box = await element.boundingBox();
|
|
17152
|
+
if (!box) return;
|
|
17153
|
+
const { innerHeight } = await page.evaluate(() => ({
|
|
17154
|
+
innerHeight: window.innerHeight
|
|
17155
|
+
}));
|
|
17156
|
+
const elementCenter = box.y + box.height / 2;
|
|
17157
|
+
const viewportCenter = innerHeight / 2;
|
|
17158
|
+
const deltaY = elementCenter - viewportCenter;
|
|
17159
|
+
if (Math.abs(deltaY) < 5) return;
|
|
17160
|
+
await this.scrollDelta(page, deltaY, speed);
|
|
17161
|
+
} catch {
|
|
17162
|
+
}
|
|
17163
|
+
}
|
|
17164
|
+
/**
|
|
17165
|
+
* Move cursor to center of a container — needed before scrolling a container.
|
|
17166
|
+
* Cursor ON: Bézier move (visible)
|
|
17167
|
+
* Cursor OFF: CDP mouse.move (hidden)
|
|
17168
|
+
*/
|
|
17169
|
+
async moveToContainer(page, containerSelector) {
|
|
17170
|
+
try {
|
|
17171
|
+
if (page.isClosed()) return;
|
|
17172
|
+
const el = await page.$(containerSelector);
|
|
17173
|
+
if (!el) return;
|
|
17174
|
+
const box = await el.boundingBox();
|
|
17175
|
+
if (!box) return;
|
|
17176
|
+
const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
|
|
17177
|
+
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
|
|
17178
|
+
if (this.enabled) {
|
|
17179
|
+
const cursor = this.getOrCreateCursor(page);
|
|
17180
|
+
await cursor.moveTo({ x, y });
|
|
17181
|
+
} else {
|
|
17182
|
+
await page.mouse.move(x, y);
|
|
17183
|
+
}
|
|
17184
|
+
} catch {
|
|
17185
|
+
}
|
|
17186
|
+
}
|
|
17187
|
+
/** Get or create ghost-cursor instance for the given page */
|
|
17188
|
+
getOrCreateCursor(page) {
|
|
17189
|
+
let cursor = this.cursorMap.get(page);
|
|
17190
|
+
if (!cursor) {
|
|
17191
|
+
cursor = new import_ghost_cursor.GhostCursor(page, {
|
|
17192
|
+
defaultOptions: {
|
|
17193
|
+
// Override defaults for all actions — human-like timing
|
|
17194
|
+
click: {
|
|
17195
|
+
hesitate: 120,
|
|
17196
|
+
// default if not overridden per-call
|
|
17197
|
+
waitForClick: 60,
|
|
17198
|
+
// hold mouse button 60ms
|
|
17199
|
+
moveDelay: 0
|
|
17200
|
+
// SDK manages delay separately
|
|
17201
|
+
},
|
|
17202
|
+
move: {
|
|
17203
|
+
moveDelay: 0
|
|
17204
|
+
// SDK manages delay separately
|
|
17205
|
+
},
|
|
17206
|
+
moveTo: {
|
|
17207
|
+
moveDelay: 0
|
|
17208
|
+
}
|
|
17209
|
+
}
|
|
17210
|
+
});
|
|
17211
|
+
this.cursorMap.set(page, cursor);
|
|
17212
|
+
}
|
|
17213
|
+
return cursor;
|
|
17214
|
+
}
|
|
17215
|
+
};
|
|
17216
|
+
/**
|
|
17217
|
+
* Minimum distance (px) to activate segmented move.
|
|
17218
|
+
* Below threshold → ghost-cursor moves normally (fast enough, looks natural).
|
|
17219
|
+
* Above threshold → split into small segments with delay between each.
|
|
17220
|
+
*/
|
|
17221
|
+
_HiraCursor.SEGMENT_THRESHOLD = 500;
|
|
17222
|
+
/** Length of each segment (px) when splitting long-distance moves */
|
|
17223
|
+
_HiraCursor.SEGMENT_SIZE = 650;
|
|
17224
|
+
/** Delay between segments (ms) — randomized within this range */
|
|
17225
|
+
_HiraCursor.SEGMENT_DELAY = [30, 60];
|
|
17226
|
+
var HiraCursor = _HiraCursor;
|
|
17227
|
+
|
|
16812
17228
|
// src/utils/browser.utils.ts
|
|
16813
17229
|
var BrowserUtils = class {
|
|
16814
17230
|
constructor(context) {
|
|
16815
|
-
|
|
17231
|
+
/** Currently active iframe — null means main frame. Changed via activeIframe() */
|
|
17232
|
+
this.activeFrame = null;
|
|
17233
|
+
var _a, _b;
|
|
16816
17234
|
this.ctx = context;
|
|
16817
17235
|
this.logger = context.logger;
|
|
16818
17236
|
this.outputDefs = (_a = context.outputDefinitions) != null ? _a : [];
|
|
16819
17237
|
this.validOutputKeys = new Set(this.outputDefs.map((d) => d.key));
|
|
17238
|
+
this.defaultPage = context.page;
|
|
17239
|
+
this.activePage = context.page;
|
|
17240
|
+
const cursorEnabled = ((_b = context.execution) == null ? void 0 : _b.virtualCursor) !== false;
|
|
17241
|
+
this.hiraCursor = new HiraCursor(cursorEnabled);
|
|
17242
|
+
this.enableFocusEmulation(this.activePage).catch(() => {
|
|
17243
|
+
});
|
|
17244
|
+
this.applyStealthScripts(this.activePage).catch(() => {
|
|
17245
|
+
});
|
|
17246
|
+
this.hiraCursor.inject(this.activePage).catch(() => {
|
|
17247
|
+
});
|
|
17248
|
+
}
|
|
17249
|
+
/**
|
|
17250
|
+
* CDP trick — Chromium thinks tab is always focused, not throttled
|
|
17251
|
+
* when user switches to another tab or clicks elsewhere.
|
|
17252
|
+
*/
|
|
17253
|
+
async enableFocusEmulation(page) {
|
|
17254
|
+
try {
|
|
17255
|
+
const cdp = await page.createCDPSession();
|
|
17256
|
+
await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true });
|
|
17257
|
+
} catch {
|
|
17258
|
+
}
|
|
17259
|
+
}
|
|
17260
|
+
/**
|
|
17261
|
+
* Apply stealth scripts to remove automation fingerprints.
|
|
17262
|
+
* Uses evaluateOnNewDocument — runs BEFORE any page scripts on every navigation.
|
|
17263
|
+
* Persists across navigations on the same page instance.
|
|
17264
|
+
*/
|
|
17265
|
+
async applyStealthScripts(page) {
|
|
17266
|
+
try {
|
|
17267
|
+
const stealthFn = () => {
|
|
17268
|
+
Object.defineProperty(navigator, "webdriver", {
|
|
17269
|
+
get: () => void 0
|
|
17270
|
+
});
|
|
17271
|
+
Object.defineProperty(navigator, "plugins", {
|
|
17272
|
+
get: () => [1, 2, 3, 4, 5]
|
|
17273
|
+
});
|
|
17274
|
+
Object.defineProperty(navigator, "languages", {
|
|
17275
|
+
get: () => ["en-US", "en"]
|
|
17276
|
+
});
|
|
17277
|
+
if (window.cdc_adoQpoasnfa76pfcZLmcfl_Array) {
|
|
17278
|
+
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
|
|
17279
|
+
}
|
|
17280
|
+
if (window.cdc_adoQpoasnfa76pfcZLmcfl_Promise) {
|
|
17281
|
+
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
|
|
17282
|
+
}
|
|
17283
|
+
if (window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol) {
|
|
17284
|
+
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
|
|
17285
|
+
}
|
|
17286
|
+
};
|
|
17287
|
+
await page.evaluate(stealthFn);
|
|
17288
|
+
await page.evaluateOnNewDocument(stealthFn);
|
|
17289
|
+
} catch {
|
|
17290
|
+
}
|
|
16820
17291
|
}
|
|
16821
17292
|
checkAbort() {
|
|
16822
17293
|
const signal = global.__HIRA_ABORT_SIGNAL__;
|
|
16823
17294
|
if (signal == null ? void 0 : signal.aborted) throw new Error("cancelled");
|
|
16824
17295
|
}
|
|
17296
|
+
/**
|
|
17297
|
+
* Scroll element vào viewport mượt mà — segments + delay.
|
|
17298
|
+
* Chỉ scroll nếu element nằm ngoài viewport.
|
|
17299
|
+
*/
|
|
17300
|
+
async smoothScrollToElement(element) {
|
|
17301
|
+
try {
|
|
17302
|
+
for (let i = 0; i < 15; i++) {
|
|
17303
|
+
const box = await element.boundingBox();
|
|
17304
|
+
if (!box) return;
|
|
17305
|
+
const vh = await this.activePage.evaluate(() => window.innerHeight);
|
|
17306
|
+
const center = box.y + box.height / 2;
|
|
17307
|
+
const margin = vh * 0.15;
|
|
17308
|
+
if (center >= margin && center <= vh - margin) break;
|
|
17309
|
+
const delta = center - vh / 2;
|
|
17310
|
+
const maxSeg = 300 + Math.random() * 200;
|
|
17311
|
+
const segment = Math.abs(delta) > maxSeg ? delta > 0 ? maxSeg : -maxSeg : delta;
|
|
17312
|
+
await this.hiraCursor.scrollDelta(this.activePage, segment, 50);
|
|
17313
|
+
await this.rawSleep(150 + Math.random() * 200);
|
|
17314
|
+
}
|
|
17315
|
+
} catch {
|
|
17316
|
+
try {
|
|
17317
|
+
await element.scrollIntoView();
|
|
17318
|
+
} catch {
|
|
17319
|
+
}
|
|
17320
|
+
}
|
|
17321
|
+
}
|
|
17322
|
+
/**
|
|
17323
|
+
* Pause execution for the given duration.
|
|
17324
|
+
*
|
|
17325
|
+
* @param ms - Duration in milliseconds
|
|
17326
|
+
* @example await utils.sleep(2000); // wait 2 seconds
|
|
17327
|
+
*/
|
|
16825
17328
|
async sleep(ms) {
|
|
17329
|
+
this.checkAbort();
|
|
16826
17330
|
await this.log("debug", `\u23F3 Sleep ${ms}ms`);
|
|
16827
17331
|
await this.rawSleep(ms);
|
|
16828
17332
|
}
|
|
16829
|
-
|
|
16830
|
-
|
|
17333
|
+
/**
|
|
17334
|
+
* Wait for an element to appear and become visible in the DOM.
|
|
17335
|
+
* Returns null if not found (soft fail — does NOT throw).
|
|
17336
|
+
* Supports CSS selectors and XPath (auto-detected by `//` or `(` prefix).
|
|
17337
|
+
*
|
|
17338
|
+
* @param selector - CSS selector or XPath expression
|
|
17339
|
+
* @param timeout - Max wait time in ms (default: 8000)
|
|
17340
|
+
* @param scope - Optional Frame to search within
|
|
17341
|
+
* @returns The element handle, or null if not found
|
|
17342
|
+
*
|
|
17343
|
+
* @example
|
|
17344
|
+
* const el = await utils.waitForElement("#my-btn");
|
|
17345
|
+
* if (el) await el.click();
|
|
17346
|
+
*/
|
|
17347
|
+
async waitForElement(selector, timeout = 8e3, scope) {
|
|
17348
|
+
var _a;
|
|
17349
|
+
const target = (_a = scope != null ? scope : this.activeFrame) != null ? _a : this.activePage;
|
|
16831
17350
|
try {
|
|
16832
17351
|
this.checkAbort();
|
|
16833
17352
|
const options = { timeout, visible: true };
|
|
@@ -16844,7 +17363,57 @@ var BrowserUtils = class {
|
|
|
16844
17363
|
return null;
|
|
16845
17364
|
}
|
|
16846
17365
|
}
|
|
17366
|
+
/**
|
|
17367
|
+
* Resolve an element: if waitTimeout > 0, wait for it; otherwise query directly with $().
|
|
17368
|
+
*/
|
|
17369
|
+
async resolveElement(selector, waitTimeout, scope) {
|
|
17370
|
+
var _a;
|
|
17371
|
+
if (waitTimeout && waitTimeout > 0) {
|
|
17372
|
+
return this.waitForElement(selector, waitTimeout, scope);
|
|
17373
|
+
}
|
|
17374
|
+
const target = (_a = scope != null ? scope : this.activeFrame) != null ? _a : this.activePage;
|
|
17375
|
+
const resolved = selector.startsWith("//") || selector.startsWith("(") ? `xpath/${selector}` : selector;
|
|
17376
|
+
return target.$(resolved);
|
|
17377
|
+
}
|
|
17378
|
+
/**
|
|
17379
|
+
* Click on an element or at specific coordinates.
|
|
17380
|
+
* Scrolls the element into view before clicking.
|
|
17381
|
+
* Throws if the element is not found.
|
|
17382
|
+
*
|
|
17383
|
+
* @param target - CSS selector, XPath, ElementHandle, or `{ x, y }` coordinates
|
|
17384
|
+
* @param options.delay - Delay in ms before clicking (default: 1000)
|
|
17385
|
+
* @param options.waitTimeout - Max wait time for element to appear (default: 2000)
|
|
17386
|
+
* @param options.frame - Optional Frame to search within
|
|
17387
|
+
* @returns true on success
|
|
17388
|
+
*
|
|
17389
|
+
* @example
|
|
17390
|
+
* await utils.click("#submit-btn");
|
|
17391
|
+
* await utils.click("#btn", { delay: 0 }); // click immediately
|
|
17392
|
+
* await utils.click({ x: 500, y: 300 }); // click at coordinates
|
|
17393
|
+
* const pos = await utils.getPosition("#btn", { randomXY: true });
|
|
17394
|
+
* await utils.click(pos!); // click random point inside element
|
|
17395
|
+
*/
|
|
16847
17396
|
async click(target, options) {
|
|
17397
|
+
var _a, _b, _c, _d, _e;
|
|
17398
|
+
if (typeof target === "object" && !(target instanceof import_puppeteer_core3.ElementHandle) && "x" in target && "y" in target) {
|
|
17399
|
+
try {
|
|
17400
|
+
this.checkAbort();
|
|
17401
|
+
await this.log("info", `\u{1F5B1}\uFE0F Click: (${Math.round(target.x)}, ${Math.round(target.y)})`);
|
|
17402
|
+
const clickDelay = (_a = options == null ? void 0 : options.delay) != null ? _a : 1e3;
|
|
17403
|
+
if (clickDelay > 0) await this.sleep(clickDelay);
|
|
17404
|
+
if (this.hiraCursor.isEnabled) {
|
|
17405
|
+
await this.hiraCursor.moveTo(this.activePage, target.x, target.y);
|
|
17406
|
+
await this.rawSleep(50 + Math.random() * 50);
|
|
17407
|
+
}
|
|
17408
|
+
await this.activePage.mouse.click(target.x, target.y);
|
|
17409
|
+
await this.actionDelay();
|
|
17410
|
+
return true;
|
|
17411
|
+
} catch (error) {
|
|
17412
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17413
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17414
|
+
throw new Error(`Click failed: (${target.x}, ${target.y}) \u2014 ${msg}`);
|
|
17415
|
+
}
|
|
17416
|
+
}
|
|
16848
17417
|
const label = typeof target === "string" ? target : "[ElementHandle]";
|
|
16849
17418
|
try {
|
|
16850
17419
|
this.checkAbort();
|
|
@@ -16853,82 +17422,239 @@ var BrowserUtils = class {
|
|
|
16853
17422
|
if (typeof target === "string") {
|
|
16854
17423
|
element = await this.waitForElement(
|
|
16855
17424
|
target,
|
|
16856
|
-
options == null ? void 0 : options.
|
|
17425
|
+
(_b = options == null ? void 0 : options.waitTimeout) != null ? _b : 2e3,
|
|
16857
17426
|
options == null ? void 0 : options.frame
|
|
16858
17427
|
);
|
|
17428
|
+
if (!element) {
|
|
17429
|
+
const scope = (_d = (_c = options == null ? void 0 : options.frame) != null ? _c : this.activeFrame) != null ? _d : this.activePage;
|
|
17430
|
+
const resolved = target.startsWith("//") || target.startsWith("(") ? `xpath/${target}` : target;
|
|
17431
|
+
element = await scope.$(resolved);
|
|
17432
|
+
}
|
|
16859
17433
|
} else {
|
|
16860
17434
|
element = target;
|
|
16861
17435
|
}
|
|
16862
17436
|
if (!element) {
|
|
16863
|
-
|
|
16864
|
-
|
|
16865
|
-
|
|
16866
|
-
|
|
16867
|
-
|
|
17437
|
+
throw new Error(`Click failed \u2014 element not found: ${this.shortSelector(label)}`);
|
|
17438
|
+
}
|
|
17439
|
+
await this.smoothScrollToElement(element);
|
|
17440
|
+
const clickDelay = (_e = options == null ? void 0 : options.delay) != null ? _e : 1e3;
|
|
17441
|
+
if (clickDelay > 0) await this.sleep(clickDelay);
|
|
17442
|
+
const box = await element.boundingBox();
|
|
17443
|
+
if (this.hiraCursor.isEnabled && !this.activeFrame && !(options == null ? void 0 : options.frame) && box) {
|
|
17444
|
+
const clicked = await this.hiraCursor.clickElement(this.activePage, element);
|
|
17445
|
+
if (!clicked) {
|
|
17446
|
+
await element.click();
|
|
17447
|
+
}
|
|
17448
|
+
} else {
|
|
17449
|
+
await element.click();
|
|
16868
17450
|
}
|
|
16869
|
-
await element.scrollIntoView();
|
|
16870
|
-
if (options == null ? void 0 : options.delay) await this.sleep(options.delay);
|
|
16871
|
-
await element.click();
|
|
16872
17451
|
await this.actionDelay();
|
|
16873
17452
|
return true;
|
|
16874
17453
|
} catch (error) {
|
|
16875
17454
|
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
16876
17455
|
const msg = error instanceof Error ? error.message : String(error);
|
|
17456
|
+
throw new Error(`Click failed: ${this.shortSelector(label)} \u2014 ${msg}`);
|
|
17457
|
+
}
|
|
17458
|
+
}
|
|
17459
|
+
/**
|
|
17460
|
+
* Get the position (x, y) of an element.
|
|
17461
|
+
* Returns center point by default, or a random point within bounds with `randomXY`.
|
|
17462
|
+
*
|
|
17463
|
+
* @param selector - CSS selector or XPath
|
|
17464
|
+
* @param options.randomXY - If true, returns random point within element (10% margin from edges)
|
|
17465
|
+
* @param options.waitTimeout - Max wait time for element (default: 2000ms)
|
|
17466
|
+
* @param options.frame - Optional Frame to search within
|
|
17467
|
+
* @returns `{ x, y }` or null if element not found / no bounding box
|
|
17468
|
+
*
|
|
17469
|
+
* @example
|
|
17470
|
+
* const center = await utils.getPosition("#btn");
|
|
17471
|
+
* const rand = await utils.getPosition("#btn", { randomXY: true });
|
|
17472
|
+
* if (rand) await utils.click(rand);
|
|
17473
|
+
*/
|
|
17474
|
+
async getPosition(selector, options) {
|
|
17475
|
+
var _a;
|
|
17476
|
+
try {
|
|
17477
|
+
this.checkAbort();
|
|
17478
|
+
const element = await this.waitForElement(
|
|
17479
|
+
selector,
|
|
17480
|
+
(_a = options == null ? void 0 : options.waitTimeout) != null ? _a : 2e3,
|
|
17481
|
+
options == null ? void 0 : options.frame
|
|
17482
|
+
);
|
|
17483
|
+
if (!element) return null;
|
|
17484
|
+
await this.smoothScrollToElement(element);
|
|
17485
|
+
const box = await element.boundingBox();
|
|
17486
|
+
if (!box) return null;
|
|
17487
|
+
if (options == null ? void 0 : options.randomXY) {
|
|
17488
|
+
const mx = box.width * 0.1;
|
|
17489
|
+
const my = box.height * 0.1;
|
|
17490
|
+
return {
|
|
17491
|
+
x: box.x + mx + Math.random() * (box.width - 2 * mx),
|
|
17492
|
+
y: box.y + my + Math.random() * (box.height - 2 * my)
|
|
17493
|
+
};
|
|
17494
|
+
}
|
|
17495
|
+
return {
|
|
17496
|
+
x: box.x + box.width / 2,
|
|
17497
|
+
y: box.y + box.height / 2
|
|
17498
|
+
};
|
|
17499
|
+
} catch (error) {
|
|
17500
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17501
|
+
await this.log("warn", `\u26A0\uFE0F getPosition failed: ${this.shortSelector(selector)}`);
|
|
17502
|
+
return null;
|
|
17503
|
+
}
|
|
17504
|
+
}
|
|
17505
|
+
/**
|
|
17506
|
+
* Select a value from a `<select>` dropdown.
|
|
17507
|
+
* Human-like flow: cursor moves to select → click to open → page.select() → close.
|
|
17508
|
+
*
|
|
17509
|
+
* NOTE: Native `<option>` elements CAN NOT be clicked via Puppeteer
|
|
17510
|
+
* ("Node is either not clickable or not an Element").
|
|
17511
|
+
* `page.select()` is the ONLY reliable method.
|
|
17512
|
+
*
|
|
17513
|
+
* @param selector - CSS selector or XPath of the `<select>` element
|
|
17514
|
+
* @param value - The `value` attribute of the option to select
|
|
17515
|
+
* @param options.delay - Delay before clicking (default: 500ms)
|
|
17516
|
+
* @param options.waitTimeout - Max wait for element (default: 2000ms)
|
|
17517
|
+
*
|
|
17518
|
+
* @example
|
|
17519
|
+
* await utils.select("#country", "vn");
|
|
17520
|
+
*/
|
|
17521
|
+
async select(selector, value, options) {
|
|
17522
|
+
var _a, _b, _c;
|
|
17523
|
+
try {
|
|
17524
|
+
this.checkAbort();
|
|
16877
17525
|
await this.log(
|
|
16878
|
-
"
|
|
16879
|
-
`\
|
|
17526
|
+
"info",
|
|
17527
|
+
`\u{1F4CB} Select "${value}" \u2192 ${this.shortSelector(selector)}`
|
|
16880
17528
|
);
|
|
16881
|
-
|
|
17529
|
+
await this.click(selector, {
|
|
17530
|
+
delay: (_a = options == null ? void 0 : options.delay) != null ? _a : 500,
|
|
17531
|
+
waitTimeout: options == null ? void 0 : options.waitTimeout,
|
|
17532
|
+
frame: options == null ? void 0 : options.frame
|
|
17533
|
+
});
|
|
17534
|
+
await this.rawSleep(300 + Math.random() * 200);
|
|
17535
|
+
const target = (_c = (_b = options == null ? void 0 : options.frame) != null ? _b : this.activeFrame) != null ? _c : this.activePage;
|
|
17536
|
+
const resolved = selector.startsWith("//") || selector.startsWith("(") ? `xpath/${selector}` : selector;
|
|
17537
|
+
await target.select(resolved, value);
|
|
17538
|
+
await this.rawSleep(100);
|
|
17539
|
+
await this.activePage.keyboard.press("Escape");
|
|
17540
|
+
await this.actionDelay();
|
|
17541
|
+
return true;
|
|
17542
|
+
} catch (error) {
|
|
17543
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17544
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17545
|
+
throw new Error(`Select failed: ${this.shortSelector(selector)} \u2014 ${msg}`);
|
|
16882
17546
|
}
|
|
16883
17547
|
}
|
|
17548
|
+
/**
|
|
17549
|
+
* Type text into an input element. Throws if the element is not found.
|
|
17550
|
+
*
|
|
17551
|
+
* @param selector - CSS selector or XPath of the input
|
|
17552
|
+
* @param text - The text to type
|
|
17553
|
+
* @param options.mode - Typing mode:
|
|
17554
|
+
* - `"replace"` (default): Clear existing text, then type new text
|
|
17555
|
+
* - `"append"`: Type without clearing — appends to existing text
|
|
17556
|
+
* - `"paste"`: Set value directly via JS (fast, no keystroke simulation)
|
|
17557
|
+
* @param options.delay - Delay between keystrokes in ms (default: 50). Ignored in paste mode.
|
|
17558
|
+
* @param options.waitTimeout - Max wait time for element to appear in ms (default: 0 — instant)
|
|
17559
|
+
* @param options.frame - Optional Frame to search within
|
|
17560
|
+
* @returns true on success
|
|
17561
|
+
*
|
|
17562
|
+
* @example
|
|
17563
|
+
* await utils.type("#email", "user@example.com"); // replace mode
|
|
17564
|
+
* await utils.type("#input", " more text", { mode: "append" }); // append
|
|
17565
|
+
* await utils.type("#address", "0x1234...abcd", { mode: "paste" }); // instant paste
|
|
17566
|
+
*/
|
|
16884
17567
|
async type(selector, text, options) {
|
|
16885
|
-
var _a;
|
|
17568
|
+
var _a, _b, _c, _d;
|
|
17569
|
+
const mode = (_a = options == null ? void 0 : options.mode) != null ? _a : "replace";
|
|
16886
17570
|
const masked = text.length > 20 ? text.slice(0, 20) + "..." : text;
|
|
16887
17571
|
try {
|
|
16888
17572
|
this.checkAbort();
|
|
16889
17573
|
await this.log(
|
|
16890
17574
|
"info",
|
|
16891
|
-
`\u2328\uFE0F Type "${masked}" \u2192 ${this.shortSelector(selector)}`
|
|
17575
|
+
`\u2328\uFE0F Type [${mode}] "${masked}" \u2192 ${this.shortSelector(selector)}`
|
|
16892
17576
|
);
|
|
16893
|
-
const element = await this.
|
|
17577
|
+
const element = await this.resolveElement(
|
|
16894
17578
|
selector,
|
|
16895
|
-
|
|
17579
|
+
options == null ? void 0 : options.waitTimeout,
|
|
16896
17580
|
options == null ? void 0 : options.frame
|
|
16897
17581
|
);
|
|
16898
17582
|
if (!element) {
|
|
16899
|
-
|
|
16900
|
-
|
|
16901
|
-
|
|
17583
|
+
throw new Error(`Type failed \u2014 element not found: ${this.shortSelector(selector)}`);
|
|
17584
|
+
}
|
|
17585
|
+
await this.smoothScrollToElement(element);
|
|
17586
|
+
if (mode === "paste") {
|
|
17587
|
+
if (this.hiraCursor.isEnabled && !this.activeFrame && !(options == null ? void 0 : options.frame)) {
|
|
17588
|
+
const clicked = await this.hiraCursor.clickElement(this.activePage, element);
|
|
17589
|
+
if (!clicked) await element.click();
|
|
17590
|
+
} else {
|
|
17591
|
+
await element.click();
|
|
17592
|
+
}
|
|
17593
|
+
await this.rawSleep(100 + Math.random() * 100);
|
|
17594
|
+
const target = (_b = this.activeFrame) != null ? _b : this.activePage;
|
|
17595
|
+
await target.evaluate(
|
|
17596
|
+
(el, val) => {
|
|
17597
|
+
el.value = val;
|
|
17598
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
17599
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
17600
|
+
},
|
|
17601
|
+
element,
|
|
17602
|
+
text
|
|
16902
17603
|
);
|
|
16903
|
-
|
|
17604
|
+
} else if (mode === "append") {
|
|
17605
|
+
if (this.hiraCursor.isEnabled && !this.activeFrame && !(options == null ? void 0 : options.frame)) {
|
|
17606
|
+
const clicked = await this.hiraCursor.clickElement(this.activePage, element);
|
|
17607
|
+
if (!clicked) await element.click();
|
|
17608
|
+
} else {
|
|
17609
|
+
await element.click();
|
|
17610
|
+
}
|
|
17611
|
+
await element.press("End");
|
|
17612
|
+
await element.type(text, { delay: (_c = options == null ? void 0 : options.delay) != null ? _c : 50 });
|
|
17613
|
+
} else {
|
|
17614
|
+
if (this.hiraCursor.isEnabled && !this.activeFrame && !(options == null ? void 0 : options.frame)) {
|
|
17615
|
+
const clicked = await this.hiraCursor.clickElement(this.activePage, element);
|
|
17616
|
+
if (!clicked) await element.click();
|
|
17617
|
+
} else {
|
|
17618
|
+
await element.click();
|
|
17619
|
+
}
|
|
17620
|
+
await this.activePage.keyboard.down("Control");
|
|
17621
|
+
await this.activePage.keyboard.press("a");
|
|
17622
|
+
await this.activePage.keyboard.up("Control");
|
|
17623
|
+
await element.press("Backspace");
|
|
17624
|
+
await element.type(text, { delay: (_d = options == null ? void 0 : options.delay) != null ? _d : 50 });
|
|
16904
17625
|
}
|
|
16905
|
-
await element.scrollIntoView();
|
|
16906
|
-
await element.click({ clickCount: 3 });
|
|
16907
|
-
await element.press("Backspace");
|
|
16908
|
-
await element.type(text, { delay: (_a = options == null ? void 0 : options.delay) != null ? _a : 50 });
|
|
16909
17626
|
await this.actionDelay();
|
|
16910
17627
|
return true;
|
|
16911
17628
|
} catch (error) {
|
|
16912
17629
|
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
16913
17630
|
const msg = error instanceof Error ? error.message : String(error);
|
|
16914
|
-
|
|
16915
|
-
"error",
|
|
16916
|
-
`\u274C Type failed: ${this.shortSelector(selector)} \u2014 ${msg}`
|
|
16917
|
-
);
|
|
16918
|
-
return false;
|
|
17631
|
+
throw new Error(`Type failed: ${this.shortSelector(selector)} \u2014 ${msg}`);
|
|
16919
17632
|
}
|
|
16920
17633
|
}
|
|
16921
|
-
|
|
17634
|
+
/**
|
|
17635
|
+
* Get the text content of an element. Returns null if not found (soft fail).
|
|
17636
|
+
*
|
|
17637
|
+
* @param selector - CSS selector or XPath
|
|
17638
|
+
* @param options.waitTimeout - Max wait time for element to appear in ms (default: 0 — instant)
|
|
17639
|
+
* @param options.frame - Optional Frame to search within
|
|
17640
|
+
* @returns Trimmed text content, or null if element not found
|
|
17641
|
+
*
|
|
17642
|
+
* @example
|
|
17643
|
+
* const price = await utils.getText(".price");
|
|
17644
|
+
* const el = await utils.getText(".lazy-el", { waitTimeout: 8000 });
|
|
17645
|
+
*/
|
|
17646
|
+
async getText(selector, options) {
|
|
17647
|
+
var _a, _b;
|
|
16922
17648
|
try {
|
|
16923
17649
|
this.checkAbort();
|
|
16924
17650
|
await this.log("debug", `\u{1F4C4} getText: ${this.shortSelector(selector)}`);
|
|
16925
|
-
const scope = frame != null ?
|
|
16926
|
-
const element = await this.
|
|
17651
|
+
const scope = (_b = (_a = options == null ? void 0 : options.frame) != null ? _a : this.activeFrame) != null ? _b : this.activePage;
|
|
17652
|
+
const element = await this.resolveElement(selector, options == null ? void 0 : options.waitTimeout, options == null ? void 0 : options.frame);
|
|
16927
17653
|
if (!element) return null;
|
|
16928
17654
|
const text = await scope.evaluate(
|
|
16929
17655
|
(el) => {
|
|
16930
|
-
var
|
|
16931
|
-
return ((
|
|
17656
|
+
var _a2;
|
|
17657
|
+
return ((_a2 = el.textContent) == null ? void 0 : _a2.trim()) || "";
|
|
16932
17658
|
},
|
|
16933
17659
|
element
|
|
16934
17660
|
);
|
|
@@ -16942,158 +17668,636 @@ var BrowserUtils = class {
|
|
|
16942
17668
|
return null;
|
|
16943
17669
|
}
|
|
16944
17670
|
}
|
|
16945
|
-
|
|
17671
|
+
/**
|
|
17672
|
+
* Check if an element exists and is visible on the page.
|
|
17673
|
+
* Returns false if not found (soft fail — does NOT throw).
|
|
17674
|
+
*
|
|
17675
|
+
* @param selector - CSS selector or XPath
|
|
17676
|
+
* @param timeout - Max wait time in ms (default: 4000)
|
|
17677
|
+
* @param frame - Optional Frame to search within
|
|
17678
|
+
* @returns true if element exists and is visible
|
|
17679
|
+
*
|
|
17680
|
+
* @example
|
|
17681
|
+
* if (await utils.exists("#popup-overlay")) {
|
|
17682
|
+
* await utils.click("#close-popup");
|
|
17683
|
+
* }
|
|
17684
|
+
*/
|
|
17685
|
+
async exists(selector, timeout = 4e3, frame) {
|
|
16946
17686
|
this.checkAbort();
|
|
16947
17687
|
const el = await this.waitForElement(selector, timeout, frame);
|
|
16948
17688
|
return el !== null;
|
|
16949
17689
|
}
|
|
17690
|
+
/**
|
|
17691
|
+
* Navigate the active page to a URL. Waits until the page fully loads.
|
|
17692
|
+
* Throws on navigation failure or timeout.
|
|
17693
|
+
*
|
|
17694
|
+
* @param url - The URL to navigate to
|
|
17695
|
+
* @param options.waitUntil - When to consider navigation complete (default: "load")
|
|
17696
|
+
* @returns true on success
|
|
17697
|
+
*
|
|
17698
|
+
* @example
|
|
17699
|
+
* await utils.goto("https://example.com");
|
|
17700
|
+
* await utils.goto("https://app.com", { waitUntil: "networkidle0" });
|
|
17701
|
+
*/
|
|
16950
17702
|
async goto(url2, options) {
|
|
16951
17703
|
try {
|
|
16952
17704
|
this.checkAbort();
|
|
16953
17705
|
await this.log("info", `\u{1F310} Navigate \u2192 ${url2}`);
|
|
16954
|
-
|
|
16955
|
-
|
|
16956
|
-
|
|
17706
|
+
this.hiraCursor.markDirty(this.activePage);
|
|
17707
|
+
await this.activePage.goto(url2, {
|
|
17708
|
+
waitUntil: (options == null ? void 0 : options.waitUntil) || "load",
|
|
17709
|
+
timeout: 6e4
|
|
16957
17710
|
});
|
|
16958
17711
|
await this.actionDelay();
|
|
17712
|
+
await this.hiraCursor.inject(this.activePage);
|
|
17713
|
+
return true;
|
|
17714
|
+
} catch (error) {
|
|
17715
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17716
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17717
|
+
throw new Error(`Navigate failed: ${url2} \u2014 ${msg}`);
|
|
17718
|
+
}
|
|
17719
|
+
}
|
|
17720
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
17721
|
+
// Scroll methods — CDP mouseWheel (cả cursor ON và OFF đều giống người)
|
|
17722
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
17723
|
+
/**
|
|
17724
|
+
* Scroll the page or a container by a specific amount.
|
|
17725
|
+
* Uses CDP mouseWheel events (human-like, not JS scrollBy).
|
|
17726
|
+
*
|
|
17727
|
+
* @param direction - "up" or "down"
|
|
17728
|
+
* @param amount - Pixels to scroll
|
|
17729
|
+
* @param options.container - CSS selector of scroll container
|
|
17730
|
+
* @param options.speed - Scroll speed 1-100 (default: 50)
|
|
17731
|
+
*
|
|
17732
|
+
* @example
|
|
17733
|
+
* await utils.scroll("down", 500);
|
|
17734
|
+
* await utils.scroll("up", 200, { container: "#chat-messages" });
|
|
17735
|
+
*/
|
|
17736
|
+
async scroll(direction, amount, options) {
|
|
17737
|
+
var _a, _b;
|
|
17738
|
+
try {
|
|
17739
|
+
this.checkAbort();
|
|
17740
|
+
const arrow = direction === "down" ? "\u2193" : "\u2191";
|
|
17741
|
+
const containerLabel = (options == null ? void 0 : options.container) ? ` (${this.shortSelector(options.container)})` : "";
|
|
17742
|
+
await this.log("info", `\u{1F4DC} Scroll ${arrow} ${amount}px${containerLabel}`);
|
|
17743
|
+
if (options == null ? void 0 : options.container) {
|
|
17744
|
+
await this.hiraCursor.moveToContainer(this.activePage, options.container);
|
|
17745
|
+
}
|
|
17746
|
+
const deltaY = direction === "down" ? amount : -amount;
|
|
17747
|
+
if (options == null ? void 0 : options.container) {
|
|
17748
|
+
await this.hiraCursor.scrollContainerDelta(this.activePage, options.container, deltaY, (_a = options == null ? void 0 : options.speed) != null ? _a : 50);
|
|
17749
|
+
} else {
|
|
17750
|
+
await this.hiraCursor.scrollDelta(this.activePage, deltaY, (_b = options == null ? void 0 : options.speed) != null ? _b : 50);
|
|
17751
|
+
}
|
|
17752
|
+
await this.actionDelay();
|
|
17753
|
+
} catch (error) {
|
|
17754
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17755
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17756
|
+
await this.log("warn", `\u{1F4DC} Scroll failed: ${msg}`);
|
|
17757
|
+
}
|
|
17758
|
+
}
|
|
17759
|
+
/**
|
|
17760
|
+
* Scroll to the top or bottom of the page.
|
|
17761
|
+
* Scrolls in segments with pauses — giống người lướt dần đến đích.
|
|
17762
|
+
*
|
|
17763
|
+
* @param position - "top" or "bottom"
|
|
17764
|
+
* @param options.speed - Scroll speed 1-100 (default: 50)
|
|
17765
|
+
*
|
|
17766
|
+
* @example
|
|
17767
|
+
* await utils.scrollTo("bottom");
|
|
17768
|
+
* await utils.scrollTo("top");
|
|
17769
|
+
*/
|
|
17770
|
+
async scrollTo(position, options) {
|
|
17771
|
+
var _a;
|
|
17772
|
+
try {
|
|
17773
|
+
this.checkAbort();
|
|
17774
|
+
await this.log("info", `\u{1F4DC} Scroll to ${position}`);
|
|
17775
|
+
const speed = (_a = options == null ? void 0 : options.speed) != null ? _a : 50;
|
|
17776
|
+
const { scrollY, scrollHeight, innerHeight } = await this.activePage.evaluate(() => ({
|
|
17777
|
+
scrollY: window.scrollY,
|
|
17778
|
+
scrollHeight: document.body.scrollHeight,
|
|
17779
|
+
innerHeight: window.innerHeight
|
|
17780
|
+
}));
|
|
17781
|
+
let remaining = position === "top" ? scrollY : scrollHeight - innerHeight - scrollY;
|
|
17782
|
+
if (remaining < 5) return;
|
|
17783
|
+
const direction = position === "top" ? -1 : 1;
|
|
17784
|
+
while (remaining > 5) {
|
|
17785
|
+
this.checkAbort();
|
|
17786
|
+
const segment = Math.min(300 + Math.random() * 300, remaining);
|
|
17787
|
+
await this.hiraCursor.scrollDelta(this.activePage, direction * segment, speed);
|
|
17788
|
+
remaining -= segment;
|
|
17789
|
+
if (remaining > 5) {
|
|
17790
|
+
await this.sleep(200 + Math.random() * 300);
|
|
17791
|
+
}
|
|
17792
|
+
}
|
|
17793
|
+
await this.actionDelay();
|
|
17794
|
+
} catch (error) {
|
|
17795
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17796
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17797
|
+
await this.log("warn", `\u{1F4DC} Scroll to ${position} failed: ${msg}`);
|
|
17798
|
+
}
|
|
17799
|
+
}
|
|
17800
|
+
/**
|
|
17801
|
+
* Scroll an element into the viewport.
|
|
17802
|
+
* Scrolls in segments with pauses — không nhảy tới đích.
|
|
17803
|
+
*
|
|
17804
|
+
* @param selector - CSS selector or XPath of the element
|
|
17805
|
+
* @param options.speed - Scroll speed 1-100 (default: 50)
|
|
17806
|
+
* @returns true if element found and scrolled
|
|
17807
|
+
*
|
|
17808
|
+
* @example
|
|
17809
|
+
* await utils.scrollToElement("#footer");
|
|
17810
|
+
* await utils.scrollToElement("//button[text()='Load more']");
|
|
17811
|
+
*/
|
|
17812
|
+
async scrollToElement(selector, options) {
|
|
17813
|
+
var _a;
|
|
17814
|
+
try {
|
|
17815
|
+
this.checkAbort();
|
|
17816
|
+
await this.log("info", `\u{1F4DC} Scroll to element: ${this.shortSelector(selector)}`);
|
|
17817
|
+
const element = await this.resolveElement(selector);
|
|
17818
|
+
if (!element) {
|
|
17819
|
+
await this.log("warn", `\u{1F4DC} Element not found: ${this.shortSelector(selector)}`);
|
|
17820
|
+
return false;
|
|
17821
|
+
}
|
|
17822
|
+
const speed = (_a = options == null ? void 0 : options.speed) != null ? _a : 50;
|
|
17823
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
17824
|
+
this.checkAbort();
|
|
17825
|
+
const box = await element.boundingBox();
|
|
17826
|
+
if (!box) return false;
|
|
17827
|
+
const viewportHeight = await this.activePage.evaluate(() => window.innerHeight);
|
|
17828
|
+
const elementCenter = box.y + box.height / 2;
|
|
17829
|
+
const viewportCenter = viewportHeight / 2;
|
|
17830
|
+
const deltaY = elementCenter - viewportCenter;
|
|
17831
|
+
if (Math.abs(deltaY) < 50) break;
|
|
17832
|
+
const maxSegment = 300 + Math.random() * 200;
|
|
17833
|
+
const segment = Math.abs(deltaY) > maxSegment ? deltaY > 0 ? maxSegment : -maxSegment : deltaY;
|
|
17834
|
+
await this.hiraCursor.scrollDelta(this.activePage, segment, speed);
|
|
17835
|
+
await this.sleep(200 + Math.random() * 300);
|
|
17836
|
+
}
|
|
17837
|
+
await this.actionDelay();
|
|
16959
17838
|
return true;
|
|
16960
17839
|
} catch (error) {
|
|
16961
17840
|
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
16962
17841
|
const msg = error instanceof Error ? error.message : String(error);
|
|
16963
|
-
await this.log("
|
|
17842
|
+
await this.log("warn", `\u{1F4DC} Scroll to element failed: ${msg}`);
|
|
16964
17843
|
return false;
|
|
16965
17844
|
}
|
|
16966
17845
|
}
|
|
17846
|
+
/**
|
|
17847
|
+
* Simulate natural browsing scroll — cuộn xuống xen kẽ lướt ngược, delay random.
|
|
17848
|
+
* Giống người đang đọc/lướt web tự nhiên.
|
|
17849
|
+
*
|
|
17850
|
+
* @param options.duration - Thời gian scroll tính bằng giây (default: 5)
|
|
17851
|
+
* @param options.direction - Main direction (default: "down")
|
|
17852
|
+
* @param options.backChance - Chance to scroll back 0-1 (default: 0.15)
|
|
17853
|
+
* @param options.backAmount - [min, max] px to scroll back (default: [50, 150])
|
|
17854
|
+
* @param options.stepSize - [min, max] px per step (default: [200, 400])
|
|
17855
|
+
* @param options.stepDelay - [min, max] ms delay between steps (default: [300, 800])
|
|
17856
|
+
* @param options.container - CSS selector of scroll container
|
|
17857
|
+
*
|
|
17858
|
+
* @example
|
|
17859
|
+
* await utils.randomScroll(); // 5s lướt xuống
|
|
17860
|
+
* await utils.randomScroll({ duration: 10 }); // 10s
|
|
17861
|
+
* await utils.randomScroll({ backChance: 0.3, container: "#feed" });
|
|
17862
|
+
*/
|
|
17863
|
+
async randomScroll(options) {
|
|
17864
|
+
var _a, _b, _c, _d, _e, _f;
|
|
17865
|
+
try {
|
|
17866
|
+
this.checkAbort();
|
|
17867
|
+
const duration = ((_a = options == null ? void 0 : options.duration) != null ? _a : 5) * 1e3;
|
|
17868
|
+
const direction = (_b = options == null ? void 0 : options.direction) != null ? _b : "down";
|
|
17869
|
+
const backChance = (_c = options == null ? void 0 : options.backChance) != null ? _c : 0.15;
|
|
17870
|
+
const backAmount = (_d = options == null ? void 0 : options.backAmount) != null ? _d : [50, 150];
|
|
17871
|
+
const stepSize = (_e = options == null ? void 0 : options.stepSize) != null ? _e : [200, 400];
|
|
17872
|
+
const stepDelay = (_f = options == null ? void 0 : options.stepDelay) != null ? _f : [300, 800];
|
|
17873
|
+
const arrow = direction === "down" ? "\u2193" : "\u2191";
|
|
17874
|
+
const containerLabel = (options == null ? void 0 : options.container) ? ` (${this.shortSelector(options.container)})` : "";
|
|
17875
|
+
const startTime = Date.now();
|
|
17876
|
+
const sign = direction === "down" ? 1 : -1;
|
|
17877
|
+
if (options == null ? void 0 : options.container) {
|
|
17878
|
+
await this.hiraCursor.moveToContainer(this.activePage, options.container);
|
|
17879
|
+
}
|
|
17880
|
+
let steps = 0;
|
|
17881
|
+
let totalScrolled = 0;
|
|
17882
|
+
while (Date.now() - startTime < duration) {
|
|
17883
|
+
this.checkAbort();
|
|
17884
|
+
if (steps > 0 && Math.random() < backChance) {
|
|
17885
|
+
const back = backAmount[0] + Math.random() * (backAmount[1] - backAmount[0]);
|
|
17886
|
+
if (options == null ? void 0 : options.container) {
|
|
17887
|
+
await this.hiraCursor.scrollContainerDelta(this.activePage, options.container, -sign * back, 40);
|
|
17888
|
+
} else {
|
|
17889
|
+
await this.hiraCursor.scrollDelta(this.activePage, -sign * back, 40);
|
|
17890
|
+
}
|
|
17891
|
+
} else {
|
|
17892
|
+
const step = stepSize[0] + Math.random() * (stepSize[1] - stepSize[0]);
|
|
17893
|
+
if (options == null ? void 0 : options.container) {
|
|
17894
|
+
await this.hiraCursor.scrollContainerDelta(this.activePage, options.container, sign * step, 40);
|
|
17895
|
+
} else {
|
|
17896
|
+
await this.hiraCursor.scrollDelta(this.activePage, sign * step, 40);
|
|
17897
|
+
}
|
|
17898
|
+
totalScrolled += step;
|
|
17899
|
+
}
|
|
17900
|
+
steps++;
|
|
17901
|
+
if (Date.now() - startTime < duration) {
|
|
17902
|
+
const delay = stepDelay[0] + Math.random() * (stepDelay[1] - stepDelay[0]);
|
|
17903
|
+
await this.sleep(delay);
|
|
17904
|
+
}
|
|
17905
|
+
}
|
|
17906
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
17907
|
+
await this.log("info", `\u{1F5B1}\uFE0F Random scroll ${arrow} ~${Math.round(totalScrolled)}px${containerLabel} \u2014 ${steps} steps, ${elapsed}s`);
|
|
17908
|
+
await this.actionDelay();
|
|
17909
|
+
} catch (error) {
|
|
17910
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17911
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17912
|
+
await this.log("warn", `\u{1F5B1}\uFE0F Random scroll failed: ${msg}`);
|
|
17913
|
+
}
|
|
17914
|
+
}
|
|
17915
|
+
/**
|
|
17916
|
+
* Wait for the current page to complete a navigation (e.g. after a form submit).
|
|
17917
|
+
* Throws on timeout.
|
|
17918
|
+
*
|
|
17919
|
+
* @param options.timeout - Max wait time in ms (default: 60000)
|
|
17920
|
+
* @param options.waitUntil - When to consider navigation complete (default: "load")
|
|
17921
|
+
* @returns true on success
|
|
17922
|
+
*
|
|
17923
|
+
* @example
|
|
17924
|
+
* await utils.click("#submit");
|
|
17925
|
+
* await utils.waitForNavigation();
|
|
17926
|
+
*/
|
|
16967
17927
|
async waitForNavigation(options) {
|
|
16968
17928
|
try {
|
|
16969
17929
|
this.checkAbort();
|
|
16970
17930
|
await this.log("debug", "\u{1F504} Waiting for navigation...");
|
|
16971
|
-
await this.
|
|
16972
|
-
timeout: (options == null ? void 0 : options.timeout) ||
|
|
16973
|
-
waitUntil: (options == null ? void 0 : options.waitUntil) || "
|
|
17931
|
+
await this.activePage.waitForNavigation({
|
|
17932
|
+
timeout: (options == null ? void 0 : options.timeout) || 16e3,
|
|
17933
|
+
waitUntil: (options == null ? void 0 : options.waitUntil) || "load"
|
|
16974
17934
|
});
|
|
16975
17935
|
return true;
|
|
16976
17936
|
} catch (err) {
|
|
16977
17937
|
if (err instanceof Error && err.message === "cancelled") throw err;
|
|
16978
|
-
|
|
16979
|
-
return false;
|
|
17938
|
+
throw new Error("Navigation timeout");
|
|
16980
17939
|
}
|
|
16981
17940
|
}
|
|
17941
|
+
/**
|
|
17942
|
+
* Take a screenshot of the active page. Returns null on failure (soft fail).
|
|
17943
|
+
*
|
|
17944
|
+
* @param path - Optional file path to save the screenshot. If omitted, returns base64 string.
|
|
17945
|
+
* @returns Screenshot buffer (if path given) or base64 string, or null on failure
|
|
17946
|
+
*
|
|
17947
|
+
* @example
|
|
17948
|
+
* await utils.screenshot("./debug.png"); // save to file
|
|
17949
|
+
* const base64 = await utils.screenshot(); // get base64
|
|
17950
|
+
*/
|
|
16982
17951
|
async screenshot(path2) {
|
|
16983
17952
|
try {
|
|
16984
17953
|
this.checkAbort();
|
|
16985
17954
|
await this.log("info", `\u{1F4F8} Screenshot${path2 ? `: ${path2}` : ""}`);
|
|
16986
|
-
return path2 ? await this.
|
|
17955
|
+
return path2 ? await this.activePage.screenshot({ path: path2 }) : await this.activePage.screenshot({ encoding: "base64" });
|
|
16987
17956
|
} catch (error) {
|
|
17957
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
16988
17958
|
const msg = error instanceof Error ? error.message : String(error);
|
|
16989
|
-
await this.log("
|
|
17959
|
+
await this.log("warn", `\u26A0\uFE0F Screenshot failed \u2014 ${msg}`);
|
|
16990
17960
|
return null;
|
|
16991
17961
|
}
|
|
16992
17962
|
}
|
|
16993
|
-
|
|
16994
|
-
|
|
17963
|
+
/**
|
|
17964
|
+
* Switch scope into an iframe within the current page.
|
|
17965
|
+
* After switching, all methods (click, type, exists...) operate inside the iframe.
|
|
17966
|
+
* Use `activeMainFrame()` to exit back to the main page.
|
|
17967
|
+
* Throws if the iframe is not found.
|
|
17968
|
+
*
|
|
17969
|
+
* @param selector - CSS selector or XPath of the iframe element
|
|
17970
|
+
* @param options.waitTimeout - Max wait time for iframe element to appear in ms (default: 0 — instant)
|
|
17971
|
+
* @returns The Frame handle
|
|
17972
|
+
*
|
|
17973
|
+
* @example
|
|
17974
|
+
* await utils.activeIframe("#my-iframe");
|
|
17975
|
+
* await utils.click("#btn-inside-iframe");
|
|
17976
|
+
* await utils.activeMainFrame();
|
|
17977
|
+
*/
|
|
17978
|
+
async activeIframe(selector, options) {
|
|
17979
|
+
try {
|
|
17980
|
+
this.checkAbort();
|
|
17981
|
+
await this.log("info", `\u{1F5BC}\uFE0F Active iframe: ${this.shortSelector(selector)}`);
|
|
17982
|
+
const resolved = selector.startsWith("//") || selector.startsWith("(") ? `xpath/${selector}` : selector;
|
|
17983
|
+
const el = (options == null ? void 0 : options.waitTimeout) && options.waitTimeout > 0 ? await this.activePage.waitForSelector(resolved, { timeout: options.waitTimeout }) : await this.activePage.$(resolved);
|
|
17984
|
+
if (!el) {
|
|
17985
|
+
throw new Error(`Iframe not found: ${this.shortSelector(selector)}`);
|
|
17986
|
+
}
|
|
17987
|
+
const frame = await el.contentFrame();
|
|
17988
|
+
if (!frame) {
|
|
17989
|
+
throw new Error(`Cannot get contentFrame from: ${this.shortSelector(selector)}`);
|
|
17990
|
+
}
|
|
17991
|
+
this.activeFrame = frame;
|
|
17992
|
+
return frame;
|
|
17993
|
+
} catch (error) {
|
|
17994
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17995
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17996
|
+
throw new Error(`activeIframe failed: ${this.shortSelector(selector)} \u2014 ${msg}`);
|
|
17997
|
+
}
|
|
17998
|
+
}
|
|
17999
|
+
/**
|
|
18000
|
+
* Exit the current iframe and return to the main frame of the active page.
|
|
18001
|
+
* After calling this, all methods operate on the main page (outside any iframe).
|
|
18002
|
+
*
|
|
18003
|
+
* @example
|
|
18004
|
+
* await utils.activeIframe("#my-iframe");
|
|
18005
|
+
* await utils.click("#btn-in-iframe");
|
|
18006
|
+
* await utils.activeMainFrame(); // back to main page
|
|
18007
|
+
*/
|
|
18008
|
+
async activeMainFrame() {
|
|
18009
|
+
this.checkAbort();
|
|
18010
|
+
this.activeFrame = null;
|
|
18011
|
+
await this.log("info", `\u{1F5BC}\uFE0F Switched to main frame`);
|
|
18012
|
+
}
|
|
18013
|
+
// ── Internal: tìm tab mới ─────────────────────────────────
|
|
18014
|
+
async _findNewTab(opts) {
|
|
18015
|
+
const { matcher, timeout = 8e3 } = opts != null ? opts : {};
|
|
18016
|
+
const label = matcher ? typeof matcher === "string" ? matcher : matcher.toString() : "(any)";
|
|
16995
18017
|
this.checkAbort();
|
|
16996
|
-
await this.log("info", `\
|
|
18018
|
+
await this.log("info", `\u23F3 Waiting for new tab ${label}... (${timeout}ms)`);
|
|
18019
|
+
const isMatch = (text) => {
|
|
18020
|
+
if (!matcher) return true;
|
|
18021
|
+
if (!text) return false;
|
|
18022
|
+
if (typeof matcher === "string") return text.includes(matcher);
|
|
18023
|
+
return matcher.test(text);
|
|
18024
|
+
};
|
|
16997
18025
|
try {
|
|
18026
|
+
const currentCount = (await this.ctx.browser.pages()).length;
|
|
18027
|
+
const start = Date.now();
|
|
18028
|
+
while (Date.now() - start < timeout) {
|
|
18029
|
+
const pages = await this.ctx.browser.pages();
|
|
18030
|
+
if (pages.length > currentCount) {
|
|
18031
|
+
for (let i = pages.length - 1; i >= currentCount; i--) {
|
|
18032
|
+
const p = pages[i];
|
|
18033
|
+
try {
|
|
18034
|
+
await p.waitForNavigation({
|
|
18035
|
+
waitUntil: "domcontentloaded",
|
|
18036
|
+
timeout: 3e3
|
|
18037
|
+
}).catch(() => {
|
|
18038
|
+
});
|
|
18039
|
+
} catch {
|
|
18040
|
+
}
|
|
18041
|
+
if (!matcher) {
|
|
18042
|
+
await this.log("success", `\u2705 New tab detected \u2014 tab[${i}]`);
|
|
18043
|
+
return p;
|
|
18044
|
+
}
|
|
18045
|
+
try {
|
|
18046
|
+
const title = await p.title();
|
|
18047
|
+
const url2 = p.url();
|
|
18048
|
+
if (isMatch(title) || isMatch(url2)) {
|
|
18049
|
+
await this.log("success", `\u2705 New tab matched: ${label} \u2014 tab[${i}]`);
|
|
18050
|
+
return p;
|
|
18051
|
+
}
|
|
18052
|
+
} catch {
|
|
18053
|
+
}
|
|
18054
|
+
}
|
|
18055
|
+
}
|
|
18056
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
18057
|
+
}
|
|
18058
|
+
throw new Error(`New tab not found: ${label} (timeout: ${timeout}ms)`);
|
|
18059
|
+
} catch (error) {
|
|
18060
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
18061
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
18062
|
+
throw new Error(`waitForNewTab failed \u2014 ${msg}`);
|
|
18063
|
+
}
|
|
18064
|
+
}
|
|
18065
|
+
// ── Internal: tìm popup đã có (hoặc chờ xuất hiện) ──────
|
|
18066
|
+
async _findPopup(opts) {
|
|
18067
|
+
const { matcher, timeout = 8e3 } = opts != null ? opts : {};
|
|
18068
|
+
const label = matcher ? typeof matcher === "string" ? matcher : matcher.toString() : "(any new)";
|
|
18069
|
+
this.checkAbort();
|
|
18070
|
+
await this.log("info", `\u23F3 Waiting for popup ${label}... (${timeout}ms)`);
|
|
18071
|
+
const isMatch = (text) => {
|
|
18072
|
+
if (!matcher) return true;
|
|
18073
|
+
if (!text) return false;
|
|
18074
|
+
if (typeof matcher === "string") return text.includes(matcher);
|
|
18075
|
+
return matcher.test(text);
|
|
18076
|
+
};
|
|
18077
|
+
try {
|
|
18078
|
+
const currentPages = new Set((await this.ctx.browser.pages()).map((p) => p));
|
|
16998
18079
|
const start = Date.now();
|
|
16999
|
-
const isMatch = (text) => {
|
|
17000
|
-
if (!text) return false;
|
|
17001
|
-
if (typeof matcher === "string") return text.includes(matcher);
|
|
17002
|
-
return matcher.test(text);
|
|
17003
|
-
};
|
|
17004
18080
|
while (Date.now() - start < timeout) {
|
|
17005
18081
|
const pages = await this.ctx.browser.pages();
|
|
17006
18082
|
for (let i = pages.length - 1; i >= 0; i--) {
|
|
17007
18083
|
const page = pages[i];
|
|
17008
|
-
|
|
17009
|
-
|
|
17010
|
-
|
|
17011
|
-
|
|
17012
|
-
|
|
18084
|
+
if (!matcher && currentPages.has(page)) continue;
|
|
18085
|
+
try {
|
|
18086
|
+
const title = await page.title();
|
|
18087
|
+
const url2 = page.url();
|
|
18088
|
+
if (isMatch(title) || isMatch(url2)) {
|
|
18089
|
+
await this.log("success", `\u2705 Popup found: ${label} \u2014 tab[${i}]`);
|
|
18090
|
+
return page;
|
|
18091
|
+
}
|
|
18092
|
+
} catch {
|
|
17013
18093
|
}
|
|
17014
18094
|
}
|
|
17015
18095
|
await new Promise((r) => setTimeout(r, 500));
|
|
17016
18096
|
}
|
|
17017
|
-
|
|
17018
|
-
"warn",
|
|
17019
|
-
`\u26A0\uFE0F Popup not found: ${label} (timeout: ${timeout}ms)`
|
|
17020
|
-
);
|
|
17021
|
-
return null;
|
|
18097
|
+
throw new Error(`Popup not found: ${label} (timeout: ${timeout}ms)`);
|
|
17022
18098
|
} catch (error) {
|
|
18099
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17023
18100
|
const msg = error instanceof Error ? error.message : String(error);
|
|
17024
|
-
|
|
17025
|
-
|
|
18101
|
+
throw new Error(`waitForPopup failed: ${label} \u2014 ${msg}`);
|
|
18102
|
+
}
|
|
18103
|
+
}
|
|
18104
|
+
/**
|
|
18105
|
+
* Wait for a new tab to appear, but do NOT switch to it.
|
|
18106
|
+
* The active page remains unchanged. Throws if no new tab appears before timeout.
|
|
18107
|
+
*
|
|
18108
|
+
* @param opts.matcher - String or RegExp to match the new tab's title or URL. If omitted, any new tab matches.
|
|
18109
|
+
* @param opts.timeout - Max wait time in ms (default: 8000)
|
|
18110
|
+
* @returns The new Page handle (without switching)
|
|
18111
|
+
*
|
|
18112
|
+
* @example
|
|
18113
|
+
* await utils.click("#open-link");
|
|
18114
|
+
* const page = await utils.waitForNewTab();
|
|
18115
|
+
* const page = await utils.waitForNewTab({ timeout: 20000 });
|
|
18116
|
+
* const page = await utils.waitForNewTab({ matcher: "Google" });
|
|
18117
|
+
*/
|
|
18118
|
+
async waitForNewTab(opts) {
|
|
18119
|
+
return this._findNewTab(opts);
|
|
18120
|
+
}
|
|
18121
|
+
/**
|
|
18122
|
+
* Switch to a new tab immediately (or wait if timeout is specified).
|
|
18123
|
+
* After calling this, all methods operate on the new tab.
|
|
18124
|
+
* Use `activeDefault()` to return to the original tab.
|
|
18125
|
+
*
|
|
18126
|
+
* @param opts.matcher - String or RegExp to match the new tab's title or URL
|
|
18127
|
+
* @param opts.timeout - Max wait time in ms (default: 0 — instant, throws if not found)
|
|
18128
|
+
* @returns The new Page handle (now active)
|
|
18129
|
+
*
|
|
18130
|
+
* @example
|
|
18131
|
+
* await utils.click("#open-link");
|
|
18132
|
+
* await utils.activeNewTab(); // switch immediately
|
|
18133
|
+
* await utils.activeNewTab({ timeout: 8000 }); // wait up to 8s
|
|
18134
|
+
* await utils.type("#input", "hello"); // types on the new tab
|
|
18135
|
+
* await utils.activeDefault(); // back to original tab
|
|
18136
|
+
*/
|
|
18137
|
+
async activeNewTab(opts) {
|
|
18138
|
+
var _a;
|
|
18139
|
+
const page = await this._findNewTab({ ...opts, timeout: (_a = opts == null ? void 0 : opts.timeout) != null ? _a : 0 });
|
|
18140
|
+
if (page) {
|
|
18141
|
+
await page.bringToFront();
|
|
18142
|
+
await this.enableFocusEmulation(page);
|
|
18143
|
+
await this.applyStealthScripts(page);
|
|
18144
|
+
this.activePage = page;
|
|
18145
|
+
this.activeFrame = null;
|
|
18146
|
+
await this.hiraCursor.inject(page);
|
|
18147
|
+
}
|
|
18148
|
+
return page;
|
|
18149
|
+
}
|
|
18150
|
+
/**
|
|
18151
|
+
* Wait for a popup/tab matching the given criteria to appear, but do NOT switch to it.
|
|
18152
|
+
* Throws if no matching popup appears before timeout.
|
|
18153
|
+
*
|
|
18154
|
+
* @param opts.matcher - String or RegExp to match title or URL. If omitted, any new popup matches.
|
|
18155
|
+
* @param opts.timeout - Max wait time in ms (default: 8000)
|
|
18156
|
+
* @returns The popup Page handle (without switching)
|
|
18157
|
+
*
|
|
18158
|
+
* @example
|
|
18159
|
+
* await utils.click("#connect-wallet");
|
|
18160
|
+
* const popup = await utils.waitForPopup({ matcher: "MetaMask" });
|
|
18161
|
+
*/
|
|
18162
|
+
async waitForPopup(opts) {
|
|
18163
|
+
return this._findPopup(opts);
|
|
18164
|
+
}
|
|
18165
|
+
/**
|
|
18166
|
+
* Switch to a popup/tab immediately (or wait if timeout is specified).
|
|
18167
|
+
* After calling this, all methods operate on the popup.
|
|
18168
|
+
* Use `activeDefault()` to return to the original tab.
|
|
18169
|
+
*
|
|
18170
|
+
* @param opts.matcher - String or RegExp to match title or URL
|
|
18171
|
+
* @param opts.timeout - Max wait time in ms (default: 0 — instant, throws if not found)
|
|
18172
|
+
* @returns The popup Page handle (now active)
|
|
18173
|
+
*
|
|
18174
|
+
* @example
|
|
18175
|
+
* await utils.click("#connect-wallet");
|
|
18176
|
+
* await utils.activePopup({ matcher: "MetaMask" });
|
|
18177
|
+
* await utils.click("#approve"); // clicks on the popup
|
|
18178
|
+
* await utils.activeDefault(); // back to original tab
|
|
18179
|
+
*/
|
|
18180
|
+
async activePopup(opts) {
|
|
18181
|
+
var _a;
|
|
18182
|
+
const page = await this._findPopup({ ...opts, timeout: (_a = opts == null ? void 0 : opts.timeout) != null ? _a : 0 });
|
|
18183
|
+
if (page) {
|
|
18184
|
+
await page.bringToFront();
|
|
18185
|
+
await this.enableFocusEmulation(page);
|
|
18186
|
+
await this.applyStealthScripts(page);
|
|
18187
|
+
this.activePage = page;
|
|
18188
|
+
this.activeFrame = null;
|
|
18189
|
+
await this.hiraCursor.inject(page);
|
|
17026
18190
|
}
|
|
18191
|
+
return page;
|
|
17027
18192
|
}
|
|
17028
|
-
|
|
18193
|
+
/**
|
|
18194
|
+
* Switch focus to a tab by its index (0-based, ordered by creation time).
|
|
18195
|
+
* All subsequent methods (click, type, goto...) will operate on this tab.
|
|
18196
|
+
* User interactions (opening tabs, clicking browser) do NOT affect the active tab.
|
|
18197
|
+
* Throws if the index is out of range.
|
|
18198
|
+
*
|
|
18199
|
+
* @param index - Zero-based tab index
|
|
18200
|
+
* @returns The Page handle of the activated tab
|
|
18201
|
+
*
|
|
18202
|
+
* @example
|
|
18203
|
+
* await utils.activeTab(1); // switch to second tab
|
|
18204
|
+
* await utils.click("#btn"); // clicks on tab 1
|
|
18205
|
+
* await utils.activeTab(0); // back to first tab
|
|
18206
|
+
*/
|
|
18207
|
+
async activeTab(index) {
|
|
17029
18208
|
try {
|
|
17030
18209
|
this.checkAbort();
|
|
17031
|
-
await this.log("info", `\u{1F504}
|
|
18210
|
+
await this.log("info", `\u{1F504} Active tab[${index}]...`);
|
|
17032
18211
|
const pages = await this.ctx.browser.pages();
|
|
17033
18212
|
if (index >= 0 && index < pages.length) {
|
|
17034
18213
|
const page = pages[index];
|
|
17035
18214
|
await page.bringToFront();
|
|
18215
|
+
await this.enableFocusEmulation(page);
|
|
18216
|
+
await this.applyStealthScripts(page);
|
|
18217
|
+
this.activePage = page;
|
|
18218
|
+
this.activeFrame = null;
|
|
18219
|
+
await this.hiraCursor.inject(page);
|
|
17036
18220
|
return page;
|
|
17037
18221
|
}
|
|
17038
|
-
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
);
|
|
17042
|
-
|
|
17043
|
-
} catch {
|
|
17044
|
-
return null;
|
|
18222
|
+
throw new Error(`Tab[${index}] not found (total: ${pages.length})`);
|
|
18223
|
+
} catch (error) {
|
|
18224
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
18225
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
18226
|
+
throw new Error(`activeTab failed \u2014 ${msg}`);
|
|
17045
18227
|
}
|
|
17046
18228
|
}
|
|
18229
|
+
/**
|
|
18230
|
+
* Close the currently active tab and switch back to the default page.
|
|
18231
|
+
*
|
|
18232
|
+
* @example
|
|
18233
|
+
* await utils.activeTab(1);
|
|
18234
|
+
* await utils.closeCurrentTab(); // closes tab 1, returns to tab 0
|
|
18235
|
+
*/
|
|
17047
18236
|
async closeCurrentTab() {
|
|
17048
|
-
|
|
17049
|
-
|
|
17050
|
-
|
|
17051
|
-
|
|
17052
|
-
|
|
17053
|
-
}
|
|
17054
|
-
} catch {
|
|
18237
|
+
this.checkAbort();
|
|
18238
|
+
if (!this.activePage.isClosed()) {
|
|
18239
|
+
await this.log("info", `\u{1F5D1}\uFE0F Closing tab: ${this.activePage.url()}`);
|
|
18240
|
+
await this.activePage.close();
|
|
18241
|
+
this.activePage = this.defaultPage;
|
|
17055
18242
|
}
|
|
17056
18243
|
}
|
|
18244
|
+
/**
|
|
18245
|
+
* Close all tabs except the currently active one.
|
|
18246
|
+
*
|
|
18247
|
+
* @example
|
|
18248
|
+
* await utils.closeOtherTabs(); // keeps only the active tab open
|
|
18249
|
+
*/
|
|
17057
18250
|
async closeOtherTabs() {
|
|
17058
|
-
|
|
17059
|
-
|
|
17060
|
-
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
await Promise.all(toClose.map((p) => p.close()));
|
|
17064
|
-
} catch (error) {
|
|
17065
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
17066
|
-
await this.log("error", `\u274C closeOtherTabs failed \u2014 ${msg}`);
|
|
17067
|
-
}
|
|
18251
|
+
this.checkAbort();
|
|
18252
|
+
const pages = await this.ctx.browser.pages();
|
|
18253
|
+
const toClose = pages.filter((p) => p !== this.activePage && !p.isClosed());
|
|
18254
|
+
await this.log("info", `\u{1F5D1}\uFE0F Closing ${toClose.length} other tab(s)...`);
|
|
18255
|
+
await Promise.all(toClose.map((p) => p.close()));
|
|
17068
18256
|
}
|
|
17069
18257
|
/**
|
|
17070
|
-
* Close ALL tabs
|
|
17071
|
-
* Useful for full cleanup before flow ends.
|
|
18258
|
+
* Close ALL tabs including the current one.
|
|
18259
|
+
* Useful for full cleanup before a flow ends.
|
|
18260
|
+
*
|
|
18261
|
+
* @example
|
|
18262
|
+
* await utils.closeAllTabs();
|
|
17072
18263
|
*/
|
|
17073
18264
|
async closeAllTabs() {
|
|
17074
|
-
|
|
17075
|
-
|
|
17076
|
-
|
|
17077
|
-
|
|
17078
|
-
|
|
17079
|
-
await Promise.all(toClose.map((p) => p.close()));
|
|
17080
|
-
} catch (error) {
|
|
17081
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
17082
|
-
await this.log("error", `\u274C closeAllTabs failed \u2014 ${msg}`);
|
|
17083
|
-
}
|
|
18265
|
+
this.checkAbort();
|
|
18266
|
+
const pages = await this.ctx.browser.pages();
|
|
18267
|
+
const toClose = pages.filter((p) => !p.isClosed());
|
|
18268
|
+
await this.log("info", `\u{1F5D1}\uFE0F Closing all ${toClose.length} tab(s)...`);
|
|
18269
|
+
await Promise.all(toClose.map((p) => p.close()));
|
|
17084
18270
|
}
|
|
17085
18271
|
/**
|
|
17086
|
-
* Switch back to the default (initial)
|
|
17087
|
-
*
|
|
18272
|
+
* Switch back to the default (initial) tab — the first tab opened when the flow started.
|
|
18273
|
+
* Resets both the active page and active frame (exits any iframe).
|
|
18274
|
+
* Typically used after `activePopup()` or `activeTab()` to return to the main tab.
|
|
18275
|
+
*
|
|
18276
|
+
* @returns The default Page handle
|
|
18277
|
+
*
|
|
18278
|
+
* @example
|
|
18279
|
+
* await utils.activePopup({ matcher: "MetaMask" });
|
|
18280
|
+
* await utils.click("#approve");
|
|
18281
|
+
* await utils.activeDefault(); // back to original tab
|
|
17088
18282
|
*/
|
|
17089
|
-
async
|
|
18283
|
+
async activeDefault() {
|
|
17090
18284
|
this.checkAbort();
|
|
17091
|
-
await this.log("info", `\u{1F504} Switching to default page
|
|
17092
|
-
if (!this.
|
|
17093
|
-
await this.
|
|
18285
|
+
await this.log("info", `\u{1F504} Switching to default page...`);
|
|
18286
|
+
if (!this.defaultPage.isClosed()) {
|
|
18287
|
+
await this.defaultPage.bringToFront();
|
|
18288
|
+
await this.enableFocusEmulation(this.defaultPage);
|
|
17094
18289
|
}
|
|
17095
|
-
|
|
18290
|
+
this.activePage = this.defaultPage;
|
|
18291
|
+
this.activeFrame = null;
|
|
18292
|
+
await this.hiraCursor.inject(this.activePage);
|
|
18293
|
+
return this.activePage;
|
|
17096
18294
|
}
|
|
18295
|
+
/**
|
|
18296
|
+
* Log a config object in a readable format.
|
|
18297
|
+
*
|
|
18298
|
+
* @param config - Key-value object to log
|
|
18299
|
+
* @param label - Label for the log entry (default: "Config")
|
|
18300
|
+
*/
|
|
17097
18301
|
async logConfig(config, label = "Config") {
|
|
17098
18302
|
const lines = Object.entries(config).map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`).join(",\n");
|
|
17099
18303
|
await this.log("info", `\u{1F4CB} ${label}:
|
|
@@ -17101,6 +18305,7 @@ var BrowserUtils = class {
|
|
|
17101
18305
|
${lines}
|
|
17102
18306
|
}`);
|
|
17103
18307
|
}
|
|
18308
|
+
/** Log the current profile input values for debugging. */
|
|
17104
18309
|
async logProfileInput() {
|
|
17105
18310
|
await this.logConfig(
|
|
17106
18311
|
this.ctx.profileInput,
|
|
@@ -17108,8 +18313,8 @@ ${lines}
|
|
|
17108
18313
|
);
|
|
17109
18314
|
}
|
|
17110
18315
|
/**
|
|
17111
|
-
* Log
|
|
17112
|
-
*
|
|
18316
|
+
* Log all current output values (including values from previous runs and newly written values).
|
|
18317
|
+
* Useful for debugging — see what outputs have been set.
|
|
17113
18318
|
*/
|
|
17114
18319
|
async logProfileOutput() {
|
|
17115
18320
|
const output = this.ctx.output;
|
|
@@ -17120,6 +18325,7 @@ ${lines}
|
|
|
17120
18325
|
}
|
|
17121
18326
|
await this.logConfig(output, "Profile Output");
|
|
17122
18327
|
}
|
|
18328
|
+
/** Log the current global input values for debugging. */
|
|
17123
18329
|
async logGlobalInput() {
|
|
17124
18330
|
await this.logConfig(
|
|
17125
18331
|
this.ctx.globalInput,
|
|
@@ -17127,16 +18333,24 @@ ${lines}
|
|
|
17127
18333
|
);
|
|
17128
18334
|
}
|
|
17129
18335
|
/**
|
|
17130
|
-
*
|
|
17131
|
-
*
|
|
17132
|
-
*
|
|
18336
|
+
* Write an output value for the current profile.
|
|
18337
|
+
* Dispatched via a dedicated channel (type: "profile_output") — separate from logs.
|
|
18338
|
+
* Also logs the value to console/UI for debugging.
|
|
18339
|
+
*
|
|
18340
|
+
* ⚠️ Key must be defined in `config.output[]` — throws Error if invalid.
|
|
17133
18341
|
*
|
|
17134
|
-
*
|
|
18342
|
+
* Accepted value types:
|
|
18343
|
+
* - `string | number | boolean`
|
|
18344
|
+
* - `Array` (max 20 elements, each must be primitive)
|
|
18345
|
+
* - `Object` (max 10 entries, values must be primitive)
|
|
17135
18346
|
*
|
|
17136
|
-
*
|
|
17137
|
-
* -
|
|
17138
|
-
*
|
|
17139
|
-
*
|
|
18347
|
+
* @param key - Output key (must match config.output definition)
|
|
18348
|
+
* @param value - The value to write
|
|
18349
|
+
*
|
|
18350
|
+
* @example
|
|
18351
|
+
* await utils.writeOutput("status", "success");
|
|
18352
|
+
* await utils.writeOutput("balance", 1234.56);
|
|
18353
|
+
* await utils.writeOutput("tokens", ["ETH", "USDT"]);
|
|
17140
18354
|
*/
|
|
17141
18355
|
async writeOutput(key, value) {
|
|
17142
18356
|
this.checkAbort();
|
|
@@ -17157,9 +18371,16 @@ ${lines}
|
|
|
17157
18371
|
});
|
|
17158
18372
|
}
|
|
17159
18373
|
/**
|
|
17160
|
-
*
|
|
17161
|
-
* key
|
|
17162
|
-
*
|
|
18374
|
+
* Update a profile input field for the currently running profile.
|
|
18375
|
+
* The key must be defined in the profileInput schema.
|
|
18376
|
+
* The updated value is sent to the server to persist in AgentFlowConfig after execution.
|
|
18377
|
+
*
|
|
18378
|
+
* @param key - Profile input key (must match schema definition)
|
|
18379
|
+
* @param value - The new value (string, number, or boolean)
|
|
18380
|
+
*
|
|
18381
|
+
* @example
|
|
18382
|
+
* await utils.writeProfileInput("lastLoginDate", "2024-01-15");
|
|
18383
|
+
* await utils.writeProfileInput("retryCount", 3);
|
|
17163
18384
|
*/
|
|
17164
18385
|
async writeProfileInput(key, value) {
|
|
17165
18386
|
this.checkAbort();
|