@reconcrap/boss-recommend-mcp 2.0.47 → 2.0.48
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/bin/boss-recommend-mcp.js +4 -4
- package/config/screening-config.example.json +27 -27
- package/package.json +1 -1
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +39 -39
- package/skills/boss-chat/SKILL.md +93 -93
- package/skills/boss-recommend-pipeline/README.md +12 -12
- package/skills/boss-recommend-pipeline/SKILL.md +180 -180
- package/skills/boss-recruit-pipeline/README.md +17 -17
- package/skills/boss-recruit-pipeline/SKILL.md +58 -58
- package/src/chat-mcp.js +1780 -1780
- package/src/chat-runtime-config.js +749 -749
- package/src/cli.js +3054 -3054
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +1586 -1453
- package/src/core/capture/index.js +1201 -1201
- package/src/core/cv-acquisition/index.js +238 -238
- package/src/core/cv-capture-target/index.js +299 -299
- package/src/core/greet-quota/index.js +54 -54
- package/src/core/infinite-list/index.js +1326 -1326
- package/src/core/reporting/legacy-csv.js +341 -341
- package/src/core/run/timing.js +33 -33
- package/src/core/self-heal/index.js +973 -973
- package/src/core/self-heal/viewport.js +564 -564
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +221 -221
- package/src/domains/chat/detail.js +1668 -1668
- package/src/domains/chat/index.js +7 -7
- package/src/domains/chat/jobs.js +592 -592
- package/src/domains/chat/page-guard.js +98 -98
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +1977 -1977
- package/src/domains/recommend/actions.js +457 -457
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/constants.js +165 -165
- package/src/domains/recommend/filters.js +610 -610
- package/src/domains/recommend/index.js +10 -10
- package/src/domains/recommend/jobs.js +316 -316
- package/src/domains/recommend/refresh.js +472 -472
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +167 -167
- package/src/domains/recruit/detail.js +461 -461
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +451 -451
- package/src/domains/recruit/refresh.js +44 -44
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1207 -1207
- package/src/domains/recruit/search.js +1202 -1202
- package/src/recommend-mcp.js +22 -22
- package/src/recruit-mcp.js +1338 -1338
|
@@ -1,1207 +1,1207 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { createRunLifecycleManager } from "../../core/run/index.js";
|
|
4
|
-
import {
|
|
5
|
-
addTiming,
|
|
6
|
-
imageEvidenceFilePath,
|
|
7
|
-
measureTiming
|
|
8
|
-
} from "../../core/run/timing.js";
|
|
9
|
-
import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
|
|
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";
|
|
17
|
-
import {
|
|
18
|
-
compactCvAcquisitionState,
|
|
19
|
-
countParsedNetworkProfiles,
|
|
20
|
-
createCvAcquisitionState,
|
|
21
|
-
DEFAULT_MAX_IMAGE_PAGES,
|
|
22
|
-
getCvNetworkWaitPlan,
|
|
23
|
-
recordCvImageFallback,
|
|
24
|
-
recordCvNetworkHit,
|
|
25
|
-
recordCvNetworkMiss,
|
|
26
|
-
summarizeImageEvidence,
|
|
27
|
-
waitForCvNetworkEvents
|
|
28
|
-
} from "../../core/cv-acquisition/index.js";
|
|
29
|
-
import {
|
|
30
|
-
compactInfiniteListState,
|
|
31
|
-
createInfiniteListState,
|
|
32
|
-
detectInfiniteListBottomMarker,
|
|
33
|
-
getNextInfiniteListCandidate,
|
|
34
|
-
markInfiniteListCandidateProcessed,
|
|
35
|
-
resetInfiniteListForRefreshRound,
|
|
36
|
-
resolveInfiniteListFallbackPoint
|
|
37
|
-
} from "../../core/infinite-list/index.js";
|
|
38
|
-
import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
39
|
-
import {
|
|
40
|
-
callScreeningLlm,
|
|
41
|
-
compactScreeningLlmResult,
|
|
42
|
-
createFailedLlmScreeningResult,
|
|
43
|
-
llmResultToScreening,
|
|
44
|
-
screenCandidate
|
|
45
|
-
} from "../../core/screening/index.js";
|
|
46
|
-
import {
|
|
47
|
-
closeRecruitDetail,
|
|
48
|
-
createRecruitDetailNetworkRecorder,
|
|
49
|
-
extractRecruitDetailCandidate,
|
|
50
|
-
openRecruitCardDetail,
|
|
51
|
-
waitForRecruitDetailNetworkEvents
|
|
52
|
-
} from "./detail.js";
|
|
53
|
-
import {
|
|
54
|
-
readRecruitCardCandidate,
|
|
55
|
-
waitForRecruitCardNodeIds
|
|
56
|
-
} from "./cards.js";
|
|
57
|
-
import {
|
|
58
|
-
applyRecruitSearchParams,
|
|
59
|
-
hasRecruitSearchParams,
|
|
60
|
-
normalizeRecruitSearchParams
|
|
61
|
-
} from "./search.js";
|
|
62
|
-
import { refreshRecruitSearchAtEnd } from "./refresh.js";
|
|
63
|
-
import { getRecruitRoots } from "./roots.js";
|
|
64
|
-
import {
|
|
65
|
-
RECRUIT_BOTTOM_MARKER_SELECTORS,
|
|
66
|
-
RECRUIT_BOTTOM_REFRESH_SELECTORS,
|
|
67
|
-
RECRUIT_CARD_SELECTOR,
|
|
68
|
-
RECRUIT_LIST_CONTAINER_SELECTORS
|
|
69
|
-
} from "./constants.js";
|
|
70
|
-
|
|
71
|
-
function compactScreening(screening) {
|
|
72
|
-
return {
|
|
73
|
-
status: screening.status,
|
|
74
|
-
passed: screening.passed,
|
|
75
|
-
score: screening.score,
|
|
76
|
-
reasons: screening.reasons,
|
|
77
|
-
candidate: {
|
|
78
|
-
domain: screening.candidate?.domain || "recruit",
|
|
79
|
-
source: screening.candidate?.source || "",
|
|
80
|
-
id: screening.candidate?.id || null,
|
|
81
|
-
identity: screening.candidate?.identity || {}
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function compactCandidate(candidate) {
|
|
87
|
-
return {
|
|
88
|
-
id: candidate?.id || null,
|
|
89
|
-
identity: candidate?.identity || {},
|
|
90
|
-
text_length: candidate?.text?.raw?.length || 0,
|
|
91
|
-
tag_count: candidate?.tags?.length || 0
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function compactDetail(detailResult) {
|
|
96
|
-
if (!detailResult) return null;
|
|
97
|
-
return {
|
|
98
|
-
popup_text_length: detailResult.detail?.popup_text?.length || 0,
|
|
99
|
-
resume_text_length: detailResult.detail?.resume_text?.length || 0,
|
|
100
|
-
network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
|
|
101
|
-
parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
|
|
102
|
-
cv_acquisition: detailResult.cv_acquisition || null,
|
|
103
|
-
image_evidence: summarizeImageEvidence(detailResult.image_evidence),
|
|
104
|
-
llm_screening: compactScreeningLlmResult(detailResult.llm_result),
|
|
105
|
-
close_result: detailResult.close_result
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function normalizeScreeningMode(value) {
|
|
110
|
-
const normalized = String(value || "llm").trim().toLowerCase();
|
|
111
|
-
return ["deterministic", "local", "local_scorer"].includes(normalized)
|
|
112
|
-
? "deterministic"
|
|
113
|
-
: "llm";
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function createMissingLlmConfigResult() {
|
|
117
|
-
return createFailedLlmScreeningResult(new Error("LLM screening config is required for production search runs"));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function normalizeSearchParams(searchParams = {}) {
|
|
121
|
-
return normalizeRecruitSearchParams(searchParams);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function compactRefreshAttempt(refreshAttempt) {
|
|
125
|
-
if (!refreshAttempt) return null;
|
|
126
|
-
return {
|
|
127
|
-
ok: Boolean(refreshAttempt.ok),
|
|
128
|
-
method: refreshAttempt.method || "",
|
|
129
|
-
forced_recent_viewed: Boolean(refreshAttempt.forced_recent_viewed),
|
|
130
|
-
card_count: refreshAttempt.card_count || 0,
|
|
131
|
-
search_params: refreshAttempt.search_params || null,
|
|
132
|
-
application: refreshAttempt.application
|
|
133
|
-
? {
|
|
134
|
-
applied: Boolean(refreshAttempt.application.applied),
|
|
135
|
-
post_search_state: refreshAttempt.application.post_search_state,
|
|
136
|
-
steps: (refreshAttempt.application.steps || []).map((step) => ({
|
|
137
|
-
step: step.step,
|
|
138
|
-
applied: step.result?.applied,
|
|
139
|
-
clicked: step.result?.clicked,
|
|
140
|
-
searched: step.result?.searched,
|
|
141
|
-
reason: step.result?.reason || null
|
|
142
|
-
}))
|
|
143
|
-
}
|
|
144
|
-
: null
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function compactError(error, fallbackCode = "RECRUIT_RUN_ERROR") {
|
|
149
|
-
if (!error) return null;
|
|
150
|
-
const result = {
|
|
151
|
-
code: error.code || fallbackCode,
|
|
152
|
-
message: error.message || String(error)
|
|
153
|
-
};
|
|
154
|
-
if (error.refresh_attempt) {
|
|
155
|
-
result.refresh_attempt = error.refresh_attempt;
|
|
156
|
-
}
|
|
157
|
-
if (error.list_end_reason) {
|
|
158
|
-
result.list_end_reason = error.list_end_reason;
|
|
159
|
-
}
|
|
160
|
-
if (error.target_count != null) {
|
|
161
|
-
result.target_count = error.target_count;
|
|
162
|
-
}
|
|
163
|
-
if (error.processed_count != null) {
|
|
164
|
-
result.processed_count = error.processed_count;
|
|
165
|
-
}
|
|
166
|
-
return result;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function createRecruitCloseFailureError(closeResult) {
|
|
170
|
-
const error = new Error(closeResult?.reason || "Recruit detail did not close before recovery");
|
|
171
|
-
error.code = "DETAIL_CLOSE_FAILED";
|
|
172
|
-
error.close_result = closeResult || null;
|
|
173
|
-
return error;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function createRecruitRefreshFailureError(refreshAttempt, {
|
|
177
|
-
listEndReason = "",
|
|
178
|
-
targetCount = 0,
|
|
179
|
-
processedCount = 0
|
|
180
|
-
} = {}) {
|
|
181
|
-
const reason = refreshAttempt?.application?.post_search_state?.ok === false
|
|
182
|
-
? "search_result_not_ready"
|
|
183
|
-
: refreshAttempt?.application?.post_search_state?.counts?.candidate_card === 0
|
|
184
|
-
? "no_cards_after_refresh"
|
|
185
|
-
: "refresh_failed";
|
|
186
|
-
const error = new Error(`Recruit/search refresh failed before target was reached (${reason})`);
|
|
187
|
-
error.code = "RECRUIT_END_REFRESH_FAILED";
|
|
188
|
-
error.refresh_attempt = refreshAttempt || null;
|
|
189
|
-
error.list_end_reason = listEndReason || null;
|
|
190
|
-
error.target_count = targetCount;
|
|
191
|
-
error.processed_count = processedCount;
|
|
192
|
-
return error;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function isRefreshableListStall(reason = "") {
|
|
196
|
-
return new Set([
|
|
197
|
-
"stable_visible_signature",
|
|
198
|
-
"max_scrolls_exhausted",
|
|
199
|
-
"scroll_failed",
|
|
200
|
-
"scroll_anchor_unavailable"
|
|
201
|
-
]).has(String(reason || ""));
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function isStaleRecruitNodeError(error) {
|
|
205
|
-
const message = String(error?.message || error || "");
|
|
206
|
-
return /Could not find node with given id|No node with given id|Node is detached|Cannot find node/i.test(message);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function isRecoverableRecruitImageCaptureError(error) {
|
|
210
|
-
const code = String(error?.code || "");
|
|
211
|
-
if (code === "IMAGE_CAPTURE_TIMEOUT" || code === "IMAGE_CAPTURE_TOTAL_TIMEOUT") return true;
|
|
212
|
-
if (isStaleRecruitNodeError(error)) return true;
|
|
213
|
-
return /Image fallback capture timed out/i.test(String(error?.message || error || ""));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function isRecoverableRecruitDetailError(error) {
|
|
217
|
-
return isStaleRecruitNodeError(error);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function compactRecoverableDetailError(error) {
|
|
221
|
-
return compactError(error, isStaleRecruitNodeError(error) ? "DETAIL_STALE_NODE" : "DETAIL_OPEN_FAILED");
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function collectPartialImageEvidencePaths(basePath = "", extension = "jpg", maxCount = 12) {
|
|
225
|
-
const resolved = String(basePath || "").trim();
|
|
226
|
-
if (!resolved) return [];
|
|
227
|
-
const parsed = path.parse(resolved);
|
|
228
|
-
const ext = parsed.ext || `.${String(extension || "jpg").replace(/^\./, "") || "jpg"}`;
|
|
229
|
-
const files = [];
|
|
230
|
-
for (let index = 0; index < Math.max(1, Number(maxCount) || 1); index += 1) {
|
|
231
|
-
const page = String(index + 1).padStart(2, "0");
|
|
232
|
-
const candidatePath = path.join(parsed.dir, `${parsed.name}-page-${page}${ext}`);
|
|
233
|
-
if (fs.existsSync(candidatePath)) files.push(candidatePath);
|
|
234
|
-
}
|
|
235
|
-
return files;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export function createRecoverableRecruitImageCaptureEvidence(error, {
|
|
239
|
-
elapsedMs = 0,
|
|
240
|
-
filePath = "",
|
|
241
|
-
extension = "jpg",
|
|
242
|
-
maxScreenshots = DEFAULT_MAX_IMAGE_PAGES
|
|
243
|
-
} = {}) {
|
|
244
|
-
const filePaths = collectPartialImageEvidencePaths(filePath, extension, maxScreenshots);
|
|
245
|
-
return {
|
|
246
|
-
schema_version: 1,
|
|
247
|
-
ok: false,
|
|
248
|
-
source: "image-scroll-sequence",
|
|
249
|
-
elapsed_ms: Math.max(0, Math.round(Number(error?.elapsed_ms ?? elapsedMs) || 0)),
|
|
250
|
-
capture_count: filePaths.length,
|
|
251
|
-
screenshot_count: filePaths.length,
|
|
252
|
-
unique_screenshot_count: filePaths.length,
|
|
253
|
-
dropped_duplicate_count: 0,
|
|
254
|
-
total_byte_length: 0,
|
|
255
|
-
original_total_byte_length: 0,
|
|
256
|
-
llm_screenshot_count: 0,
|
|
257
|
-
llm_total_byte_length: 0,
|
|
258
|
-
llm_original_total_byte_length: 0,
|
|
259
|
-
llm_composition_error: null,
|
|
260
|
-
error_code: error?.code || (isStaleRecruitNodeError(error) ? "IMAGE_CAPTURE_STALE_NODE" : "IMAGE_CAPTURE_FAILED"),
|
|
261
|
-
error: error?.message || String(error || "Image capture failed"),
|
|
262
|
-
file_paths: filePaths,
|
|
263
|
-
llm_file_paths: []
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function createImageCaptureFailureScreening(candidate, error) {
|
|
268
|
-
return {
|
|
269
|
-
status: "fail",
|
|
270
|
-
passed: false,
|
|
271
|
-
score: 0,
|
|
272
|
-
reasons: ["image_capture_failed"],
|
|
273
|
-
error: compactError(error, "IMAGE_CAPTURE_FAILED"),
|
|
274
|
-
candidate
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function createRecoverableDetailFailureScreening(candidate, error) {
|
|
279
|
-
return {
|
|
280
|
-
status: "fail",
|
|
281
|
-
passed: false,
|
|
282
|
-
score: 0,
|
|
283
|
-
reasons: isStaleRecruitNodeError(error)
|
|
284
|
-
? ["detail_open_failed", "stale_node"]
|
|
285
|
-
: ["detail_open_failed"],
|
|
286
|
-
error: compactRecoverableDetailError(error),
|
|
287
|
-
candidate
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export function countRecruitResultStatuses(results = []) {
|
|
292
|
-
return {
|
|
293
|
-
processed: results.length,
|
|
294
|
-
screened: results.length,
|
|
295
|
-
detail_opened: results.filter((item) => item.detail).length,
|
|
296
|
-
passed: results.filter((item) => item.screening?.passed).length,
|
|
297
|
-
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
298
|
-
image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
|
|
299
|
-
detail_open_failed: results.filter((item) => (
|
|
300
|
-
item.error?.code === "DETAIL_STALE_NODE"
|
|
301
|
-
|| item.error?.code === "DETAIL_OPEN_FAILED"
|
|
302
|
-
)).length,
|
|
303
|
-
transient_recovered: results.filter((item) => (
|
|
304
|
-
item.error?.code === "DETAIL_STALE_NODE"
|
|
305
|
-
|| item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
|
|
306
|
-
|| item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
|
|
307
|
-
|| item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
|
|
308
|
-
)).length
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
export async function runRecruitWorkflow({
|
|
313
|
-
client,
|
|
314
|
-
targetUrl = "",
|
|
315
|
-
criteria = "",
|
|
316
|
-
searchParams = {},
|
|
317
|
-
maxCandidates = 5,
|
|
318
|
-
detailLimit = null,
|
|
319
|
-
closeDetail = true,
|
|
320
|
-
delayMs = 0,
|
|
321
|
-
cardTimeoutMs = 90000,
|
|
322
|
-
resetBeforeSearch = true,
|
|
323
|
-
resetTimeoutMs = 180000,
|
|
324
|
-
cityOptionTimeoutMs = 30000,
|
|
325
|
-
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
326
|
-
imageWheelDeltaY = 650,
|
|
327
|
-
cvAcquisitionMode = "unknown",
|
|
328
|
-
listMaxScrolls = 20,
|
|
329
|
-
listStableSignatureLimit = 5,
|
|
330
|
-
listWheelDeltaY = 850,
|
|
331
|
-
listSettleMs = 2200,
|
|
332
|
-
listFallbackPoint = null,
|
|
333
|
-
refreshOnEnd = true,
|
|
334
|
-
maxRefreshRounds = 2,
|
|
335
|
-
refreshResetSettleMs = 5000,
|
|
336
|
-
screeningMode = "llm",
|
|
337
|
-
llmConfig = null,
|
|
338
|
-
llmTimeoutMs = 120000,
|
|
339
|
-
llmImageLimit = 8,
|
|
340
|
-
llmImageDetail = "high",
|
|
341
|
-
imageOutputDir = "",
|
|
342
|
-
humanRestEnabled = false,
|
|
343
|
-
humanBehavior = null
|
|
344
|
-
} = {}, runControl) {
|
|
345
|
-
if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
|
|
346
|
-
const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
|
|
347
|
-
legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
|
|
348
|
-
});
|
|
349
|
-
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
350
|
-
configureHumanInteraction(client, {
|
|
351
|
-
enabled: effectiveHumanBehavior.enabled,
|
|
352
|
-
clickMovementEnabled: effectiveHumanBehavior.clickMovement,
|
|
353
|
-
textEntryEnabled: effectiveHumanBehavior.textEntry,
|
|
354
|
-
safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
|
|
355
|
-
actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
|
|
356
|
-
});
|
|
357
|
-
const humanRestController = createHumanRestController({
|
|
358
|
-
enabled: effectiveHumanRestEnabled,
|
|
359
|
-
shortRestEnabled: effectiveHumanBehavior.shortRest,
|
|
360
|
-
batchRestEnabled: effectiveHumanBehavior.batchRest
|
|
361
|
-
});
|
|
362
|
-
const normalizedSearchParams = normalizeSearchParams(searchParams);
|
|
363
|
-
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
364
|
-
const useLlmScreening = normalizedScreeningMode !== "deterministic";
|
|
365
|
-
const limit = Math.max(1, Number(maxCandidates) || 1);
|
|
366
|
-
const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
|
|
367
|
-
const networkRecorder = detailCountLimit > 0
|
|
368
|
-
? createRecruitDetailNetworkRecorder(client)
|
|
369
|
-
: null;
|
|
370
|
-
const cvAcquisitionState = createCvAcquisitionState({ mode: cvAcquisitionMode });
|
|
371
|
-
const listState = createInfiniteListState({
|
|
372
|
-
domain: "recruit",
|
|
373
|
-
listName: "search-results"
|
|
374
|
-
});
|
|
375
|
-
const viewportGuard = createViewportRunGuard({
|
|
376
|
-
client,
|
|
377
|
-
domain: "recruit",
|
|
378
|
-
root: "frame",
|
|
379
|
-
frameOwnerRoot: "frameOwner",
|
|
380
|
-
runControl,
|
|
381
|
-
getRoots: getRecruitRoots
|
|
382
|
-
});
|
|
383
|
-
async function ensureRecruitViewport(rootState, phase) {
|
|
384
|
-
const result = await viewportGuard.ensure(rootState, { phase });
|
|
385
|
-
return result.rootState || rootState;
|
|
386
|
-
}
|
|
387
|
-
const results = [];
|
|
388
|
-
const refreshAttempts = [];
|
|
389
|
-
let refreshRounds = 0;
|
|
390
|
-
let contextRecoveryAttempts = 0;
|
|
391
|
-
const candidateRecoveryCounts = new Map();
|
|
392
|
-
let cardNodeIds = [];
|
|
393
|
-
let listEndReason = "";
|
|
394
|
-
let lastHumanEvent = null;
|
|
395
|
-
const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
|
|
396
|
-
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
397
|
-
containerSelectors: RECRUIT_LIST_CONTAINER_SELECTORS,
|
|
398
|
-
itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
|
|
399
|
-
itemSelectors: [RECRUIT_CARD_SELECTOR],
|
|
400
|
-
viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
|
|
401
|
-
validateViewportPoint: true
|
|
402
|
-
}));
|
|
403
|
-
|
|
404
|
-
function recordHumanEvent(event = null) {
|
|
405
|
-
if (!event) return lastHumanEvent;
|
|
406
|
-
lastHumanEvent = {
|
|
407
|
-
at: new Date().toISOString(),
|
|
408
|
-
...event
|
|
409
|
-
};
|
|
410
|
-
return lastHumanEvent;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
async function maybeHumanActionCooldown(phase, timings = {}) {
|
|
414
|
-
if (!effectiveHumanBehavior.actionCooldown) return null;
|
|
415
|
-
const pauseMs = humanDelay(280, 90, {
|
|
416
|
-
minMs: 80,
|
|
417
|
-
maxMs: 720
|
|
418
|
-
});
|
|
419
|
-
if (pauseMs > 0) {
|
|
420
|
-
await runControl.sleep(pauseMs);
|
|
421
|
-
addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
|
|
422
|
-
}
|
|
423
|
-
return recordHumanEvent({
|
|
424
|
-
kind: "action_cooldown",
|
|
425
|
-
phase,
|
|
426
|
-
pause_ms: pauseMs
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function updateRecruitProgress(extra = {}) {
|
|
431
|
-
const counts = countRecruitResultStatuses(results);
|
|
432
|
-
const listSnapshot = compactInfiniteListState(listState);
|
|
433
|
-
const humanRestState = humanRestController.getState();
|
|
434
|
-
runControl.updateProgress({
|
|
435
|
-
card_count: cardNodeIds.length,
|
|
436
|
-
target_count: limit,
|
|
437
|
-
...counts,
|
|
438
|
-
screening_mode: normalizedScreeningMode,
|
|
439
|
-
unique_seen: listSnapshot.seen_count,
|
|
440
|
-
scroll_count: listSnapshot.scroll_count,
|
|
441
|
-
refresh_rounds: refreshRounds,
|
|
442
|
-
refresh_attempts: refreshAttempts.length,
|
|
443
|
-
context_recoveries: contextRecoveryAttempts,
|
|
444
|
-
list_end_reason: listEndReason || null,
|
|
445
|
-
viewport_checks: viewportGuard.getStats().checks,
|
|
446
|
-
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
447
|
-
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
448
|
-
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
449
|
-
human_rest_enabled: effectiveHumanRestEnabled,
|
|
450
|
-
human_rest_count: humanRestState.rest_count,
|
|
451
|
-
human_rest_ms: humanRestState.total_rest_ms,
|
|
452
|
-
last_human_event: lastHumanEvent,
|
|
453
|
-
...extra
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function checkpointInProgressCandidate({
|
|
458
|
-
index = results.length,
|
|
459
|
-
candidateKey = "",
|
|
460
|
-
cardNodeId = null,
|
|
461
|
-
detailStep = "",
|
|
462
|
-
error = null
|
|
463
|
-
} = {}) {
|
|
464
|
-
runControl.checkpoint({
|
|
465
|
-
in_progress_candidate: {
|
|
466
|
-
index,
|
|
467
|
-
key: candidateKey,
|
|
468
|
-
card_node_id: cardNodeId,
|
|
469
|
-
detail_step: detailStep || null,
|
|
470
|
-
counters: countRecruitResultStatuses(results),
|
|
471
|
-
error: compactError(error, "RECRUIT_IN_PROGRESS_ERROR")
|
|
472
|
-
},
|
|
473
|
-
candidate_list: compactInfiniteListState(listState)
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
async function recoverAndReapplyRecruitContext(reason = "context_recovery", error = null, {
|
|
478
|
-
forceRecentViewed = true
|
|
479
|
-
} = {}) {
|
|
480
|
-
await runControl.waitIfPaused();
|
|
481
|
-
runControl.throwIfCanceled();
|
|
482
|
-
const started = Date.now();
|
|
483
|
-
runControl.setPhase("recruit:recover-context");
|
|
484
|
-
contextRecoveryAttempts += 1;
|
|
485
|
-
const refreshResult = await refreshRecruitSearchAtEnd(client, {
|
|
486
|
-
searchParams: normalizedSearchParams,
|
|
487
|
-
requireCards: true,
|
|
488
|
-
searchTimeoutMs: cardTimeoutMs,
|
|
489
|
-
resetTimeoutMs,
|
|
490
|
-
resetSettleMs: refreshResetSettleMs,
|
|
491
|
-
cityOptionTimeoutMs,
|
|
492
|
-
forceRecentViewed
|
|
493
|
-
});
|
|
494
|
-
const compactRefresh = {
|
|
495
|
-
...compactRefreshAttempt(refreshResult),
|
|
496
|
-
context_recovery: true,
|
|
497
|
-
recovery_reason: reason,
|
|
498
|
-
trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
|
|
499
|
-
elapsed_ms: Date.now() - started
|
|
500
|
-
};
|
|
501
|
-
refreshAttempts.push(compactRefresh);
|
|
502
|
-
runControl.checkpoint({
|
|
503
|
-
context_recovery: {
|
|
504
|
-
attempt: contextRecoveryAttempts,
|
|
505
|
-
reason,
|
|
506
|
-
trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
|
|
507
|
-
refresh: compactRefresh,
|
|
508
|
-
counters: countRecruitResultStatuses(results)
|
|
509
|
-
},
|
|
510
|
-
candidate_list: compactInfiniteListState(listState)
|
|
511
|
-
});
|
|
512
|
-
if (!refreshResult.ok) {
|
|
513
|
-
updateRecruitProgress({
|
|
514
|
-
refresh_method: refreshResult.method || null,
|
|
515
|
-
refresh_forced_recent_viewed: forceRecentViewed,
|
|
516
|
-
recovery_reason: reason
|
|
517
|
-
});
|
|
518
|
-
throw new Error(`Recruit context recovery failed after ${reason}: ${refreshResult.application?.reason || "refresh returned no cards"}`);
|
|
519
|
-
}
|
|
520
|
-
rootState = await getRecruitRoots(client);
|
|
521
|
-
rootState = await ensureRecruitViewport(rootState, "recover_after");
|
|
522
|
-
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
523
|
-
timeoutMs: cardTimeoutMs,
|
|
524
|
-
intervalMs: 300
|
|
525
|
-
});
|
|
526
|
-
resetInfiniteListForRefreshRound(listState, {
|
|
527
|
-
reason: `context_recovery:${reason}`,
|
|
528
|
-
round: contextRecoveryAttempts,
|
|
529
|
-
method: refreshResult.method,
|
|
530
|
-
metadata: {
|
|
531
|
-
card_count: cardNodeIds.length,
|
|
532
|
-
forced_recent_viewed: forceRecentViewed,
|
|
533
|
-
counters: countRecruitResultStatuses(results)
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
listEndReason = "";
|
|
537
|
-
updateRecruitProgress({
|
|
538
|
-
card_count: cardNodeIds.length,
|
|
539
|
-
refresh_method: refreshResult.method || null,
|
|
540
|
-
refresh_forced_recent_viewed: forceRecentViewed,
|
|
541
|
-
recovery_reason: reason
|
|
542
|
-
});
|
|
543
|
-
return refreshResult;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
runControl.setPhase("recruit:cleanup");
|
|
547
|
-
await closeRecruitDetail(client, { attemptsLimit: 2 });
|
|
548
|
-
|
|
549
|
-
await runControl.waitIfPaused();
|
|
550
|
-
runControl.throwIfCanceled();
|
|
551
|
-
runControl.setPhase("recruit:roots");
|
|
552
|
-
let rootState = await getRecruitRoots(client);
|
|
553
|
-
rootState = await ensureRecruitViewport(rootState, "roots");
|
|
554
|
-
runControl.checkpoint({
|
|
555
|
-
iframe_selector: rootState.iframe.selector,
|
|
556
|
-
iframe_document_node_id: rootState.iframe.documentNodeId,
|
|
557
|
-
search_params: normalizedSearchParams
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
if (hasRecruitSearchParams(normalizedSearchParams)) {
|
|
561
|
-
await runControl.waitIfPaused();
|
|
562
|
-
runControl.throwIfCanceled();
|
|
563
|
-
runControl.setPhase("recruit:search");
|
|
564
|
-
const searchResult = await applyRecruitSearchParams(client, {
|
|
565
|
-
searchParams: normalizedSearchParams,
|
|
566
|
-
requireCards: true,
|
|
567
|
-
resetBeforeApply: resetBeforeSearch,
|
|
568
|
-
searchTimeoutMs: cardTimeoutMs,
|
|
569
|
-
resetTimeoutMs,
|
|
570
|
-
cityOptionTimeoutMs
|
|
571
|
-
});
|
|
572
|
-
runControl.checkpoint({
|
|
573
|
-
search: {
|
|
574
|
-
search_params: searchResult.search_params,
|
|
575
|
-
before_counts: searchResult.before_counts,
|
|
576
|
-
post_search_state: searchResult.post_search_state,
|
|
577
|
-
steps: searchResult.steps.map((step) => ({
|
|
578
|
-
step: step.step,
|
|
579
|
-
applied: step.result?.applied,
|
|
580
|
-
clicked: step.result?.clicked,
|
|
581
|
-
searched: step.result?.searched,
|
|
582
|
-
reason: step.result?.reason || null
|
|
583
|
-
}))
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
rootState = await getRecruitRoots(client);
|
|
587
|
-
rootState = await ensureRecruitViewport(rootState, "search");
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
await runControl.waitIfPaused();
|
|
591
|
-
runControl.throwIfCanceled();
|
|
592
|
-
runControl.setPhase("recruit:cards");
|
|
593
|
-
rootState = await ensureRecruitViewport(rootState, "cards");
|
|
594
|
-
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
595
|
-
timeoutMs: cardTimeoutMs,
|
|
596
|
-
intervalMs: 300
|
|
597
|
-
});
|
|
598
|
-
if (!cardNodeIds.length) {
|
|
599
|
-
throw new Error("No recruit/search candidate cards found for run service");
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
updateRecruitProgress({
|
|
603
|
-
list_end_reason: null
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
while (results.length < limit) {
|
|
607
|
-
const candidateStarted = Date.now();
|
|
608
|
-
const timings = {};
|
|
609
|
-
await runControl.waitIfPaused();
|
|
610
|
-
runControl.throwIfCanceled();
|
|
611
|
-
runControl.setPhase("recruit:candidate");
|
|
612
|
-
rootState = await ensureRecruitViewport(rootState, "candidate_loop");
|
|
613
|
-
|
|
614
|
-
const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
|
|
615
|
-
client,
|
|
616
|
-
state: listState,
|
|
617
|
-
maxScrolls: listMaxScrolls,
|
|
618
|
-
stableSignatureLimit: listStableSignatureLimit,
|
|
619
|
-
wheelDeltaY: listWheelDeltaY,
|
|
620
|
-
settleMs: listSettleMs,
|
|
621
|
-
listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
622
|
-
fallbackPoint: listFallbackResolver,
|
|
623
|
-
findNodeIds: async () => {
|
|
624
|
-
let currentRootState = await getRecruitRoots(client);
|
|
625
|
-
currentRootState = await ensureRecruitViewport(currentRootState, "candidate_find_nodes");
|
|
626
|
-
rootState = currentRootState;
|
|
627
|
-
const currentCardNodeIds = await waitForRecruitCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
628
|
-
timeoutMs: Math.min(cardTimeoutMs, 5000),
|
|
629
|
-
intervalMs: 300
|
|
630
|
-
});
|
|
631
|
-
cardNodeIds = currentCardNodeIds;
|
|
632
|
-
return currentCardNodeIds;
|
|
633
|
-
},
|
|
634
|
-
readCandidate: async (nodeId, { visibleIndex }) => readRecruitCardCandidate(client, nodeId, {
|
|
635
|
-
targetUrl,
|
|
636
|
-
source: "recruit-run-card",
|
|
637
|
-
metadata: {
|
|
638
|
-
run_candidate_index: results.length,
|
|
639
|
-
visible_index: visibleIndex,
|
|
640
|
-
search_params: normalizedSearchParams
|
|
641
|
-
}
|
|
642
|
-
}),
|
|
643
|
-
detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
|
|
644
|
-
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
645
|
-
markerSelectors: RECRUIT_BOTTOM_MARKER_SELECTORS,
|
|
646
|
-
refreshSelectors: RECRUIT_BOTTOM_REFRESH_SELECTORS,
|
|
647
|
-
textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
|
|
648
|
-
maxTextScanNodes: 500
|
|
649
|
-
})
|
|
650
|
-
}));
|
|
651
|
-
if (!nextCandidateResult.ok) {
|
|
652
|
-
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
653
|
-
if (
|
|
654
|
-
(nextCandidateResult.end_reached || isRefreshableListStall(nextCandidateResult.reason))
|
|
655
|
-
&& refreshOnEnd
|
|
656
|
-
&& results.length < limit
|
|
657
|
-
&& refreshRounds < Math.max(0, Number(maxRefreshRounds) || 0)
|
|
658
|
-
) {
|
|
659
|
-
await runControl.waitIfPaused();
|
|
660
|
-
runControl.throwIfCanceled();
|
|
661
|
-
runControl.setPhase("recruit:refresh");
|
|
662
|
-
refreshRounds += 1;
|
|
663
|
-
const refreshResult = await refreshRecruitSearchAtEnd(client, {
|
|
664
|
-
searchParams: normalizedSearchParams,
|
|
665
|
-
requireCards: true,
|
|
666
|
-
searchTimeoutMs: cardTimeoutMs,
|
|
667
|
-
resetTimeoutMs,
|
|
668
|
-
resetSettleMs: refreshResetSettleMs,
|
|
669
|
-
cityOptionTimeoutMs
|
|
670
|
-
});
|
|
671
|
-
const compactRefresh = compactRefreshAttempt(refreshResult);
|
|
672
|
-
refreshAttempts.push(compactRefresh);
|
|
673
|
-
runControl.checkpoint({
|
|
674
|
-
refresh_round: refreshRounds,
|
|
675
|
-
refresh: compactRefresh
|
|
676
|
-
});
|
|
677
|
-
updateRecruitProgress({
|
|
678
|
-
card_count: refreshResult.card_count || cardNodeIds.length,
|
|
679
|
-
refresh_method: refreshResult.method || null,
|
|
680
|
-
refresh_forced_recent_viewed: true,
|
|
681
|
-
list_end_reason: listEndReason
|
|
682
|
-
});
|
|
683
|
-
if (refreshResult.ok) {
|
|
684
|
-
rootState = await getRecruitRoots(client);
|
|
685
|
-
rootState = await ensureRecruitViewport(rootState, "refresh_after");
|
|
686
|
-
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
687
|
-
timeoutMs: cardTimeoutMs,
|
|
688
|
-
intervalMs: 300
|
|
689
|
-
});
|
|
690
|
-
resetInfiniteListForRefreshRound(listState, {
|
|
691
|
-
reason: listEndReason,
|
|
692
|
-
round: refreshRounds,
|
|
693
|
-
method: refreshResult.method,
|
|
694
|
-
metadata: {
|
|
695
|
-
card_count: cardNodeIds.length,
|
|
696
|
-
forced_recent_viewed: true
|
|
697
|
-
}
|
|
698
|
-
});
|
|
699
|
-
listEndReason = "";
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
throw createRecruitRefreshFailureError(compactRefresh, {
|
|
703
|
-
listEndReason,
|
|
704
|
-
targetCount: limit,
|
|
705
|
-
processedCount: results.length
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
break;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
const index = results.length;
|
|
712
|
-
const cardNodeId = nextCandidateResult.item.node_id;
|
|
713
|
-
const candidateKey = nextCandidateResult.item.key;
|
|
714
|
-
const cardCandidate = nextCandidateResult.item.candidate;
|
|
715
|
-
|
|
716
|
-
let screeningCandidate = cardCandidate;
|
|
717
|
-
let detailResult = null;
|
|
718
|
-
let recoverableDetailError = null;
|
|
719
|
-
let detailStep = "not_started";
|
|
720
|
-
if (index < detailCountLimit) {
|
|
721
|
-
try {
|
|
722
|
-
await runControl.waitIfPaused();
|
|
723
|
-
runControl.throwIfCanceled();
|
|
724
|
-
runControl.setPhase("recruit:detail");
|
|
725
|
-
detailStep = "ensure_viewport";
|
|
726
|
-
rootState = await ensureRecruitViewport(rootState, "detail");
|
|
727
|
-
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
728
|
-
detailStep = "open_detail";
|
|
729
|
-
networkRecorder.clear();
|
|
730
|
-
await maybeHumanActionCooldown("before_detail_open", timings);
|
|
731
|
-
const openedDetail = await openRecruitCardDetail(client, cardNodeId);
|
|
732
|
-
addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
|
|
733
|
-
addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
|
|
734
|
-
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
735
|
-
detailStep = "wait_network";
|
|
736
|
-
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
737
|
-
waitForRecruitDetailNetworkEvents,
|
|
738
|
-
networkRecorder,
|
|
739
|
-
{
|
|
740
|
-
waitPlan,
|
|
741
|
-
minCount: 1,
|
|
742
|
-
requireLoaded: true,
|
|
743
|
-
intervalMs: 120
|
|
744
|
-
}
|
|
745
|
-
));
|
|
746
|
-
if (networkWait?.elapsed_ms != null) {
|
|
747
|
-
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
748
|
-
}
|
|
749
|
-
detailStep = "extract_detail";
|
|
750
|
-
detailResult = await extractRecruitDetailCandidate(client, {
|
|
751
|
-
cardCandidate,
|
|
752
|
-
cardNodeId,
|
|
753
|
-
detailState: openedDetail.detail_state,
|
|
754
|
-
networkEvents: networkRecorder.events,
|
|
755
|
-
targetUrl,
|
|
756
|
-
closeDetail: false,
|
|
757
|
-
networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
|
|
758
|
-
networkParseIntervalMs: 250
|
|
759
|
-
});
|
|
760
|
-
addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
|
|
761
|
-
const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
762
|
-
let source = "network";
|
|
763
|
-
let imageEvidence = null;
|
|
764
|
-
let captureTarget = null;
|
|
765
|
-
let captureTargetWait = null;
|
|
766
|
-
if (parsedNetworkProfileCount > 0) {
|
|
767
|
-
recordCvNetworkHit(cvAcquisitionState, {
|
|
768
|
-
parsedNetworkProfileCount,
|
|
769
|
-
waitResult: networkWait
|
|
770
|
-
});
|
|
771
|
-
} else {
|
|
772
|
-
detailStep = "wait_capture_target";
|
|
773
|
-
captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
|
|
774
|
-
domain: "recruit",
|
|
775
|
-
timeoutMs: 6000,
|
|
776
|
-
intervalMs: 250
|
|
777
|
-
});
|
|
778
|
-
captureTarget = captureTargetWait.target || null;
|
|
779
|
-
const captureNodeId = captureTarget?.node_id || null;
|
|
780
|
-
if (captureNodeId) {
|
|
781
|
-
const imageEvidencePath = imageEvidenceFilePath({
|
|
782
|
-
imageOutputDir,
|
|
783
|
-
domain: "recruit",
|
|
784
|
-
runId: runControl?.runId,
|
|
785
|
-
index,
|
|
786
|
-
extension: "jpg"
|
|
787
|
-
});
|
|
788
|
-
try {
|
|
789
|
-
detailStep = "capture_image";
|
|
790
|
-
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
791
|
-
filePath: imageEvidencePath,
|
|
792
|
-
format: "jpeg",
|
|
793
|
-
quality: 72,
|
|
794
|
-
optimize: true,
|
|
795
|
-
resizeMaxWidth: 1100,
|
|
796
|
-
captureViewport: false,
|
|
797
|
-
padding: 0,
|
|
798
|
-
maxScreenshots: maxImagePages,
|
|
799
|
-
wheelDeltaY: imageWheelDeltaY,
|
|
800
|
-
settleMs: 350,
|
|
801
|
-
scrollMethod: "dom-anchor-fallback-input",
|
|
802
|
-
scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
803
|
-
stepTimeoutMs: 45000,
|
|
804
|
-
totalTimeoutMs: 90000,
|
|
805
|
-
duplicateStopCount: 1,
|
|
806
|
-
skipDuplicateScreenshots: true,
|
|
807
|
-
composeForLlm: true,
|
|
808
|
-
llmPagesPerImage: 3,
|
|
809
|
-
llmResizeMaxWidth: 1100,
|
|
810
|
-
llmQuality: 72,
|
|
811
|
-
metadata: {
|
|
812
|
-
domain: "recruit",
|
|
813
|
-
capture_mode: "scroll_sequence",
|
|
814
|
-
acquisition_reason: "network_miss_image_fallback",
|
|
815
|
-
run_candidate_index: index,
|
|
816
|
-
candidate_key: candidateKey,
|
|
817
|
-
capture_target: captureTarget,
|
|
818
|
-
capture_target_wait: captureTargetWait
|
|
819
|
-
}
|
|
820
|
-
}));
|
|
821
|
-
source = "image";
|
|
822
|
-
} catch (error) {
|
|
823
|
-
if (!isRecoverableRecruitImageCaptureError(error)) throw error;
|
|
824
|
-
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
825
|
-
if (recoveryCount < 1) {
|
|
826
|
-
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
827
|
-
timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
|
|
828
|
-
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
829
|
-
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
830
|
-
await recoverAndReapplyRecruitContext(`image_capture:${detailStep}`, error, {
|
|
831
|
-
forceRecentViewed: true
|
|
832
|
-
});
|
|
833
|
-
continue;
|
|
834
|
-
}
|
|
835
|
-
imageEvidence = createRecoverableRecruitImageCaptureEvidence(error, {
|
|
836
|
-
elapsedMs: timings.screenshot_capture_ms,
|
|
837
|
-
filePath: imageEvidencePath,
|
|
838
|
-
extension: "jpg",
|
|
839
|
-
maxScreenshots: maxImagePages
|
|
840
|
-
});
|
|
841
|
-
source = "image_capture_failed";
|
|
842
|
-
}
|
|
843
|
-
recordCvImageFallback(cvAcquisitionState, {
|
|
844
|
-
reason: source === "image_capture_failed"
|
|
845
|
-
? "network_miss_image_capture_failed"
|
|
846
|
-
: "network_miss_image_fallback",
|
|
847
|
-
parsedNetworkProfileCount,
|
|
848
|
-
waitResult: networkWait,
|
|
849
|
-
imageEvidence
|
|
850
|
-
});
|
|
851
|
-
} else {
|
|
852
|
-
source = "missing_capture_node";
|
|
853
|
-
recordCvNetworkMiss(cvAcquisitionState, {
|
|
854
|
-
reason: "network_miss_no_capture_node",
|
|
855
|
-
parsedNetworkProfileCount,
|
|
856
|
-
waitResult: networkWait
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
detailResult.image_evidence = imageEvidence;
|
|
862
|
-
detailResult.cv_acquisition = {
|
|
863
|
-
source,
|
|
864
|
-
mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
|
|
865
|
-
wait_plan: waitPlan,
|
|
866
|
-
network_wait: networkWait,
|
|
867
|
-
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
868
|
-
image_evidence: summarizeImageEvidence(imageEvidence),
|
|
869
|
-
capture_target: captureTarget || null,
|
|
870
|
-
capture_target_wait: captureTargetWait
|
|
871
|
-
};
|
|
872
|
-
screeningCandidate = detailResult.candidate;
|
|
873
|
-
if (closeDetail) {
|
|
874
|
-
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
|
|
875
|
-
await maybeHumanActionCooldown("after_detail_close", timings);
|
|
876
|
-
if (!detailResult.close_result?.closed) {
|
|
877
|
-
const closeError = createRecruitCloseFailureError(detailResult.close_result);
|
|
878
|
-
const recovery = await recoverAndReapplyRecruitContext("detail_close_failed", closeError, {
|
|
879
|
-
forceRecentViewed: true
|
|
880
|
-
});
|
|
881
|
-
detailResult.cv_acquisition = {
|
|
882
|
-
...(detailResult.cv_acquisition || {}),
|
|
883
|
-
close_recovery: {
|
|
884
|
-
ok: Boolean(recovery.ok),
|
|
885
|
-
method: recovery.method || "",
|
|
886
|
-
forced_recent_viewed: Boolean(recovery.forced_recent_viewed),
|
|
887
|
-
card_count: recovery.card_count || 0
|
|
888
|
-
}
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
} else {
|
|
892
|
-
detailResult.close_result = null;
|
|
893
|
-
}
|
|
894
|
-
} catch (error) {
|
|
895
|
-
if (!isRecoverableRecruitDetailError(error)) throw error;
|
|
896
|
-
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
897
|
-
if (recoveryCount < 1) {
|
|
898
|
-
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
899
|
-
timings.detail_recovery_trigger = compactRecoverableDetailError(error);
|
|
900
|
-
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
901
|
-
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
902
|
-
await recoverAndReapplyRecruitContext(`detail:${detailStep}`, error, {
|
|
903
|
-
forceRecentViewed: true
|
|
904
|
-
});
|
|
905
|
-
continue;
|
|
906
|
-
}
|
|
907
|
-
recoverableDetailError = error;
|
|
908
|
-
detailResult = null;
|
|
909
|
-
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
910
|
-
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
await runControl.waitIfPaused();
|
|
915
|
-
runControl.throwIfCanceled();
|
|
916
|
-
runControl.setPhase("recruit:screening");
|
|
917
|
-
let llmResult = null;
|
|
918
|
-
if (useLlmScreening) {
|
|
919
|
-
if (recoverableDetailError || detailResult?.image_evidence?.ok === false) {
|
|
920
|
-
llmResult = null;
|
|
921
|
-
} else if (!llmConfig) {
|
|
922
|
-
llmResult = createMissingLlmConfigResult();
|
|
923
|
-
} else {
|
|
924
|
-
try {
|
|
925
|
-
const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
|
|
926
|
-
? "vision_model_ms"
|
|
927
|
-
: "text_model_ms";
|
|
928
|
-
llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
|
|
929
|
-
candidate: screeningCandidate,
|
|
930
|
-
criteria,
|
|
931
|
-
config: llmConfig,
|
|
932
|
-
timeoutMs: llmTimeoutMs,
|
|
933
|
-
imageEvidence: detailResult?.image_evidence || null,
|
|
934
|
-
maxImages: llmImageLimit,
|
|
935
|
-
imageDetail: llmImageDetail
|
|
936
|
-
}));
|
|
937
|
-
} catch (error) {
|
|
938
|
-
llmResult = createFailedLlmScreeningResult(error);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
if (detailResult) detailResult.llm_result = llmResult;
|
|
942
|
-
}
|
|
943
|
-
const screening = recoverableDetailError
|
|
944
|
-
? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
|
|
945
|
-
: detailResult?.image_evidence?.ok === false
|
|
946
|
-
? createImageCaptureFailureScreening(screeningCandidate, {
|
|
947
|
-
code: detailResult.image_evidence.error_code,
|
|
948
|
-
message: detailResult.image_evidence.error
|
|
949
|
-
})
|
|
950
|
-
: useLlmScreening
|
|
951
|
-
? llmResultToScreening(llmResult, screeningCandidate)
|
|
952
|
-
: screenCandidate(screeningCandidate, { criteria });
|
|
953
|
-
timings.total_ms = Date.now() - candidateStarted;
|
|
954
|
-
const compactResult = {
|
|
955
|
-
index,
|
|
956
|
-
candidate_key: candidateKey,
|
|
957
|
-
card_node_id: cardNodeId,
|
|
958
|
-
candidate: compactCandidate(screeningCandidate),
|
|
959
|
-
detail: compactDetail(detailResult),
|
|
960
|
-
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
961
|
-
screening: compactScreening(screening),
|
|
962
|
-
error: recoverableDetailError
|
|
963
|
-
? compactRecoverableDetailError(recoverableDetailError)
|
|
964
|
-
: detailResult?.image_evidence?.ok === false
|
|
965
|
-
? compactError({
|
|
966
|
-
code: detailResult.image_evidence.error_code,
|
|
967
|
-
message: detailResult.image_evidence.error
|
|
968
|
-
}, "IMAGE_CAPTURE_FAILED")
|
|
969
|
-
: null,
|
|
970
|
-
timings
|
|
971
|
-
};
|
|
972
|
-
results.push(compactResult);
|
|
973
|
-
markInfiniteListCandidateProcessed(listState, candidateKey, {
|
|
974
|
-
metadata: {
|
|
975
|
-
result_index: index,
|
|
976
|
-
candidate_id: screeningCandidate.id || null
|
|
977
|
-
}
|
|
978
|
-
});
|
|
979
|
-
|
|
980
|
-
updateRecruitProgress({
|
|
981
|
-
last_candidate_id: screeningCandidate.id || null,
|
|
982
|
-
last_candidate_key: candidateKey,
|
|
983
|
-
last_score: screening.score
|
|
984
|
-
});
|
|
985
|
-
const checkpointStarted = Date.now();
|
|
986
|
-
runControl.checkpoint({
|
|
987
|
-
results,
|
|
988
|
-
last_candidate: {
|
|
989
|
-
id: screeningCandidate.id || null,
|
|
990
|
-
key: candidateKey,
|
|
991
|
-
identity: screeningCandidate.identity || {},
|
|
992
|
-
screening: {
|
|
993
|
-
status: screening.status,
|
|
994
|
-
passed: screening.passed,
|
|
995
|
-
score: screening.score
|
|
996
|
-
},
|
|
997
|
-
llm_screening: compactScreeningLlmResult(llmResult),
|
|
998
|
-
error: compactResult.error
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1001
|
-
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
1002
|
-
|
|
1003
|
-
if (effectiveHumanRestEnabled) {
|
|
1004
|
-
const restStarted = Date.now();
|
|
1005
|
-
const restResult = await humanRestController.takeBreakIfNeeded({
|
|
1006
|
-
sleepFn: (ms) => runControl.sleep(ms)
|
|
1007
|
-
});
|
|
1008
|
-
const restElapsed = Date.now() - restStarted;
|
|
1009
|
-
if (restResult.rested) {
|
|
1010
|
-
recordHumanEvent({
|
|
1011
|
-
kind: "rest",
|
|
1012
|
-
pause_ms: restResult.pause_ms || restElapsed,
|
|
1013
|
-
events: restResult.events || []
|
|
1014
|
-
});
|
|
1015
|
-
compactResult.human_rest = restResult;
|
|
1016
|
-
addTiming(compactResult.timings, "human_rest_ms", restElapsed);
|
|
1017
|
-
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
1018
|
-
updateRecruitProgress({
|
|
1019
|
-
human_rest_last: restResult
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
if (delayMs > 0) {
|
|
1025
|
-
const sleepStarted = Date.now();
|
|
1026
|
-
await runControl.sleep(delayMs);
|
|
1027
|
-
addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
|
|
1028
|
-
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
runControl.setPhase("recruit:done");
|
|
1033
|
-
return {
|
|
1034
|
-
domain: "recruit",
|
|
1035
|
-
target_url: targetUrl,
|
|
1036
|
-
search_params: normalizedSearchParams,
|
|
1037
|
-
card_count: cardNodeIds.length,
|
|
1038
|
-
candidate_list: compactInfiniteListState(listState),
|
|
1039
|
-
viewport_health: {
|
|
1040
|
-
stats: viewportGuard.getStats(),
|
|
1041
|
-
events: viewportGuard.getEvents()
|
|
1042
|
-
},
|
|
1043
|
-
human_behavior: effectiveHumanBehavior,
|
|
1044
|
-
human_rest: humanRestController.getState(),
|
|
1045
|
-
last_human_event: lastHumanEvent,
|
|
1046
|
-
list_end_reason: listEndReason || null,
|
|
1047
|
-
refresh_rounds: refreshRounds,
|
|
1048
|
-
refresh_attempts: refreshAttempts,
|
|
1049
|
-
context_recoveries: contextRecoveryAttempts,
|
|
1050
|
-
...countRecruitResultStatuses(results),
|
|
1051
|
-
results
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
export function createRecruitRunService({
|
|
1056
|
-
lifecycle,
|
|
1057
|
-
idPrefix = "recruit",
|
|
1058
|
-
workflow = runRecruitWorkflow,
|
|
1059
|
-
onSnapshot = null
|
|
1060
|
-
} = {}) {
|
|
1061
|
-
const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
|
|
1062
|
-
|
|
1063
|
-
function startRecruitRun({
|
|
1064
|
-
client,
|
|
1065
|
-
targetUrl = "",
|
|
1066
|
-
criteria = "",
|
|
1067
|
-
searchParams = {},
|
|
1068
|
-
maxCandidates = 5,
|
|
1069
|
-
detailLimit = null,
|
|
1070
|
-
closeDetail = true,
|
|
1071
|
-
delayMs = 0,
|
|
1072
|
-
cardTimeoutMs = 90000,
|
|
1073
|
-
resetBeforeSearch = true,
|
|
1074
|
-
resetTimeoutMs = 180000,
|
|
1075
|
-
cityOptionTimeoutMs = 30000,
|
|
1076
|
-
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
1077
|
-
imageWheelDeltaY = 650,
|
|
1078
|
-
cvAcquisitionMode = "unknown",
|
|
1079
|
-
listMaxScrolls = 20,
|
|
1080
|
-
listStableSignatureLimit = 5,
|
|
1081
|
-
listWheelDeltaY = 850,
|
|
1082
|
-
listSettleMs = 2200,
|
|
1083
|
-
listFallbackPoint = null,
|
|
1084
|
-
refreshOnEnd = true,
|
|
1085
|
-
maxRefreshRounds = 2,
|
|
1086
|
-
refreshResetSettleMs = 5000,
|
|
1087
|
-
screeningMode = "llm",
|
|
1088
|
-
llmConfig = null,
|
|
1089
|
-
llmTimeoutMs = 120000,
|
|
1090
|
-
llmImageLimit = 8,
|
|
1091
|
-
llmImageDetail = "high",
|
|
1092
|
-
imageOutputDir = "",
|
|
1093
|
-
humanRestEnabled = false,
|
|
1094
|
-
humanBehavior = null,
|
|
1095
|
-
name = "recruit-domain-run"
|
|
1096
|
-
} = {}) {
|
|
1097
|
-
if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
|
|
1098
|
-
const normalizedSearchParams = normalizeSearchParams(searchParams);
|
|
1099
|
-
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
1100
|
-
const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
|
|
1101
|
-
const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
|
|
1102
|
-
const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
|
|
1103
|
-
legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
|
|
1104
|
-
});
|
|
1105
|
-
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
1106
|
-
return manager.startRun({
|
|
1107
|
-
name,
|
|
1108
|
-
context: {
|
|
1109
|
-
domain: "recruit",
|
|
1110
|
-
target_url: targetUrl,
|
|
1111
|
-
criteria_present: Boolean(criteria),
|
|
1112
|
-
search_params: normalizedSearchParams,
|
|
1113
|
-
max_candidates: maxCandidates,
|
|
1114
|
-
detail_limit: normalizedDetailLimit,
|
|
1115
|
-
close_detail: closeDetail,
|
|
1116
|
-
reset_before_search: resetBeforeSearch,
|
|
1117
|
-
reset_timeout_ms: resetTimeoutMs,
|
|
1118
|
-
city_option_timeout_ms: cityOptionTimeoutMs,
|
|
1119
|
-
cv_acquisition_mode: cvAcquisitionMode,
|
|
1120
|
-
max_image_pages: maxImagePages,
|
|
1121
|
-
image_wheel_delta_y: imageWheelDeltaY,
|
|
1122
|
-
list_max_scrolls: listMaxScrolls,
|
|
1123
|
-
list_stable_signature_limit: listStableSignatureLimit,
|
|
1124
|
-
list_wheel_delta_y: listWheelDeltaY,
|
|
1125
|
-
list_settle_ms: listSettleMs,
|
|
1126
|
-
list_fallback_point: listFallbackPoint,
|
|
1127
|
-
refresh_on_end: refreshOnEnd,
|
|
1128
|
-
max_refresh_rounds: maxRefreshRounds,
|
|
1129
|
-
refresh_reset_settle_ms: refreshResetSettleMs,
|
|
1130
|
-
screening_mode: normalizedScreeningMode,
|
|
1131
|
-
llm_configured: Boolean(llmConfig),
|
|
1132
|
-
llm_timeout_ms: llmTimeoutMs,
|
|
1133
|
-
llm_image_limit: llmImageLimit,
|
|
1134
|
-
llm_image_detail: llmImageDetail,
|
|
1135
|
-
image_output_dir: imageOutputDir || "",
|
|
1136
|
-
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
1137
|
-
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1138
|
-
human_behavior: effectiveHumanBehavior,
|
|
1139
|
-
human_rest_enabled: effectiveHumanRestEnabled
|
|
1140
|
-
},
|
|
1141
|
-
progress: {
|
|
1142
|
-
card_count: 0,
|
|
1143
|
-
target_count: candidateLimit,
|
|
1144
|
-
processed: 0,
|
|
1145
|
-
screened: 0,
|
|
1146
|
-
detail_opened: 0,
|
|
1147
|
-
llm_screened: 0,
|
|
1148
|
-
passed: 0,
|
|
1149
|
-
image_capture_failed: 0,
|
|
1150
|
-
detail_open_failed: 0,
|
|
1151
|
-
transient_recovered: 0,
|
|
1152
|
-
context_recoveries: 0,
|
|
1153
|
-
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
1154
|
-
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1155
|
-
human_rest_enabled: effectiveHumanRestEnabled,
|
|
1156
|
-
human_rest_count: 0,
|
|
1157
|
-
human_rest_ms: 0,
|
|
1158
|
-
last_human_event: null
|
|
1159
|
-
},
|
|
1160
|
-
checkpoint: {},
|
|
1161
|
-
task: (runControl) => workflow({
|
|
1162
|
-
client,
|
|
1163
|
-
targetUrl,
|
|
1164
|
-
criteria,
|
|
1165
|
-
searchParams: normalizedSearchParams,
|
|
1166
|
-
maxCandidates,
|
|
1167
|
-
detailLimit: normalizedDetailLimit,
|
|
1168
|
-
closeDetail,
|
|
1169
|
-
delayMs,
|
|
1170
|
-
cardTimeoutMs,
|
|
1171
|
-
resetBeforeSearch,
|
|
1172
|
-
resetTimeoutMs,
|
|
1173
|
-
cityOptionTimeoutMs,
|
|
1174
|
-
maxImagePages,
|
|
1175
|
-
imageWheelDeltaY,
|
|
1176
|
-
cvAcquisitionMode,
|
|
1177
|
-
listMaxScrolls,
|
|
1178
|
-
listStableSignatureLimit,
|
|
1179
|
-
listWheelDeltaY,
|
|
1180
|
-
listSettleMs,
|
|
1181
|
-
listFallbackPoint,
|
|
1182
|
-
refreshOnEnd,
|
|
1183
|
-
maxRefreshRounds,
|
|
1184
|
-
refreshResetSettleMs,
|
|
1185
|
-
screeningMode: normalizedScreeningMode,
|
|
1186
|
-
llmConfig,
|
|
1187
|
-
llmTimeoutMs,
|
|
1188
|
-
llmImageLimit,
|
|
1189
|
-
llmImageDetail,
|
|
1190
|
-
imageOutputDir,
|
|
1191
|
-
humanRestEnabled: effectiveHumanRestEnabled,
|
|
1192
|
-
humanBehavior: effectiveHumanBehavior
|
|
1193
|
-
}, runControl)
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
return {
|
|
1198
|
-
startRecruitRun,
|
|
1199
|
-
getRecruitRun: manager.getRun,
|
|
1200
|
-
pauseRecruitRun: manager.pauseRun,
|
|
1201
|
-
resumeRecruitRun: manager.resumeRun,
|
|
1202
|
-
cancelRecruitRun: manager.cancelRun,
|
|
1203
|
-
waitForRecruitRun: manager.waitForRun,
|
|
1204
|
-
listRecruitRuns: manager.listRuns,
|
|
1205
|
-
manager
|
|
1206
|
-
};
|
|
1207
|
-
}
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createRunLifecycleManager } from "../../core/run/index.js";
|
|
4
|
+
import {
|
|
5
|
+
addTiming,
|
|
6
|
+
imageEvidenceFilePath,
|
|
7
|
+
measureTiming
|
|
8
|
+
} from "../../core/run/timing.js";
|
|
9
|
+
import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
|
|
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";
|
|
17
|
+
import {
|
|
18
|
+
compactCvAcquisitionState,
|
|
19
|
+
countParsedNetworkProfiles,
|
|
20
|
+
createCvAcquisitionState,
|
|
21
|
+
DEFAULT_MAX_IMAGE_PAGES,
|
|
22
|
+
getCvNetworkWaitPlan,
|
|
23
|
+
recordCvImageFallback,
|
|
24
|
+
recordCvNetworkHit,
|
|
25
|
+
recordCvNetworkMiss,
|
|
26
|
+
summarizeImageEvidence,
|
|
27
|
+
waitForCvNetworkEvents
|
|
28
|
+
} from "../../core/cv-acquisition/index.js";
|
|
29
|
+
import {
|
|
30
|
+
compactInfiniteListState,
|
|
31
|
+
createInfiniteListState,
|
|
32
|
+
detectInfiniteListBottomMarker,
|
|
33
|
+
getNextInfiniteListCandidate,
|
|
34
|
+
markInfiniteListCandidateProcessed,
|
|
35
|
+
resetInfiniteListForRefreshRound,
|
|
36
|
+
resolveInfiniteListFallbackPoint
|
|
37
|
+
} from "../../core/infinite-list/index.js";
|
|
38
|
+
import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
39
|
+
import {
|
|
40
|
+
callScreeningLlm,
|
|
41
|
+
compactScreeningLlmResult,
|
|
42
|
+
createFailedLlmScreeningResult,
|
|
43
|
+
llmResultToScreening,
|
|
44
|
+
screenCandidate
|
|
45
|
+
} from "../../core/screening/index.js";
|
|
46
|
+
import {
|
|
47
|
+
closeRecruitDetail,
|
|
48
|
+
createRecruitDetailNetworkRecorder,
|
|
49
|
+
extractRecruitDetailCandidate,
|
|
50
|
+
openRecruitCardDetail,
|
|
51
|
+
waitForRecruitDetailNetworkEvents
|
|
52
|
+
} from "./detail.js";
|
|
53
|
+
import {
|
|
54
|
+
readRecruitCardCandidate,
|
|
55
|
+
waitForRecruitCardNodeIds
|
|
56
|
+
} from "./cards.js";
|
|
57
|
+
import {
|
|
58
|
+
applyRecruitSearchParams,
|
|
59
|
+
hasRecruitSearchParams,
|
|
60
|
+
normalizeRecruitSearchParams
|
|
61
|
+
} from "./search.js";
|
|
62
|
+
import { refreshRecruitSearchAtEnd } from "./refresh.js";
|
|
63
|
+
import { getRecruitRoots } from "./roots.js";
|
|
64
|
+
import {
|
|
65
|
+
RECRUIT_BOTTOM_MARKER_SELECTORS,
|
|
66
|
+
RECRUIT_BOTTOM_REFRESH_SELECTORS,
|
|
67
|
+
RECRUIT_CARD_SELECTOR,
|
|
68
|
+
RECRUIT_LIST_CONTAINER_SELECTORS
|
|
69
|
+
} from "./constants.js";
|
|
70
|
+
|
|
71
|
+
function compactScreening(screening) {
|
|
72
|
+
return {
|
|
73
|
+
status: screening.status,
|
|
74
|
+
passed: screening.passed,
|
|
75
|
+
score: screening.score,
|
|
76
|
+
reasons: screening.reasons,
|
|
77
|
+
candidate: {
|
|
78
|
+
domain: screening.candidate?.domain || "recruit",
|
|
79
|
+
source: screening.candidate?.source || "",
|
|
80
|
+
id: screening.candidate?.id || null,
|
|
81
|
+
identity: screening.candidate?.identity || {}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function compactCandidate(candidate) {
|
|
87
|
+
return {
|
|
88
|
+
id: candidate?.id || null,
|
|
89
|
+
identity: candidate?.identity || {},
|
|
90
|
+
text_length: candidate?.text?.raw?.length || 0,
|
|
91
|
+
tag_count: candidate?.tags?.length || 0
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function compactDetail(detailResult) {
|
|
96
|
+
if (!detailResult) return null;
|
|
97
|
+
return {
|
|
98
|
+
popup_text_length: detailResult.detail?.popup_text?.length || 0,
|
|
99
|
+
resume_text_length: detailResult.detail?.resume_text?.length || 0,
|
|
100
|
+
network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
|
|
101
|
+
parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
|
|
102
|
+
cv_acquisition: detailResult.cv_acquisition || null,
|
|
103
|
+
image_evidence: summarizeImageEvidence(detailResult.image_evidence),
|
|
104
|
+
llm_screening: compactScreeningLlmResult(detailResult.llm_result),
|
|
105
|
+
close_result: detailResult.close_result
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeScreeningMode(value) {
|
|
110
|
+
const normalized = String(value || "llm").trim().toLowerCase();
|
|
111
|
+
return ["deterministic", "local", "local_scorer"].includes(normalized)
|
|
112
|
+
? "deterministic"
|
|
113
|
+
: "llm";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createMissingLlmConfigResult() {
|
|
117
|
+
return createFailedLlmScreeningResult(new Error("LLM screening config is required for production search runs"));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeSearchParams(searchParams = {}) {
|
|
121
|
+
return normalizeRecruitSearchParams(searchParams);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function compactRefreshAttempt(refreshAttempt) {
|
|
125
|
+
if (!refreshAttempt) return null;
|
|
126
|
+
return {
|
|
127
|
+
ok: Boolean(refreshAttempt.ok),
|
|
128
|
+
method: refreshAttempt.method || "",
|
|
129
|
+
forced_recent_viewed: Boolean(refreshAttempt.forced_recent_viewed),
|
|
130
|
+
card_count: refreshAttempt.card_count || 0,
|
|
131
|
+
search_params: refreshAttempt.search_params || null,
|
|
132
|
+
application: refreshAttempt.application
|
|
133
|
+
? {
|
|
134
|
+
applied: Boolean(refreshAttempt.application.applied),
|
|
135
|
+
post_search_state: refreshAttempt.application.post_search_state,
|
|
136
|
+
steps: (refreshAttempt.application.steps || []).map((step) => ({
|
|
137
|
+
step: step.step,
|
|
138
|
+
applied: step.result?.applied,
|
|
139
|
+
clicked: step.result?.clicked,
|
|
140
|
+
searched: step.result?.searched,
|
|
141
|
+
reason: step.result?.reason || null
|
|
142
|
+
}))
|
|
143
|
+
}
|
|
144
|
+
: null
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function compactError(error, fallbackCode = "RECRUIT_RUN_ERROR") {
|
|
149
|
+
if (!error) return null;
|
|
150
|
+
const result = {
|
|
151
|
+
code: error.code || fallbackCode,
|
|
152
|
+
message: error.message || String(error)
|
|
153
|
+
};
|
|
154
|
+
if (error.refresh_attempt) {
|
|
155
|
+
result.refresh_attempt = error.refresh_attempt;
|
|
156
|
+
}
|
|
157
|
+
if (error.list_end_reason) {
|
|
158
|
+
result.list_end_reason = error.list_end_reason;
|
|
159
|
+
}
|
|
160
|
+
if (error.target_count != null) {
|
|
161
|
+
result.target_count = error.target_count;
|
|
162
|
+
}
|
|
163
|
+
if (error.processed_count != null) {
|
|
164
|
+
result.processed_count = error.processed_count;
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createRecruitCloseFailureError(closeResult) {
|
|
170
|
+
const error = new Error(closeResult?.reason || "Recruit detail did not close before recovery");
|
|
171
|
+
error.code = "DETAIL_CLOSE_FAILED";
|
|
172
|
+
error.close_result = closeResult || null;
|
|
173
|
+
return error;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function createRecruitRefreshFailureError(refreshAttempt, {
|
|
177
|
+
listEndReason = "",
|
|
178
|
+
targetCount = 0,
|
|
179
|
+
processedCount = 0
|
|
180
|
+
} = {}) {
|
|
181
|
+
const reason = refreshAttempt?.application?.post_search_state?.ok === false
|
|
182
|
+
? "search_result_not_ready"
|
|
183
|
+
: refreshAttempt?.application?.post_search_state?.counts?.candidate_card === 0
|
|
184
|
+
? "no_cards_after_refresh"
|
|
185
|
+
: "refresh_failed";
|
|
186
|
+
const error = new Error(`Recruit/search refresh failed before target was reached (${reason})`);
|
|
187
|
+
error.code = "RECRUIT_END_REFRESH_FAILED";
|
|
188
|
+
error.refresh_attempt = refreshAttempt || null;
|
|
189
|
+
error.list_end_reason = listEndReason || null;
|
|
190
|
+
error.target_count = targetCount;
|
|
191
|
+
error.processed_count = processedCount;
|
|
192
|
+
return error;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isRefreshableListStall(reason = "") {
|
|
196
|
+
return new Set([
|
|
197
|
+
"stable_visible_signature",
|
|
198
|
+
"max_scrolls_exhausted",
|
|
199
|
+
"scroll_failed",
|
|
200
|
+
"scroll_anchor_unavailable"
|
|
201
|
+
]).has(String(reason || ""));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function isStaleRecruitNodeError(error) {
|
|
205
|
+
const message = String(error?.message || error || "");
|
|
206
|
+
return /Could not find node with given id|No node with given id|Node is detached|Cannot find node/i.test(message);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function isRecoverableRecruitImageCaptureError(error) {
|
|
210
|
+
const code = String(error?.code || "");
|
|
211
|
+
if (code === "IMAGE_CAPTURE_TIMEOUT" || code === "IMAGE_CAPTURE_TOTAL_TIMEOUT") return true;
|
|
212
|
+
if (isStaleRecruitNodeError(error)) return true;
|
|
213
|
+
return /Image fallback capture timed out/i.test(String(error?.message || error || ""));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function isRecoverableRecruitDetailError(error) {
|
|
217
|
+
return isStaleRecruitNodeError(error);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function compactRecoverableDetailError(error) {
|
|
221
|
+
return compactError(error, isStaleRecruitNodeError(error) ? "DETAIL_STALE_NODE" : "DETAIL_OPEN_FAILED");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function collectPartialImageEvidencePaths(basePath = "", extension = "jpg", maxCount = 12) {
|
|
225
|
+
const resolved = String(basePath || "").trim();
|
|
226
|
+
if (!resolved) return [];
|
|
227
|
+
const parsed = path.parse(resolved);
|
|
228
|
+
const ext = parsed.ext || `.${String(extension || "jpg").replace(/^\./, "") || "jpg"}`;
|
|
229
|
+
const files = [];
|
|
230
|
+
for (let index = 0; index < Math.max(1, Number(maxCount) || 1); index += 1) {
|
|
231
|
+
const page = String(index + 1).padStart(2, "0");
|
|
232
|
+
const candidatePath = path.join(parsed.dir, `${parsed.name}-page-${page}${ext}`);
|
|
233
|
+
if (fs.existsSync(candidatePath)) files.push(candidatePath);
|
|
234
|
+
}
|
|
235
|
+
return files;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function createRecoverableRecruitImageCaptureEvidence(error, {
|
|
239
|
+
elapsedMs = 0,
|
|
240
|
+
filePath = "",
|
|
241
|
+
extension = "jpg",
|
|
242
|
+
maxScreenshots = DEFAULT_MAX_IMAGE_PAGES
|
|
243
|
+
} = {}) {
|
|
244
|
+
const filePaths = collectPartialImageEvidencePaths(filePath, extension, maxScreenshots);
|
|
245
|
+
return {
|
|
246
|
+
schema_version: 1,
|
|
247
|
+
ok: false,
|
|
248
|
+
source: "image-scroll-sequence",
|
|
249
|
+
elapsed_ms: Math.max(0, Math.round(Number(error?.elapsed_ms ?? elapsedMs) || 0)),
|
|
250
|
+
capture_count: filePaths.length,
|
|
251
|
+
screenshot_count: filePaths.length,
|
|
252
|
+
unique_screenshot_count: filePaths.length,
|
|
253
|
+
dropped_duplicate_count: 0,
|
|
254
|
+
total_byte_length: 0,
|
|
255
|
+
original_total_byte_length: 0,
|
|
256
|
+
llm_screenshot_count: 0,
|
|
257
|
+
llm_total_byte_length: 0,
|
|
258
|
+
llm_original_total_byte_length: 0,
|
|
259
|
+
llm_composition_error: null,
|
|
260
|
+
error_code: error?.code || (isStaleRecruitNodeError(error) ? "IMAGE_CAPTURE_STALE_NODE" : "IMAGE_CAPTURE_FAILED"),
|
|
261
|
+
error: error?.message || String(error || "Image capture failed"),
|
|
262
|
+
file_paths: filePaths,
|
|
263
|
+
llm_file_paths: []
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function createImageCaptureFailureScreening(candidate, error) {
|
|
268
|
+
return {
|
|
269
|
+
status: "fail",
|
|
270
|
+
passed: false,
|
|
271
|
+
score: 0,
|
|
272
|
+
reasons: ["image_capture_failed"],
|
|
273
|
+
error: compactError(error, "IMAGE_CAPTURE_FAILED"),
|
|
274
|
+
candidate
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function createRecoverableDetailFailureScreening(candidate, error) {
|
|
279
|
+
return {
|
|
280
|
+
status: "fail",
|
|
281
|
+
passed: false,
|
|
282
|
+
score: 0,
|
|
283
|
+
reasons: isStaleRecruitNodeError(error)
|
|
284
|
+
? ["detail_open_failed", "stale_node"]
|
|
285
|
+
: ["detail_open_failed"],
|
|
286
|
+
error: compactRecoverableDetailError(error),
|
|
287
|
+
candidate
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function countRecruitResultStatuses(results = []) {
|
|
292
|
+
return {
|
|
293
|
+
processed: results.length,
|
|
294
|
+
screened: results.length,
|
|
295
|
+
detail_opened: results.filter((item) => item.detail).length,
|
|
296
|
+
passed: results.filter((item) => item.screening?.passed).length,
|
|
297
|
+
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
298
|
+
image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
|
|
299
|
+
detail_open_failed: results.filter((item) => (
|
|
300
|
+
item.error?.code === "DETAIL_STALE_NODE"
|
|
301
|
+
|| item.error?.code === "DETAIL_OPEN_FAILED"
|
|
302
|
+
)).length,
|
|
303
|
+
transient_recovered: results.filter((item) => (
|
|
304
|
+
item.error?.code === "DETAIL_STALE_NODE"
|
|
305
|
+
|| item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
|
|
306
|
+
|| item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
|
|
307
|
+
|| item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
|
|
308
|
+
)).length
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function runRecruitWorkflow({
|
|
313
|
+
client,
|
|
314
|
+
targetUrl = "",
|
|
315
|
+
criteria = "",
|
|
316
|
+
searchParams = {},
|
|
317
|
+
maxCandidates = 5,
|
|
318
|
+
detailLimit = null,
|
|
319
|
+
closeDetail = true,
|
|
320
|
+
delayMs = 0,
|
|
321
|
+
cardTimeoutMs = 90000,
|
|
322
|
+
resetBeforeSearch = true,
|
|
323
|
+
resetTimeoutMs = 180000,
|
|
324
|
+
cityOptionTimeoutMs = 30000,
|
|
325
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
326
|
+
imageWheelDeltaY = 650,
|
|
327
|
+
cvAcquisitionMode = "unknown",
|
|
328
|
+
listMaxScrolls = 20,
|
|
329
|
+
listStableSignatureLimit = 5,
|
|
330
|
+
listWheelDeltaY = 850,
|
|
331
|
+
listSettleMs = 2200,
|
|
332
|
+
listFallbackPoint = null,
|
|
333
|
+
refreshOnEnd = true,
|
|
334
|
+
maxRefreshRounds = 2,
|
|
335
|
+
refreshResetSettleMs = 5000,
|
|
336
|
+
screeningMode = "llm",
|
|
337
|
+
llmConfig = null,
|
|
338
|
+
llmTimeoutMs = 120000,
|
|
339
|
+
llmImageLimit = 8,
|
|
340
|
+
llmImageDetail = "high",
|
|
341
|
+
imageOutputDir = "",
|
|
342
|
+
humanRestEnabled = false,
|
|
343
|
+
humanBehavior = null
|
|
344
|
+
} = {}, runControl) {
|
|
345
|
+
if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
|
|
346
|
+
const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
|
|
347
|
+
legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
|
|
348
|
+
});
|
|
349
|
+
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
350
|
+
configureHumanInteraction(client, {
|
|
351
|
+
enabled: effectiveHumanBehavior.enabled,
|
|
352
|
+
clickMovementEnabled: effectiveHumanBehavior.clickMovement,
|
|
353
|
+
textEntryEnabled: effectiveHumanBehavior.textEntry,
|
|
354
|
+
safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
|
|
355
|
+
actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
|
|
356
|
+
});
|
|
357
|
+
const humanRestController = createHumanRestController({
|
|
358
|
+
enabled: effectiveHumanRestEnabled,
|
|
359
|
+
shortRestEnabled: effectiveHumanBehavior.shortRest,
|
|
360
|
+
batchRestEnabled: effectiveHumanBehavior.batchRest
|
|
361
|
+
});
|
|
362
|
+
const normalizedSearchParams = normalizeSearchParams(searchParams);
|
|
363
|
+
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
364
|
+
const useLlmScreening = normalizedScreeningMode !== "deterministic";
|
|
365
|
+
const limit = Math.max(1, Number(maxCandidates) || 1);
|
|
366
|
+
const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
|
|
367
|
+
const networkRecorder = detailCountLimit > 0
|
|
368
|
+
? createRecruitDetailNetworkRecorder(client)
|
|
369
|
+
: null;
|
|
370
|
+
const cvAcquisitionState = createCvAcquisitionState({ mode: cvAcquisitionMode });
|
|
371
|
+
const listState = createInfiniteListState({
|
|
372
|
+
domain: "recruit",
|
|
373
|
+
listName: "search-results"
|
|
374
|
+
});
|
|
375
|
+
const viewportGuard = createViewportRunGuard({
|
|
376
|
+
client,
|
|
377
|
+
domain: "recruit",
|
|
378
|
+
root: "frame",
|
|
379
|
+
frameOwnerRoot: "frameOwner",
|
|
380
|
+
runControl,
|
|
381
|
+
getRoots: getRecruitRoots
|
|
382
|
+
});
|
|
383
|
+
async function ensureRecruitViewport(rootState, phase) {
|
|
384
|
+
const result = await viewportGuard.ensure(rootState, { phase });
|
|
385
|
+
return result.rootState || rootState;
|
|
386
|
+
}
|
|
387
|
+
const results = [];
|
|
388
|
+
const refreshAttempts = [];
|
|
389
|
+
let refreshRounds = 0;
|
|
390
|
+
let contextRecoveryAttempts = 0;
|
|
391
|
+
const candidateRecoveryCounts = new Map();
|
|
392
|
+
let cardNodeIds = [];
|
|
393
|
+
let listEndReason = "";
|
|
394
|
+
let lastHumanEvent = null;
|
|
395
|
+
const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
|
|
396
|
+
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
397
|
+
containerSelectors: RECRUIT_LIST_CONTAINER_SELECTORS,
|
|
398
|
+
itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
|
|
399
|
+
itemSelectors: [RECRUIT_CARD_SELECTOR],
|
|
400
|
+
viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
|
|
401
|
+
validateViewportPoint: true
|
|
402
|
+
}));
|
|
403
|
+
|
|
404
|
+
function recordHumanEvent(event = null) {
|
|
405
|
+
if (!event) return lastHumanEvent;
|
|
406
|
+
lastHumanEvent = {
|
|
407
|
+
at: new Date().toISOString(),
|
|
408
|
+
...event
|
|
409
|
+
};
|
|
410
|
+
return lastHumanEvent;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function maybeHumanActionCooldown(phase, timings = {}) {
|
|
414
|
+
if (!effectiveHumanBehavior.actionCooldown) return null;
|
|
415
|
+
const pauseMs = humanDelay(280, 90, {
|
|
416
|
+
minMs: 80,
|
|
417
|
+
maxMs: 720
|
|
418
|
+
});
|
|
419
|
+
if (pauseMs > 0) {
|
|
420
|
+
await runControl.sleep(pauseMs);
|
|
421
|
+
addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
|
|
422
|
+
}
|
|
423
|
+
return recordHumanEvent({
|
|
424
|
+
kind: "action_cooldown",
|
|
425
|
+
phase,
|
|
426
|
+
pause_ms: pauseMs
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function updateRecruitProgress(extra = {}) {
|
|
431
|
+
const counts = countRecruitResultStatuses(results);
|
|
432
|
+
const listSnapshot = compactInfiniteListState(listState);
|
|
433
|
+
const humanRestState = humanRestController.getState();
|
|
434
|
+
runControl.updateProgress({
|
|
435
|
+
card_count: cardNodeIds.length,
|
|
436
|
+
target_count: limit,
|
|
437
|
+
...counts,
|
|
438
|
+
screening_mode: normalizedScreeningMode,
|
|
439
|
+
unique_seen: listSnapshot.seen_count,
|
|
440
|
+
scroll_count: listSnapshot.scroll_count,
|
|
441
|
+
refresh_rounds: refreshRounds,
|
|
442
|
+
refresh_attempts: refreshAttempts.length,
|
|
443
|
+
context_recoveries: contextRecoveryAttempts,
|
|
444
|
+
list_end_reason: listEndReason || null,
|
|
445
|
+
viewport_checks: viewportGuard.getStats().checks,
|
|
446
|
+
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
447
|
+
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
448
|
+
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
449
|
+
human_rest_enabled: effectiveHumanRestEnabled,
|
|
450
|
+
human_rest_count: humanRestState.rest_count,
|
|
451
|
+
human_rest_ms: humanRestState.total_rest_ms,
|
|
452
|
+
last_human_event: lastHumanEvent,
|
|
453
|
+
...extra
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function checkpointInProgressCandidate({
|
|
458
|
+
index = results.length,
|
|
459
|
+
candidateKey = "",
|
|
460
|
+
cardNodeId = null,
|
|
461
|
+
detailStep = "",
|
|
462
|
+
error = null
|
|
463
|
+
} = {}) {
|
|
464
|
+
runControl.checkpoint({
|
|
465
|
+
in_progress_candidate: {
|
|
466
|
+
index,
|
|
467
|
+
key: candidateKey,
|
|
468
|
+
card_node_id: cardNodeId,
|
|
469
|
+
detail_step: detailStep || null,
|
|
470
|
+
counters: countRecruitResultStatuses(results),
|
|
471
|
+
error: compactError(error, "RECRUIT_IN_PROGRESS_ERROR")
|
|
472
|
+
},
|
|
473
|
+
candidate_list: compactInfiniteListState(listState)
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function recoverAndReapplyRecruitContext(reason = "context_recovery", error = null, {
|
|
478
|
+
forceRecentViewed = true
|
|
479
|
+
} = {}) {
|
|
480
|
+
await runControl.waitIfPaused();
|
|
481
|
+
runControl.throwIfCanceled();
|
|
482
|
+
const started = Date.now();
|
|
483
|
+
runControl.setPhase("recruit:recover-context");
|
|
484
|
+
contextRecoveryAttempts += 1;
|
|
485
|
+
const refreshResult = await refreshRecruitSearchAtEnd(client, {
|
|
486
|
+
searchParams: normalizedSearchParams,
|
|
487
|
+
requireCards: true,
|
|
488
|
+
searchTimeoutMs: cardTimeoutMs,
|
|
489
|
+
resetTimeoutMs,
|
|
490
|
+
resetSettleMs: refreshResetSettleMs,
|
|
491
|
+
cityOptionTimeoutMs,
|
|
492
|
+
forceRecentViewed
|
|
493
|
+
});
|
|
494
|
+
const compactRefresh = {
|
|
495
|
+
...compactRefreshAttempt(refreshResult),
|
|
496
|
+
context_recovery: true,
|
|
497
|
+
recovery_reason: reason,
|
|
498
|
+
trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
|
|
499
|
+
elapsed_ms: Date.now() - started
|
|
500
|
+
};
|
|
501
|
+
refreshAttempts.push(compactRefresh);
|
|
502
|
+
runControl.checkpoint({
|
|
503
|
+
context_recovery: {
|
|
504
|
+
attempt: contextRecoveryAttempts,
|
|
505
|
+
reason,
|
|
506
|
+
trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
|
|
507
|
+
refresh: compactRefresh,
|
|
508
|
+
counters: countRecruitResultStatuses(results)
|
|
509
|
+
},
|
|
510
|
+
candidate_list: compactInfiniteListState(listState)
|
|
511
|
+
});
|
|
512
|
+
if (!refreshResult.ok) {
|
|
513
|
+
updateRecruitProgress({
|
|
514
|
+
refresh_method: refreshResult.method || null,
|
|
515
|
+
refresh_forced_recent_viewed: forceRecentViewed,
|
|
516
|
+
recovery_reason: reason
|
|
517
|
+
});
|
|
518
|
+
throw new Error(`Recruit context recovery failed after ${reason}: ${refreshResult.application?.reason || "refresh returned no cards"}`);
|
|
519
|
+
}
|
|
520
|
+
rootState = await getRecruitRoots(client);
|
|
521
|
+
rootState = await ensureRecruitViewport(rootState, "recover_after");
|
|
522
|
+
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
523
|
+
timeoutMs: cardTimeoutMs,
|
|
524
|
+
intervalMs: 300
|
|
525
|
+
});
|
|
526
|
+
resetInfiniteListForRefreshRound(listState, {
|
|
527
|
+
reason: `context_recovery:${reason}`,
|
|
528
|
+
round: contextRecoveryAttempts,
|
|
529
|
+
method: refreshResult.method,
|
|
530
|
+
metadata: {
|
|
531
|
+
card_count: cardNodeIds.length,
|
|
532
|
+
forced_recent_viewed: forceRecentViewed,
|
|
533
|
+
counters: countRecruitResultStatuses(results)
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
listEndReason = "";
|
|
537
|
+
updateRecruitProgress({
|
|
538
|
+
card_count: cardNodeIds.length,
|
|
539
|
+
refresh_method: refreshResult.method || null,
|
|
540
|
+
refresh_forced_recent_viewed: forceRecentViewed,
|
|
541
|
+
recovery_reason: reason
|
|
542
|
+
});
|
|
543
|
+
return refreshResult;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
runControl.setPhase("recruit:cleanup");
|
|
547
|
+
await closeRecruitDetail(client, { attemptsLimit: 2 });
|
|
548
|
+
|
|
549
|
+
await runControl.waitIfPaused();
|
|
550
|
+
runControl.throwIfCanceled();
|
|
551
|
+
runControl.setPhase("recruit:roots");
|
|
552
|
+
let rootState = await getRecruitRoots(client);
|
|
553
|
+
rootState = await ensureRecruitViewport(rootState, "roots");
|
|
554
|
+
runControl.checkpoint({
|
|
555
|
+
iframe_selector: rootState.iframe.selector,
|
|
556
|
+
iframe_document_node_id: rootState.iframe.documentNodeId,
|
|
557
|
+
search_params: normalizedSearchParams
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
if (hasRecruitSearchParams(normalizedSearchParams)) {
|
|
561
|
+
await runControl.waitIfPaused();
|
|
562
|
+
runControl.throwIfCanceled();
|
|
563
|
+
runControl.setPhase("recruit:search");
|
|
564
|
+
const searchResult = await applyRecruitSearchParams(client, {
|
|
565
|
+
searchParams: normalizedSearchParams,
|
|
566
|
+
requireCards: true,
|
|
567
|
+
resetBeforeApply: resetBeforeSearch,
|
|
568
|
+
searchTimeoutMs: cardTimeoutMs,
|
|
569
|
+
resetTimeoutMs,
|
|
570
|
+
cityOptionTimeoutMs
|
|
571
|
+
});
|
|
572
|
+
runControl.checkpoint({
|
|
573
|
+
search: {
|
|
574
|
+
search_params: searchResult.search_params,
|
|
575
|
+
before_counts: searchResult.before_counts,
|
|
576
|
+
post_search_state: searchResult.post_search_state,
|
|
577
|
+
steps: searchResult.steps.map((step) => ({
|
|
578
|
+
step: step.step,
|
|
579
|
+
applied: step.result?.applied,
|
|
580
|
+
clicked: step.result?.clicked,
|
|
581
|
+
searched: step.result?.searched,
|
|
582
|
+
reason: step.result?.reason || null
|
|
583
|
+
}))
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
rootState = await getRecruitRoots(client);
|
|
587
|
+
rootState = await ensureRecruitViewport(rootState, "search");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
await runControl.waitIfPaused();
|
|
591
|
+
runControl.throwIfCanceled();
|
|
592
|
+
runControl.setPhase("recruit:cards");
|
|
593
|
+
rootState = await ensureRecruitViewport(rootState, "cards");
|
|
594
|
+
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
595
|
+
timeoutMs: cardTimeoutMs,
|
|
596
|
+
intervalMs: 300
|
|
597
|
+
});
|
|
598
|
+
if (!cardNodeIds.length) {
|
|
599
|
+
throw new Error("No recruit/search candidate cards found for run service");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
updateRecruitProgress({
|
|
603
|
+
list_end_reason: null
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
while (results.length < limit) {
|
|
607
|
+
const candidateStarted = Date.now();
|
|
608
|
+
const timings = {};
|
|
609
|
+
await runControl.waitIfPaused();
|
|
610
|
+
runControl.throwIfCanceled();
|
|
611
|
+
runControl.setPhase("recruit:candidate");
|
|
612
|
+
rootState = await ensureRecruitViewport(rootState, "candidate_loop");
|
|
613
|
+
|
|
614
|
+
const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
|
|
615
|
+
client,
|
|
616
|
+
state: listState,
|
|
617
|
+
maxScrolls: listMaxScrolls,
|
|
618
|
+
stableSignatureLimit: listStableSignatureLimit,
|
|
619
|
+
wheelDeltaY: listWheelDeltaY,
|
|
620
|
+
settleMs: listSettleMs,
|
|
621
|
+
listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
622
|
+
fallbackPoint: listFallbackResolver,
|
|
623
|
+
findNodeIds: async () => {
|
|
624
|
+
let currentRootState = await getRecruitRoots(client);
|
|
625
|
+
currentRootState = await ensureRecruitViewport(currentRootState, "candidate_find_nodes");
|
|
626
|
+
rootState = currentRootState;
|
|
627
|
+
const currentCardNodeIds = await waitForRecruitCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
628
|
+
timeoutMs: Math.min(cardTimeoutMs, 5000),
|
|
629
|
+
intervalMs: 300
|
|
630
|
+
});
|
|
631
|
+
cardNodeIds = currentCardNodeIds;
|
|
632
|
+
return currentCardNodeIds;
|
|
633
|
+
},
|
|
634
|
+
readCandidate: async (nodeId, { visibleIndex }) => readRecruitCardCandidate(client, nodeId, {
|
|
635
|
+
targetUrl,
|
|
636
|
+
source: "recruit-run-card",
|
|
637
|
+
metadata: {
|
|
638
|
+
run_candidate_index: results.length,
|
|
639
|
+
visible_index: visibleIndex,
|
|
640
|
+
search_params: normalizedSearchParams
|
|
641
|
+
}
|
|
642
|
+
}),
|
|
643
|
+
detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
|
|
644
|
+
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
645
|
+
markerSelectors: RECRUIT_BOTTOM_MARKER_SELECTORS,
|
|
646
|
+
refreshSelectors: RECRUIT_BOTTOM_REFRESH_SELECTORS,
|
|
647
|
+
textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
|
|
648
|
+
maxTextScanNodes: 500
|
|
649
|
+
})
|
|
650
|
+
}));
|
|
651
|
+
if (!nextCandidateResult.ok) {
|
|
652
|
+
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
653
|
+
if (
|
|
654
|
+
(nextCandidateResult.end_reached || isRefreshableListStall(nextCandidateResult.reason))
|
|
655
|
+
&& refreshOnEnd
|
|
656
|
+
&& results.length < limit
|
|
657
|
+
&& refreshRounds < Math.max(0, Number(maxRefreshRounds) || 0)
|
|
658
|
+
) {
|
|
659
|
+
await runControl.waitIfPaused();
|
|
660
|
+
runControl.throwIfCanceled();
|
|
661
|
+
runControl.setPhase("recruit:refresh");
|
|
662
|
+
refreshRounds += 1;
|
|
663
|
+
const refreshResult = await refreshRecruitSearchAtEnd(client, {
|
|
664
|
+
searchParams: normalizedSearchParams,
|
|
665
|
+
requireCards: true,
|
|
666
|
+
searchTimeoutMs: cardTimeoutMs,
|
|
667
|
+
resetTimeoutMs,
|
|
668
|
+
resetSettleMs: refreshResetSettleMs,
|
|
669
|
+
cityOptionTimeoutMs
|
|
670
|
+
});
|
|
671
|
+
const compactRefresh = compactRefreshAttempt(refreshResult);
|
|
672
|
+
refreshAttempts.push(compactRefresh);
|
|
673
|
+
runControl.checkpoint({
|
|
674
|
+
refresh_round: refreshRounds,
|
|
675
|
+
refresh: compactRefresh
|
|
676
|
+
});
|
|
677
|
+
updateRecruitProgress({
|
|
678
|
+
card_count: refreshResult.card_count || cardNodeIds.length,
|
|
679
|
+
refresh_method: refreshResult.method || null,
|
|
680
|
+
refresh_forced_recent_viewed: true,
|
|
681
|
+
list_end_reason: listEndReason
|
|
682
|
+
});
|
|
683
|
+
if (refreshResult.ok) {
|
|
684
|
+
rootState = await getRecruitRoots(client);
|
|
685
|
+
rootState = await ensureRecruitViewport(rootState, "refresh_after");
|
|
686
|
+
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
687
|
+
timeoutMs: cardTimeoutMs,
|
|
688
|
+
intervalMs: 300
|
|
689
|
+
});
|
|
690
|
+
resetInfiniteListForRefreshRound(listState, {
|
|
691
|
+
reason: listEndReason,
|
|
692
|
+
round: refreshRounds,
|
|
693
|
+
method: refreshResult.method,
|
|
694
|
+
metadata: {
|
|
695
|
+
card_count: cardNodeIds.length,
|
|
696
|
+
forced_recent_viewed: true
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
listEndReason = "";
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
throw createRecruitRefreshFailureError(compactRefresh, {
|
|
703
|
+
listEndReason,
|
|
704
|
+
targetCount: limit,
|
|
705
|
+
processedCount: results.length
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const index = results.length;
|
|
712
|
+
const cardNodeId = nextCandidateResult.item.node_id;
|
|
713
|
+
const candidateKey = nextCandidateResult.item.key;
|
|
714
|
+
const cardCandidate = nextCandidateResult.item.candidate;
|
|
715
|
+
|
|
716
|
+
let screeningCandidate = cardCandidate;
|
|
717
|
+
let detailResult = null;
|
|
718
|
+
let recoverableDetailError = null;
|
|
719
|
+
let detailStep = "not_started";
|
|
720
|
+
if (index < detailCountLimit) {
|
|
721
|
+
try {
|
|
722
|
+
await runControl.waitIfPaused();
|
|
723
|
+
runControl.throwIfCanceled();
|
|
724
|
+
runControl.setPhase("recruit:detail");
|
|
725
|
+
detailStep = "ensure_viewport";
|
|
726
|
+
rootState = await ensureRecruitViewport(rootState, "detail");
|
|
727
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
728
|
+
detailStep = "open_detail";
|
|
729
|
+
networkRecorder.clear();
|
|
730
|
+
await maybeHumanActionCooldown("before_detail_open", timings);
|
|
731
|
+
const openedDetail = await openRecruitCardDetail(client, cardNodeId);
|
|
732
|
+
addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
|
|
733
|
+
addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
|
|
734
|
+
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
735
|
+
detailStep = "wait_network";
|
|
736
|
+
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
737
|
+
waitForRecruitDetailNetworkEvents,
|
|
738
|
+
networkRecorder,
|
|
739
|
+
{
|
|
740
|
+
waitPlan,
|
|
741
|
+
minCount: 1,
|
|
742
|
+
requireLoaded: true,
|
|
743
|
+
intervalMs: 120
|
|
744
|
+
}
|
|
745
|
+
));
|
|
746
|
+
if (networkWait?.elapsed_ms != null) {
|
|
747
|
+
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
748
|
+
}
|
|
749
|
+
detailStep = "extract_detail";
|
|
750
|
+
detailResult = await extractRecruitDetailCandidate(client, {
|
|
751
|
+
cardCandidate,
|
|
752
|
+
cardNodeId,
|
|
753
|
+
detailState: openedDetail.detail_state,
|
|
754
|
+
networkEvents: networkRecorder.events,
|
|
755
|
+
targetUrl,
|
|
756
|
+
closeDetail: false,
|
|
757
|
+
networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
|
|
758
|
+
networkParseIntervalMs: 250
|
|
759
|
+
});
|
|
760
|
+
addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
|
|
761
|
+
const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
762
|
+
let source = "network";
|
|
763
|
+
let imageEvidence = null;
|
|
764
|
+
let captureTarget = null;
|
|
765
|
+
let captureTargetWait = null;
|
|
766
|
+
if (parsedNetworkProfileCount > 0) {
|
|
767
|
+
recordCvNetworkHit(cvAcquisitionState, {
|
|
768
|
+
parsedNetworkProfileCount,
|
|
769
|
+
waitResult: networkWait
|
|
770
|
+
});
|
|
771
|
+
} else {
|
|
772
|
+
detailStep = "wait_capture_target";
|
|
773
|
+
captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
|
|
774
|
+
domain: "recruit",
|
|
775
|
+
timeoutMs: 6000,
|
|
776
|
+
intervalMs: 250
|
|
777
|
+
});
|
|
778
|
+
captureTarget = captureTargetWait.target || null;
|
|
779
|
+
const captureNodeId = captureTarget?.node_id || null;
|
|
780
|
+
if (captureNodeId) {
|
|
781
|
+
const imageEvidencePath = imageEvidenceFilePath({
|
|
782
|
+
imageOutputDir,
|
|
783
|
+
domain: "recruit",
|
|
784
|
+
runId: runControl?.runId,
|
|
785
|
+
index,
|
|
786
|
+
extension: "jpg"
|
|
787
|
+
});
|
|
788
|
+
try {
|
|
789
|
+
detailStep = "capture_image";
|
|
790
|
+
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
791
|
+
filePath: imageEvidencePath,
|
|
792
|
+
format: "jpeg",
|
|
793
|
+
quality: 72,
|
|
794
|
+
optimize: true,
|
|
795
|
+
resizeMaxWidth: 1100,
|
|
796
|
+
captureViewport: false,
|
|
797
|
+
padding: 0,
|
|
798
|
+
maxScreenshots: maxImagePages,
|
|
799
|
+
wheelDeltaY: imageWheelDeltaY,
|
|
800
|
+
settleMs: 350,
|
|
801
|
+
scrollMethod: "dom-anchor-fallback-input",
|
|
802
|
+
scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
803
|
+
stepTimeoutMs: 45000,
|
|
804
|
+
totalTimeoutMs: 90000,
|
|
805
|
+
duplicateStopCount: 1,
|
|
806
|
+
skipDuplicateScreenshots: true,
|
|
807
|
+
composeForLlm: true,
|
|
808
|
+
llmPagesPerImage: 3,
|
|
809
|
+
llmResizeMaxWidth: 1100,
|
|
810
|
+
llmQuality: 72,
|
|
811
|
+
metadata: {
|
|
812
|
+
domain: "recruit",
|
|
813
|
+
capture_mode: "scroll_sequence",
|
|
814
|
+
acquisition_reason: "network_miss_image_fallback",
|
|
815
|
+
run_candidate_index: index,
|
|
816
|
+
candidate_key: candidateKey,
|
|
817
|
+
capture_target: captureTarget,
|
|
818
|
+
capture_target_wait: captureTargetWait
|
|
819
|
+
}
|
|
820
|
+
}));
|
|
821
|
+
source = "image";
|
|
822
|
+
} catch (error) {
|
|
823
|
+
if (!isRecoverableRecruitImageCaptureError(error)) throw error;
|
|
824
|
+
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
825
|
+
if (recoveryCount < 1) {
|
|
826
|
+
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
827
|
+
timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
|
|
828
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
829
|
+
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
830
|
+
await recoverAndReapplyRecruitContext(`image_capture:${detailStep}`, error, {
|
|
831
|
+
forceRecentViewed: true
|
|
832
|
+
});
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
imageEvidence = createRecoverableRecruitImageCaptureEvidence(error, {
|
|
836
|
+
elapsedMs: timings.screenshot_capture_ms,
|
|
837
|
+
filePath: imageEvidencePath,
|
|
838
|
+
extension: "jpg",
|
|
839
|
+
maxScreenshots: maxImagePages
|
|
840
|
+
});
|
|
841
|
+
source = "image_capture_failed";
|
|
842
|
+
}
|
|
843
|
+
recordCvImageFallback(cvAcquisitionState, {
|
|
844
|
+
reason: source === "image_capture_failed"
|
|
845
|
+
? "network_miss_image_capture_failed"
|
|
846
|
+
: "network_miss_image_fallback",
|
|
847
|
+
parsedNetworkProfileCount,
|
|
848
|
+
waitResult: networkWait,
|
|
849
|
+
imageEvidence
|
|
850
|
+
});
|
|
851
|
+
} else {
|
|
852
|
+
source = "missing_capture_node";
|
|
853
|
+
recordCvNetworkMiss(cvAcquisitionState, {
|
|
854
|
+
reason: "network_miss_no_capture_node",
|
|
855
|
+
parsedNetworkProfileCount,
|
|
856
|
+
waitResult: networkWait
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
detailResult.image_evidence = imageEvidence;
|
|
862
|
+
detailResult.cv_acquisition = {
|
|
863
|
+
source,
|
|
864
|
+
mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
|
|
865
|
+
wait_plan: waitPlan,
|
|
866
|
+
network_wait: networkWait,
|
|
867
|
+
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
868
|
+
image_evidence: summarizeImageEvidence(imageEvidence),
|
|
869
|
+
capture_target: captureTarget || null,
|
|
870
|
+
capture_target_wait: captureTargetWait
|
|
871
|
+
};
|
|
872
|
+
screeningCandidate = detailResult.candidate;
|
|
873
|
+
if (closeDetail) {
|
|
874
|
+
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
|
|
875
|
+
await maybeHumanActionCooldown("after_detail_close", timings);
|
|
876
|
+
if (!detailResult.close_result?.closed) {
|
|
877
|
+
const closeError = createRecruitCloseFailureError(detailResult.close_result);
|
|
878
|
+
const recovery = await recoverAndReapplyRecruitContext("detail_close_failed", closeError, {
|
|
879
|
+
forceRecentViewed: true
|
|
880
|
+
});
|
|
881
|
+
detailResult.cv_acquisition = {
|
|
882
|
+
...(detailResult.cv_acquisition || {}),
|
|
883
|
+
close_recovery: {
|
|
884
|
+
ok: Boolean(recovery.ok),
|
|
885
|
+
method: recovery.method || "",
|
|
886
|
+
forced_recent_viewed: Boolean(recovery.forced_recent_viewed),
|
|
887
|
+
card_count: recovery.card_count || 0
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
} else {
|
|
892
|
+
detailResult.close_result = null;
|
|
893
|
+
}
|
|
894
|
+
} catch (error) {
|
|
895
|
+
if (!isRecoverableRecruitDetailError(error)) throw error;
|
|
896
|
+
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
897
|
+
if (recoveryCount < 1) {
|
|
898
|
+
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
899
|
+
timings.detail_recovery_trigger = compactRecoverableDetailError(error);
|
|
900
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
901
|
+
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
902
|
+
await recoverAndReapplyRecruitContext(`detail:${detailStep}`, error, {
|
|
903
|
+
forceRecentViewed: true
|
|
904
|
+
});
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
recoverableDetailError = error;
|
|
908
|
+
detailResult = null;
|
|
909
|
+
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
910
|
+
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
await runControl.waitIfPaused();
|
|
915
|
+
runControl.throwIfCanceled();
|
|
916
|
+
runControl.setPhase("recruit:screening");
|
|
917
|
+
let llmResult = null;
|
|
918
|
+
if (useLlmScreening) {
|
|
919
|
+
if (recoverableDetailError || detailResult?.image_evidence?.ok === false) {
|
|
920
|
+
llmResult = null;
|
|
921
|
+
} else if (!llmConfig) {
|
|
922
|
+
llmResult = createMissingLlmConfigResult();
|
|
923
|
+
} else {
|
|
924
|
+
try {
|
|
925
|
+
const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
|
|
926
|
+
? "vision_model_ms"
|
|
927
|
+
: "text_model_ms";
|
|
928
|
+
llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
|
|
929
|
+
candidate: screeningCandidate,
|
|
930
|
+
criteria,
|
|
931
|
+
config: llmConfig,
|
|
932
|
+
timeoutMs: llmTimeoutMs,
|
|
933
|
+
imageEvidence: detailResult?.image_evidence || null,
|
|
934
|
+
maxImages: llmImageLimit,
|
|
935
|
+
imageDetail: llmImageDetail
|
|
936
|
+
}));
|
|
937
|
+
} catch (error) {
|
|
938
|
+
llmResult = createFailedLlmScreeningResult(error);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (detailResult) detailResult.llm_result = llmResult;
|
|
942
|
+
}
|
|
943
|
+
const screening = recoverableDetailError
|
|
944
|
+
? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
|
|
945
|
+
: detailResult?.image_evidence?.ok === false
|
|
946
|
+
? createImageCaptureFailureScreening(screeningCandidate, {
|
|
947
|
+
code: detailResult.image_evidence.error_code,
|
|
948
|
+
message: detailResult.image_evidence.error
|
|
949
|
+
})
|
|
950
|
+
: useLlmScreening
|
|
951
|
+
? llmResultToScreening(llmResult, screeningCandidate)
|
|
952
|
+
: screenCandidate(screeningCandidate, { criteria });
|
|
953
|
+
timings.total_ms = Date.now() - candidateStarted;
|
|
954
|
+
const compactResult = {
|
|
955
|
+
index,
|
|
956
|
+
candidate_key: candidateKey,
|
|
957
|
+
card_node_id: cardNodeId,
|
|
958
|
+
candidate: compactCandidate(screeningCandidate),
|
|
959
|
+
detail: compactDetail(detailResult),
|
|
960
|
+
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
961
|
+
screening: compactScreening(screening),
|
|
962
|
+
error: recoverableDetailError
|
|
963
|
+
? compactRecoverableDetailError(recoverableDetailError)
|
|
964
|
+
: detailResult?.image_evidence?.ok === false
|
|
965
|
+
? compactError({
|
|
966
|
+
code: detailResult.image_evidence.error_code,
|
|
967
|
+
message: detailResult.image_evidence.error
|
|
968
|
+
}, "IMAGE_CAPTURE_FAILED")
|
|
969
|
+
: null,
|
|
970
|
+
timings
|
|
971
|
+
};
|
|
972
|
+
results.push(compactResult);
|
|
973
|
+
markInfiniteListCandidateProcessed(listState, candidateKey, {
|
|
974
|
+
metadata: {
|
|
975
|
+
result_index: index,
|
|
976
|
+
candidate_id: screeningCandidate.id || null
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
updateRecruitProgress({
|
|
981
|
+
last_candidate_id: screeningCandidate.id || null,
|
|
982
|
+
last_candidate_key: candidateKey,
|
|
983
|
+
last_score: screening.score
|
|
984
|
+
});
|
|
985
|
+
const checkpointStarted = Date.now();
|
|
986
|
+
runControl.checkpoint({
|
|
987
|
+
results,
|
|
988
|
+
last_candidate: {
|
|
989
|
+
id: screeningCandidate.id || null,
|
|
990
|
+
key: candidateKey,
|
|
991
|
+
identity: screeningCandidate.identity || {},
|
|
992
|
+
screening: {
|
|
993
|
+
status: screening.status,
|
|
994
|
+
passed: screening.passed,
|
|
995
|
+
score: screening.score
|
|
996
|
+
},
|
|
997
|
+
llm_screening: compactScreeningLlmResult(llmResult),
|
|
998
|
+
error: compactResult.error
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
1002
|
+
|
|
1003
|
+
if (effectiveHumanRestEnabled) {
|
|
1004
|
+
const restStarted = Date.now();
|
|
1005
|
+
const restResult = await humanRestController.takeBreakIfNeeded({
|
|
1006
|
+
sleepFn: (ms) => runControl.sleep(ms)
|
|
1007
|
+
});
|
|
1008
|
+
const restElapsed = Date.now() - restStarted;
|
|
1009
|
+
if (restResult.rested) {
|
|
1010
|
+
recordHumanEvent({
|
|
1011
|
+
kind: "rest",
|
|
1012
|
+
pause_ms: restResult.pause_ms || restElapsed,
|
|
1013
|
+
events: restResult.events || []
|
|
1014
|
+
});
|
|
1015
|
+
compactResult.human_rest = restResult;
|
|
1016
|
+
addTiming(compactResult.timings, "human_rest_ms", restElapsed);
|
|
1017
|
+
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
1018
|
+
updateRecruitProgress({
|
|
1019
|
+
human_rest_last: restResult
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (delayMs > 0) {
|
|
1025
|
+
const sleepStarted = Date.now();
|
|
1026
|
+
await runControl.sleep(delayMs);
|
|
1027
|
+
addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
|
|
1028
|
+
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
runControl.setPhase("recruit:done");
|
|
1033
|
+
return {
|
|
1034
|
+
domain: "recruit",
|
|
1035
|
+
target_url: targetUrl,
|
|
1036
|
+
search_params: normalizedSearchParams,
|
|
1037
|
+
card_count: cardNodeIds.length,
|
|
1038
|
+
candidate_list: compactInfiniteListState(listState),
|
|
1039
|
+
viewport_health: {
|
|
1040
|
+
stats: viewportGuard.getStats(),
|
|
1041
|
+
events: viewportGuard.getEvents()
|
|
1042
|
+
},
|
|
1043
|
+
human_behavior: effectiveHumanBehavior,
|
|
1044
|
+
human_rest: humanRestController.getState(),
|
|
1045
|
+
last_human_event: lastHumanEvent,
|
|
1046
|
+
list_end_reason: listEndReason || null,
|
|
1047
|
+
refresh_rounds: refreshRounds,
|
|
1048
|
+
refresh_attempts: refreshAttempts,
|
|
1049
|
+
context_recoveries: contextRecoveryAttempts,
|
|
1050
|
+
...countRecruitResultStatuses(results),
|
|
1051
|
+
results
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
export function createRecruitRunService({
|
|
1056
|
+
lifecycle,
|
|
1057
|
+
idPrefix = "recruit",
|
|
1058
|
+
workflow = runRecruitWorkflow,
|
|
1059
|
+
onSnapshot = null
|
|
1060
|
+
} = {}) {
|
|
1061
|
+
const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
|
|
1062
|
+
|
|
1063
|
+
function startRecruitRun({
|
|
1064
|
+
client,
|
|
1065
|
+
targetUrl = "",
|
|
1066
|
+
criteria = "",
|
|
1067
|
+
searchParams = {},
|
|
1068
|
+
maxCandidates = 5,
|
|
1069
|
+
detailLimit = null,
|
|
1070
|
+
closeDetail = true,
|
|
1071
|
+
delayMs = 0,
|
|
1072
|
+
cardTimeoutMs = 90000,
|
|
1073
|
+
resetBeforeSearch = true,
|
|
1074
|
+
resetTimeoutMs = 180000,
|
|
1075
|
+
cityOptionTimeoutMs = 30000,
|
|
1076
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
1077
|
+
imageWheelDeltaY = 650,
|
|
1078
|
+
cvAcquisitionMode = "unknown",
|
|
1079
|
+
listMaxScrolls = 20,
|
|
1080
|
+
listStableSignatureLimit = 5,
|
|
1081
|
+
listWheelDeltaY = 850,
|
|
1082
|
+
listSettleMs = 2200,
|
|
1083
|
+
listFallbackPoint = null,
|
|
1084
|
+
refreshOnEnd = true,
|
|
1085
|
+
maxRefreshRounds = 2,
|
|
1086
|
+
refreshResetSettleMs = 5000,
|
|
1087
|
+
screeningMode = "llm",
|
|
1088
|
+
llmConfig = null,
|
|
1089
|
+
llmTimeoutMs = 120000,
|
|
1090
|
+
llmImageLimit = 8,
|
|
1091
|
+
llmImageDetail = "high",
|
|
1092
|
+
imageOutputDir = "",
|
|
1093
|
+
humanRestEnabled = false,
|
|
1094
|
+
humanBehavior = null,
|
|
1095
|
+
name = "recruit-domain-run"
|
|
1096
|
+
} = {}) {
|
|
1097
|
+
if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
|
|
1098
|
+
const normalizedSearchParams = normalizeSearchParams(searchParams);
|
|
1099
|
+
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
1100
|
+
const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
|
|
1101
|
+
const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
|
|
1102
|
+
const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
|
|
1103
|
+
legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
|
|
1104
|
+
});
|
|
1105
|
+
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
1106
|
+
return manager.startRun({
|
|
1107
|
+
name,
|
|
1108
|
+
context: {
|
|
1109
|
+
domain: "recruit",
|
|
1110
|
+
target_url: targetUrl,
|
|
1111
|
+
criteria_present: Boolean(criteria),
|
|
1112
|
+
search_params: normalizedSearchParams,
|
|
1113
|
+
max_candidates: maxCandidates,
|
|
1114
|
+
detail_limit: normalizedDetailLimit,
|
|
1115
|
+
close_detail: closeDetail,
|
|
1116
|
+
reset_before_search: resetBeforeSearch,
|
|
1117
|
+
reset_timeout_ms: resetTimeoutMs,
|
|
1118
|
+
city_option_timeout_ms: cityOptionTimeoutMs,
|
|
1119
|
+
cv_acquisition_mode: cvAcquisitionMode,
|
|
1120
|
+
max_image_pages: maxImagePages,
|
|
1121
|
+
image_wheel_delta_y: imageWheelDeltaY,
|
|
1122
|
+
list_max_scrolls: listMaxScrolls,
|
|
1123
|
+
list_stable_signature_limit: listStableSignatureLimit,
|
|
1124
|
+
list_wheel_delta_y: listWheelDeltaY,
|
|
1125
|
+
list_settle_ms: listSettleMs,
|
|
1126
|
+
list_fallback_point: listFallbackPoint,
|
|
1127
|
+
refresh_on_end: refreshOnEnd,
|
|
1128
|
+
max_refresh_rounds: maxRefreshRounds,
|
|
1129
|
+
refresh_reset_settle_ms: refreshResetSettleMs,
|
|
1130
|
+
screening_mode: normalizedScreeningMode,
|
|
1131
|
+
llm_configured: Boolean(llmConfig),
|
|
1132
|
+
llm_timeout_ms: llmTimeoutMs,
|
|
1133
|
+
llm_image_limit: llmImageLimit,
|
|
1134
|
+
llm_image_detail: llmImageDetail,
|
|
1135
|
+
image_output_dir: imageOutputDir || "",
|
|
1136
|
+
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
1137
|
+
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1138
|
+
human_behavior: effectiveHumanBehavior,
|
|
1139
|
+
human_rest_enabled: effectiveHumanRestEnabled
|
|
1140
|
+
},
|
|
1141
|
+
progress: {
|
|
1142
|
+
card_count: 0,
|
|
1143
|
+
target_count: candidateLimit,
|
|
1144
|
+
processed: 0,
|
|
1145
|
+
screened: 0,
|
|
1146
|
+
detail_opened: 0,
|
|
1147
|
+
llm_screened: 0,
|
|
1148
|
+
passed: 0,
|
|
1149
|
+
image_capture_failed: 0,
|
|
1150
|
+
detail_open_failed: 0,
|
|
1151
|
+
transient_recovered: 0,
|
|
1152
|
+
context_recoveries: 0,
|
|
1153
|
+
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
1154
|
+
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1155
|
+
human_rest_enabled: effectiveHumanRestEnabled,
|
|
1156
|
+
human_rest_count: 0,
|
|
1157
|
+
human_rest_ms: 0,
|
|
1158
|
+
last_human_event: null
|
|
1159
|
+
},
|
|
1160
|
+
checkpoint: {},
|
|
1161
|
+
task: (runControl) => workflow({
|
|
1162
|
+
client,
|
|
1163
|
+
targetUrl,
|
|
1164
|
+
criteria,
|
|
1165
|
+
searchParams: normalizedSearchParams,
|
|
1166
|
+
maxCandidates,
|
|
1167
|
+
detailLimit: normalizedDetailLimit,
|
|
1168
|
+
closeDetail,
|
|
1169
|
+
delayMs,
|
|
1170
|
+
cardTimeoutMs,
|
|
1171
|
+
resetBeforeSearch,
|
|
1172
|
+
resetTimeoutMs,
|
|
1173
|
+
cityOptionTimeoutMs,
|
|
1174
|
+
maxImagePages,
|
|
1175
|
+
imageWheelDeltaY,
|
|
1176
|
+
cvAcquisitionMode,
|
|
1177
|
+
listMaxScrolls,
|
|
1178
|
+
listStableSignatureLimit,
|
|
1179
|
+
listWheelDeltaY,
|
|
1180
|
+
listSettleMs,
|
|
1181
|
+
listFallbackPoint,
|
|
1182
|
+
refreshOnEnd,
|
|
1183
|
+
maxRefreshRounds,
|
|
1184
|
+
refreshResetSettleMs,
|
|
1185
|
+
screeningMode: normalizedScreeningMode,
|
|
1186
|
+
llmConfig,
|
|
1187
|
+
llmTimeoutMs,
|
|
1188
|
+
llmImageLimit,
|
|
1189
|
+
llmImageDetail,
|
|
1190
|
+
imageOutputDir,
|
|
1191
|
+
humanRestEnabled: effectiveHumanRestEnabled,
|
|
1192
|
+
humanBehavior: effectiveHumanBehavior
|
|
1193
|
+
}, runControl)
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return {
|
|
1198
|
+
startRecruitRun,
|
|
1199
|
+
getRecruitRun: manager.getRun,
|
|
1200
|
+
pauseRecruitRun: manager.pauseRun,
|
|
1201
|
+
resumeRecruitRun: manager.resumeRun,
|
|
1202
|
+
cancelRecruitRun: manager.cancelRun,
|
|
1203
|
+
waitForRecruitRun: manager.waitForRun,
|
|
1204
|
+
listRecruitRuns: manager.listRuns,
|
|
1205
|
+
manager
|
|
1206
|
+
};
|
|
1207
|
+
}
|