@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
|
@@ -38,6 +38,76 @@ function withPadding(rect, padding = 0) {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function normalizeRandom(random) {
|
|
42
|
+
return typeof random === "function" ? random : Math.random;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function randomBetween(random, min, max) {
|
|
46
|
+
const lower = Number(min) || 0;
|
|
47
|
+
const upper = Number(max) || lower;
|
|
48
|
+
if (upper <= lower) return lower;
|
|
49
|
+
return lower + normalizeRandom(random)() * (upper - lower);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeRatio(raw, fallback, { min = 0, max = 1 } = {}) {
|
|
53
|
+
const parsed = Number(raw);
|
|
54
|
+
const value = Number.isFinite(parsed) ? parsed : fallback;
|
|
55
|
+
return Math.min(max, Math.max(min, value));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeScrollDeltaJitter({
|
|
59
|
+
enabled = false,
|
|
60
|
+
minRatio = 0.65,
|
|
61
|
+
maxRatio = 0.9,
|
|
62
|
+
minOverlapRatio = 0.2,
|
|
63
|
+
preserveCoverage = true,
|
|
64
|
+
random = Math.random
|
|
65
|
+
} = {}) {
|
|
66
|
+
const safeMinRatio = normalizeRatio(minRatio, 0.65, { min: 0.1, max: 1 });
|
|
67
|
+
const safeMaxRatio = Math.max(safeMinRatio, normalizeRatio(maxRatio, 0.9, { min: safeMinRatio, max: 1 }));
|
|
68
|
+
return {
|
|
69
|
+
enabled: enabled === true,
|
|
70
|
+
min_ratio: safeMinRatio,
|
|
71
|
+
max_ratio: safeMaxRatio,
|
|
72
|
+
min_overlap_ratio: normalizeRatio(minOverlapRatio, 0.2, { min: 0, max: 0.8 }),
|
|
73
|
+
preserve_coverage: preserveCoverage !== false,
|
|
74
|
+
random: normalizeRandom(random)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveCoverageSafeScrollDelta({
|
|
79
|
+
baseDelta,
|
|
80
|
+
clipHeight,
|
|
81
|
+
jitter
|
|
82
|
+
} = {}) {
|
|
83
|
+
const safeBase = Math.max(1, Number(baseDelta) || 650);
|
|
84
|
+
if (!jitter?.enabled) {
|
|
85
|
+
return {
|
|
86
|
+
deltaY: safeBase,
|
|
87
|
+
jittered: false,
|
|
88
|
+
base_delta_y: safeBase
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const safeClipHeight = Math.max(1, Number(clipHeight) || 1);
|
|
92
|
+
const maxDeltaForOverlap = Math.max(1, Math.floor(safeClipHeight * (1 - jitter.min_overlap_ratio)));
|
|
93
|
+
const upper = Math.max(1, Math.min(Math.round(safeBase * jitter.max_ratio), maxDeltaForOverlap));
|
|
94
|
+
const lower = Math.min(upper, Math.max(1, Math.round(safeBase * jitter.min_ratio)));
|
|
95
|
+
const deltaY = Math.max(1, Math.round(randomBetween(jitter.random, lower, upper)));
|
|
96
|
+
return {
|
|
97
|
+
deltaY,
|
|
98
|
+
jittered: true,
|
|
99
|
+
base_delta_y: safeBase,
|
|
100
|
+
min_delta_y: lower,
|
|
101
|
+
max_delta_y: upper,
|
|
102
|
+
min_ratio: jitter.min_ratio,
|
|
103
|
+
max_ratio: jitter.max_ratio,
|
|
104
|
+
min_overlap_ratio: jitter.min_overlap_ratio,
|
|
105
|
+
clip_height: safeClipHeight,
|
|
106
|
+
max_delta_for_overlap: maxDeltaForOverlap,
|
|
107
|
+
preserve_coverage: jitter.preserve_coverage
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
41
111
|
export async function captureNodeHtml(client, nodeId, {
|
|
42
112
|
domain = "unknown",
|
|
43
113
|
source = "dom",
|
|
@@ -709,6 +779,12 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
709
779
|
scrollAnchorSelector = DEFAULT_SCROLL_ANCHOR_SELECTOR,
|
|
710
780
|
scrollAnchorMaxProbeNodes = 260,
|
|
711
781
|
scrollAnchorMinGap = 180,
|
|
782
|
+
scrollDeltaJitterEnabled = false,
|
|
783
|
+
scrollDeltaJitterMinRatio = 0.65,
|
|
784
|
+
scrollDeltaJitterMaxRatio = 0.9,
|
|
785
|
+
scrollDeltaJitterMinOverlapRatio = 0.2,
|
|
786
|
+
scrollDeltaJitterPreserveCoverage = true,
|
|
787
|
+
scrollDeltaJitterRandom = Math.random,
|
|
712
788
|
stopBoundarySelector = "",
|
|
713
789
|
stopBoundaryTextPatterns = [],
|
|
714
790
|
stopBoundaryMaxProbeNodes = 180,
|
|
@@ -721,10 +797,21 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
721
797
|
const sequenceStarted = Date.now();
|
|
722
798
|
const normalizedScrollMethod = normalizeScrollMethod(scrollMethod);
|
|
723
799
|
const maxScreenshotCount = Math.max(1, Number(maxScreenshots) || 1);
|
|
800
|
+
const scrollDeltaJitter = normalizeScrollDeltaJitter({
|
|
801
|
+
enabled: scrollDeltaJitterEnabled,
|
|
802
|
+
minRatio: scrollDeltaJitterMinRatio,
|
|
803
|
+
maxRatio: scrollDeltaJitterMaxRatio,
|
|
804
|
+
minOverlapRatio: scrollDeltaJitterMinOverlapRatio,
|
|
805
|
+
preserveCoverage: scrollDeltaJitterPreserveCoverage,
|
|
806
|
+
random: scrollDeltaJitterRandom
|
|
807
|
+
});
|
|
808
|
+
const maxCaptureIterations = scrollDeltaJitter.enabled && scrollDeltaJitter.preserve_coverage
|
|
809
|
+
? Math.max(maxScreenshotCount, Math.ceil(maxScreenshotCount / scrollDeltaJitter.min_ratio))
|
|
810
|
+
: maxScreenshotCount;
|
|
724
811
|
const anchorPlan = normalizedScrollMethod !== "input"
|
|
725
812
|
? await collectDomScrollAnchors(client, nodeId, {
|
|
726
813
|
selector: scrollAnchorSelector,
|
|
727
|
-
maxScreenshots:
|
|
814
|
+
maxScreenshots: maxCaptureIterations,
|
|
728
815
|
maxProbeNodes: scrollAnchorMaxProbeNodes,
|
|
729
816
|
minAnchorGap: scrollAnchorMinGap,
|
|
730
817
|
stepTimeoutMs
|
|
@@ -793,7 +880,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
793
880
|
}
|
|
794
881
|
}
|
|
795
882
|
|
|
796
|
-
for (let index = 0; index <
|
|
883
|
+
for (let index = 0; index < maxCaptureIterations; index += 1) {
|
|
797
884
|
assertCaptureTotalBudget(sequenceStarted, totalTimeoutMs, `capture_page_${index + 1}`);
|
|
798
885
|
captureCount += 1;
|
|
799
886
|
const captureStarted = Date.now();
|
|
@@ -920,7 +1007,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
920
1007
|
break;
|
|
921
1008
|
}
|
|
922
1009
|
|
|
923
|
-
if (index <
|
|
1010
|
+
if (index < maxCaptureIterations - 1) {
|
|
924
1011
|
assertCaptureTotalBudget(sequenceStarted, totalTimeoutMs, `scroll_after_page_${index + 1}`);
|
|
925
1012
|
let scrolledByDomAnchor = false;
|
|
926
1013
|
const nextAnchor = anchorPlan?.anchors?.[index + 1] || null;
|
|
@@ -956,6 +1043,11 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
956
1043
|
if (!scrolledByDomAnchor && normalizedScrollMethod !== "dom-anchor") {
|
|
957
1044
|
const x = box.center.x;
|
|
958
1045
|
const y = box.center.y;
|
|
1046
|
+
const scrollDelta = resolveCoverageSafeScrollDelta({
|
|
1047
|
+
baseDelta: wheelDeltaY,
|
|
1048
|
+
clipHeight: effectiveClip.height,
|
|
1049
|
+
jitter: scrollDeltaJitter
|
|
1050
|
+
});
|
|
959
1051
|
await withCaptureTimeout(client.Input.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "none" }), {
|
|
960
1052
|
label: `scroll_mouse_move_${index + 1}`,
|
|
961
1053
|
timeoutMs: Math.min(Math.max(3000, Number(stepTimeoutMs) || 45000), 10000)
|
|
@@ -965,7 +1057,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
965
1057
|
x,
|
|
966
1058
|
y,
|
|
967
1059
|
deltaX: 0,
|
|
968
|
-
deltaY:
|
|
1060
|
+
deltaY: scrollDelta.deltaY
|
|
969
1061
|
}), {
|
|
970
1062
|
label: `scroll_wheel_${index + 1}`,
|
|
971
1063
|
timeoutMs: Math.min(Math.max(3000, Number(stepTimeoutMs) || 45000), 10000)
|
|
@@ -973,7 +1065,10 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
973
1065
|
currentScrollMetadata = {
|
|
974
1066
|
before_capture: `wheel_down_${index + 1}`,
|
|
975
1067
|
method: "Input.dispatchMouseEvent",
|
|
976
|
-
fallback_from_dom_anchor: Boolean(anchorPlan && normalizedScrollMethod === "dom-anchor-fallback-input")
|
|
1068
|
+
fallback_from_dom_anchor: Boolean(anchorPlan && normalizedScrollMethod === "dom-anchor-fallback-input"),
|
|
1069
|
+
wheel_delta_y: scrollDelta.deltaY,
|
|
1070
|
+
wheel_delta_base_y: scrollDelta.base_delta_y,
|
|
1071
|
+
wheel_delta_jitter: scrollDelta.jittered ? scrollDelta : null
|
|
977
1072
|
};
|
|
978
1073
|
}
|
|
979
1074
|
if (settleMs > 0) await sleep(settleMs);
|
|
@@ -1031,15 +1126,24 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
1031
1126
|
step_timeout_ms: Math.max(0, Number(stepTimeoutMs) || 0),
|
|
1032
1127
|
total_timeout_ms: Math.max(0, Number(totalTimeoutMs) || 0),
|
|
1033
1128
|
scroll_method: normalizedScrollMethod,
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1129
|
+
requested_max_screenshots: maxScreenshotCount,
|
|
1130
|
+
effective_max_screenshots: maxCaptureIterations,
|
|
1131
|
+
scroll_anchor_selector: scrollAnchorSelector,
|
|
1132
|
+
scroll_anchor_max_probe_nodes: Math.max(1, Number(scrollAnchorMaxProbeNodes) || 260),
|
|
1133
|
+
scroll_anchor_min_gap: Math.max(0, Number(scrollAnchorMinGap) || 0),
|
|
1134
|
+
scroll_delta_jitter: {
|
|
1135
|
+
enabled: scrollDeltaJitter.enabled,
|
|
1136
|
+
min_ratio: scrollDeltaJitter.min_ratio,
|
|
1137
|
+
max_ratio: scrollDeltaJitter.max_ratio,
|
|
1138
|
+
min_overlap_ratio: scrollDeltaJitter.min_overlap_ratio,
|
|
1139
|
+
preserve_coverage: scrollDeltaJitter.preserve_coverage
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
scroll_anchor_plan: anchorPlan,
|
|
1143
|
+
stop_boundary_plan: stopBoundaryPlan,
|
|
1144
|
+
stop_boundary_checks: stopBoundaryChecks,
|
|
1145
|
+
stop_boundary_result: stopBoundaryResult,
|
|
1146
|
+
file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
|
|
1043
1147
|
screenshots,
|
|
1044
1148
|
metadata
|
|
1045
1149
|
};
|
|
@@ -180,6 +180,83 @@ function normalizePoint(point) {
|
|
|
180
180
|
return { x, y };
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
function normalizeRandom(random) {
|
|
184
|
+
return typeof random === "function" ? random : Math.random;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function randomBetween(random, min, max) {
|
|
188
|
+
const lower = Number(min) || 0;
|
|
189
|
+
const upper = Number(max) || lower;
|
|
190
|
+
if (upper <= lower) return lower;
|
|
191
|
+
return lower + random() * (upper - lower);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function clampNumber(value, min, max) {
|
|
195
|
+
const number = Number(value);
|
|
196
|
+
if (!Number.isFinite(number)) return min;
|
|
197
|
+
return Math.min(max, Math.max(min, number));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function resolveInfiniteListScrollTiming({
|
|
201
|
+
wheelDeltaY = 850,
|
|
202
|
+
settleMs = 1200,
|
|
203
|
+
listScrollJitterEnabled = false,
|
|
204
|
+
listScrollJitterMinRatio = 0.85,
|
|
205
|
+
listScrollJitterMaxRatio = 1.15,
|
|
206
|
+
listSettleJitterMinRatio = 0.75,
|
|
207
|
+
listSettleJitterMaxRatio = 1.35,
|
|
208
|
+
random = Math.random
|
|
209
|
+
} = {}) {
|
|
210
|
+
const baseDeltaY = Math.max(1, Number(wheelDeltaY) || 850);
|
|
211
|
+
const baseSettleMs = Math.max(0, Number(settleMs) || 0);
|
|
212
|
+
if (listScrollJitterEnabled !== true) {
|
|
213
|
+
return {
|
|
214
|
+
wheelDeltaY: baseDeltaY,
|
|
215
|
+
settleMs: baseSettleMs,
|
|
216
|
+
wheel_delta_jitter: {
|
|
217
|
+
enabled: false,
|
|
218
|
+
base_delta_y: baseDeltaY,
|
|
219
|
+
actual_delta_y: baseDeltaY
|
|
220
|
+
},
|
|
221
|
+
settle_jitter: {
|
|
222
|
+
enabled: false,
|
|
223
|
+
base_settle_ms: baseSettleMs,
|
|
224
|
+
actual_settle_ms: baseSettleMs
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const nextRandom = normalizeRandom(random);
|
|
229
|
+
const minDeltaRatio = clampNumber(listScrollJitterMinRatio, 0.5, 1.5);
|
|
230
|
+
const maxDeltaRatio = clampNumber(listScrollJitterMaxRatio, minDeltaRatio, 1.5);
|
|
231
|
+
const minSettleRatio = clampNumber(listSettleJitterMinRatio, 0.4, 2);
|
|
232
|
+
const maxSettleRatio = clampNumber(listSettleJitterMaxRatio, minSettleRatio, 2);
|
|
233
|
+
const deltaRatio = randomBetween(nextRandom, minDeltaRatio, maxDeltaRatio);
|
|
234
|
+
const settleRatio = randomBetween(nextRandom, minSettleRatio, maxSettleRatio);
|
|
235
|
+
const actualDeltaY = Math.max(1, Math.round(baseDeltaY * deltaRatio));
|
|
236
|
+
const actualSettleMs = Math.max(0, Math.round(baseSettleMs * settleRatio));
|
|
237
|
+
return {
|
|
238
|
+
wheelDeltaY: actualDeltaY,
|
|
239
|
+
settleMs: actualSettleMs,
|
|
240
|
+
wheel_delta_jitter: {
|
|
241
|
+
enabled: true,
|
|
242
|
+
preserve_coverage: true,
|
|
243
|
+
base_delta_y: baseDeltaY,
|
|
244
|
+
actual_delta_y: actualDeltaY,
|
|
245
|
+
ratio: deltaRatio,
|
|
246
|
+
min_ratio: minDeltaRatio,
|
|
247
|
+
max_ratio: maxDeltaRatio
|
|
248
|
+
},
|
|
249
|
+
settle_jitter: {
|
|
250
|
+
enabled: true,
|
|
251
|
+
base_settle_ms: baseSettleMs,
|
|
252
|
+
actual_settle_ms: actualSettleMs,
|
|
253
|
+
ratio: settleRatio,
|
|
254
|
+
min_ratio: minSettleRatio,
|
|
255
|
+
max_ratio: maxSettleRatio
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
183
260
|
function resolveViewportPoint(viewportPoint, viewport) {
|
|
184
261
|
if (!viewportPoint) return null;
|
|
185
262
|
if (viewport && ("xRatio" in viewportPoint || "yRatio" in viewportPoint)) {
|
|
@@ -891,7 +968,13 @@ export function firstUnseenInfiniteListItem(state, items = []) {
|
|
|
891
968
|
export async function scrollInfiniteListByVisibleItems(client, items = [], {
|
|
892
969
|
wheelDeltaY = 850,
|
|
893
970
|
settleMs = 1200,
|
|
894
|
-
fallbackPoint = null
|
|
971
|
+
fallbackPoint = null,
|
|
972
|
+
listScrollJitterEnabled = false,
|
|
973
|
+
listScrollJitterMinRatio = 0.85,
|
|
974
|
+
listScrollJitterMaxRatio = 1.15,
|
|
975
|
+
listSettleJitterMinRatio = 0.75,
|
|
976
|
+
listSettleJitterMaxRatio = 1.35,
|
|
977
|
+
random = Math.random
|
|
895
978
|
} = {}) {
|
|
896
979
|
const candidates = items.filter((item) => item?.node_id);
|
|
897
980
|
if (!candidates.length) {
|
|
@@ -902,7 +985,18 @@ export async function scrollInfiniteListByVisibleItems(client, items = [], {
|
|
|
902
985
|
}
|
|
903
986
|
|
|
904
987
|
const errors = [];
|
|
905
|
-
const
|
|
988
|
+
const scrollTiming = resolveInfiniteListScrollTiming({
|
|
989
|
+
wheelDeltaY,
|
|
990
|
+
settleMs,
|
|
991
|
+
listScrollJitterEnabled,
|
|
992
|
+
listScrollJitterMinRatio,
|
|
993
|
+
listScrollJitterMaxRatio,
|
|
994
|
+
listSettleJitterMinRatio,
|
|
995
|
+
listSettleJitterMaxRatio,
|
|
996
|
+
random
|
|
997
|
+
});
|
|
998
|
+
const wheelDelta = scrollTiming.wheelDeltaY;
|
|
999
|
+
const actualSettleMs = scrollTiming.settleMs;
|
|
906
1000
|
async function synthesizeGesture(x, y) {
|
|
907
1001
|
if (typeof client?.Input?.synthesizeScrollGesture !== "function") return null;
|
|
908
1002
|
try {
|
|
@@ -941,15 +1035,19 @@ export async function scrollInfiniteListByVisibleItems(client, items = [], {
|
|
|
941
1035
|
deltaY: wheelDelta
|
|
942
1036
|
});
|
|
943
1037
|
const gesture = await synthesizeGesture(x, y);
|
|
944
|
-
if (
|
|
1038
|
+
if (actualSettleMs > 0) await sleep(actualSettleMs);
|
|
945
1039
|
return {
|
|
946
1040
|
ok: true,
|
|
947
1041
|
anchor_key: anchor.key,
|
|
948
1042
|
anchor_node_id: anchor.node_id,
|
|
949
1043
|
point: { x, y },
|
|
950
1044
|
wheel_delta_y: wheelDelta,
|
|
1045
|
+
base_wheel_delta_y: Math.max(1, Number(wheelDeltaY) || 850),
|
|
1046
|
+
wheel_delta_jitter: scrollTiming.wheel_delta_jitter,
|
|
951
1047
|
gesture,
|
|
952
|
-
settle_ms:
|
|
1048
|
+
settle_ms: actualSettleMs,
|
|
1049
|
+
base_settle_ms: Math.max(0, Number(settleMs) || 0),
|
|
1050
|
+
settle_jitter: scrollTiming.settle_jitter,
|
|
953
1051
|
skipped_stale_anchor_count: errors.length
|
|
954
1052
|
};
|
|
955
1053
|
} catch (error) {
|
|
@@ -994,7 +1092,7 @@ export async function scrollInfiniteListByVisibleItems(client, items = [], {
|
|
|
994
1092
|
deltaY: wheelDelta
|
|
995
1093
|
});
|
|
996
1094
|
const gesture = await synthesizeGesture(x, y);
|
|
997
|
-
if (
|
|
1095
|
+
if (actualSettleMs > 0) await sleep(actualSettleMs);
|
|
998
1096
|
return {
|
|
999
1097
|
ok: true,
|
|
1000
1098
|
mode: "fallback_point",
|
|
@@ -1010,8 +1108,12 @@ export async function scrollInfiniteListByVisibleItems(client, items = [], {
|
|
|
1010
1108
|
assist,
|
|
1011
1109
|
point: { x, y },
|
|
1012
1110
|
wheel_delta_y: wheelDelta,
|
|
1111
|
+
base_wheel_delta_y: Math.max(1, Number(wheelDeltaY) || 850),
|
|
1112
|
+
wheel_delta_jitter: scrollTiming.wheel_delta_jitter,
|
|
1013
1113
|
gesture,
|
|
1014
|
-
settle_ms:
|
|
1114
|
+
settle_ms: actualSettleMs,
|
|
1115
|
+
base_settle_ms: Math.max(0, Number(settleMs) || 0),
|
|
1116
|
+
settle_jitter: scrollTiming.settle_jitter,
|
|
1015
1117
|
skipped_stale_anchor_count: errors.length,
|
|
1016
1118
|
stale_anchor_errors: errors
|
|
1017
1119
|
};
|
|
@@ -1037,7 +1139,13 @@ export async function getNextInfiniteListCandidate({
|
|
|
1037
1139
|
minScrollsBeforeEnd = 3,
|
|
1038
1140
|
wheelDeltaY = 850,
|
|
1039
1141
|
settleMs = 1200,
|
|
1040
|
-
fallbackPoint = null
|
|
1142
|
+
fallbackPoint = null,
|
|
1143
|
+
listScrollJitterEnabled = false,
|
|
1144
|
+
listScrollJitterMinRatio = 0.85,
|
|
1145
|
+
listScrollJitterMaxRatio = 1.15,
|
|
1146
|
+
listSettleJitterMinRatio = 0.75,
|
|
1147
|
+
listSettleJitterMaxRatio = 1.35,
|
|
1148
|
+
random = Math.random
|
|
1041
1149
|
} = {}) {
|
|
1042
1150
|
if (!client) throw new Error("getNextInfiniteListCandidate requires client");
|
|
1043
1151
|
if (!state) throw new Error("getNextInfiniteListCandidate requires state");
|
|
@@ -1173,7 +1281,13 @@ export async function getNextInfiniteListCandidate({
|
|
|
1173
1281
|
const scrollResult = await scrollInfiniteListByVisibleItems(client, items, {
|
|
1174
1282
|
wheelDeltaY,
|
|
1175
1283
|
settleMs,
|
|
1176
|
-
fallbackPoint
|
|
1284
|
+
fallbackPoint,
|
|
1285
|
+
listScrollJitterEnabled,
|
|
1286
|
+
listScrollJitterMinRatio,
|
|
1287
|
+
listScrollJitterMaxRatio,
|
|
1288
|
+
listSettleJitterMinRatio,
|
|
1289
|
+
listSettleJitterMaxRatio,
|
|
1290
|
+
random
|
|
1177
1291
|
});
|
|
1178
1292
|
state.scroll_count += scrollResult.ok ? 1 : 0;
|
|
1179
1293
|
attempts[attempts.length - 1].scroll_result = scrollResult;
|
|
@@ -2,7 +2,11 @@ import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
|
|
|
2
2
|
import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
|
|
3
3
|
import {
|
|
4
4
|
clickPoint,
|
|
5
|
+
configureHumanInteraction,
|
|
6
|
+
createHumanRestController,
|
|
5
7
|
getNodeBox,
|
|
8
|
+
humanDelay,
|
|
9
|
+
normalizeHumanBehaviorOptions,
|
|
6
10
|
scrollNodeIntoView,
|
|
7
11
|
sleep
|
|
8
12
|
} from "../../core/browser/index.js";
|
|
@@ -685,9 +689,27 @@ export async function runChatWorkflow({
|
|
|
685
689
|
listWheelDeltaY = 850,
|
|
686
690
|
listSettleMs = 2200,
|
|
687
691
|
listFallbackPoint = null,
|
|
688
|
-
imageOutputDir = ""
|
|
692
|
+
imageOutputDir = "",
|
|
693
|
+
humanRestEnabled = false,
|
|
694
|
+
humanBehavior = null
|
|
689
695
|
} = {}, runControl) {
|
|
690
696
|
if (!client) throw new Error("runChatWorkflow requires a guarded CDP client");
|
|
697
|
+
const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
|
|
698
|
+
legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
|
|
699
|
+
});
|
|
700
|
+
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
701
|
+
configureHumanInteraction(client, {
|
|
702
|
+
enabled: effectiveHumanBehavior.enabled,
|
|
703
|
+
clickMovementEnabled: effectiveHumanBehavior.clickMovement,
|
|
704
|
+
textEntryEnabled: effectiveHumanBehavior.textEntry,
|
|
705
|
+
safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
|
|
706
|
+
actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
|
|
707
|
+
});
|
|
708
|
+
const humanRestController = createHumanRestController({
|
|
709
|
+
enabled: effectiveHumanRestEnabled,
|
|
710
|
+
shortRestEnabled: effectiveHumanBehavior.shortRest,
|
|
711
|
+
batchRestEnabled: effectiveHumanBehavior.batchRest
|
|
712
|
+
});
|
|
691
713
|
const normalizedDetailSource = normalizeDetailSource(detailSource);
|
|
692
714
|
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
693
715
|
const useLlmScreening = normalizedScreeningMode !== "deterministic";
|
|
@@ -732,6 +754,33 @@ export async function runChatWorkflow({
|
|
|
732
754
|
let requestSatisfiedCount = 0;
|
|
733
755
|
let requestSkippedCount = 0;
|
|
734
756
|
let contextSetup = {};
|
|
757
|
+
let lastHumanEvent = null;
|
|
758
|
+
|
|
759
|
+
function recordHumanEvent(event = null) {
|
|
760
|
+
if (!event) return lastHumanEvent;
|
|
761
|
+
lastHumanEvent = {
|
|
762
|
+
at: new Date().toISOString(),
|
|
763
|
+
...event
|
|
764
|
+
};
|
|
765
|
+
return lastHumanEvent;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function maybeHumanActionCooldown(phase, timings = {}) {
|
|
769
|
+
if (!effectiveHumanBehavior.actionCooldown) return null;
|
|
770
|
+
const pauseMs = humanDelay(280, 90, {
|
|
771
|
+
minMs: 80,
|
|
772
|
+
maxMs: 720
|
|
773
|
+
});
|
|
774
|
+
if (pauseMs > 0) {
|
|
775
|
+
await runControl.sleep(pauseMs);
|
|
776
|
+
addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
|
|
777
|
+
}
|
|
778
|
+
return recordHumanEvent({
|
|
779
|
+
kind: "action_cooldown",
|
|
780
|
+
phase,
|
|
781
|
+
pause_ms: pauseMs
|
|
782
|
+
});
|
|
783
|
+
}
|
|
735
784
|
|
|
736
785
|
runControl.setPhase("chat:cleanup");
|
|
737
786
|
let initialTopLevelState = await getChatTopLevelState(client);
|
|
@@ -870,7 +919,13 @@ export async function runChatWorkflow({
|
|
|
870
919
|
scroll_count: compactInfiniteListState(listState).scroll_count,
|
|
871
920
|
list_end_reason: listEndReason,
|
|
872
921
|
viewport_checks: viewportGuard.getStats().checks,
|
|
873
|
-
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
922
|
+
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
923
|
+
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
924
|
+
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
925
|
+
human_rest_enabled: effectiveHumanRestEnabled,
|
|
926
|
+
human_rest_count: humanRestController.getState().rest_count,
|
|
927
|
+
human_rest_ms: humanRestController.getState().total_rest_ms,
|
|
928
|
+
last_human_event: lastHumanEvent
|
|
874
929
|
});
|
|
875
930
|
runControl.setPhase("chat:done");
|
|
876
931
|
return {
|
|
@@ -888,6 +943,9 @@ export async function runChatWorkflow({
|
|
|
888
943
|
stats: viewportGuard.getStats(),
|
|
889
944
|
events: viewportGuard.getEvents()
|
|
890
945
|
},
|
|
946
|
+
human_behavior: effectiveHumanBehavior,
|
|
947
|
+
human_rest: humanRestController.getState(),
|
|
948
|
+
last_human_event: lastHumanEvent,
|
|
891
949
|
list_end_reason: listEndReason,
|
|
892
950
|
target_pass_count: passTarget,
|
|
893
951
|
process_until_list_end: Boolean(processUntilListEnd),
|
|
@@ -993,6 +1051,7 @@ export async function runChatWorkflow({
|
|
|
993
1051
|
stableSignatureLimit: listStableSignatureLimit,
|
|
994
1052
|
wheelDeltaY: listWheelDeltaY,
|
|
995
1053
|
settleMs: listSettleMs,
|
|
1054
|
+
listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
996
1055
|
fallbackPoint: listFallbackResolver,
|
|
997
1056
|
findNodeIds: async () => {
|
|
998
1057
|
const currentRootState = await ensureChatViewport(await getChatRoots(client), "candidate_find_nodes");
|
|
@@ -1074,6 +1133,7 @@ export async function runChatWorkflow({
|
|
|
1074
1133
|
|
|
1075
1134
|
detailStep = "select_candidate";
|
|
1076
1135
|
networkRecorder.clear();
|
|
1136
|
+
await maybeHumanActionCooldown("before_detail_open", timings);
|
|
1077
1137
|
const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
|
|
1078
1138
|
cardNodeId,
|
|
1079
1139
|
candidate: cardCandidate,
|
|
@@ -1178,6 +1238,7 @@ export async function runChatWorkflow({
|
|
|
1178
1238
|
if (!detailResult) {
|
|
1179
1239
|
detailStep = "open_online_resume";
|
|
1180
1240
|
networkRecorder.clear();
|
|
1241
|
+
await maybeHumanActionCooldown("before_resume_open", timings);
|
|
1181
1242
|
const openedResume = await measureTiming(timings, "detail_open_ms", () => openChatOnlineResume(client, {
|
|
1182
1243
|
timeoutMs: readyTimeoutMs
|
|
1183
1244
|
}));
|
|
@@ -1332,6 +1393,7 @@ export async function runChatWorkflow({
|
|
|
1332
1393
|
wheelDeltaY: imageWheelDeltaY,
|
|
1333
1394
|
settleMs: 350,
|
|
1334
1395
|
scrollMethod: "dom-anchor-fallback-input",
|
|
1396
|
+
scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
1335
1397
|
stepTimeoutMs: 45000,
|
|
1336
1398
|
totalTimeoutMs: 90000,
|
|
1337
1399
|
duplicateStopCount: 1,
|
|
@@ -1479,6 +1541,7 @@ export async function runChatWorkflow({
|
|
|
1479
1541
|
full_cv_evidence: fullCvEvidence
|
|
1480
1542
|
});
|
|
1481
1543
|
closeResult = await measureTiming(timings, "close_detail_ms", () => closeChatResumeModal(client));
|
|
1544
|
+
await maybeHumanActionCooldown("after_detail_close", timings);
|
|
1482
1545
|
if (!closeResult?.closed) {
|
|
1483
1546
|
closeRecovery = await recoverAndReapplyChatContext(
|
|
1484
1547
|
"resume_modal_close_failed:close_resume_modal",
|
|
@@ -1575,6 +1638,7 @@ export async function runChatWorkflow({
|
|
|
1575
1638
|
: screenCandidate(screeningCandidate, { criteria });
|
|
1576
1639
|
let postAction = null;
|
|
1577
1640
|
if (requestResumeForPassed && screening.passed) {
|
|
1641
|
+
await maybeHumanActionCooldown("before_post_action", timings);
|
|
1578
1642
|
postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
|
|
1579
1643
|
greetingText,
|
|
1580
1644
|
dryRun: dryRunRequestCv
|
|
@@ -1627,6 +1691,12 @@ export async function runChatWorkflow({
|
|
|
1627
1691
|
list_end_reason: listEndReason || null,
|
|
1628
1692
|
viewport_checks: viewportGuard.getStats().checks,
|
|
1629
1693
|
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
1694
|
+
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
1695
|
+
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1696
|
+
human_rest_enabled: effectiveHumanRestEnabled,
|
|
1697
|
+
human_rest_count: humanRestController.getState().rest_count,
|
|
1698
|
+
human_rest_ms: humanRestController.getState().total_rest_ms,
|
|
1699
|
+
last_human_event: lastHumanEvent,
|
|
1630
1700
|
last_candidate_id: screeningCandidate.id || null,
|
|
1631
1701
|
last_candidate_key: candidateKey,
|
|
1632
1702
|
last_score: screening.score
|
|
@@ -1649,6 +1719,31 @@ export async function runChatWorkflow({
|
|
|
1649
1719
|
});
|
|
1650
1720
|
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
1651
1721
|
|
|
1722
|
+
if (effectiveHumanRestEnabled) {
|
|
1723
|
+
const restStarted = Date.now();
|
|
1724
|
+
const restResult = await humanRestController.takeBreakIfNeeded({
|
|
1725
|
+
sleepFn: (ms) => runControl.sleep(ms)
|
|
1726
|
+
});
|
|
1727
|
+
const restElapsed = Date.now() - restStarted;
|
|
1728
|
+
if (restResult.rested) {
|
|
1729
|
+
recordHumanEvent({
|
|
1730
|
+
kind: "rest",
|
|
1731
|
+
pause_ms: restResult.pause_ms || restElapsed,
|
|
1732
|
+
events: restResult.events || []
|
|
1733
|
+
});
|
|
1734
|
+
compactResult.human_rest = restResult;
|
|
1735
|
+
addTiming(compactResult.timings, "human_rest_ms", restElapsed);
|
|
1736
|
+
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
1737
|
+
runControl.updateProgress({
|
|
1738
|
+
human_rest_enabled: effectiveHumanRestEnabled,
|
|
1739
|
+
human_rest_count: humanRestController.getState().rest_count,
|
|
1740
|
+
human_rest_ms: humanRestController.getState().total_rest_ms,
|
|
1741
|
+
human_rest_last: restResult,
|
|
1742
|
+
last_human_event: lastHumanEvent
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1652
1747
|
if (delayMs > 0) {
|
|
1653
1748
|
const sleepStarted = Date.now();
|
|
1654
1749
|
await runControl.sleep(delayMs);
|
|
@@ -1669,6 +1764,9 @@ export async function runChatWorkflow({
|
|
|
1669
1764
|
stats: viewportGuard.getStats(),
|
|
1670
1765
|
events: viewportGuard.getEvents()
|
|
1671
1766
|
},
|
|
1767
|
+
human_behavior: effectiveHumanBehavior,
|
|
1768
|
+
human_rest: humanRestController.getState(),
|
|
1769
|
+
last_human_event: lastHumanEvent,
|
|
1672
1770
|
list_end_reason: listEndReason || null,
|
|
1673
1771
|
target_pass_count: passTarget,
|
|
1674
1772
|
process_until_list_end: Boolean(processUntilListEnd),
|
|
@@ -1730,6 +1828,8 @@ export function createChatRunService({
|
|
|
1730
1828
|
listSettleMs = 2200,
|
|
1731
1829
|
listFallbackPoint = null,
|
|
1732
1830
|
imageOutputDir = "",
|
|
1831
|
+
humanRestEnabled = false,
|
|
1832
|
+
humanBehavior = null,
|
|
1733
1833
|
name = "chat-domain-run"
|
|
1734
1834
|
} = {}) {
|
|
1735
1835
|
if (!client) throw new Error("startChatRun requires a guarded CDP client");
|
|
@@ -1737,6 +1837,10 @@ export function createChatRunService({
|
|
|
1737
1837
|
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
1738
1838
|
const processedLimit = Math.max(1, Number(maxCandidates) || 1);
|
|
1739
1839
|
const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
|
|
1840
|
+
const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
|
|
1841
|
+
legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
|
|
1842
|
+
});
|
|
1843
|
+
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
1740
1844
|
return manager.startRun({
|
|
1741
1845
|
name,
|
|
1742
1846
|
context: {
|
|
@@ -1769,7 +1873,11 @@ export function createChatRunService({
|
|
|
1769
1873
|
list_settle_ms: listSettleMs,
|
|
1770
1874
|
list_fallback_point: listFallbackPoint,
|
|
1771
1875
|
online_resume_button_timeout_ms: onlineResumeButtonTimeoutMs,
|
|
1772
|
-
image_output_dir: imageOutputDir || ""
|
|
1876
|
+
image_output_dir: imageOutputDir || "",
|
|
1877
|
+
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
1878
|
+
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1879
|
+
human_behavior: effectiveHumanBehavior,
|
|
1880
|
+
human_rest_enabled: effectiveHumanRestEnabled
|
|
1773
1881
|
},
|
|
1774
1882
|
progress: {
|
|
1775
1883
|
card_count: 0,
|
|
@@ -1784,7 +1892,13 @@ export function createChatRunService({
|
|
|
1784
1892
|
skipped: 0,
|
|
1785
1893
|
requested: 0,
|
|
1786
1894
|
request_satisfied: 0,
|
|
1787
|
-
request_skipped: 0
|
|
1895
|
+
request_skipped: 0,
|
|
1896
|
+
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
1897
|
+
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1898
|
+
human_rest_enabled: effectiveHumanRestEnabled,
|
|
1899
|
+
human_rest_count: 0,
|
|
1900
|
+
human_rest_ms: 0,
|
|
1901
|
+
last_human_event: null
|
|
1788
1902
|
},
|
|
1789
1903
|
checkpoint: {},
|
|
1790
1904
|
task: (runControl) => workflow({
|
|
@@ -1821,7 +1935,9 @@ export function createChatRunService({
|
|
|
1821
1935
|
listWheelDeltaY,
|
|
1822
1936
|
listSettleMs,
|
|
1823
1937
|
listFallbackPoint,
|
|
1824
|
-
imageOutputDir
|
|
1938
|
+
imageOutputDir,
|
|
1939
|
+
humanRestEnabled: effectiveHumanRestEnabled,
|
|
1940
|
+
humanBehavior: effectiveHumanBehavior
|
|
1825
1941
|
}, runControl)
|
|
1826
1942
|
});
|
|
1827
1943
|
}
|