@reconcrap/boss-recommend-mcp 2.0.42 → 2.0.44

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.
@@ -8,7 +8,13 @@ import {
8
8
  } from "../../core/run/timing.js";
9
9
  import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
10
10
  import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
11
- import { sleep } from "../../core/browser/index.js";
11
+ import {
12
+ configureHumanInteraction,
13
+ createHumanRestController,
14
+ humanDelay,
15
+ normalizeHumanBehaviorOptions,
16
+ sleep
17
+ } from "../../core/browser/index.js";
12
18
  import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
13
19
  import {
14
20
  compactCvAcquisitionState,
@@ -420,17 +426,31 @@ export function countRecommendResultStatuses(results = [], {
420
426
  };
421
427
  }
422
428
 
423
- function countPassedResults(results = []) {
424
- return countRecommendResultStatuses(results).passed;
425
- }
426
-
427
- function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
428
- if (!error) return null;
429
- return {
430
- code: error.code || fallbackCode,
431
- message: error.message || String(error)
432
- };
433
- }
429
+ function countPassedResults(results = []) {
430
+ return countRecommendResultStatuses(results).passed;
431
+ }
432
+
433
+ function compactCloseResult(closeResult) {
434
+ if (!closeResult) return null;
435
+ return {
436
+ closed: Boolean(closeResult.closed),
437
+ reason: closeResult.reason || null,
438
+ attempts: closeResult.attempts || [],
439
+ verification: closeResult.verification || null
440
+ };
441
+ }
442
+
443
+ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
444
+ if (!error) return null;
445
+ const result = {
446
+ code: error.code || fallbackCode,
447
+ message: error.message || String(error)
448
+ };
449
+ if (error.close_result) {
450
+ result.close_result = compactCloseResult(error.close_result);
451
+ }
452
+ return result;
453
+ }
434
454
 
435
455
  function createRecommendCloseFailureError(closeResult) {
436
456
  const error = new Error(closeResult?.reason || "Recommend detail did not close before recovery");
@@ -554,13 +574,31 @@ export async function runRecommendWorkflow({
554
574
  actionAfterClickDelayMs = 900,
555
575
  screeningMode = "llm",
556
576
  llmConfig = null,
557
- llmTimeoutMs = 120000,
558
- llmImageLimit = 8,
559
- llmImageDetail = "high",
560
- imageOutputDir = ""
561
- } = {}, runControl) {
562
- if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
563
- const normalizedFilter = normalizeFilter(filter);
577
+ llmTimeoutMs = 120000,
578
+ llmImageLimit = 8,
579
+ llmImageDetail = "high",
580
+ imageOutputDir = "",
581
+ humanRestEnabled = false,
582
+ humanBehavior = null
583
+ } = {}, runControl) {
584
+ if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
585
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
586
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
587
+ });
588
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
589
+ configureHumanInteraction(client, {
590
+ enabled: effectiveHumanBehavior.enabled,
591
+ clickMovementEnabled: effectiveHumanBehavior.clickMovement,
592
+ textEntryEnabled: effectiveHumanBehavior.textEntry,
593
+ safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
594
+ actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
595
+ });
596
+ const humanRestController = createHumanRestController({
597
+ enabled: effectiveHumanRestEnabled,
598
+ shortRestEnabled: effectiveHumanBehavior.shortRest,
599
+ batchRestEnabled: effectiveHumanBehavior.batchRest
600
+ });
601
+ const normalizedFilter = normalizeFilter(filter);
564
602
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
565
603
  const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
566
604
  const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
