@reconcrap/boss-recommend-mcp 2.0.43 → 2.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/config/screening-config.example.json +11 -1
- package/package.json +1 -2
- package/src/chat-mcp.js +4 -0
- package/src/chat-runtime-config.js +85 -1
- package/src/cli.js +13 -1
- package/src/core/browser/index.js +652 -4
- package/src/core/capture/index.js +118 -14
- package/src/core/infinite-list/index.js +122 -8
- package/src/domains/chat/run-service.js +121 -5
- package/src/domains/recommend/run-service.js +200 -95
- package/src/domains/recruit/run-service.js +108 -4
- package/src/index.js +58 -0
- package/src/recommend-mcp.js +22 -18
- package/src/recruit-mcp.js +44 -0
- package/scripts/live-recommend-recovery-smoke.js +0 -305
|
@@ -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 {
|
|
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,
|
|
@@ -568,13 +574,31 @@ export async function runRecommendWorkflow({
|
|
|
568
574
|
actionAfterClickDelayMs = 900,
|
|
569
575
|
screeningMode = "llm",
|
|
570
576
|
llmConfig = null,
|
|
571
|
-
llmTimeoutMs = 120000,
|
|
572
|
-
llmImageLimit = 8,
|
|
573
|
-
llmImageDetail = "high",
|
|
574
|
-
imageOutputDir = ""
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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);
|
|
578
602
|
const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
|
|
579
603
|
const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
|
|
580
604
|
const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
|
|
@@ -611,26 +635,54 @@ export async function runRecommendWorkflow({
|
|
|
611
635
|
let greetCount = 0;
|
|
612
636
|
const candidateRecoveryCounts = new Map();
|
|
613
637
|
let jobSelection = null;
|
|
614
|
-
let pageScopeSelection = null;
|
|
615
|
-
let filterResult = null;
|
|
616
|
-
let cardNodeIds = [];
|
|
617
|
-
let listEndReason = "";
|
|
618
|
-
|
|
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, {
|
|
619
644
|
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
620
645
|
containerSelectors: RECOMMEND_LIST_CONTAINER_SELECTORS,
|
|
621
646
|
itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
|
|
622
647
|
itemSelectors: [RECOMMEND_CARD_SELECTOR],
|
|
623
648
|
viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
|
|
624
649
|
validateViewportPoint: true
|
|
625
|
-
}));
|
|
626
|
-
|
|
627
|
-
function
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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",
|
|
634
686
|
...counts,
|
|
635
687
|
screening_mode: normalizedScreeningMode,
|
|
636
688
|
unique_seen: listSnapshot.seen_count,
|
|
@@ -638,12 +690,18 @@ export async function runRecommendWorkflow({
|
|
|
638
690
|
refresh_rounds: refreshRounds,
|
|
639
691
|
refresh_attempts: refreshAttempts.length,
|
|
640
692
|
context_recoveries: contextRecoveryAttempts,
|
|
641
|
-
list_end_reason: listEndReason || null,
|
|
642
|
-
viewport_checks: viewportGuard.getStats().checks,
|
|
643
|
-
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
+
}
|
|
647
705
|
|
|
648
706
|
function checkpointInProgressCandidate({
|
|
649
707
|
index = results.length,
|
|
@@ -835,10 +893,11 @@ export async function runRecommendWorkflow({
|
|
|
835
893
|
client,
|
|
836
894
|
state: listState,
|
|
837
895
|
maxScrolls: listMaxScrolls,
|
|
838
|
-
stableSignatureLimit: listStableSignatureLimit,
|
|
839
|
-
wheelDeltaY: listWheelDeltaY,
|
|
840
|
-
settleMs: listSettleMs,
|
|
841
|
-
|
|
896
|
+
stableSignatureLimit: listStableSignatureLimit,
|
|
897
|
+
wheelDeltaY: listWheelDeltaY,
|
|
898
|
+
settleMs: listSettleMs,
|
|
899
|
+
listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
900
|
+
fallbackPoint: listFallbackResolver,
|
|
842
901
|
findNodeIds: async () => {
|
|
843
902
|
let currentRootState = await getRecommendRoots(client);
|
|
844
903
|
currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
|
|
@@ -940,10 +999,11 @@ export async function runRecommendWorkflow({
|
|
|
940
999
|
runControl.setPhase("recommend:detail");
|
|
941
1000
|
detailStep = "ensure_viewport";
|
|
942
1001
|
rootState = await ensureRecommendViewport(rootState, "detail");
|
|
943
|
-
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
944
|
-
detailStep = "open_detail";
|
|
945
|
-
networkRecorder.clear();
|
|
946
|
-
|
|
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, {
|
|
947
1007
|
cardNodeId,
|
|
948
1008
|
candidateKey,
|
|
949
1009
|
cardCandidate,
|
|
@@ -1022,11 +1082,12 @@ export async function runRecommendWorkflow({
|
|
|
1022
1082
|
resizeMaxWidth: 1100,
|
|
1023
1083
|
captureViewport: false,
|
|
1024
1084
|
padding: 0,
|
|
1025
|
-
maxScreenshots: maxImagePages,
|
|
1026
|
-
wheelDeltaY: imageWheelDeltaY,
|
|
1027
|
-
settleMs: 350,
|
|
1028
|
-
scrollMethod: "dom-anchor-fallback-input",
|
|
1029
|
-
|
|
1085
|
+
maxScreenshots: maxImagePages,
|
|
1086
|
+
wheelDeltaY: imageWheelDeltaY,
|
|
1087
|
+
settleMs: 350,
|
|
1088
|
+
scrollMethod: "dom-anchor-fallback-input",
|
|
1089
|
+
scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
1090
|
+
stepTimeoutMs: 45000,
|
|
1030
1091
|
totalTimeoutMs: 90000,
|
|
1031
1092
|
duplicateStopCount: 1,
|
|
1032
1093
|
skipDuplicateScreenshots: true,
|
|
@@ -1160,9 +1221,10 @@ export async function runRecommendWorkflow({
|
|
|
1160
1221
|
if (postActionEnabled && detailResult) {
|
|
1161
1222
|
const postActionStarted = Date.now();
|
|
1162
1223
|
await runControl.waitIfPaused();
|
|
1163
|
-
runControl.throwIfCanceled();
|
|
1164
|
-
runControl.setPhase("recommend:post-action");
|
|
1165
|
-
|
|
1224
|
+
runControl.throwIfCanceled();
|
|
1225
|
+
runControl.setPhase("recommend:post-action");
|
|
1226
|
+
await maybeHumanActionCooldown("before_post_action", timings);
|
|
1227
|
+
actionDiscovery = await waitForRecommendDetailActionControls(client, {
|
|
1166
1228
|
timeoutMs: actionTimeoutMs,
|
|
1167
1229
|
intervalMs: actionIntervalMs,
|
|
1168
1230
|
requireAny: true
|
|
@@ -1181,10 +1243,11 @@ export async function runRecommendWorkflow({
|
|
|
1181
1243
|
greetCount += 1;
|
|
1182
1244
|
}
|
|
1183
1245
|
addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
|
|
1184
|
-
}
|
|
1185
|
-
if (detailResult && closeDetail) {
|
|
1186
|
-
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
|
|
1187
|
-
|
|
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) {
|
|
1188
1251
|
const closeError = createRecommendCloseFailureError(detailResult.close_result);
|
|
1189
1252
|
const recovery = await recoverAndReapplyRecommendContext("detail_close_failed", closeError, {
|
|
1190
1253
|
forceRecentNotView: true
|
|
@@ -1253,16 +1316,37 @@ export async function runRecommendWorkflow({
|
|
|
1253
1316
|
});
|
|
1254
1317
|
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
1255
1318
|
|
|
1256
|
-
if (postActionResult?.stop_run) {
|
|
1257
|
-
listEndReason = postActionResult.reason || "post_action_stop";
|
|
1258
|
-
break;
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
if (
|
|
1262
|
-
const
|
|
1263
|
-
await
|
|
1264
|
-
|
|
1265
|
-
|
|
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;
|
|
1266
1350
|
}
|
|
1267
1351
|
}
|
|
1268
1352
|
|
|
@@ -1275,11 +1359,14 @@ export async function runRecommendWorkflow({
|
|
|
1275
1359
|
filter: compactFilterResult(filterResult),
|
|
1276
1360
|
card_count: cardNodeIds.length,
|
|
1277
1361
|
candidate_list: compactInfiniteListState(listState),
|
|
1278
|
-
viewport_health: {
|
|
1279
|
-
stats: viewportGuard.getStats(),
|
|
1280
|
-
events: viewportGuard.getEvents()
|
|
1281
|
-
},
|
|
1282
|
-
|
|
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,
|
|
1283
1370
|
refresh_rounds: refreshRounds,
|
|
1284
1371
|
refresh_attempts: refreshAttempts,
|
|
1285
1372
|
context_recoveries: contextRecoveryAttempts,
|
|
@@ -1330,20 +1417,26 @@ export function createRecommendRunService({
|
|
|
1330
1417
|
actionIntervalMs = 500,
|
|
1331
1418
|
actionAfterClickDelayMs = 900,
|
|
1332
1419
|
screeningMode = "llm",
|
|
1333
|
-
llmConfig = null,
|
|
1334
|
-
llmTimeoutMs = 120000,
|
|
1335
|
-
llmImageLimit = 8,
|
|
1336
|
-
llmImageDetail = "high",
|
|
1337
|
-
imageOutputDir = "",
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
const
|
|
1344
|
-
const
|
|
1345
|
-
const
|
|
1346
|
-
const
|
|
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);
|
|
1347
1440
|
const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
|
|
1348
1441
|
return manager.startRun({
|
|
1349
1442
|
runId,
|
|
@@ -1379,12 +1472,16 @@ export function createRecommendRunService({
|
|
|
1379
1472
|
action_timeout_ms: actionTimeoutMs,
|
|
1380
1473
|
screening_mode: normalizedScreeningMode,
|
|
1381
1474
|
llm_configured: Boolean(llmConfig),
|
|
1382
|
-
llm_timeout_ms: llmTimeoutMs,
|
|
1383
|
-
llm_image_limit: llmImageLimit,
|
|
1384
|
-
llm_image_detail: llmImageDetail,
|
|
1385
|
-
image_output_dir: imageOutputDir || ""
|
|
1386
|
-
|
|
1387
|
-
|
|
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: {
|
|
1388
1485
|
card_count: 0,
|
|
1389
1486
|
target_count: candidateLimit,
|
|
1390
1487
|
target_count_semantics: "passed_candidates",
|
|
@@ -1395,11 +1492,17 @@ export function createRecommendRunService({
|
|
|
1395
1492
|
passed: 0,
|
|
1396
1493
|
greet_count: 0,
|
|
1397
1494
|
post_action_clicked: 0,
|
|
1398
|
-
image_capture_failed: 0,
|
|
1399
|
-
detail_open_failed: 0,
|
|
1400
|
-
transient_recovered: 0,
|
|
1401
|
-
context_recoveries: 0
|
|
1402
|
-
|
|
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
|
+
},
|
|
1403
1506
|
checkpoint: {},
|
|
1404
1507
|
task: (runControl) => workflow({
|
|
1405
1508
|
client,
|
|
@@ -1434,13 +1537,15 @@ export function createRecommendRunService({
|
|
|
1434
1537
|
actionAfterClickDelayMs,
|
|
1435
1538
|
screeningMode: normalizedScreeningMode,
|
|
1436
1539
|
llmConfig,
|
|
1437
|
-
llmTimeoutMs,
|
|
1438
|
-
llmImageLimit,
|
|
1439
|
-
llmImageDetail,
|
|
1440
|
-
imageOutputDir
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1540
|
+
llmTimeoutMs,
|
|
1541
|
+
llmImageLimit,
|
|
1542
|
+
llmImageDetail,
|
|
1543
|
+
imageOutputDir,
|
|
1544
|
+
humanRestEnabled: effectiveHumanRestEnabled,
|
|
1545
|
+
humanBehavior: effectiveHumanBehavior
|
|
1546
|
+
}, runControl)
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1444
1549
|
|
|
1445
1550
|
return {
|
|
1446
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
|
}
|