@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/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
- var _a;
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
- async waitForElement(selector, timeout = 1e4, scope) {
16830
- const target = scope != null ? scope : this.ctx.page;
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.timeout,
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
- await this.log(
16864
- "error",
16865
- `\u274C Click failed \u2014 element not found: ${this.shortSelector(label)}`
16866
- );
16867
- return false;
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
- "error",
16879
- `\u274C Click failed: ${this.shortSelector(label)} \u2014 ${msg}`
17526
+ "info",
17527
+ `\u{1F4CB} Select "${value}" \u2192 ${this.shortSelector(selector)}`
16880
17528
  );
16881
- return false;
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.waitForElement(
17577
+ const element = await this.resolveElement(
16894
17578
  selector,
16895
- 1e4,
17579
+ options == null ? void 0 : options.waitTimeout,
16896
17580
  options == null ? void 0 : options.frame
16897
17581
  );
16898
17582
  if (!element) {
16899
- await this.log(
16900
- "error",
16901
- `\u274C Type failed \u2014 element not found: ${this.shortSelector(selector)}`
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
- return false;
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
- await this.log(
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
- async getText(selector, frame) {
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 ? frame : this.ctx.page;
16926
- const element = await this.waitForElement(selector, 1e4, frame);
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 _a;
16931
- return ((_a = el.textContent) == null ? void 0 : _a.trim()) || "";
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
- async exists(selector, timeout = 3e3, frame) {
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
- await this.ctx.page.goto(url2, {
16955
- waitUntil: (options == null ? void 0 : options.waitUntil) || "domcontentloaded",
16956
- timeout: 3e4
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("error", `\u274C Navigate failed: ${url2} \u2014 ${msg}`);
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.ctx.page.waitForNavigation({
16972
- timeout: (options == null ? void 0 : options.timeout) || 3e4,
16973
- waitUntil: (options == null ? void 0 : options.waitUntil) || "domcontentloaded"
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
- await this.log("warn", "\u26A0\uFE0F Navigation timeout");
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.ctx.page.screenshot({ path: path2 }) : await this.ctx.page.screenshot({ encoding: "base64" });
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("error", `\u274C Screenshot failed \u2014 ${msg}`);
17959
+ await this.log("warn", `\u26A0\uFE0F Screenshot failed \u2014 ${msg}`);
16990
17960
  return null;
16991
17961
  }
16992
17962
  }
16993
- async switchToPopup(matcher, timeout = 1e4) {
16994
- const label = typeof matcher === "string" ? matcher : matcher.toString();
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", `\u{1F504} Switching to popup: ${label}`);
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
- const title = await page.title();
17009
- const url2 = page.url();
17010
- if (isMatch(title) || isMatch(url2)) {
17011
- await page.bringToFront();
17012
- return page;
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
- await this.log(
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
- await this.log("error", `\u274C switchToPopup failed: ${label} \u2014 ${msg}`);
17025
- return null;
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
- async switchToTabIndex(index) {
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} Switching to tab[${index}]...`);
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
- await this.log(
17039
- "warn",
17040
- `\u26A0\uFE0F Tab[${index}] not found (total: ${pages.length})`
17041
- );
17042
- return null;
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
- try {
17049
- this.checkAbort();
17050
- if (!this.ctx.page.isClosed()) {
17051
- await this.log("info", `\u{1F5D1}\uFE0F Closing tab: ${this.ctx.page.url()}`);
17052
- await this.ctx.page.close();
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
- try {
17059
- this.checkAbort();
17060
- const pages = await this.ctx.browser.pages();
17061
- const toClose = pages.filter((p) => p !== this.ctx.page && !p.isClosed());
17062
- await this.log("info", `\u{1F5D1}\uFE0F Closing ${toClose.length} other tab(s)...`);
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 (including the current one).
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
- try {
17075
- this.checkAbort();
17076
- const pages = await this.ctx.browser.pages();
17077
- const toClose = pages.filter((p) => !p.isClosed());
17078
- await this.log("info", `\u{1F5D1}\uFE0F Closing all ${toClose.length} tab(s)...`);
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) page — the page stored in context.
17087
- * Typically used after switchToPopup() to return to the main tab.
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 switchToDefault() {
18283
+ async activeDefault() {
17090
18284
  this.checkAbort();
17091
- await this.log("info", `\u{1F504} Switching to default page: ${this.ctx.page.url()}`);
17092
- if (!this.ctx.page.isClosed()) {
17093
- await this.ctx.page.bringToFront();
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
- return this.ctx.page;
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 toàn bộ output hiện tại (bao gồm cả giá trị từ lần chạy trước và giá trị mới ghi).
17112
- * Dùng để debugxem giá trị output hiện tại.
18316
+ * Log all current output values (including values from previous runs and newly written values).
18317
+ * Useful for debuggingsee 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
- * Ghi kết quả tự do cho profile đang chạy.
17131
- * Gửi qua kênh riêng (type: "profile_output") — không lẫn log.
17132
- * Đồng thời log ra console/UI để flow dev dễ debug.
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
- * ⚠️ Key phải được định nghĩa trong config.output[] — nếu không sẽ throw Error.
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
- * value hợp lệ:
17137
- * - string | number | boolean
17138
- * - array tối đa 20 phần tử (primitive)
17139
- * - object 1 cấp tối đa 10 entry (value phải là primitive)
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
- * Cập nhật lại một field trong profileInput của profile đang chạy.
17161
- * key phải field đã được định nghĩa trong profileInput schema.
17162
- * Kết quả được gửi về server để update AgentFlowConfig sau khi execution xong.
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();