@@ -597,26 +635,54 @@ export async function runRecommendWorkflow({
597
635
  let greetCount = 0;
598
636
  const candidateRecoveryCounts = new Map();
599
637
  let jobSelection = null;
600
- let pageScopeSelection = null;
601
- let filterResult = null;
602
- let cardNodeIds = [];
603
- let listEndReason = "";
604
- const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
638
+ let pageScopeSelection = null;
639
+ let filterResult = null;
640
+ let cardNodeIds = [];
641
+ let listEndReason = "";
642
+ let lastHumanEvent = null;
643
+ const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
605
644
  rootNodeId: rootState?.iframe?.documentNodeId,
606
645
  containerSelectors: RECOMMEND_LIST_CONTAINER_SELECTORS,
607
646
  itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
608
647
  itemSelectors: [RECOMMEND_CARD_SELECTOR],
609
648
  viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
610
649
  validateViewportPoint: true
611
- }));
612
-
613
- function updateRecommendProgress(extra = {}) {
614
- const counts = countRecommendResultStatuses(results, { greetCount });
615
- const listSnapshot = compactInfiniteListState(listState);
616
- runControl.updateProgress({
617
- card_count: cardNodeIds.length,
618
- target_count: targetPassCount,
619
- target_count_semantics: "passed_candidates",
650
+ }));
651
+
652
+ function recordHumanEvent(event = null) {
653
+ if (!event) return lastHumanEvent;
654
+ lastHumanEvent = {
655
+ at: new Date().toISOString(),
656
+ ...event
657
+ };
658
+ return lastHumanEvent;
659
+ }
660
+
661
+ async function maybeHumanActionCooldown(phase, timings = {}) {
662
+ if (!effectiveHumanBehavior.actionCooldown) return null;
663
+ const pauseMs = humanDelay(280, 90, {
664
+ minMs: 80,
665
+ maxMs: 720
666
+ });
667
+ if (pauseMs > 0) {
668
+ await runControl.sleep(pauseMs);
669
+ addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
670
+ }
671
+ return recordHumanEvent({
672
+ kind: "action_cooldown",
673
+ phase,
674
+ pause_ms: pauseMs
675
+ });
676
+ }
677
+
678
+ function updateRecommendProgress(extra = {}) {
679
+ const counts = countRecommendResultStatuses(results, { greetCount });
680
+ const listSnapshot = compactInfiniteListState(listState);
681
+ const humanRestState = humanRestController.getState();
682
+ runControl.updateProgress({
683
+ card_count: cardNodeIds.length,
684
+ target_count: targetPassCount,
685
+ target_count_semantics: "passed_candidates",
620
686
  ...counts,
621
687
  screening_mode: normalizedScreeningMode,
622
688
  unique_seen: listSnapshot.seen_count,
@@ -624,12 +690,18 @@ export async function runRecommendWorkflow({
624
690
  refresh_rounds: refreshRounds,
625
691
  refresh_attempts: refreshAttempts.length,
626
692
  context_recoveries: contextRecoveryAttempts,
627
- list_end_reason: listEndReason || null,
628
- viewport_checks: viewportGuard.getStats().checks,
629
- viewport_recoveries: viewportGuard.getStats().recoveries,
630
- ...extra
631
- });
632
- }
693
+ list_end_reason: listEndReason || null,
694
+ viewport_checks: viewportGuard.getStats().checks,
695
+ viewport_recoveries: viewportGuard.getStats().recoveries,
696
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
697
+ human_behavior_profile: effectiveHumanBehavior.profile,
698
+ human_rest_enabled: effectiveHumanRestEnabled,
699
+ human_rest_count: humanRestState.rest_count,
700
+ human_rest_ms: humanRestState.total_rest_ms,
701
+ last_human_event: lastHumanEvent,
702
+ ...extra
703
+ });
704
+ }
633
705
 
634
706
  function checkpointInProgressCandidate({
635
707
  index = results.length,
@@ -821,10 +893,11 @@ export async function runRecommendWorkflow({
821
893
  client,
822
894
  state: listState,
823
895
  maxScrolls: listMaxScrolls,
824
- stableSignatureLimit: listStableSignatureLimit,
825
- wheelDeltaY: listWheelDeltaY,
826
- settleMs: listSettleMs,
827
- fallbackPoint: listFallbackResolver,
896
+ stableSignatureLimit: listStableSignatureLimit,
897
+ wheelDeltaY: listWheelDeltaY,
898
+ settleMs: listSettleMs,
899
+ listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
900
+ fallbackPoint: listFallbackResolver,
828
901
  findNodeIds: async () => {
829
902
  let currentRootState = await getRecommendRoots(client);
830
903
  currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
@@ -926,10 +999,11 @@ export async function runRecommendWorkflow({
926
999
  runControl.setPhase("recommend:detail");
927
1000
  detailStep = "ensure_viewport";
928
1001
  rootState = await ensureRecommendViewport(rootState, "detail");
929
- checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
930
- detailStep = "open_detail";
931
- networkRecorder.clear();
932
- const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
1002
+ checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
1003
+ detailStep = "open_detail";
1004
+ networkRecorder.clear();
1005
+ await maybeHumanActionCooldown("before_detail_open", timings);
1006
+ const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
933
1007
  cardNodeId,
934
1008
  candidateKey,
935
1009
  cardCandidate,
@@ -1008,11 +1082,12 @@ export async function runRecommendWorkflow({
1008
1082
  resizeMaxWidth: 1100,
1009
1083
  captureViewport: false,
1010
1084
  padding: 0,
1011
- maxScreenshots: maxImagePages,
1012
- wheelDeltaY: imageWheelDeltaY,
1013
- settleMs: 350,
1014
- scrollMethod: "dom-anchor-fallback-input",
1015
- stepTimeoutMs: 45000,
1085
+ maxScreenshots: maxImagePages,
1086
+ wheelDeltaY: imageWheelDeltaY,
1087
+ settleMs: 350,
1088
+ scrollMethod: "dom-anchor-fallback-input",
1089
+ scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1090
+ stepTimeoutMs: 45000,
1016
1091
  totalTimeoutMs: 90000,
1017
1092
  duplicateStopCount: 1,
1018
1093
  skipDuplicateScreenshots: true,
@@ -1146,9 +1221,10 @@ export async function runRecommendWorkflow({
1146
1221
  if (postActionEnabled && detailResult) {
1147
1222
  const postActionStarted = Date.now();
1148
1223
  await runControl.waitIfPaused();
1149
- runControl.throwIfCanceled();
1150
- runControl.setPhase("recommend:post-action");
1151
- actionDiscovery = await waitForRecommendDetailActionControls(client, {
1224
+ runControl.throwIfCanceled();
1225
+ runControl.setPhase("recommend:post-action");
1226
+ await maybeHumanActionCooldown("before_post_action", timings);
1227
+ actionDiscovery = await waitForRecommendDetailActionControls(client, {
1152
1228
  timeoutMs: actionTimeoutMs,
1153
1229
  intervalMs: actionIntervalMs,
1154
1230
  requireAny: true
@@ -1167,10 +1243,11 @@ export async function runRecommendWorkflow({
1167
1243
  greetCount += 1;
1168
1244
  }
1169
1245
  addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
1170
- }
1171
- if (detailResult && closeDetail) {
1172
- detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1173
- if (!detailResult.close_result?.closed) {
1246
+ }
1247
+ if (detailResult && closeDetail) {
1248
+ detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1249
+ await maybeHumanActionCooldown("after_detail_close", timings);
1250
+ if (!detailResult.close_result?.closed) {
1174
1251
  const closeError = createRecommendCloseFailureError(detailResult.close_result);
1175
1252
  const recovery = await recoverAndReapplyRecommendContext("detail_close_failed", closeError, {
1176
1253
  forceRecentNotView: true
@@ -1239,16 +1316,37 @@ export async function runRecommendWorkflow({
1239
1316
  });
1240
1317
  addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
1241
1318
 
1242
- if (postActionResult?.stop_run) {
1243
- listEndReason = postActionResult.reason || "post_action_stop";
1244
- break;
1245
- }
1246
-
1247
- if (delayMs > 0) {
1248
- const sleepStarted = Date.now();
1249
- await runControl.sleep(delayMs);
1250
- addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
1251
- compactResult.timings.total_ms = Date.now() - candidateStarted;
1319
+ if (postActionResult?.stop_run) {
1320
+ listEndReason = postActionResult.reason || "post_action_stop";
1321
+ break;
1322
+ }
1323
+
1324
+ if (effectiveHumanRestEnabled) {
1325
+ const restStarted = Date.now();
1326
+ const restResult = await humanRestController.takeBreakIfNeeded({
1327
+ sleepFn: (ms) => runControl.sleep(ms)
1328
+ });
1329
+ const restElapsed = Date.now() - restStarted;
1330
+ if (restResult.rested) {
1331
+ recordHumanEvent({
1332
+ kind: "rest",
1333
+ pause_ms: restResult.pause_ms || restElapsed,
1334
+ events: restResult.events || []
1335
+ });
1336
+ compactResult.human_rest = restResult;
1337
+ addTiming(compactResult.timings, "human_rest_ms", restElapsed);
1338
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
1339
+ updateRecommendProgress({
1340
+ human_rest_last: restResult
1341
+ });
1342
+ }
1343
+ }
1344
+
1345
+ if (delayMs > 0) {
1346
+ const sleepStarted = Date.now();
1347
+ await runControl.sleep(delayMs);
1348
+ addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
1349
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
1252
1350
  }
1253
1351
  }
1254
1352
 
@@ -1261,11 +1359,14 @@ export async function runRecommendWorkflow({
1261
1359
  filter: compactFilterResult(filterResult),
1262
1360
  card_count: cardNodeIds.length,
1263
1361
  candidate_list: compactInfiniteListState(listState),
1264
- viewport_health: {
1265
- stats: viewportGuard.getStats(),
1266
- events: viewportGuard.getEvents()
1267
- },
1268
- list_end_reason: listEndReason || null,
1362
+ viewport_health: {
1363
+ stats: viewportGuard.getStats(),
1364
+ events: viewportGuard.getEvents()
1365
+ },
1366
+ human_behavior: effectiveHumanBehavior,
1367
+ human_rest: humanRestController.getState(),
1368
+ last_human_event: lastHumanEvent,
1369
+ list_end_reason: listEndReason || null,
1269
1370
  refresh_rounds: refreshRounds,
1270
1371
  refresh_attempts: refreshAttempts,
1271
1372
  context_recoveries: contextRecoveryAttempts,
@@ -1316,20 +1417,26 @@ export function createRecommendRunService({
1316
1417
  actionIntervalMs = 500,
1317
1418
  actionAfterClickDelayMs = 900,
1318
1419
  screeningMode = "llm",
1319
- llmConfig = null,
1320
- llmTimeoutMs = 120000,
1321
- llmImageLimit = 8,
1322
- llmImageDetail = "high",
1323
- imageOutputDir = "",
1324
- name = "recommend-domain-run"
1325
- } = {}) {
1326
- if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
1327
- const normalizedFilter = normalizeFilter(filter);
1328
- const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
1329
- const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
1330
- const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
1331
- const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1332
- const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
1420
+ llmConfig = null,
1421
+ llmTimeoutMs = 120000,
1422
+ llmImageLimit = 8,
1423
+ llmImageDetail = "high",
1424
+ imageOutputDir = "",
1425
+ humanRestEnabled = false,
1426
+ humanBehavior = null,
1427
+ name = "recommend-domain-run"
1428
+ } = {}) {
1429
+ if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
1430
+ const normalizedFilter = normalizeFilter(filter);
1431
+ const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
1432
+ const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
1433
+ const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
1434
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1435
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1436
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1437
+ });
1438
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1439
+ const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
1333
1440
  const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
1334
1441
  return manager.startRun({
1335
1442
  runId,
@@ -1365,12 +1472,16 @@ export function createRecommendRunService({
1365
1472
  action_timeout_ms: actionTimeoutMs,
1366
1473
  screening_mode: normalizedScreeningMode,
1367
1474
  llm_configured: Boolean(llmConfig),
1368
- llm_timeout_ms: llmTimeoutMs,
1369
- llm_image_limit: llmImageLimit,
1370
- llm_image_detail: llmImageDetail,
1371
- image_output_dir: imageOutputDir || ""
1372
- },
1373
- progress: {
1475
+ llm_timeout_ms: llmTimeoutMs,
1476
+ llm_image_limit: llmImageLimit,
1477
+ llm_image_detail: llmImageDetail,
1478
+ image_output_dir: imageOutputDir || "",
1479
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1480
+ human_behavior_profile: effectiveHumanBehavior.profile,
1481
+ human_behavior: effectiveHumanBehavior,
1482
+ human_rest_enabled: effectiveHumanRestEnabled
1483
+ },
1484
+ progress: {
1374
1485
  card_count: 0,
1375
1486
  target_count: candidateLimit,
1376
1487
  target_count_semantics: "passed_candidates",
@@ -1381,11 +1492,17 @@ export function createRecommendRunService({
1381
1492
  passed: 0,
1382
1493
  greet_count: 0,
1383
1494
  post_action_clicked: 0,
1384
- image_capture_failed: 0,
1385
- detail_open_failed: 0,
1386
- transient_recovered: 0,
1387
- context_recoveries: 0
1388
- },
1495
+ image_capture_failed: 0,
1496
+ detail_open_failed: 0,
1497
+ transient_recovered: 0,
1498
+ context_recoveries: 0,
1499
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1500
+ human_behavior_profile: effectiveHumanBehavior.profile,
1501
+ human_rest_enabled: effectiveHumanRestEnabled,
1502
+ human_rest_count: 0,
1503
+ human_rest_ms: 0,
1504
+ last_human_event: null
1505
+ },
1389
1506
  checkpoint: {},
1390
1507
  task: (runControl) => workflow({
1391
1508
  client,
@@ -1420,13 +1537,15 @@ export function createRecommendRunService({
1420
1537
  actionAfterClickDelayMs,
1421
1538
  screeningMode: normalizedScreeningMode,
1422
1539
  llmConfig,
1423
- llmTimeoutMs,
1424
- llmImageLimit,
1425
- llmImageDetail,
1426
- imageOutputDir
1427
- }, runControl)
1428
- });
1429
- }
1540
+ llmTimeoutMs,
1541
+ llmImageLimit,
1542
+ llmImageDetail,
1543
+ imageOutputDir,
1544
+ humanRestEnabled: effectiveHumanRestEnabled,
1545
+ humanBehavior: effectiveHumanBehavior
1546
+ }, runControl)
1547
+ });
1548
+ }
1430
1549
 
1431
1550
  return {
1432
1551
  startRecommendRun,
@@ -8,6 +8,12 @@ import {
8
8
  } from "../../core/run/timing.js";
9
9
  import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
10
10
  import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
11
+ import {
12
+ configureHumanInteraction,
13
+ createHumanRestController,
14
+ humanDelay,
15
+ normalizeHumanBehaviorOptions
16
+ } from "../../core/browser/index.js";
11
17
  import {
12
18
  compactCvAcquisitionState,
13
19
  countParsedNetworkProfiles,
@@ -291,9 +297,27 @@ export async function runRecruitWorkflow({
291
297
  llmTimeoutMs = 120000,
292
298
  llmImageLimit = 8,
293
299
  llmImageDetail = "high",
294
- imageOutputDir = ""
300
+ imageOutputDir = "",
301
+ humanRestEnabled = false,
302
+ humanBehavior = null
295
303
  } = {}, runControl) {
296
304
  if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
305
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
306
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
307
+ });
308
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
309
+ configureHumanInteraction(client, {
310
+ enabled: effectiveHumanBehavior.enabled,
311
+ clickMovementEnabled: effectiveHumanBehavior.clickMovement,
312
+ textEntryEnabled: effectiveHumanBehavior.textEntry,
313
+ safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
314
+ actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
315
+ });
316
+ const humanRestController = createHumanRestController({
317
+ enabled: effectiveHumanRestEnabled,
318
+ shortRestEnabled: effectiveHumanBehavior.shortRest,
319
+ batchRestEnabled: effectiveHumanBehavior.batchRest
320
+ });
297
321
  const normalizedSearchParams = normalizeSearchParams(searchParams);
298
322
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
299
323
  const useLlmScreening = normalizedScreeningMode !== "deterministic";
@@ -326,6 +350,7 @@ export async function runRecruitWorkflow({
326
350
  const candidateRecoveryCounts = new Map();
327
351
  let cardNodeIds = [];
328
352
  let listEndReason = "";
353
+ let lastHumanEvent = null;
329
354
  const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
330
355
  rootNodeId: rootState?.iframe?.documentNodeId,
331
356
  containerSelectors: RECRUIT_LIST_CONTAINER_SELECTORS,
@@ -335,9 +360,36 @@ export async function runRecruitWorkflow({
335
360
  validateViewportPoint: true
336
361
  }));
337
362
 
363
+ function recordHumanEvent(event = null) {
364
+ if (!event) return lastHumanEvent;
365
+ lastHumanEvent = {
366
+ at: new Date().toISOString(),
367
+ ...event
368
+ };
369
+ return lastHumanEvent;
370
+ }
371
+
372
+ async function maybeHumanActionCooldown(phase, timings = {}) {
373
+ if (!effectiveHumanBehavior.actionCooldown) return null;
374
+ const pauseMs = humanDelay(280, 90, {
375
+ minMs: 80,
376
+ maxMs: 720
377
+ });
378
+ if (pauseMs > 0) {
379
+ await runControl.sleep(pauseMs);
380
+ addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
381
+ }
382
+ return recordHumanEvent({
383
+ kind: "action_cooldown",
384
+ phase,
385
+ pause_ms: pauseMs
386
+ });
387
+ }
388
+
338
389
  function updateRecruitProgress(extra = {}) {
339
390
  const counts = countRecruitResultStatuses(results);
340
391
  const listSnapshot = compactInfiniteListState(listState);
392
+ const humanRestState = humanRestController.getState();
341
393
  runControl.updateProgress({
342
394
  card_count: cardNodeIds.length,
343
395
  target_count: limit,
@@ -351,6 +403,12 @@ export async function runRecruitWorkflow({
351
403
  list_end_reason: listEndReason || null,
352
404
  viewport_checks: viewportGuard.getStats().checks,
353
405
  viewport_recoveries: viewportGuard.getStats().recoveries,
406
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
407
+ human_behavior_profile: effectiveHumanBehavior.profile,
408
+ human_rest_enabled: effectiveHumanRestEnabled,
409
+ human_rest_count: humanRestState.rest_count,
410
+ human_rest_ms: humanRestState.total_rest_ms,
411
+ last_human_event: lastHumanEvent,
354
412
  ...extra
355
413
  });
356
414
  }
@@ -519,6 +577,7 @@ export async function runRecruitWorkflow({
519
577
  stableSignatureLimit: listStableSignatureLimit,
520
578
  wheelDeltaY: listWheelDeltaY,
521
579
  settleMs: listSettleMs,
580
+ listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
522
581
  fallbackPoint: listFallbackResolver,
523
582
  findNodeIds: async () => {
524
583
  let currentRootState = await getRecruitRoots(client);
@@ -622,6 +681,7 @@ export async function runRecruitWorkflow({
622
681
  checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
623
682
  detailStep = "open_detail";
624
683
  networkRecorder.clear();
684
+ await maybeHumanActionCooldown("before_detail_open", timings);
625
685
  const openedDetail = await openRecruitCardDetail(client, cardNodeId);
626
686
  addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
627
687
  addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
@@ -693,6 +753,7 @@ export async function runRecruitWorkflow({
693
753
  wheelDeltaY: imageWheelDeltaY,
694
754
  settleMs: 350,
695
755
  scrollMethod: "dom-anchor-fallback-input",
756
+ scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
696
757
  stepTimeoutMs: 45000,
697
758
  totalTimeoutMs: 90000,
698
759
  duplicateStopCount: 1,
@@ -765,6 +826,7 @@ export async function runRecruitWorkflow({
765
826
  screeningCandidate = detailResult.candidate;
766
827
  if (closeDetail) {
767
828
  detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
829
+ await maybeHumanActionCooldown("after_detail_close", timings);
768
830
  if (!detailResult.close_result?.closed) {
769
831
  const closeError = createRecruitCloseFailureError(detailResult.close_result);
770
832
  const recovery = await recoverAndReapplyRecruitContext("detail_close_failed", closeError, {
@@ -892,6 +954,27 @@ export async function runRecruitWorkflow({
892
954
  });
893
955
  addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
894
956
 
957
+ if (effectiveHumanRestEnabled) {
958
+ const restStarted = Date.now();
959
+ const restResult = await humanRestController.takeBreakIfNeeded({
960
+ sleepFn: (ms) => runControl.sleep(ms)
961
+ });
962
+ const restElapsed = Date.now() - restStarted;
963
+ if (restResult.rested) {
964
+ recordHumanEvent({
965
+ kind: "rest",
966
+ pause_ms: restResult.pause_ms || restElapsed,
967
+ events: restResult.events || []
968
+ });
969
+ compactResult.human_rest = restResult;
970
+ addTiming(compactResult.timings, "human_rest_ms", restElapsed);
971
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
972
+ updateRecruitProgress({
973
+ human_rest_last: restResult
974
+ });
975
+ }
976
+ }
977
+
895
978
  if (delayMs > 0) {
896
979
  const sleepStarted = Date.now();
897
980
  await runControl.sleep(delayMs);
@@ -911,6 +994,9 @@ export async function runRecruitWorkflow({
911
994
  stats: viewportGuard.getStats(),
912
995
  events: viewportGuard.getEvents()
913
996
  },
997
+ human_behavior: effectiveHumanBehavior,
998
+ human_rest: humanRestController.getState(),
999
+ last_human_event: lastHumanEvent,
914
1000
  list_end_reason: listEndReason || null,
915
1001
  refresh_rounds: refreshRounds,
916
1002
  refresh_attempts: refreshAttempts,
@@ -958,6 +1044,8 @@ export function createRecruitRunService({
958
1044
  llmImageLimit = 8,
959
1045
  llmImageDetail = "high",
960
1046
  imageOutputDir = "",
1047
+ humanRestEnabled = false,
1048
+ humanBehavior = null,
961
1049
  name = "recruit-domain-run"
962
1050
  } = {}) {
963
1051
  if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
@@ -965,6 +1053,10 @@ export function createRecruitRunService({
965
1053
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
966
1054
  const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
967
1055
  const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
1056
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1057
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1058
+ });
1059
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
968
1060
  return manager.startRun({
969
1061
  name,
970
1062
  context: {
@@ -994,7 +1086,11 @@ export function createRecruitRunService({
994
1086
  llm_timeout_ms: llmTimeoutMs,
995
1087
  llm_image_limit: llmImageLimit,
996
1088
  llm_image_detail: llmImageDetail,
997
- image_output_dir: imageOutputDir || ""
1089
+ image_output_dir: imageOutputDir || "",
1090
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1091
+ human_behavior_profile: effectiveHumanBehavior.profile,
1092
+ human_behavior: effectiveHumanBehavior,
1093
+ human_rest_enabled: effectiveHumanRestEnabled
998
1094
  },
999
1095
  progress: {
1000
1096
  card_count: 0,
@@ -1007,7 +1103,13 @@ export function createRecruitRunService({
1007
1103
  image_capture_failed: 0,
1008
1104
  detail_open_failed: 0,
1009
1105
  transient_recovered: 0,
1010
- context_recoveries: 0
1106
+ context_recoveries: 0,
1107
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1108
+ human_behavior_profile: effectiveHumanBehavior.profile,
1109
+ human_rest_enabled: effectiveHumanRestEnabled,
1110
+ human_rest_count: 0,
1111
+ human_rest_ms: 0,
1112
+ last_human_event: null
1011
1113
  },
1012
1114
  checkpoint: {},
1013
1115
  task: (runControl) => workflow({
@@ -1039,7 +1141,9 @@ export function createRecruitRunService({
1039
1141
  llmTimeoutMs,
1040
1142
  llmImageLimit,
1041
1143
  llmImageDetail,
1042
- imageOutputDir
1144
+ imageOutputDir,
1145
+ humanRestEnabled: effectiveHumanRestEnabled,
1146
+ humanBehavior: effectiveHumanBehavior
1043
1147
  }, runControl)
1044
1148
  });
1045
1149
  }