@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/bin/boss-recommend-mcp.js +4 -4
- package/config/screening-config.example.json +33 -33
- package/package.json +8 -8
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +42 -42
- package/skills/boss-chat/SKILL.md +106 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +219 -214
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +127 -127
- package/src/chat-runtime-config.js +775 -775
- package/src/cli.js +573 -573
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +2419 -2415
- 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 +71 -71
- package/src/core/infinite-list/index.js +1326 -1326
- package/src/core/reporting/legacy-csv.js +334 -332
- package/src/core/run/index.js +32 -32
- package/src/core/run/timing.js +33 -33
- package/src/core/screening/index.js +2135 -2135
- package/src/core/self-heal/index.js +973 -973
- package/src/core/self-heal/viewport.js +564 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +113 -113
- package/src/domains/chat/index.js +7 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +122 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +571 -571
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +472 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +333 -333
- package/src/domains/recommend/constants.js +228 -159
- package/src/domains/recommend/detail.js +650 -650
- package/src/domains/recommend/filters.js +748 -377
- package/src/domains/recommend/index.js +4 -3
- package/src/domains/recommend/jobs.js +542 -542
- package/src/domains/recommend/location.js +736 -0
- package/src/domains/recommend/refresh.js +504 -361
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +987 -854
- 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 +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1620 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +13 -0
- package/src/parser.js +376 -8
- package/src/recommend-mcp.js +929 -915
- package/src/recommend-scheduler.js +496 -496
- package/src/recruit-mcp.js +2121 -2121
|
@@ -1,1620 +1,1620 @@
|
|
|
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
|
-
sleep
|
|
17
|
-
} from "../../core/browser/index.js";
|
|
18
|
-
import {
|
|
19
|
-
compactCvAcquisitionState,
|
|
20
|
-
countParsedNetworkProfiles,
|
|
21
|
-
createCvAcquisitionState,
|
|
22
|
-
DEFAULT_MAX_IMAGE_PAGES,
|
|
23
|
-
getCvNetworkWaitPlan,
|
|
24
|
-
recordCvImageFallback,
|
|
25
|
-
recordCvNetworkHit,
|
|
26
|
-
recordCvNetworkMiss,
|
|
27
|
-
summarizeImageEvidence,
|
|
28
|
-
waitForCvNetworkEvents
|
|
29
|
-
} from "../../core/cv-acquisition/index.js";
|
|
30
|
-
import {
|
|
31
|
-
compactInfiniteListState,
|
|
32
|
-
createInfiniteListState,
|
|
33
|
-
detectInfiniteListBottomMarker,
|
|
34
|
-
getNextInfiniteListCandidate,
|
|
35
|
-
markInfiniteListCandidateProcessed,
|
|
36
|
-
resetInfiniteListForRefreshRound,
|
|
37
|
-
resolveInfiniteListFallbackPoint
|
|
38
|
-
} from "../../core/infinite-list/index.js";
|
|
39
|
-
import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
40
|
-
import {
|
|
41
|
-
callScreeningLlm,
|
|
42
|
-
compactScreeningLlmResult,
|
|
43
|
-
createFatalLlmRunError,
|
|
44
|
-
createFailedLlmScreeningResult,
|
|
45
|
-
isFatalLlmProviderError,
|
|
46
|
-
llmResultToScreening,
|
|
47
|
-
screenCandidate
|
|
48
|
-
} from "../../core/screening/index.js";
|
|
49
|
-
import {
|
|
50
|
-
closeRecruitBlockingPanels,
|
|
51
|
-
closeRecruitDetail,
|
|
52
|
-
createRecruitDetailNetworkRecorder,
|
|
53
|
-
extractRecruitDetailCandidate,
|
|
54
|
-
openRecruitCardDetail,
|
|
55
|
-
waitForRecruitDetailNetworkEvents
|
|
56
|
-
} from "./detail.js";
|
|
57
|
-
import {
|
|
58
|
-
clickRecruitActionControl,
|
|
59
|
-
waitForRecruitDetailActionControls
|
|
60
|
-
} from "./actions.js";
|
|
61
|
-
import {
|
|
62
|
-
readRecruitCardCandidate,
|
|
63
|
-
waitForRecruitCardNodeIds
|
|
64
|
-
} from "./cards.js";
|
|
65
|
-
import {
|
|
66
|
-
applyRecruitSearchParams,
|
|
67
|
-
hasRecruitSearchParams,
|
|
68
|
-
normalizeRecruitSearchParams
|
|
69
|
-
} from "./search.js";
|
|
70
|
-
import { refreshRecruitSearchAtEnd } from "./refresh.js";
|
|
71
|
-
import { getRecruitRoots } from "./roots.js";
|
|
72
|
-
import {
|
|
73
|
-
RECRUIT_BOTTOM_MARKER_SELECTORS,
|
|
74
|
-
RECRUIT_BOTTOM_REFRESH_SELECTORS,
|
|
75
|
-
RECRUIT_CARD_SELECTOR,
|
|
76
|
-
RECRUIT_LIST_CONTAINER_SELECTORS
|
|
77
|
-
} from "./constants.js";
|
|
78
|
-
import {
|
|
79
|
-
describeGreetQuotaAfterSpend,
|
|
80
|
-
GREET_CREDITS_EXHAUSTED_CODE
|
|
81
|
-
} from "../../core/greet-quota/index.js";
|
|
82
|
-
|
|
83
|
-
function compactScreening(screening) {
|
|
84
|
-
return {
|
|
85
|
-
status: screening.status,
|
|
86
|
-
passed: screening.passed,
|
|
87
|
-
score: screening.score,
|
|
88
|
-
reasons: screening.reasons,
|
|
89
|
-
candidate: {
|
|
90
|
-
domain: screening.candidate?.domain || "recruit",
|
|
91
|
-
source: screening.candidate?.source || "",
|
|
92
|
-
id: screening.candidate?.id || null,
|
|
93
|
-
identity: screening.candidate?.identity || {}
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function compactCandidate(candidate) {
|
|
99
|
-
return {
|
|
100
|
-
id: candidate?.id || null,
|
|
101
|
-
identity: candidate?.identity || {},
|
|
102
|
-
text_length: candidate?.text?.raw?.length || 0,
|
|
103
|
-
tag_count: candidate?.tags?.length || 0
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function compactDetail(detailResult) {
|
|
108
|
-
if (!detailResult) return null;
|
|
109
|
-
return {
|
|
110
|
-
popup_text_length: detailResult.detail?.popup_text?.length || 0,
|
|
111
|
-
resume_text_length: detailResult.detail?.resume_text?.length || 0,
|
|
112
|
-
card_box: detailResult.card_box || null,
|
|
113
|
-
open_attempts: detailResult.open_attempts || [],
|
|
114
|
-
network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
|
|
115
|
-
parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
|
|
116
|
-
cv_acquisition: detailResult.cv_acquisition || null,
|
|
117
|
-
image_evidence: summarizeImageEvidence(detailResult.image_evidence),
|
|
118
|
-
llm_screening: compactScreeningLlmResult(detailResult.llm_result),
|
|
119
|
-
close_result: detailResult.close_result
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function normalizeScreeningMode(value) {
|
|
124
|
-
const normalized = String(value || "llm").trim().toLowerCase();
|
|
125
|
-
return ["deterministic", "local", "local_scorer"].includes(normalized)
|
|
126
|
-
? "deterministic"
|
|
127
|
-
: "llm";
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function createMissingLlmConfigResult() {
|
|
131
|
-
return createFailedLlmScreeningResult(new Error("LLM screening config is required for production search runs"));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function normalizeRecruitPostAction(value) {
|
|
135
|
-
const normalized = String(value || "").trim().toLowerCase();
|
|
136
|
-
if (["", "none", "skip", "no", "不执行", "无", "什么也不做"].includes(normalized)) return "none";
|
|
137
|
-
if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
|
|
138
|
-
return "none";
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function resolveRecruitPostAction({
|
|
142
|
-
postAction = "none",
|
|
143
|
-
greetCount = 0,
|
|
144
|
-
maxGreetCount = null
|
|
145
|
-
} = {}) {
|
|
146
|
-
const requested = normalizeRecruitPostAction(postAction);
|
|
147
|
-
const currentGreetCount = Number.isInteger(greetCount) && greetCount >= 0 ? greetCount : 0;
|
|
148
|
-
const limit = Number.isInteger(maxGreetCount) && maxGreetCount > 0 ? maxGreetCount : null;
|
|
149
|
-
if (requested === "greet" && limit !== null && currentGreetCount >= limit) {
|
|
150
|
-
return {
|
|
151
|
-
requested,
|
|
152
|
-
effective: "none",
|
|
153
|
-
reason: "greet_limit_reached",
|
|
154
|
-
greet_count: currentGreetCount,
|
|
155
|
-
max_greet_count: limit
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
requested,
|
|
160
|
-
effective: requested,
|
|
161
|
-
reason: "requested_action",
|
|
162
|
-
greet_count: currentGreetCount,
|
|
163
|
-
max_greet_count: limit
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function compactActionDiscovery(discovery) {
|
|
168
|
-
if (!discovery) return null;
|
|
169
|
-
return {
|
|
170
|
-
ok: Boolean(discovery.ok),
|
|
171
|
-
elapsed_ms: discovery.elapsed_ms,
|
|
172
|
-
summary: discovery.summary || null
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function runRecruitPostAction({
|
|
177
|
-
client,
|
|
178
|
-
rootNodeIds = [],
|
|
179
|
-
screening,
|
|
180
|
-
actionDiscovery,
|
|
181
|
-
postAction = "none",
|
|
182
|
-
greetCount = 0,
|
|
183
|
-
maxGreetCount = null,
|
|
184
|
-
executePostAction = true,
|
|
185
|
-
afterClickDelayMs = 900,
|
|
186
|
-
lastGreetQuotaAfterSpend = null
|
|
187
|
-
} = {}) {
|
|
188
|
-
const plan = resolveRecruitPostAction({
|
|
189
|
-
postAction,
|
|
190
|
-
greetCount,
|
|
191
|
-
maxGreetCount
|
|
192
|
-
});
|
|
193
|
-
const result = {
|
|
194
|
-
requested: postAction,
|
|
195
|
-
execute_post_action: Boolean(executePostAction),
|
|
196
|
-
plan,
|
|
197
|
-
eligible: Boolean(screening?.passed),
|
|
198
|
-
action_attempted: false,
|
|
199
|
-
action_clicked: false,
|
|
200
|
-
counted_as_greet: false,
|
|
201
|
-
reason: ""
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
if (!screening?.passed) {
|
|
205
|
-
result.reason = "screening_not_passed";
|
|
206
|
-
return result;
|
|
207
|
-
}
|
|
208
|
-
if (plan.effective === "none") {
|
|
209
|
-
result.reason = plan.reason === "greet_limit_reached" ? "greet_limit_reached" : "post_action_none";
|
|
210
|
-
return result;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const summary = actionDiscovery?.summary || {};
|
|
214
|
-
const control = summary.greet?.control || summary.greet;
|
|
215
|
-
if (!control?.found && !control?.node_id) {
|
|
216
|
-
if (plan.effective === "greet" && lastGreetQuotaAfterSpend?.exhausted_after_spend) {
|
|
217
|
-
result.reason = "greet_credits_exhausted";
|
|
218
|
-
result.out_of_greet_credits = true;
|
|
219
|
-
result.stop_run = true;
|
|
220
|
-
result.greet_quota_after_last_click = lastGreetQuotaAfterSpend;
|
|
221
|
-
return result;
|
|
222
|
-
}
|
|
223
|
-
result.reason = `${plan.effective}_control_not_found`;
|
|
224
|
-
return result;
|
|
225
|
-
}
|
|
226
|
-
result.control = control;
|
|
227
|
-
|
|
228
|
-
if (plan.effective === "greet" && control.continue_chat) {
|
|
229
|
-
result.reason = "already_connected_continue_chat";
|
|
230
|
-
result.already_connected = true;
|
|
231
|
-
return result;
|
|
232
|
-
}
|
|
233
|
-
if (plan.effective === "greet" && control.greet_quota?.exhausted) {
|
|
234
|
-
result.reason = "greet_credits_exhausted";
|
|
235
|
-
result.out_of_greet_credits = true;
|
|
236
|
-
result.stop_run = true;
|
|
237
|
-
return result;
|
|
238
|
-
}
|
|
239
|
-
if (plan.effective === "greet" && control.available === false) {
|
|
240
|
-
result.reason = "greet_control_not_available";
|
|
241
|
-
return result;
|
|
242
|
-
}
|
|
243
|
-
if (control.disabled) {
|
|
244
|
-
result.reason = `${plan.effective}_control_disabled`;
|
|
245
|
-
return result;
|
|
246
|
-
}
|
|
247
|
-
if (!executePostAction) {
|
|
248
|
-
result.reason = "dry_run_post_action";
|
|
249
|
-
result.would_click = true;
|
|
250
|
-
return result;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
result.action_attempted = true;
|
|
254
|
-
result.control_before = control;
|
|
255
|
-
let clickResult;
|
|
256
|
-
try {
|
|
257
|
-
clickResult = await clickRecruitActionControl(client, {
|
|
258
|
-
...control,
|
|
259
|
-
kind: plan.effective
|
|
260
|
-
});
|
|
261
|
-
} catch (error) {
|
|
262
|
-
if (error?.code === GREET_CREDITS_EXHAUSTED_CODE) {
|
|
263
|
-
result.reason = "greet_credits_exhausted";
|
|
264
|
-
result.out_of_greet_credits = true;
|
|
265
|
-
result.stop_run = true;
|
|
266
|
-
result.greet_quota = error.greet_quota || control.greet_quota || null;
|
|
267
|
-
return result;
|
|
268
|
-
}
|
|
269
|
-
throw error;
|
|
270
|
-
}
|
|
271
|
-
result.click_result = clickResult;
|
|
272
|
-
result.action_clicked = true;
|
|
273
|
-
result.greet_quota_after_click = describeGreetQuotaAfterSpend(
|
|
274
|
-
clickResult.greet_quota?.found ? clickResult.greet_quota : control.greet_quota || control.label || ""
|
|
275
|
-
);
|
|
276
|
-
result.counted_as_greet = plan.effective === "greet";
|
|
277
|
-
result.reason = "clicked";
|
|
278
|
-
if (afterClickDelayMs > 0) await sleep(afterClickDelayMs);
|
|
279
|
-
try {
|
|
280
|
-
const afterDiscovery = await waitForRecruitDetailActionControls(client, {
|
|
281
|
-
rootNodeIds,
|
|
282
|
-
timeoutMs: 2500,
|
|
283
|
-
intervalMs: 300,
|
|
284
|
-
requireAny: false
|
|
285
|
-
});
|
|
286
|
-
const afterControl = afterDiscovery?.summary?.greet?.control || afterDiscovery?.summary?.greet || null;
|
|
287
|
-
result.action_discovery_after = compactActionDiscovery(afterDiscovery);
|
|
288
|
-
result.control_after = afterControl;
|
|
289
|
-
if (plan.effective === "greet") {
|
|
290
|
-
result.verified_after_click = Boolean(
|
|
291
|
-
afterControl?.continue_chat
|
|
292
|
-
|| String(afterControl?.label || "").includes("继续沟通")
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
} catch (error) {
|
|
296
|
-
result.verify_error = {
|
|
297
|
-
message: error?.message || String(error)
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
return result;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function normalizeSearchParams(searchParams = {}) {
|
|
304
|
-
return normalizeRecruitSearchParams(searchParams);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function compactRefreshAttempt(refreshAttempt) {
|
|
308
|
-
if (!refreshAttempt) return null;
|
|
309
|
-
return {
|
|
310
|
-
ok: Boolean(refreshAttempt.ok),
|
|
311
|
-
method: refreshAttempt.method || "",
|
|
312
|
-
forced_recent_viewed: Boolean(refreshAttempt.forced_recent_viewed),
|
|
313
|
-
card_count: refreshAttempt.card_count || 0,
|
|
314
|
-
search_params: refreshAttempt.search_params || null,
|
|
315
|
-
recovery_settle: refreshAttempt.recovery_settle
|
|
316
|
-
? {
|
|
317
|
-
ok: Boolean(refreshAttempt.recovery_settle.ok),
|
|
318
|
-
status: refreshAttempt.recovery_settle.status || "",
|
|
319
|
-
reason: refreshAttempt.recovery_settle.reason || "",
|
|
320
|
-
elapsed_ms: refreshAttempt.recovery_settle.elapsed_ms || 0
|
|
321
|
-
}
|
|
322
|
-
: null,
|
|
323
|
-
application: refreshAttempt.application
|
|
324
|
-
? {
|
|
325
|
-
applied: Boolean(refreshAttempt.application.applied),
|
|
326
|
-
post_search_state: refreshAttempt.application.post_search_state,
|
|
327
|
-
steps: (refreshAttempt.application.steps || []).map((step) => ({
|
|
328
|
-
step: step.step,
|
|
329
|
-
applied: step.result?.applied,
|
|
330
|
-
clicked: step.result?.clicked,
|
|
331
|
-
searched: step.result?.searched,
|
|
332
|
-
reason: step.result?.reason || null
|
|
333
|
-
}))
|
|
334
|
-
}
|
|
335
|
-
: null
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function compactError(error, fallbackCode = "RECRUIT_RUN_ERROR") {
|
|
340
|
-
if (!error) return null;
|
|
341
|
-
const result = {
|
|
342
|
-
code: error.code || fallbackCode,
|
|
343
|
-
message: error.message || String(error)
|
|
344
|
-
};
|
|
345
|
-
if (error.close_result) {
|
|
346
|
-
result.close_result = compactCloseResult(error.close_result);
|
|
347
|
-
}
|
|
348
|
-
if (error.phase) {
|
|
349
|
-
result.phase = error.phase;
|
|
350
|
-
}
|
|
351
|
-
if (error.refresh_attempt) {
|
|
352
|
-
result.refresh_attempt = error.refresh_attempt;
|
|
353
|
-
}
|
|
354
|
-
if (error.list_end_reason) {
|
|
355
|
-
result.list_end_reason = error.list_end_reason;
|
|
356
|
-
}
|
|
357
|
-
if (error.target_count != null) {
|
|
358
|
-
result.target_count = error.target_count;
|
|
359
|
-
}
|
|
360
|
-
if (error.processed_count != null) {
|
|
361
|
-
result.processed_count = error.processed_count;
|
|
362
|
-
}
|
|
363
|
-
return result;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function compactCloseResult(closeResult) {
|
|
367
|
-
if (!closeResult) return null;
|
|
368
|
-
const result = {
|
|
369
|
-
closed: Boolean(closeResult.closed),
|
|
370
|
-
reason: closeResult.reason || null,
|
|
371
|
-
probe: closeResult.probe || null,
|
|
372
|
-
attempts: closeResult.attempts || [],
|
|
373
|
-
verification: closeResult.verification || null
|
|
374
|
-
};
|
|
375
|
-
if (closeResult.already_closed !== undefined) {
|
|
376
|
-
result.already_closed = Boolean(closeResult.already_closed);
|
|
377
|
-
}
|
|
378
|
-
return result;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function createRecruitCloseFailureError(closeResult) {
|
|
382
|
-
const error = new Error(closeResult?.reason || "Recruit detail did not close before recovery");
|
|
383
|
-
error.code = "DETAIL_CLOSE_FAILED";
|
|
384
|
-
error.close_result = closeResult || null;
|
|
385
|
-
return error;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function createRecruitBlockingPanelCloseFailureError(closeResult, phase = "") {
|
|
389
|
-
const error = new Error(closeResult?.reason || "Boss account-rights panel did not close before recovery");
|
|
390
|
-
error.code = "ACCOUNT_RIGHTS_PANEL_CLOSE_FAILED";
|
|
391
|
-
error.close_result = closeResult || null;
|
|
392
|
-
error.phase = phase || null;
|
|
393
|
-
return error;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function createRecruitRefreshFailureError(refreshAttempt, {
|
|
397
|
-
listEndReason = "",
|
|
398
|
-
targetCount = 0,
|
|
399
|
-
processedCount = 0
|
|
400
|
-
} = {}) {
|
|
401
|
-
const reason = refreshAttempt?.application?.post_search_state?.ok === false
|
|
402
|
-
? "search_result_not_ready"
|
|
403
|
-
: refreshAttempt?.application?.post_search_state?.counts?.candidate_card === 0
|
|
404
|
-
? "no_cards_after_refresh"
|
|
405
|
-
: "refresh_failed";
|
|
406
|
-
const error = new Error(`Recruit/search refresh failed before target was reached (${reason})`);
|
|
407
|
-
error.code = "RECRUIT_END_REFRESH_FAILED";
|
|
408
|
-
error.refresh_attempt = refreshAttempt || null;
|
|
409
|
-
error.list_end_reason = listEndReason || null;
|
|
410
|
-
error.target_count = targetCount;
|
|
411
|
-
error.processed_count = processedCount;
|
|
412
|
-
return error;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function isRefreshableListStall(reason = "") {
|
|
416
|
-
return new Set([
|
|
417
|
-
"stable_visible_signature",
|
|
418
|
-
"max_scrolls_exhausted",
|
|
419
|
-
"scroll_failed",
|
|
420
|
-
"scroll_anchor_unavailable"
|
|
421
|
-
]).has(String(reason || ""));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export function isStaleRecruitNodeError(error) {
|
|
425
|
-
const message = String(error?.message || error || "");
|
|
426
|
-
return /Could not find node with given id|No node with given id|Node is detached|Cannot find node/i.test(message);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
export function isRecoverableRecruitImageCaptureError(error) {
|
|
430
|
-
const code = String(error?.code || "");
|
|
431
|
-
if (code === "IMAGE_CAPTURE_TIMEOUT" || code === "IMAGE_CAPTURE_TOTAL_TIMEOUT") return true;
|
|
432
|
-
if (isStaleRecruitNodeError(error)) return true;
|
|
433
|
-
return /Image fallback capture timed out/i.test(String(error?.message || error || ""));
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
export function isRecoverableRecruitDetailError(error) {
|
|
437
|
-
return isStaleRecruitNodeError(error);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function compactRecoverableDetailError(error) {
|
|
441
|
-
return compactError(error, isStaleRecruitNodeError(error) ? "DETAIL_STALE_NODE" : "DETAIL_OPEN_FAILED");
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function collectPartialImageEvidencePaths(basePath = "", extension = "jpg", maxCount = 12) {
|
|
445
|
-
const resolved = String(basePath || "").trim();
|
|
446
|
-
if (!resolved) return [];
|
|
447
|
-
const parsed = path.parse(resolved);
|
|
448
|
-
const ext = parsed.ext || `.${String(extension || "jpg").replace(/^\./, "") || "jpg"}`;
|
|
449
|
-
const files = [];
|
|
450
|
-
for (let index = 0; index < Math.max(1, Number(maxCount) || 1); index += 1) {
|
|
451
|
-
const page = String(index + 1).padStart(2, "0");
|
|
452
|
-
const candidatePath = path.join(parsed.dir, `${parsed.name}-page-${page}${ext}`);
|
|
453
|
-
if (fs.existsSync(candidatePath)) files.push(candidatePath);
|
|
454
|
-
}
|
|
455
|
-
return files;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
export function createRecoverableRecruitImageCaptureEvidence(error, {
|
|
459
|
-
elapsedMs = 0,
|
|
460
|
-
filePath = "",
|
|
461
|
-
extension = "jpg",
|
|
462
|
-
maxScreenshots = DEFAULT_MAX_IMAGE_PAGES
|
|
463
|
-
} = {}) {
|
|
464
|
-
const filePaths = collectPartialImageEvidencePaths(filePath, extension, maxScreenshots);
|
|
465
|
-
return {
|
|
466
|
-
schema_version: 1,
|
|
467
|
-
ok: false,
|
|
468
|
-
source: "image-scroll-sequence",
|
|
469
|
-
elapsed_ms: Math.max(0, Math.round(Number(error?.elapsed_ms ?? elapsedMs) || 0)),
|
|
470
|
-
capture_count: filePaths.length,
|
|
471
|
-
screenshot_count: filePaths.length,
|
|
472
|
-
unique_screenshot_count: filePaths.length,
|
|
473
|
-
dropped_duplicate_count: 0,
|
|
474
|
-
total_byte_length: 0,
|
|
475
|
-
original_total_byte_length: 0,
|
|
476
|
-
llm_screenshot_count: 0,
|
|
477
|
-
llm_total_byte_length: 0,
|
|
478
|
-
llm_original_total_byte_length: 0,
|
|
479
|
-
llm_composition_error: null,
|
|
480
|
-
error_code: error?.code || (isStaleRecruitNodeError(error) ? "IMAGE_CAPTURE_STALE_NODE" : "IMAGE_CAPTURE_FAILED"),
|
|
481
|
-
error: error?.message || String(error || "Image capture failed"),
|
|
482
|
-
file_paths: filePaths,
|
|
483
|
-
llm_file_paths: []
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function createImageCaptureFailureScreening(candidate, error) {
|
|
488
|
-
return {
|
|
489
|
-
status: "fail",
|
|
490
|
-
passed: false,
|
|
491
|
-
score: 0,
|
|
492
|
-
reasons: ["image_capture_failed"],
|
|
493
|
-
error: compactError(error, "IMAGE_CAPTURE_FAILED"),
|
|
494
|
-
candidate
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function createRecoverableDetailFailureScreening(candidate, error) {
|
|
499
|
-
return {
|
|
500
|
-
status: "fail",
|
|
501
|
-
passed: false,
|
|
502
|
-
score: 0,
|
|
503
|
-
reasons: isStaleRecruitNodeError(error)
|
|
504
|
-
? ["detail_open_failed", "stale_node"]
|
|
505
|
-
: ["detail_open_failed"],
|
|
506
|
-
error: compactRecoverableDetailError(error),
|
|
507
|
-
candidate
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
export function countRecruitResultStatuses(results = [], {
|
|
512
|
-
greetCount = 0
|
|
513
|
-
} = {}) {
|
|
514
|
-
return {
|
|
515
|
-
processed: results.length,
|
|
516
|
-
screened: results.length,
|
|
517
|
-
detail_opened: results.filter((item) => item.detail).length,
|
|
518
|
-
passed: results.filter((item) => item.screening?.passed).length,
|
|
519
|
-
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
520
|
-
greet_count: greetCount,
|
|
521
|
-
post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
|
|
522
|
-
image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
|
|
523
|
-
detail_open_failed: results.filter((item) => (
|
|
524
|
-
item.error?.code === "DETAIL_STALE_NODE"
|
|
525
|
-
|| item.error?.code === "DETAIL_OPEN_FAILED"
|
|
526
|
-
)).length,
|
|
527
|
-
transient_recovered: results.filter((item) => (
|
|
528
|
-
item.error?.code === "DETAIL_STALE_NODE"
|
|
529
|
-
|| item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
|
|
530
|
-
|| item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
|
|
531
|
-
|| item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
|
|
532
|
-
)).length
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
export async function runRecruitWorkflow({
|
|
537
|
-
client,
|
|
538
|
-
targetUrl = "",
|
|
539
|
-
criteria = "",
|
|
540
|
-
searchParams = {},
|
|
541
|
-
maxCandidates = 5,
|
|
542
|
-
detailLimit = null,
|
|
543
|
-
closeDetail = true,
|
|
544
|
-
delayMs = 0,
|
|
545
|
-
cardTimeoutMs = 90000,
|
|
546
|
-
resetBeforeSearch = true,
|
|
547
|
-
resetTimeoutMs = 180000,
|
|
548
|
-
cityOptionTimeoutMs = 30000,
|
|
549
|
-
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
550
|
-
imageWheelDeltaY = 650,
|
|
551
|
-
cvAcquisitionMode = "unknown",
|
|
552
|
-
listMaxScrolls = 20,
|
|
553
|
-
listStableSignatureLimit = 5,
|
|
554
|
-
listWheelDeltaY = 850,
|
|
555
|
-
listSettleMs = 2200,
|
|
556
|
-
listFallbackPoint = null,
|
|
557
|
-
refreshOnEnd = true,
|
|
558
|
-
maxRefreshRounds = 2,
|
|
559
|
-
refreshResetSettleMs = 5000,
|
|
560
|
-
screeningMode = "llm",
|
|
561
|
-
llmConfig = null,
|
|
562
|
-
llmTimeoutMs = 120000,
|
|
563
|
-
llmImageLimit = 8,
|
|
564
|
-
llmImageDetail = "high",
|
|
565
|
-
imageOutputDir = "",
|
|
566
|
-
humanRestEnabled = false,
|
|
567
|
-
humanBehavior = null,
|
|
568
|
-
postAction = "none",
|
|
569
|
-
maxGreetCount = null,
|
|
570
|
-
executePostAction = true,
|
|
571
|
-
actionTimeoutMs = 8000,
|
|
572
|
-
actionIntervalMs = 400,
|
|
573
|
-
actionAfterClickDelayMs = 900
|
|
574
|
-
} = {}, runControl) {
|
|
575
|
-
if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
|
|
576
|
-
const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
|
|
577
|
-
legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
|
|
578
|
-
});
|
|
579
|
-
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
580
|
-
configureHumanInteraction(client, {
|
|
581
|
-
enabled: effectiveHumanBehavior.enabled,
|
|
582
|
-
clickMovementEnabled: effectiveHumanBehavior.clickMovement,
|
|
583
|
-
textEntryEnabled: effectiveHumanBehavior.textEntry,
|
|
584
|
-
safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
|
|
585
|
-
actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
|
|
586
|
-
});
|
|
587
|
-
const humanRestController = createHumanRestController({
|
|
588
|
-
enabled: effectiveHumanRestEnabled,
|
|
589
|
-
shortRestEnabled: effectiveHumanBehavior.shortRest,
|
|
590
|
-
batchRestEnabled: effectiveHumanBehavior.batchRest,
|
|
591
|
-
restLevel: effectiveHumanBehavior.restLevel
|
|
592
|
-
});
|
|
593
|
-
const normalizedSearchParams = normalizeSearchParams(searchParams);
|
|
594
|
-
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
595
|
-
const normalizedPostAction = normalizeRecruitPostAction(postAction);
|
|
596
|
-
const postActionEnabled = normalizedPostAction !== "none";
|
|
597
|
-
const useLlmScreening = normalizedScreeningMode !== "deterministic";
|
|
598
|
-
const searchExchangeResumeFilterRequested = normalizedSearchParams.skip_recent_colleague_contacted === true;
|
|
599
|
-
let searchExchangeResumeFilterApplied = false;
|
|
600
|
-
const limit = Math.max(1, Number(maxCandidates) || 1);
|
|
601
|
-
const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
|
|
602
|
-
const networkRecorder = detailCountLimit > 0
|
|
603
|
-
? createRecruitDetailNetworkRecorder(client)
|
|
604
|
-
: null;
|
|
605
|
-
const cvAcquisitionState = createCvAcquisitionState({ mode: cvAcquisitionMode });
|
|
606
|
-
const listState = createInfiniteListState({
|
|
607
|
-
domain: "recruit",
|
|
608
|
-
listName: "search-results"
|
|
609
|
-
});
|
|
610
|
-
const viewportGuard = createViewportRunGuard({
|
|
611
|
-
client,
|
|
612
|
-
domain: "recruit",
|
|
613
|
-
root: "frame",
|
|
614
|
-
frameOwnerRoot: "frameOwner",
|
|
615
|
-
runControl,
|
|
616
|
-
getRoots: getRecruitRoots
|
|
617
|
-
});
|
|
618
|
-
async function ensureRecruitViewport(rootState, phase) {
|
|
619
|
-
const result = await viewportGuard.ensure(rootState, { phase });
|
|
620
|
-
return result.rootState || rootState;
|
|
621
|
-
}
|
|
622
|
-
const results = [];
|
|
623
|
-
const refreshAttempts = [];
|
|
624
|
-
let greetCount = 0;
|
|
625
|
-
let lastGreetQuotaAfterSpend = null;
|
|
626
|
-
let refreshRounds = 0;
|
|
627
|
-
let contextRecoveryAttempts = 0;
|
|
628
|
-
const candidateRecoveryCounts = new Map();
|
|
629
|
-
let rootState = null;
|
|
630
|
-
let cardNodeIds = [];
|
|
631
|
-
let listEndReason = "";
|
|
632
|
-
let lastHumanEvent = null;
|
|
633
|
-
const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
|
|
634
|
-
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
635
|
-
containerSelectors: RECRUIT_LIST_CONTAINER_SELECTORS,
|
|
636
|
-
itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
|
|
637
|
-
itemSelectors: [RECRUIT_CARD_SELECTOR],
|
|
638
|
-
viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
|
|
639
|
-
validateViewportPoint: true
|
|
640
|
-
}));
|
|
641
|
-
|
|
642
|
-
function recordHumanEvent(event = null) {
|
|
643
|
-
if (!event) return lastHumanEvent;
|
|
644
|
-
lastHumanEvent = {
|
|
645
|
-
at: new Date().toISOString(),
|
|
646
|
-
...event
|
|
647
|
-
};
|
|
648
|
-
return lastHumanEvent;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
async function maybeHumanActionCooldown(phase, timings = {}) {
|
|
652
|
-
if (!effectiveHumanBehavior.actionCooldown) return null;
|
|
653
|
-
const pauseMs = humanDelay(280, 90, {
|
|
654
|
-
minMs: 80,
|
|
655
|
-
maxMs: 720
|
|
656
|
-
});
|
|
657
|
-
if (pauseMs > 0) {
|
|
658
|
-
await runControl.sleep(pauseMs);
|
|
659
|
-
addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
|
|
660
|
-
}
|
|
661
|
-
return recordHumanEvent({
|
|
662
|
-
kind: "action_cooldown",
|
|
663
|
-
phase,
|
|
664
|
-
pause_ms: pauseMs
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function updateRecruitProgress(extra = {}) {
|
|
669
|
-
const counts = countRecruitResultStatuses(results, { greetCount });
|
|
670
|
-
const listSnapshot = compactInfiniteListState(listState);
|
|
671
|
-
const humanRestState = humanRestController.getState();
|
|
672
|
-
runControl.updateProgress({
|
|
673
|
-
card_count: cardNodeIds.length,
|
|
674
|
-
target_count: limit,
|
|
675
|
-
...counts,
|
|
676
|
-
screening_mode: normalizedScreeningMode,
|
|
677
|
-
unique_seen: listSnapshot.seen_count,
|
|
678
|
-
scroll_count: listSnapshot.scroll_count,
|
|
679
|
-
refresh_rounds: refreshRounds,
|
|
680
|
-
refresh_attempts: refreshAttempts.length,
|
|
681
|
-
context_recoveries: contextRecoveryAttempts,
|
|
682
|
-
list_end_reason: listEndReason || null,
|
|
683
|
-
viewport_checks: viewportGuard.getStats().checks,
|
|
684
|
-
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
685
|
-
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
686
|
-
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
687
|
-
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
688
|
-
human_rest_enabled: effectiveHumanRestEnabled,
|
|
689
|
-
human_rest_count: humanRestState.rest_count,
|
|
690
|
-
human_rest_ms: humanRestState.total_rest_ms,
|
|
691
|
-
search_exchange_resume_filter_requested: searchExchangeResumeFilterRequested ? 1 : 0,
|
|
692
|
-
search_exchange_resume_filter_applied: searchExchangeResumeFilterApplied ? 1 : 0,
|
|
693
|
-
last_human_event: lastHumanEvent,
|
|
694
|
-
...extra
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function checkpointInProgressCandidate({
|
|
699
|
-
index = results.length,
|
|
700
|
-
candidateKey = "",
|
|
701
|
-
cardNodeId = null,
|
|
702
|
-
detailStep = "",
|
|
703
|
-
error = null
|
|
704
|
-
} = {}) {
|
|
705
|
-
runControl.checkpoint({
|
|
706
|
-
in_progress_candidate: {
|
|
707
|
-
index,
|
|
708
|
-
key: candidateKey,
|
|
709
|
-
card_node_id: cardNodeId,
|
|
710
|
-
detail_step: detailStep || null,
|
|
711
|
-
counters: countRecruitResultStatuses(results, { greetCount }),
|
|
712
|
-
error: compactError(error, "RECRUIT_IN_PROGRESS_ERROR")
|
|
713
|
-
},
|
|
714
|
-
candidate_list: compactInfiniteListState(listState)
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
async function closeRecruitBlockingPanelsForRun(phase = "cleanup") {
|
|
719
|
-
const result = await closeRecruitBlockingPanels(client, {
|
|
720
|
-
attemptsLimit: 2,
|
|
721
|
-
rootState
|
|
722
|
-
});
|
|
723
|
-
if (!result?.closed) {
|
|
724
|
-
throw createRecruitBlockingPanelCloseFailureError(result, phase);
|
|
725
|
-
}
|
|
726
|
-
return result;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
async function recoverAndReapplyRecruitContext(reason = "context_recovery", error = null, {
|
|
730
|
-
forceRecentViewed = true
|
|
731
|
-
} = {}) {
|
|
732
|
-
await runControl.waitIfPaused();
|
|
733
|
-
runControl.throwIfCanceled();
|
|
734
|
-
const started = Date.now();
|
|
735
|
-
runControl.setPhase("recruit:recover-context");
|
|
736
|
-
contextRecoveryAttempts += 1;
|
|
737
|
-
const refreshResult = await refreshRecruitSearchAtEnd(client, {
|
|
738
|
-
searchParams: normalizedSearchParams,
|
|
739
|
-
requireCards: true,
|
|
740
|
-
searchTimeoutMs: cardTimeoutMs,
|
|
741
|
-
resetTimeoutMs,
|
|
742
|
-
resetSettleMs: refreshResetSettleMs,
|
|
743
|
-
cityOptionTimeoutMs,
|
|
744
|
-
forceRecentViewed
|
|
745
|
-
});
|
|
746
|
-
let blockingPanelClose = null;
|
|
747
|
-
if (refreshResult.ok) {
|
|
748
|
-
blockingPanelClose = await closeRecruitBlockingPanels(client, {
|
|
749
|
-
attemptsLimit: 2
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
const compactRefresh = {
|
|
753
|
-
...compactRefreshAttempt(refreshResult),
|
|
754
|
-
context_recovery: true,
|
|
755
|
-
recovery_reason: reason,
|
|
756
|
-
trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
|
|
757
|
-
account_rights_panel_close: compactCloseResult(blockingPanelClose),
|
|
758
|
-
elapsed_ms: Date.now() - started
|
|
759
|
-
};
|
|
760
|
-
refreshAttempts.push(compactRefresh);
|
|
761
|
-
runControl.checkpoint({
|
|
762
|
-
context_recovery: {
|
|
763
|
-
attempt: contextRecoveryAttempts,
|
|
764
|
-
reason,
|
|
765
|
-
trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
|
|
766
|
-
refresh: compactRefresh,
|
|
767
|
-
counters: countRecruitResultStatuses(results, { greetCount })
|
|
768
|
-
},
|
|
769
|
-
candidate_list: compactInfiniteListState(listState)
|
|
770
|
-
});
|
|
771
|
-
if (!refreshResult.ok) {
|
|
772
|
-
updateRecruitProgress({
|
|
773
|
-
refresh_method: refreshResult.method || null,
|
|
774
|
-
refresh_forced_recent_viewed: forceRecentViewed,
|
|
775
|
-
recovery_reason: reason
|
|
776
|
-
});
|
|
777
|
-
throw new Error(`Recruit context recovery failed after ${reason}: ${refreshResult.application?.reason || "refresh returned no cards"}`);
|
|
778
|
-
}
|
|
779
|
-
if (!blockingPanelClose?.closed) {
|
|
780
|
-
const panelError = createRecruitBlockingPanelCloseFailureError(blockingPanelClose, `recover:${reason}`);
|
|
781
|
-
panelError.refresh_attempt = compactRefresh;
|
|
782
|
-
throw panelError;
|
|
783
|
-
}
|
|
784
|
-
rootState = await getRecruitRoots(client);
|
|
785
|
-
rootState = await ensureRecruitViewport(rootState, "recover_after");
|
|
786
|
-
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
787
|
-
timeoutMs: cardTimeoutMs,
|
|
788
|
-
intervalMs: 300
|
|
789
|
-
});
|
|
790
|
-
resetInfiniteListForRefreshRound(listState, {
|
|
791
|
-
reason: `context_recovery:${reason}`,
|
|
792
|
-
round: contextRecoveryAttempts,
|
|
793
|
-
method: refreshResult.method,
|
|
794
|
-
metadata: {
|
|
795
|
-
card_count: cardNodeIds.length,
|
|
796
|
-
forced_recent_viewed: forceRecentViewed,
|
|
797
|
-
counters: countRecruitResultStatuses(results, { greetCount })
|
|
798
|
-
}
|
|
799
|
-
});
|
|
800
|
-
listEndReason = "";
|
|
801
|
-
updateRecruitProgress({
|
|
802
|
-
card_count: cardNodeIds.length,
|
|
803
|
-
refresh_method: refreshResult.method || null,
|
|
804
|
-
refresh_forced_recent_viewed: forceRecentViewed,
|
|
805
|
-
recovery_reason: reason
|
|
806
|
-
});
|
|
807
|
-
return refreshResult;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
runControl.setPhase("recruit:cleanup");
|
|
811
|
-
await closeRecruitDetail(client, { attemptsLimit: 2 });
|
|
812
|
-
await closeRecruitBlockingPanelsForRun("cleanup");
|
|
813
|
-
|
|
814
|
-
await runControl.waitIfPaused();
|
|
815
|
-
runControl.throwIfCanceled();
|
|
816
|
-
runControl.setPhase("recruit:roots");
|
|
817
|
-
rootState = await getRecruitRoots(client);
|
|
818
|
-
rootState = await ensureRecruitViewport(rootState, "roots");
|
|
819
|
-
runControl.checkpoint({
|
|
820
|
-
iframe_selector: rootState.iframe.selector,
|
|
821
|
-
iframe_document_node_id: rootState.iframe.documentNodeId,
|
|
822
|
-
search_params: normalizedSearchParams
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
if (hasRecruitSearchParams(normalizedSearchParams)) {
|
|
826
|
-
await runControl.waitIfPaused();
|
|
827
|
-
runControl.throwIfCanceled();
|
|
828
|
-
runControl.setPhase("recruit:search");
|
|
829
|
-
const searchResult = await applyRecruitSearchParams(client, {
|
|
830
|
-
searchParams: normalizedSearchParams,
|
|
831
|
-
requireCards: true,
|
|
832
|
-
resetBeforeApply: resetBeforeSearch,
|
|
833
|
-
searchTimeoutMs: cardTimeoutMs,
|
|
834
|
-
resetTimeoutMs,
|
|
835
|
-
cityOptionTimeoutMs
|
|
836
|
-
});
|
|
837
|
-
const exchangeResumeStep = searchResult.steps.find((step) => step.step === "exchange_resume");
|
|
838
|
-
searchExchangeResumeFilterApplied = Boolean(
|
|
839
|
-
searchExchangeResumeFilterRequested
|
|
840
|
-
&& exchangeResumeStep?.result?.applied
|
|
841
|
-
&& exchangeResumeStep?.result?.requested === true
|
|
842
|
-
);
|
|
843
|
-
runControl.checkpoint({
|
|
844
|
-
search: {
|
|
845
|
-
search_params: searchResult.search_params,
|
|
846
|
-
before_counts: searchResult.before_counts,
|
|
847
|
-
post_search_state: searchResult.post_search_state,
|
|
848
|
-
steps: searchResult.steps.map((step) => ({
|
|
849
|
-
step: step.step,
|
|
850
|
-
applied: step.result?.applied,
|
|
851
|
-
clicked: step.result?.clicked,
|
|
852
|
-
searched: step.result?.searched,
|
|
853
|
-
reason: step.result?.reason || null
|
|
854
|
-
}))
|
|
855
|
-
}
|
|
856
|
-
});
|
|
857
|
-
rootState = await getRecruitRoots(client);
|
|
858
|
-
rootState = await ensureRecruitViewport(rootState, "search");
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
await runControl.waitIfPaused();
|
|
862
|
-
runControl.throwIfCanceled();
|
|
863
|
-
runControl.setPhase("recruit:cards");
|
|
864
|
-
rootState = await ensureRecruitViewport(rootState, "cards");
|
|
865
|
-
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
866
|
-
timeoutMs: cardTimeoutMs,
|
|
867
|
-
intervalMs: 300
|
|
868
|
-
});
|
|
869
|
-
if (!cardNodeIds.length) {
|
|
870
|
-
throw new Error("No recruit/search candidate cards found for run service");
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
updateRecruitProgress({
|
|
874
|
-
list_end_reason: null
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
while (results.length < limit) {
|
|
878
|
-
const candidateStarted = Date.now();
|
|
879
|
-
const timings = {};
|
|
880
|
-
await runControl.waitIfPaused();
|
|
881
|
-
runControl.throwIfCanceled();
|
|
882
|
-
runControl.setPhase("recruit:candidate");
|
|
883
|
-
rootState = await ensureRecruitViewport(rootState, "candidate_loop");
|
|
884
|
-
|
|
885
|
-
const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
|
|
886
|
-
client,
|
|
887
|
-
state: listState,
|
|
888
|
-
maxScrolls: listMaxScrolls,
|
|
889
|
-
stableSignatureLimit: listStableSignatureLimit,
|
|
890
|
-
wheelDeltaY: listWheelDeltaY,
|
|
891
|
-
settleMs: listSettleMs,
|
|
892
|
-
listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
893
|
-
fallbackPoint: listFallbackResolver,
|
|
894
|
-
findNodeIds: async () => {
|
|
895
|
-
let currentRootState = await getRecruitRoots(client);
|
|
896
|
-
currentRootState = await ensureRecruitViewport(currentRootState, "candidate_find_nodes");
|
|
897
|
-
rootState = currentRootState;
|
|
898
|
-
const currentCardNodeIds = await waitForRecruitCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
899
|
-
timeoutMs: Math.min(cardTimeoutMs, 5000),
|
|
900
|
-
intervalMs: 300
|
|
901
|
-
});
|
|
902
|
-
cardNodeIds = currentCardNodeIds;
|
|
903
|
-
return currentCardNodeIds;
|
|
904
|
-
},
|
|
905
|
-
readCandidate: async (nodeId, { visibleIndex }) => readRecruitCardCandidate(client, nodeId, {
|
|
906
|
-
targetUrl,
|
|
907
|
-
source: "recruit-run-card",
|
|
908
|
-
metadata: {
|
|
909
|
-
run_candidate_index: results.length,
|
|
910
|
-
visible_index: visibleIndex,
|
|
911
|
-
search_params: normalizedSearchParams
|
|
912
|
-
}
|
|
913
|
-
}),
|
|
914
|
-
detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
|
|
915
|
-
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
916
|
-
markerSelectors: RECRUIT_BOTTOM_MARKER_SELECTORS,
|
|
917
|
-
refreshSelectors: RECRUIT_BOTTOM_REFRESH_SELECTORS,
|
|
918
|
-
textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
|
|
919
|
-
maxTextScanNodes: 500
|
|
920
|
-
})
|
|
921
|
-
}));
|
|
922
|
-
if (!nextCandidateResult.ok) {
|
|
923
|
-
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
924
|
-
if (
|
|
925
|
-
(nextCandidateResult.end_reached || isRefreshableListStall(nextCandidateResult.reason))
|
|
926
|
-
&& refreshOnEnd
|
|
927
|
-
&& results.length < limit
|
|
928
|
-
&& refreshRounds < Math.max(0, Number(maxRefreshRounds) || 0)
|
|
929
|
-
) {
|
|
930
|
-
await runControl.waitIfPaused();
|
|
931
|
-
runControl.throwIfCanceled();
|
|
932
|
-
runControl.setPhase("recruit:refresh");
|
|
933
|
-
refreshRounds += 1;
|
|
934
|
-
const refreshResult = await refreshRecruitSearchAtEnd(client, {
|
|
935
|
-
searchParams: normalizedSearchParams,
|
|
936
|
-
requireCards: true,
|
|
937
|
-
searchTimeoutMs: cardTimeoutMs,
|
|
938
|
-
resetTimeoutMs,
|
|
939
|
-
resetSettleMs: refreshResetSettleMs,
|
|
940
|
-
cityOptionTimeoutMs
|
|
941
|
-
});
|
|
942
|
-
const compactRefresh = compactRefreshAttempt(refreshResult);
|
|
943
|
-
refreshAttempts.push(compactRefresh);
|
|
944
|
-
runControl.checkpoint({
|
|
945
|
-
refresh_round: refreshRounds,
|
|
946
|
-
refresh: compactRefresh
|
|
947
|
-
});
|
|
948
|
-
updateRecruitProgress({
|
|
949
|
-
card_count: refreshResult.card_count || cardNodeIds.length,
|
|
950
|
-
refresh_method: refreshResult.method || null,
|
|
951
|
-
refresh_forced_recent_viewed: true,
|
|
952
|
-
list_end_reason: listEndReason
|
|
953
|
-
});
|
|
954
|
-
if (refreshResult.ok) {
|
|
955
|
-
rootState = await getRecruitRoots(client);
|
|
956
|
-
rootState = await ensureRecruitViewport(rootState, "refresh_after");
|
|
957
|
-
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
958
|
-
timeoutMs: cardTimeoutMs,
|
|
959
|
-
intervalMs: 300
|
|
960
|
-
});
|
|
961
|
-
resetInfiniteListForRefreshRound(listState, {
|
|
962
|
-
reason: listEndReason,
|
|
963
|
-
round: refreshRounds,
|
|
964
|
-
method: refreshResult.method,
|
|
965
|
-
metadata: {
|
|
966
|
-
card_count: cardNodeIds.length,
|
|
967
|
-
forced_recent_viewed: true
|
|
968
|
-
}
|
|
969
|
-
});
|
|
970
|
-
listEndReason = "";
|
|
971
|
-
continue;
|
|
972
|
-
}
|
|
973
|
-
throw createRecruitRefreshFailureError(compactRefresh, {
|
|
974
|
-
listEndReason,
|
|
975
|
-
targetCount: limit,
|
|
976
|
-
processedCount: results.length
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
break;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
const index = results.length;
|
|
983
|
-
const cardNodeId = nextCandidateResult.item.node_id;
|
|
984
|
-
const candidateKey = nextCandidateResult.item.key;
|
|
985
|
-
const cardCandidate = nextCandidateResult.item.candidate;
|
|
986
|
-
|
|
987
|
-
let screeningCandidate = cardCandidate;
|
|
988
|
-
let detailResult = null;
|
|
989
|
-
let detailActionRootNodeIds = [];
|
|
990
|
-
let recoverableDetailError = null;
|
|
991
|
-
let detailStep = "not_started";
|
|
992
|
-
if (index < detailCountLimit) {
|
|
993
|
-
try {
|
|
994
|
-
await runControl.waitIfPaused();
|
|
995
|
-
runControl.throwIfCanceled();
|
|
996
|
-
runControl.setPhase("recruit:detail");
|
|
997
|
-
detailStep = "ensure_viewport";
|
|
998
|
-
rootState = await ensureRecruitViewport(rootState, "detail");
|
|
999
|
-
const blockingPanelClose = await closeRecruitBlockingPanels(client, {
|
|
1000
|
-
attemptsLimit: 2,
|
|
1001
|
-
rootState
|
|
1002
|
-
});
|
|
1003
|
-
if (!blockingPanelClose?.closed) {
|
|
1004
|
-
const panelError = createRecruitBlockingPanelCloseFailureError(
|
|
1005
|
-
blockingPanelClose,
|
|
1006
|
-
"before_detail_open"
|
|
1007
|
-
);
|
|
1008
|
-
timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
|
|
1009
|
-
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error: panelError });
|
|
1010
|
-
await recoverAndReapplyRecruitContext("account_rights_panel_before_detail", panelError, {
|
|
1011
|
-
forceRecentViewed: true
|
|
1012
|
-
});
|
|
1013
|
-
continue;
|
|
1014
|
-
}
|
|
1015
|
-
if (blockingPanelClose.already_closed === false) {
|
|
1016
|
-
timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
|
|
1017
|
-
}
|
|
1018
|
-
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
1019
|
-
detailStep = "open_detail";
|
|
1020
|
-
networkRecorder.clear();
|
|
1021
|
-
await maybeHumanActionCooldown("before_detail_open", timings);
|
|
1022
|
-
const openedDetail = await openRecruitCardDetail(client, cardNodeId);
|
|
1023
|
-
detailActionRootNodeIds = (openedDetail.detail_state?.roots || []).map((root) => root.nodeId).filter(Boolean);
|
|
1024
|
-
addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
|
|
1025
|
-
addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
|
|
1026
|
-
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
1027
|
-
detailStep = "wait_network";
|
|
1028
|
-
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
1029
|
-
waitForRecruitDetailNetworkEvents,
|
|
1030
|
-
networkRecorder,
|
|
1031
|
-
{
|
|
1032
|
-
waitPlan,
|
|
1033
|
-
minCount: 1,
|
|
1034
|
-
requireLoaded: true,
|
|
1035
|
-
intervalMs: 120
|
|
1036
|
-
}
|
|
1037
|
-
));
|
|
1038
|
-
if (networkWait?.elapsed_ms != null) {
|
|
1039
|
-
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
1040
|
-
}
|
|
1041
|
-
detailStep = "extract_detail";
|
|
1042
|
-
detailResult = await extractRecruitDetailCandidate(client, {
|
|
1043
|
-
cardCandidate,
|
|
1044
|
-
cardNodeId,
|
|
1045
|
-
detailState: openedDetail.detail_state,
|
|
1046
|
-
networkEvents: networkRecorder.events,
|
|
1047
|
-
targetUrl,
|
|
1048
|
-
closeDetail: false,
|
|
1049
|
-
networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
|
|
1050
|
-
networkParseIntervalMs: 250
|
|
1051
|
-
});
|
|
1052
|
-
detailResult.card_box = openedDetail.card_box || null;
|
|
1053
|
-
detailResult.open_attempts = openedDetail.open_attempts || [];
|
|
1054
|
-
addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
|
|
1055
|
-
const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
1056
|
-
let source = "network";
|
|
1057
|
-
let imageEvidence = null;
|
|
1058
|
-
let captureTarget = null;
|
|
1059
|
-
let captureTargetWait = null;
|
|
1060
|
-
if (parsedNetworkProfileCount > 0) {
|
|
1061
|
-
recordCvNetworkHit(cvAcquisitionState, {
|
|
1062
|
-
parsedNetworkProfileCount,
|
|
1063
|
-
waitResult: networkWait
|
|
1064
|
-
});
|
|
1065
|
-
} else {
|
|
1066
|
-
detailStep = "wait_capture_target";
|
|
1067
|
-
captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
|
|
1068
|
-
domain: "recruit",
|
|
1069
|
-
timeoutMs: 6000,
|
|
1070
|
-
intervalMs: 250
|
|
1071
|
-
});
|
|
1072
|
-
captureTarget = captureTargetWait.target || null;
|
|
1073
|
-
const captureNodeId = captureTarget?.node_id || null;
|
|
1074
|
-
if (captureNodeId) {
|
|
1075
|
-
const imageEvidencePath = imageEvidenceFilePath({
|
|
1076
|
-
imageOutputDir,
|
|
1077
|
-
domain: "recruit",
|
|
1078
|
-
runId: runControl?.runId,
|
|
1079
|
-
index,
|
|
1080
|
-
extension: "jpg"
|
|
1081
|
-
});
|
|
1082
|
-
try {
|
|
1083
|
-
detailStep = "capture_image";
|
|
1084
|
-
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
1085
|
-
filePath: imageEvidencePath,
|
|
1086
|
-
format: "jpeg",
|
|
1087
|
-
quality: 72,
|
|
1088
|
-
optimize: true,
|
|
1089
|
-
resizeMaxWidth: 1100,
|
|
1090
|
-
captureViewport: false,
|
|
1091
|
-
padding: 0,
|
|
1092
|
-
maxScreenshots: maxImagePages,
|
|
1093
|
-
wheelDeltaY: imageWheelDeltaY,
|
|
1094
|
-
settleMs: 350,
|
|
1095
|
-
scrollMethod: "dom-anchor-fallback-input",
|
|
1096
|
-
scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
1097
|
-
stepTimeoutMs: 45000,
|
|
1098
|
-
totalTimeoutMs: 90000,
|
|
1099
|
-
duplicateStopCount: 1,
|
|
1100
|
-
skipDuplicateScreenshots: true,
|
|
1101
|
-
composeForLlm: true,
|
|
1102
|
-
llmPagesPerImage: 3,
|
|
1103
|
-
llmResizeMaxWidth: 1100,
|
|
1104
|
-
llmQuality: 72,
|
|
1105
|
-
metadata: {
|
|
1106
|
-
domain: "recruit",
|
|
1107
|
-
capture_mode: "scroll_sequence",
|
|
1108
|
-
acquisition_reason: "network_miss_image_fallback",
|
|
1109
|
-
run_candidate_index: index,
|
|
1110
|
-
candidate_key: candidateKey,
|
|
1111
|
-
capture_target: captureTarget,
|
|
1112
|
-
capture_target_wait: captureTargetWait
|
|
1113
|
-
}
|
|
1114
|
-
}));
|
|
1115
|
-
source = "image";
|
|
1116
|
-
} catch (error) {
|
|
1117
|
-
if (!isRecoverableRecruitImageCaptureError(error)) throw error;
|
|
1118
|
-
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
1119
|
-
if (recoveryCount < 1) {
|
|
1120
|
-
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
1121
|
-
timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
|
|
1122
|
-
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1123
|
-
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1124
|
-
await closeRecruitBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
1125
|
-
await recoverAndReapplyRecruitContext(`image_capture:${detailStep}`, error, {
|
|
1126
|
-
forceRecentViewed: true
|
|
1127
|
-
});
|
|
1128
|
-
continue;
|
|
1129
|
-
}
|
|
1130
|
-
imageEvidence = createRecoverableRecruitImageCaptureEvidence(error, {
|
|
1131
|
-
elapsedMs: timings.screenshot_capture_ms,
|
|
1132
|
-
filePath: imageEvidencePath,
|
|
1133
|
-
extension: "jpg",
|
|
1134
|
-
maxScreenshots: maxImagePages
|
|
1135
|
-
});
|
|
1136
|
-
source = "image_capture_failed";
|
|
1137
|
-
}
|
|
1138
|
-
recordCvImageFallback(cvAcquisitionState, {
|
|
1139
|
-
reason: source === "image_capture_failed"
|
|
1140
|
-
? "network_miss_image_capture_failed"
|
|
1141
|
-
: "network_miss_image_fallback",
|
|
1142
|
-
parsedNetworkProfileCount,
|
|
1143
|
-
waitResult: networkWait,
|
|
1144
|
-
imageEvidence
|
|
1145
|
-
});
|
|
1146
|
-
} else {
|
|
1147
|
-
source = "missing_capture_node";
|
|
1148
|
-
recordCvNetworkMiss(cvAcquisitionState, {
|
|
1149
|
-
reason: "network_miss_no_capture_node",
|
|
1150
|
-
parsedNetworkProfileCount,
|
|
1151
|
-
waitResult: networkWait
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
detailResult.image_evidence = imageEvidence;
|
|
1157
|
-
detailResult.cv_acquisition = {
|
|
1158
|
-
source,
|
|
1159
|
-
mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
|
|
1160
|
-
wait_plan: waitPlan,
|
|
1161
|
-
network_wait: networkWait,
|
|
1162
|
-
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
1163
|
-
image_evidence: summarizeImageEvidence(imageEvidence),
|
|
1164
|
-
capture_target: captureTarget || null,
|
|
1165
|
-
capture_target_wait: captureTargetWait
|
|
1166
|
-
};
|
|
1167
|
-
screeningCandidate = detailResult.candidate;
|
|
1168
|
-
if (closeDetail && !postActionEnabled) {
|
|
1169
|
-
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
|
|
1170
|
-
await maybeHumanActionCooldown("after_detail_close", timings);
|
|
1171
|
-
if (!detailResult.close_result?.closed) {
|
|
1172
|
-
const closeError = createRecruitCloseFailureError(detailResult.close_result);
|
|
1173
|
-
const recovery = await recoverAndReapplyRecruitContext("detail_close_failed", closeError, {
|
|
1174
|
-
forceRecentViewed: true
|
|
1175
|
-
});
|
|
1176
|
-
detailResult.cv_acquisition = {
|
|
1177
|
-
...(detailResult.cv_acquisition || {}),
|
|
1178
|
-
close_recovery: {
|
|
1179
|
-
ok: Boolean(recovery.ok),
|
|
1180
|
-
method: recovery.method || "",
|
|
1181
|
-
forced_recent_viewed: Boolean(recovery.forced_recent_viewed),
|
|
1182
|
-
card_count: recovery.card_count || 0
|
|
1183
|
-
}
|
|
1184
|
-
};
|
|
1185
|
-
}
|
|
1186
|
-
} else {
|
|
1187
|
-
detailResult.close_result = null;
|
|
1188
|
-
}
|
|
1189
|
-
} catch (error) {
|
|
1190
|
-
if (!isRecoverableRecruitDetailError(error)) throw error;
|
|
1191
|
-
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
1192
|
-
if (recoveryCount < 1) {
|
|
1193
|
-
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
1194
|
-
timings.detail_recovery_trigger = compactRecoverableDetailError(error);
|
|
1195
|
-
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1196
|
-
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1197
|
-
await closeRecruitBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
1198
|
-
await recoverAndReapplyRecruitContext(`detail:${detailStep}`, error, {
|
|
1199
|
-
forceRecentViewed: true
|
|
1200
|
-
});
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
recoverableDetailError = error;
|
|
1204
|
-
detailResult = null;
|
|
1205
|
-
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
1206
|
-
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1207
|
-
await closeRecruitBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
await runControl.waitIfPaused();
|
|
1212
|
-
runControl.throwIfCanceled();
|
|
1213
|
-
runControl.setPhase("recruit:screening");
|
|
1214
|
-
let llmResult = null;
|
|
1215
|
-
if (useLlmScreening) {
|
|
1216
|
-
if (recoverableDetailError || detailResult?.image_evidence?.ok === false) {
|
|
1217
|
-
llmResult = null;
|
|
1218
|
-
} else if (!llmConfig) {
|
|
1219
|
-
llmResult = createMissingLlmConfigResult();
|
|
1220
|
-
} else {
|
|
1221
|
-
try {
|
|
1222
|
-
const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
|
|
1223
|
-
? "vision_model_ms"
|
|
1224
|
-
: "text_model_ms";
|
|
1225
|
-
llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
|
|
1226
|
-
candidate: screeningCandidate,
|
|
1227
|
-
criteria,
|
|
1228
|
-
config: llmConfig,
|
|
1229
|
-
timeoutMs: llmTimeoutMs,
|
|
1230
|
-
imageEvidence: detailResult?.image_evidence || null,
|
|
1231
|
-
maxImages: llmImageLimit,
|
|
1232
|
-
imageDetail: llmImageDetail
|
|
1233
|
-
}));
|
|
1234
|
-
} catch (error) {
|
|
1235
|
-
if (isFatalLlmProviderError(error)) {
|
|
1236
|
-
throw createFatalLlmRunError(error, {
|
|
1237
|
-
domain: "recruit",
|
|
1238
|
-
candidate: screeningCandidate
|
|
1239
|
-
});
|
|
1240
|
-
}
|
|
1241
|
-
llmResult = createFailedLlmScreeningResult(error);
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
if (detailResult) detailResult.llm_result = llmResult;
|
|
1245
|
-
}
|
|
1246
|
-
const screening = recoverableDetailError
|
|
1247
|
-
? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
|
|
1248
|
-
: detailResult?.image_evidence?.ok === false
|
|
1249
|
-
? createImageCaptureFailureScreening(screeningCandidate, {
|
|
1250
|
-
code: detailResult.image_evidence.error_code,
|
|
1251
|
-
message: detailResult.image_evidence.error
|
|
1252
|
-
})
|
|
1253
|
-
: useLlmScreening
|
|
1254
|
-
? llmResultToScreening(llmResult, screeningCandidate)
|
|
1255
|
-
: screenCandidate(screeningCandidate, { criteria });
|
|
1256
|
-
let actionDiscovery = null;
|
|
1257
|
-
let postActionResult = null;
|
|
1258
|
-
let closeFailureError = null;
|
|
1259
|
-
let closeRecoveryFailure = null;
|
|
1260
|
-
if (postActionEnabled && detailResult) {
|
|
1261
|
-
const postActionStarted = Date.now();
|
|
1262
|
-
await runControl.waitIfPaused();
|
|
1263
|
-
runControl.throwIfCanceled();
|
|
1264
|
-
runControl.setPhase("recruit:post-action");
|
|
1265
|
-
await maybeHumanActionCooldown("before_post_action", timings);
|
|
1266
|
-
actionDiscovery = await waitForRecruitDetailActionControls(client, {
|
|
1267
|
-
rootNodeIds: detailActionRootNodeIds,
|
|
1268
|
-
timeoutMs: actionTimeoutMs,
|
|
1269
|
-
intervalMs: actionIntervalMs,
|
|
1270
|
-
requireAny: true
|
|
1271
|
-
});
|
|
1272
|
-
postActionResult = await runRecruitPostAction({
|
|
1273
|
-
client,
|
|
1274
|
-
rootNodeIds: detailActionRootNodeIds,
|
|
1275
|
-
screening,
|
|
1276
|
-
actionDiscovery,
|
|
1277
|
-
postAction: normalizedPostAction,
|
|
1278
|
-
greetCount,
|
|
1279
|
-
maxGreetCount: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
|
|
1280
|
-
executePostAction,
|
|
1281
|
-
afterClickDelayMs: actionAfterClickDelayMs,
|
|
1282
|
-
lastGreetQuotaAfterSpend
|
|
1283
|
-
});
|
|
1284
|
-
if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
|
|
1285
|
-
greetCount += 1;
|
|
1286
|
-
}
|
|
1287
|
-
if (postActionResult.greet_quota_after_click?.found) {
|
|
1288
|
-
lastGreetQuotaAfterSpend = postActionResult.greet_quota_after_click;
|
|
1289
|
-
}
|
|
1290
|
-
addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
|
|
1291
|
-
}
|
|
1292
|
-
if (postActionEnabled && detailResult && closeDetail) {
|
|
1293
|
-
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
|
|
1294
|
-
await maybeHumanActionCooldown("after_detail_close", timings);
|
|
1295
|
-
if (!detailResult.close_result?.closed) {
|
|
1296
|
-
closeFailureError = createRecruitCloseFailureError(detailResult.close_result);
|
|
1297
|
-
try {
|
|
1298
|
-
const recovery = await recoverAndReapplyRecruitContext("detail_close_failed", closeFailureError, {
|
|
1299
|
-
forceRecentViewed: true
|
|
1300
|
-
});
|
|
1301
|
-
detailResult.cv_acquisition = {
|
|
1302
|
-
...(detailResult.cv_acquisition || {}),
|
|
1303
|
-
close_recovery: {
|
|
1304
|
-
ok: Boolean(recovery.ok),
|
|
1305
|
-
method: recovery.method || "",
|
|
1306
|
-
forced_recent_viewed: Boolean(recovery.forced_recent_viewed),
|
|
1307
|
-
card_count: recovery.card_count || 0
|
|
1308
|
-
}
|
|
1309
|
-
};
|
|
1310
|
-
} catch (error) {
|
|
1311
|
-
closeRecoveryFailure = error;
|
|
1312
|
-
detailResult.cv_acquisition = {
|
|
1313
|
-
...(detailResult.cv_acquisition || {}),
|
|
1314
|
-
close_recovery: {
|
|
1315
|
-
ok: false,
|
|
1316
|
-
reason: "context_recovery_failed",
|
|
1317
|
-
error: error?.message || String(error),
|
|
1318
|
-
forced_recent_viewed: true
|
|
1319
|
-
}
|
|
1320
|
-
};
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
timings.total_ms = Date.now() - candidateStarted;
|
|
1325
|
-
const compactResult = {
|
|
1326
|
-
index,
|
|
1327
|
-
candidate_key: candidateKey,
|
|
1328
|
-
card_node_id: cardNodeId,
|
|
1329
|
-
candidate: compactCandidate(screeningCandidate),
|
|
1330
|
-
detail: compactDetail(detailResult),
|
|
1331
|
-
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
1332
|
-
screening: compactScreening(screening),
|
|
1333
|
-
action_discovery: compactActionDiscovery(actionDiscovery),
|
|
1334
|
-
post_action: postActionResult,
|
|
1335
|
-
error: recoverableDetailError
|
|
1336
|
-
? compactRecoverableDetailError(recoverableDetailError)
|
|
1337
|
-
: closeRecoveryFailure
|
|
1338
|
-
? compactError(closeFailureError, "DETAIL_CLOSE_FAILED")
|
|
1339
|
-
: detailResult?.image_evidence?.ok === false
|
|
1340
|
-
? compactError({
|
|
1341
|
-
code: detailResult.image_evidence.error_code,
|
|
1342
|
-
message: detailResult.image_evidence.error
|
|
1343
|
-
}, "IMAGE_CAPTURE_FAILED")
|
|
1344
|
-
: null,
|
|
1345
|
-
timings
|
|
1346
|
-
};
|
|
1347
|
-
results.push(compactResult);
|
|
1348
|
-
markInfiniteListCandidateProcessed(listState, candidateKey, {
|
|
1349
|
-
metadata: {
|
|
1350
|
-
result_index: index,
|
|
1351
|
-
candidate_id: screeningCandidate.id || null
|
|
1352
|
-
}
|
|
1353
|
-
});
|
|
1354
|
-
|
|
1355
|
-
updateRecruitProgress({
|
|
1356
|
-
last_candidate_id: screeningCandidate.id || null,
|
|
1357
|
-
last_candidate_key: candidateKey,
|
|
1358
|
-
last_score: screening.score
|
|
1359
|
-
});
|
|
1360
|
-
const checkpointStarted = Date.now();
|
|
1361
|
-
runControl.checkpoint({
|
|
1362
|
-
results,
|
|
1363
|
-
last_candidate: {
|
|
1364
|
-
id: screeningCandidate.id || null,
|
|
1365
|
-
key: candidateKey,
|
|
1366
|
-
identity: screeningCandidate.identity || {},
|
|
1367
|
-
screening: {
|
|
1368
|
-
status: screening.status,
|
|
1369
|
-
passed: screening.passed,
|
|
1370
|
-
score: screening.score
|
|
1371
|
-
},
|
|
1372
|
-
llm_screening: compactScreeningLlmResult(llmResult),
|
|
1373
|
-
error: compactResult.error
|
|
1374
|
-
}
|
|
1375
|
-
});
|
|
1376
|
-
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
1377
|
-
|
|
1378
|
-
if (effectiveHumanRestEnabled) {
|
|
1379
|
-
const restStarted = Date.now();
|
|
1380
|
-
const restResult = await humanRestController.takeBreakIfNeeded({
|
|
1381
|
-
sleepFn: (ms) => runControl.sleep(ms)
|
|
1382
|
-
});
|
|
1383
|
-
const restElapsed = Date.now() - restStarted;
|
|
1384
|
-
if (restResult.rested) {
|
|
1385
|
-
recordHumanEvent({
|
|
1386
|
-
kind: "rest",
|
|
1387
|
-
pause_ms: restResult.pause_ms || restElapsed,
|
|
1388
|
-
events: restResult.events || []
|
|
1389
|
-
});
|
|
1390
|
-
compactResult.human_rest = restResult;
|
|
1391
|
-
addTiming(compactResult.timings, "human_rest_ms", restElapsed);
|
|
1392
|
-
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
1393
|
-
updateRecruitProgress({
|
|
1394
|
-
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
1395
|
-
human_rest_last: restResult
|
|
1396
|
-
});
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
if (delayMs > 0) {
|
|
1401
|
-
const sleepStarted = Date.now();
|
|
1402
|
-
await runControl.sleep(delayMs);
|
|
1403
|
-
addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
|
|
1404
|
-
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
1405
|
-
}
|
|
1406
|
-
if (postActionResult?.stop_run) {
|
|
1407
|
-
listEndReason = postActionResult.reason || "post_action_stop";
|
|
1408
|
-
break;
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
runControl.setPhase("recruit:done");
|
|
1413
|
-
return {
|
|
1414
|
-
domain: "recruit",
|
|
1415
|
-
target_url: targetUrl,
|
|
1416
|
-
search_params: normalizedSearchParams,
|
|
1417
|
-
card_count: cardNodeIds.length,
|
|
1418
|
-
candidate_list: compactInfiniteListState(listState),
|
|
1419
|
-
viewport_health: {
|
|
1420
|
-
stats: viewportGuard.getStats(),
|
|
1421
|
-
events: viewportGuard.getEvents()
|
|
1422
|
-
},
|
|
1423
|
-
human_behavior: effectiveHumanBehavior,
|
|
1424
|
-
human_rest: humanRestController.getState(),
|
|
1425
|
-
last_human_event: lastHumanEvent,
|
|
1426
|
-
list_end_reason: listEndReason || null,
|
|
1427
|
-
search_exchange_resume_filter_requested: searchExchangeResumeFilterRequested ? 1 : 0,
|
|
1428
|
-
search_exchange_resume_filter_applied: searchExchangeResumeFilterApplied ? 1 : 0,
|
|
1429
|
-
last_greet_quota_after_spend: lastGreetQuotaAfterSpend,
|
|
1430
|
-
refresh_rounds: refreshRounds,
|
|
1431
|
-
refresh_attempts: refreshAttempts,
|
|
1432
|
-
context_recoveries: contextRecoveryAttempts,
|
|
1433
|
-
...countRecruitResultStatuses(results, { greetCount }),
|
|
1434
|
-
results
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
export function createRecruitRunService({
|
|
1439
|
-
lifecycle,
|
|
1440
|
-
idPrefix = "recruit",
|
|
1441
|
-
workflow = runRecruitWorkflow,
|
|
1442
|
-
onSnapshot = null
|
|
1443
|
-
} = {}) {
|
|
1444
|
-
const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
|
|
1445
|
-
|
|
1446
|
-
function startRecruitRun({
|
|
1447
|
-
runId = "",
|
|
1448
|
-
client,
|
|
1449
|
-
targetUrl = "",
|
|
1450
|
-
criteria = "",
|
|
1451
|
-
searchParams = {},
|
|
1452
|
-
maxCandidates = 5,
|
|
1453
|
-
detailLimit = null,
|
|
1454
|
-
closeDetail = true,
|
|
1455
|
-
delayMs = 0,
|
|
1456
|
-
cardTimeoutMs = 90000,
|
|
1457
|
-
resetBeforeSearch = true,
|
|
1458
|
-
resetTimeoutMs = 180000,
|
|
1459
|
-
cityOptionTimeoutMs = 30000,
|
|
1460
|
-
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
1461
|
-
imageWheelDeltaY = 650,
|
|
1462
|
-
cvAcquisitionMode = "unknown",
|
|
1463
|
-
listMaxScrolls = 20,
|
|
1464
|
-
listStableSignatureLimit = 5,
|
|
1465
|
-
listWheelDeltaY = 850,
|
|
1466
|
-
listSettleMs = 2200,
|
|
1467
|
-
listFallbackPoint = null,
|
|
1468
|
-
refreshOnEnd = true,
|
|
1469
|
-
maxRefreshRounds = 2,
|
|
1470
|
-
refreshResetSettleMs = 5000,
|
|
1471
|
-
screeningMode = "llm",
|
|
1472
|
-
llmConfig = null,
|
|
1473
|
-
llmTimeoutMs = 120000,
|
|
1474
|
-
llmImageLimit = 8,
|
|
1475
|
-
llmImageDetail = "high",
|
|
1476
|
-
imageOutputDir = "",
|
|
1477
|
-
humanRestEnabled = false,
|
|
1478
|
-
humanBehavior = null,
|
|
1479
|
-
postAction = "none",
|
|
1480
|
-
maxGreetCount = null,
|
|
1481
|
-
executePostAction = true,
|
|
1482
|
-
actionTimeoutMs = 8000,
|
|
1483
|
-
actionIntervalMs = 400,
|
|
1484
|
-
actionAfterClickDelayMs = 900,
|
|
1485
|
-
name = "recruit-domain-run"
|
|
1486
|
-
} = {}) {
|
|
1487
|
-
if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
|
|
1488
|
-
const normalizedSearchParams = normalizeSearchParams(searchParams);
|
|
1489
|
-
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
1490
|
-
const normalizedPostAction = normalizeRecruitPostAction(postAction);
|
|
1491
|
-
const searchExchangeResumeFilterRequested = normalizedSearchParams.skip_recent_colleague_contacted === true;
|
|
1492
|
-
const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
|
|
1493
|
-
const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
|
|
1494
|
-
const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
|
|
1495
|
-
legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
|
|
1496
|
-
});
|
|
1497
|
-
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
1498
|
-
return manager.startRun({
|
|
1499
|
-
runId,
|
|
1500
|
-
name,
|
|
1501
|
-
context: {
|
|
1502
|
-
domain: "recruit",
|
|
1503
|
-
target_url: targetUrl,
|
|
1504
|
-
criteria_present: Boolean(criteria),
|
|
1505
|
-
search_params: normalizedSearchParams,
|
|
1506
|
-
max_candidates: maxCandidates,
|
|
1507
|
-
detail_limit: normalizedDetailLimit,
|
|
1508
|
-
close_detail: closeDetail,
|
|
1509
|
-
reset_before_search: resetBeforeSearch,
|
|
1510
|
-
reset_timeout_ms: resetTimeoutMs,
|
|
1511
|
-
city_option_timeout_ms: cityOptionTimeoutMs,
|
|
1512
|
-
cv_acquisition_mode: cvAcquisitionMode,
|
|
1513
|
-
max_image_pages: maxImagePages,
|
|
1514
|
-
image_wheel_delta_y: imageWheelDeltaY,
|
|
1515
|
-
list_max_scrolls: listMaxScrolls,
|
|
1516
|
-
list_stable_signature_limit: listStableSignatureLimit,
|
|
1517
|
-
list_wheel_delta_y: listWheelDeltaY,
|
|
1518
|
-
list_settle_ms: listSettleMs,
|
|
1519
|
-
list_fallback_point: listFallbackPoint,
|
|
1520
|
-
refresh_on_end: refreshOnEnd,
|
|
1521
|
-
max_refresh_rounds: maxRefreshRounds,
|
|
1522
|
-
refresh_reset_settle_ms: refreshResetSettleMs,
|
|
1523
|
-
screening_mode: normalizedScreeningMode,
|
|
1524
|
-
llm_configured: Boolean(llmConfig),
|
|
1525
|
-
llm_timeout_ms: llmTimeoutMs,
|
|
1526
|
-
llm_image_limit: llmImageLimit,
|
|
1527
|
-
llm_image_detail: llmImageDetail,
|
|
1528
|
-
image_output_dir: imageOutputDir || "",
|
|
1529
|
-
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
1530
|
-
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1531
|
-
human_behavior: effectiveHumanBehavior,
|
|
1532
|
-
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
1533
|
-
human_rest_enabled: effectiveHumanRestEnabled,
|
|
1534
|
-
search_exchange_resume_filter_requested: searchExchangeResumeFilterRequested ? 1 : 0,
|
|
1535
|
-
search_exchange_resume_filter_applied: 0,
|
|
1536
|
-
post_action: normalizedPostAction,
|
|
1537
|
-
max_greet_count: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
|
|
1538
|
-
execute_post_action: Boolean(executePostAction),
|
|
1539
|
-
action_timeout_ms: actionTimeoutMs,
|
|
1540
|
-
action_interval_ms: actionIntervalMs,
|
|
1541
|
-
action_after_click_delay_ms: actionAfterClickDelayMs
|
|
1542
|
-
},
|
|
1543
|
-
progress: {
|
|
1544
|
-
card_count: 0,
|
|
1545
|
-
target_count: candidateLimit,
|
|
1546
|
-
processed: 0,
|
|
1547
|
-
screened: 0,
|
|
1548
|
-
detail_opened: 0,
|
|
1549
|
-
llm_screened: 0,
|
|
1550
|
-
passed: 0,
|
|
1551
|
-
image_capture_failed: 0,
|
|
1552
|
-
detail_open_failed: 0,
|
|
1553
|
-
transient_recovered: 0,
|
|
1554
|
-
context_recoveries: 0,
|
|
1555
|
-
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
1556
|
-
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1557
|
-
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
1558
|
-
human_rest_enabled: effectiveHumanRestEnabled,
|
|
1559
|
-
human_rest_count: 0,
|
|
1560
|
-
human_rest_ms: 0,
|
|
1561
|
-
greet_count: 0,
|
|
1562
|
-
post_action_clicked: 0,
|
|
1563
|
-
search_exchange_resume_filter_requested: searchExchangeResumeFilterRequested ? 1 : 0,
|
|
1564
|
-
search_exchange_resume_filter_applied: 0,
|
|
1565
|
-
last_human_event: null
|
|
1566
|
-
},
|
|
1567
|
-
checkpoint: {},
|
|
1568
|
-
task: (runControl) => workflow({
|
|
1569
|
-
client,
|
|
1570
|
-
targetUrl,
|
|
1571
|
-
criteria,
|
|
1572
|
-
searchParams: normalizedSearchParams,
|
|
1573
|
-
maxCandidates,
|
|
1574
|
-
detailLimit: normalizedDetailLimit,
|
|
1575
|
-
closeDetail,
|
|
1576
|
-
delayMs,
|
|
1577
|
-
cardTimeoutMs,
|
|
1578
|
-
resetBeforeSearch,
|
|
1579
|
-
resetTimeoutMs,
|
|
1580
|
-
cityOptionTimeoutMs,
|
|
1581
|
-
maxImagePages,
|
|
1582
|
-
imageWheelDeltaY,
|
|
1583
|
-
cvAcquisitionMode,
|
|
1584
|
-
listMaxScrolls,
|
|
1585
|
-
listStableSignatureLimit,
|
|
1586
|
-
listWheelDeltaY,
|
|
1587
|
-
listSettleMs,
|
|
1588
|
-
listFallbackPoint,
|
|
1589
|
-
refreshOnEnd,
|
|
1590
|
-
maxRefreshRounds,
|
|
1591
|
-
refreshResetSettleMs,
|
|
1592
|
-
screeningMode: normalizedScreeningMode,
|
|
1593
|
-
llmConfig,
|
|
1594
|
-
llmTimeoutMs,
|
|
1595
|
-
llmImageLimit,
|
|
1596
|
-
llmImageDetail,
|
|
1597
|
-
imageOutputDir,
|
|
1598
|
-
humanRestEnabled: effectiveHumanRestEnabled,
|
|
1599
|
-
humanBehavior: effectiveHumanBehavior,
|
|
1600
|
-
postAction: normalizedPostAction,
|
|
1601
|
-
maxGreetCount,
|
|
1602
|
-
executePostAction,
|
|
1603
|
-
actionTimeoutMs,
|
|
1604
|
-
actionIntervalMs,
|
|
1605
|
-
actionAfterClickDelayMs
|
|
1606
|
-
}, runControl)
|
|
1607
|
-
});
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
return {
|
|
1611
|
-
startRecruitRun,
|
|
1612
|
-
getRecruitRun: manager.getRun,
|
|
1613
|
-
pauseRecruitRun: manager.pauseRun,
|
|
1614
|
-
resumeRecruitRun: manager.resumeRun,
|
|
1615
|
-
cancelRecruitRun: manager.cancelRun,
|
|
1616
|
-
waitForRecruitRun: manager.waitForRun,
|
|
1617
|
-
listRecruitRuns: manager.listRuns,
|
|
1618
|
-
manager
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
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
|
+
sleep
|
|
17
|
+
} from "../../core/browser/index.js";
|
|
18
|
+
import {
|
|
19
|
+
compactCvAcquisitionState,
|
|
20
|
+
countParsedNetworkProfiles,
|
|
21
|
+
createCvAcquisitionState,
|
|
22
|
+
DEFAULT_MAX_IMAGE_PAGES,
|
|
23
|
+
getCvNetworkWaitPlan,
|
|
24
|
+
recordCvImageFallback,
|
|
25
|
+
recordCvNetworkHit,
|
|
26
|
+
recordCvNetworkMiss,
|
|
27
|
+
summarizeImageEvidence,
|
|
28
|
+
waitForCvNetworkEvents
|
|
29
|
+
} from "../../core/cv-acquisition/index.js";
|
|
30
|
+
import {
|
|
31
|
+
compactInfiniteListState,
|
|
32
|
+
createInfiniteListState,
|
|
33
|
+
detectInfiniteListBottomMarker,
|
|
34
|
+
getNextInfiniteListCandidate,
|
|
35
|
+
markInfiniteListCandidateProcessed,
|
|
36
|
+
resetInfiniteListForRefreshRound,
|
|
37
|
+
resolveInfiniteListFallbackPoint
|
|
38
|
+
} from "../../core/infinite-list/index.js";
|
|
39
|
+
import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
40
|
+
import {
|
|
41
|
+
callScreeningLlm,
|
|
42
|
+
compactScreeningLlmResult,
|
|
43
|
+
createFatalLlmRunError,
|
|
44
|
+
createFailedLlmScreeningResult,
|
|
45
|
+
isFatalLlmProviderError,
|
|
46
|
+
llmResultToScreening,
|
|
47
|
+
screenCandidate
|
|
48
|
+
} from "../../core/screening/index.js";
|
|
49
|
+
import {
|
|
50
|
+
closeRecruitBlockingPanels,
|
|
51
|
+
closeRecruitDetail,
|
|
52
|
+
createRecruitDetailNetworkRecorder,
|
|
53
|
+
extractRecruitDetailCandidate,
|
|
54
|
+
openRecruitCardDetail,
|
|
55
|
+
waitForRecruitDetailNetworkEvents
|
|
56
|
+
} from "./detail.js";
|
|
57
|
+
import {
|
|
58
|
+
clickRecruitActionControl,
|
|
59
|
+
waitForRecruitDetailActionControls
|
|
60
|
+
} from "./actions.js";
|
|
61
|
+
import {
|
|
62
|
+
readRecruitCardCandidate,
|
|
63
|
+
waitForRecruitCardNodeIds
|
|
64
|
+
} from "./cards.js";
|
|
65
|
+
import {
|
|
66
|
+
applyRecruitSearchParams,
|
|
67
|
+
hasRecruitSearchParams,
|
|
68
|
+
normalizeRecruitSearchParams
|
|
69
|
+
} from "./search.js";
|
|
70
|
+
import { refreshRecruitSearchAtEnd } from "./refresh.js";
|
|
71
|
+
import { getRecruitRoots } from "./roots.js";
|
|
72
|
+
import {
|
|
73
|
+
RECRUIT_BOTTOM_MARKER_SELECTORS,
|
|
74
|
+
RECRUIT_BOTTOM_REFRESH_SELECTORS,
|
|
75
|
+
RECRUIT_CARD_SELECTOR,
|
|
76
|
+
RECRUIT_LIST_CONTAINER_SELECTORS
|
|
77
|
+
} from "./constants.js";
|
|
78
|
+
import {
|
|
79
|
+
describeGreetQuotaAfterSpend,
|
|
80
|
+
GREET_CREDITS_EXHAUSTED_CODE
|
|
81
|
+
} from "../../core/greet-quota/index.js";
|
|
82
|
+
|
|
83
|
+
function compactScreening(screening) {
|
|
84
|
+
return {
|
|
85
|
+
status: screening.status,
|
|
86
|
+
passed: screening.passed,
|
|
87
|
+
score: screening.score,
|
|
88
|
+
reasons: screening.reasons,
|
|
89
|
+
candidate: {
|
|
90
|
+
domain: screening.candidate?.domain || "recruit",
|
|
91
|
+
source: screening.candidate?.source || "",
|
|
92
|
+
id: screening.candidate?.id || null,
|
|
93
|
+
identity: screening.candidate?.identity || {}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function compactCandidate(candidate) {
|
|
99
|
+
return {
|
|
100
|
+
id: candidate?.id || null,
|
|
101
|
+
identity: candidate?.identity || {},
|
|
102
|
+
text_length: candidate?.text?.raw?.length || 0,
|
|
103
|
+
tag_count: candidate?.tags?.length || 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function compactDetail(detailResult) {
|
|
108
|
+
if (!detailResult) return null;
|
|
109
|
+
return {
|
|
110
|
+
popup_text_length: detailResult.detail?.popup_text?.length || 0,
|
|
111
|
+
resume_text_length: detailResult.detail?.resume_text?.length || 0,
|
|
112
|
+
card_box: detailResult.card_box || null,
|
|
113
|
+
open_attempts: detailResult.open_attempts || [],
|
|
114
|
+
network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
|
|
115
|
+
parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
|
|
116
|
+
cv_acquisition: detailResult.cv_acquisition || null,
|
|
117
|
+
image_evidence: summarizeImageEvidence(detailResult.image_evidence),
|
|
118
|
+
llm_screening: compactScreeningLlmResult(detailResult.llm_result),
|
|
119
|
+
close_result: detailResult.close_result
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeScreeningMode(value) {
|
|
124
|
+
const normalized = String(value || "llm").trim().toLowerCase();
|
|
125
|
+
return ["deterministic", "local", "local_scorer"].includes(normalized)
|
|
126
|
+
? "deterministic"
|
|
127
|
+
: "llm";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function createMissingLlmConfigResult() {
|
|
131
|
+
return createFailedLlmScreeningResult(new Error("LLM screening config is required for production search runs"));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeRecruitPostAction(value) {
|
|
135
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
136
|
+
if (["", "none", "skip", "no", "不执行", "无", "什么也不做"].includes(normalized)) return "none";
|
|
137
|
+
if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
|
|
138
|
+
return "none";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveRecruitPostAction({
|
|
142
|
+
postAction = "none",
|
|
143
|
+
greetCount = 0,
|
|
144
|
+
maxGreetCount = null
|
|
145
|
+
} = {}) {
|
|
146
|
+
const requested = normalizeRecruitPostAction(postAction);
|
|
147
|
+
const currentGreetCount = Number.isInteger(greetCount) && greetCount >= 0 ? greetCount : 0;
|
|
148
|
+
const limit = Number.isInteger(maxGreetCount) && maxGreetCount > 0 ? maxGreetCount : null;
|
|
149
|
+
if (requested === "greet" && limit !== null && currentGreetCount >= limit) {
|
|
150
|
+
return {
|
|
151
|
+
requested,
|
|
152
|
+
effective: "none",
|
|
153
|
+
reason: "greet_limit_reached",
|
|
154
|
+
greet_count: currentGreetCount,
|
|
155
|
+
max_greet_count: limit
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
requested,
|
|
160
|
+
effective: requested,
|
|
161
|
+
reason: "requested_action",
|
|
162
|
+
greet_count: currentGreetCount,
|
|
163
|
+
max_greet_count: limit
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function compactActionDiscovery(discovery) {
|
|
168
|
+
if (!discovery) return null;
|
|
169
|
+
return {
|
|
170
|
+
ok: Boolean(discovery.ok),
|
|
171
|
+
elapsed_ms: discovery.elapsed_ms,
|
|
172
|
+
summary: discovery.summary || null
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function runRecruitPostAction({
|
|
177
|
+
client,
|
|
178
|
+
rootNodeIds = [],
|
|
179
|
+
screening,
|
|
180
|
+
actionDiscovery,
|
|
181
|
+
postAction = "none",
|
|
182
|
+
greetCount = 0,
|
|
183
|
+
maxGreetCount = null,
|
|
184
|
+
executePostAction = true,
|
|
185
|
+
afterClickDelayMs = 900,
|
|
186
|
+
lastGreetQuotaAfterSpend = null
|
|
187
|
+
} = {}) {
|
|
188
|
+
const plan = resolveRecruitPostAction({
|
|
189
|
+
postAction,
|
|
190
|
+
greetCount,
|
|
191
|
+
maxGreetCount
|
|
192
|
+
});
|
|
193
|
+
const result = {
|
|
194
|
+
requested: postAction,
|
|
195
|
+
execute_post_action: Boolean(executePostAction),
|
|
196
|
+
plan,
|
|
197
|
+
eligible: Boolean(screening?.passed),
|
|
198
|
+
action_attempted: false,
|
|
199
|
+
action_clicked: false,
|
|
200
|
+
counted_as_greet: false,
|
|
201
|
+
reason: ""
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (!screening?.passed) {
|
|
205
|
+
result.reason = "screening_not_passed";
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
if (plan.effective === "none") {
|
|
209
|
+
result.reason = plan.reason === "greet_limit_reached" ? "greet_limit_reached" : "post_action_none";
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const summary = actionDiscovery?.summary || {};
|
|
214
|
+
const control = summary.greet?.control || summary.greet;
|
|
215
|
+
if (!control?.found && !control?.node_id) {
|
|
216
|
+
if (plan.effective === "greet" && lastGreetQuotaAfterSpend?.exhausted_after_spend) {
|
|
217
|
+
result.reason = "greet_credits_exhausted";
|
|
218
|
+
result.out_of_greet_credits = true;
|
|
219
|
+
result.stop_run = true;
|
|
220
|
+
result.greet_quota_after_last_click = lastGreetQuotaAfterSpend;
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
result.reason = `${plan.effective}_control_not_found`;
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
result.control = control;
|
|
227
|
+
|
|
228
|
+
if (plan.effective === "greet" && control.continue_chat) {
|
|
229
|
+
result.reason = "already_connected_continue_chat";
|
|
230
|
+
result.already_connected = true;
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
if (plan.effective === "greet" && control.greet_quota?.exhausted) {
|
|
234
|
+
result.reason = "greet_credits_exhausted";
|
|
235
|
+
result.out_of_greet_credits = true;
|
|
236
|
+
result.stop_run = true;
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
if (plan.effective === "greet" && control.available === false) {
|
|
240
|
+
result.reason = "greet_control_not_available";
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
if (control.disabled) {
|
|
244
|
+
result.reason = `${plan.effective}_control_disabled`;
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
if (!executePostAction) {
|
|
248
|
+
result.reason = "dry_run_post_action";
|
|
249
|
+
result.would_click = true;
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
result.action_attempted = true;
|
|
254
|
+
result.control_before = control;
|
|
255
|
+
let clickResult;
|
|
256
|
+
try {
|
|
257
|
+
clickResult = await clickRecruitActionControl(client, {
|
|
258
|
+
...control,
|
|
259
|
+
kind: plan.effective
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error?.code === GREET_CREDITS_EXHAUSTED_CODE) {
|
|
263
|
+
result.reason = "greet_credits_exhausted";
|
|
264
|
+
result.out_of_greet_credits = true;
|
|
265
|
+
result.stop_run = true;
|
|
266
|
+
result.greet_quota = error.greet_quota || control.greet_quota || null;
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
result.click_result = clickResult;
|
|
272
|
+
result.action_clicked = true;
|
|
273
|
+
result.greet_quota_after_click = describeGreetQuotaAfterSpend(
|
|
274
|
+
clickResult.greet_quota?.found ? clickResult.greet_quota : control.greet_quota || control.label || ""
|
|
275
|
+
);
|
|
276
|
+
result.counted_as_greet = plan.effective === "greet";
|
|
277
|
+
result.reason = "clicked";
|
|
278
|
+
if (afterClickDelayMs > 0) await sleep(afterClickDelayMs);
|
|
279
|
+
try {
|
|
280
|
+
const afterDiscovery = await waitForRecruitDetailActionControls(client, {
|
|
281
|
+
rootNodeIds,
|
|
282
|
+
timeoutMs: 2500,
|
|
283
|
+
intervalMs: 300,
|
|
284
|
+
requireAny: false
|
|
285
|
+
});
|
|
286
|
+
const afterControl = afterDiscovery?.summary?.greet?.control || afterDiscovery?.summary?.greet || null;
|
|
287
|
+
result.action_discovery_after = compactActionDiscovery(afterDiscovery);
|
|
288
|
+
result.control_after = afterControl;
|
|
289
|
+
if (plan.effective === "greet") {
|
|
290
|
+
result.verified_after_click = Boolean(
|
|
291
|
+
afterControl?.continue_chat
|
|
292
|
+
|| String(afterControl?.label || "").includes("继续沟通")
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
} catch (error) {
|
|
296
|
+
result.verify_error = {
|
|
297
|
+
message: error?.message || String(error)
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function normalizeSearchParams(searchParams = {}) {
|
|
304
|
+
return normalizeRecruitSearchParams(searchParams);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function compactRefreshAttempt(refreshAttempt) {
|
|
308
|
+
if (!refreshAttempt) return null;
|
|
309
|
+
return {
|
|
310
|
+
ok: Boolean(refreshAttempt.ok),
|
|
311
|
+
method: refreshAttempt.method || "",
|
|
312
|
+
forced_recent_viewed: Boolean(refreshAttempt.forced_recent_viewed),
|
|
313
|
+
card_count: refreshAttempt.card_count || 0,
|
|
314
|
+
search_params: refreshAttempt.search_params || null,
|
|
315
|
+
recovery_settle: refreshAttempt.recovery_settle
|
|
316
|
+
? {
|
|
317
|
+
ok: Boolean(refreshAttempt.recovery_settle.ok),
|
|
318
|
+
status: refreshAttempt.recovery_settle.status || "",
|
|
319
|
+
reason: refreshAttempt.recovery_settle.reason || "",
|
|
320
|
+
elapsed_ms: refreshAttempt.recovery_settle.elapsed_ms || 0
|
|
321
|
+
}
|
|
322
|
+
: null,
|
|
323
|
+
application: refreshAttempt.application
|
|
324
|
+
? {
|
|
325
|
+
applied: Boolean(refreshAttempt.application.applied),
|
|
326
|
+
post_search_state: refreshAttempt.application.post_search_state,
|
|
327
|
+
steps: (refreshAttempt.application.steps || []).map((step) => ({
|
|
328
|
+
step: step.step,
|
|
329
|
+
applied: step.result?.applied,
|
|
330
|
+
clicked: step.result?.clicked,
|
|
331
|
+
searched: step.result?.searched,
|
|
332
|
+
reason: step.result?.reason || null
|
|
333
|
+
}))
|
|
334
|
+
}
|
|
335
|
+
: null
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function compactError(error, fallbackCode = "RECRUIT_RUN_ERROR") {
|
|
340
|
+
if (!error) return null;
|
|
341
|
+
const result = {
|
|
342
|
+
code: error.code || fallbackCode,
|
|
343
|
+
message: error.message || String(error)
|
|
344
|
+
};
|
|
345
|
+
if (error.close_result) {
|
|
346
|
+
result.close_result = compactCloseResult(error.close_result);
|
|
347
|
+
}
|
|
348
|
+
if (error.phase) {
|
|
349
|
+
result.phase = error.phase;
|
|
350
|
+
}
|
|
351
|
+
if (error.refresh_attempt) {
|
|
352
|
+
result.refresh_attempt = error.refresh_attempt;
|
|
353
|
+
}
|
|
354
|
+
if (error.list_end_reason) {
|
|
355
|
+
result.list_end_reason = error.list_end_reason;
|
|
356
|
+
}
|
|
357
|
+
if (error.target_count != null) {
|
|
358
|
+
result.target_count = error.target_count;
|
|
359
|
+
}
|
|
360
|
+
if (error.processed_count != null) {
|
|
361
|
+
result.processed_count = error.processed_count;
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function compactCloseResult(closeResult) {
|
|
367
|
+
if (!closeResult) return null;
|
|
368
|
+
const result = {
|
|
369
|
+
closed: Boolean(closeResult.closed),
|
|
370
|
+
reason: closeResult.reason || null,
|
|
371
|
+
probe: closeResult.probe || null,
|
|
372
|
+
attempts: closeResult.attempts || [],
|
|
373
|
+
verification: closeResult.verification || null
|
|
374
|
+
};
|
|
375
|
+
if (closeResult.already_closed !== undefined) {
|
|
376
|
+
result.already_closed = Boolean(closeResult.already_closed);
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function createRecruitCloseFailureError(closeResult) {
|
|
382
|
+
const error = new Error(closeResult?.reason || "Recruit detail did not close before recovery");
|
|
383
|
+
error.code = "DETAIL_CLOSE_FAILED";
|
|
384
|
+
error.close_result = closeResult || null;
|
|
385
|
+
return error;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function createRecruitBlockingPanelCloseFailureError(closeResult, phase = "") {
|
|
389
|
+
const error = new Error(closeResult?.reason || "Boss account-rights panel did not close before recovery");
|
|
390
|
+
error.code = "ACCOUNT_RIGHTS_PANEL_CLOSE_FAILED";
|
|
391
|
+
error.close_result = closeResult || null;
|
|
392
|
+
error.phase = phase || null;
|
|
393
|
+
return error;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function createRecruitRefreshFailureError(refreshAttempt, {
|
|
397
|
+
listEndReason = "",
|
|
398
|
+
targetCount = 0,
|
|
399
|
+
processedCount = 0
|
|
400
|
+
} = {}) {
|
|
401
|
+
const reason = refreshAttempt?.application?.post_search_state?.ok === false
|
|
402
|
+
? "search_result_not_ready"
|
|
403
|
+
: refreshAttempt?.application?.post_search_state?.counts?.candidate_card === 0
|
|
404
|
+
? "no_cards_after_refresh"
|
|
405
|
+
: "refresh_failed";
|
|
406
|
+
const error = new Error(`Recruit/search refresh failed before target was reached (${reason})`);
|
|
407
|
+
error.code = "RECRUIT_END_REFRESH_FAILED";
|
|
408
|
+
error.refresh_attempt = refreshAttempt || null;
|
|
409
|
+
error.list_end_reason = listEndReason || null;
|
|
410
|
+
error.target_count = targetCount;
|
|
411
|
+
error.processed_count = processedCount;
|
|
412
|
+
return error;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function isRefreshableListStall(reason = "") {
|
|
416
|
+
return new Set([
|
|
417
|
+
"stable_visible_signature",
|
|
418
|
+
"max_scrolls_exhausted",
|
|
419
|
+
"scroll_failed",
|
|
420
|
+
"scroll_anchor_unavailable"
|
|
421
|
+
]).has(String(reason || ""));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function isStaleRecruitNodeError(error) {
|
|
425
|
+
const message = String(error?.message || error || "");
|
|
426
|
+
return /Could not find node with given id|No node with given id|Node is detached|Cannot find node/i.test(message);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function isRecoverableRecruitImageCaptureError(error) {
|
|
430
|
+
const code = String(error?.code || "");
|
|
431
|
+
if (code === "IMAGE_CAPTURE_TIMEOUT" || code === "IMAGE_CAPTURE_TOTAL_TIMEOUT") return true;
|
|
432
|
+
if (isStaleRecruitNodeError(error)) return true;
|
|
433
|
+
return /Image fallback capture timed out/i.test(String(error?.message || error || ""));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function isRecoverableRecruitDetailError(error) {
|
|
437
|
+
return isStaleRecruitNodeError(error);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function compactRecoverableDetailError(error) {
|
|
441
|
+
return compactError(error, isStaleRecruitNodeError(error) ? "DETAIL_STALE_NODE" : "DETAIL_OPEN_FAILED");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function collectPartialImageEvidencePaths(basePath = "", extension = "jpg", maxCount = 12) {
|
|
445
|
+
const resolved = String(basePath || "").trim();
|
|
446
|
+
if (!resolved) return [];
|
|
447
|
+
const parsed = path.parse(resolved);
|
|
448
|
+
const ext = parsed.ext || `.${String(extension || "jpg").replace(/^\./, "") || "jpg"}`;
|
|
449
|
+
const files = [];
|
|
450
|
+
for (let index = 0; index < Math.max(1, Number(maxCount) || 1); index += 1) {
|
|
451
|
+
const page = String(index + 1).padStart(2, "0");
|
|
452
|
+
const candidatePath = path.join(parsed.dir, `${parsed.name}-page-${page}${ext}`);
|
|
453
|
+
if (fs.existsSync(candidatePath)) files.push(candidatePath);
|
|
454
|
+
}
|
|
455
|
+
return files;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function createRecoverableRecruitImageCaptureEvidence(error, {
|
|
459
|
+
elapsedMs = 0,
|
|
460
|
+
filePath = "",
|
|
461
|
+
extension = "jpg",
|
|
462
|
+
maxScreenshots = DEFAULT_MAX_IMAGE_PAGES
|
|
463
|
+
} = {}) {
|
|
464
|
+
const filePaths = collectPartialImageEvidencePaths(filePath, extension, maxScreenshots);
|
|
465
|
+
return {
|
|
466
|
+
schema_version: 1,
|
|
467
|
+
ok: false,
|
|
468
|
+
source: "image-scroll-sequence",
|
|
469
|
+
elapsed_ms: Math.max(0, Math.round(Number(error?.elapsed_ms ?? elapsedMs) || 0)),
|
|
470
|
+
capture_count: filePaths.length,
|
|
471
|
+
screenshot_count: filePaths.length,
|
|
472
|
+
unique_screenshot_count: filePaths.length,
|
|
473
|
+
dropped_duplicate_count: 0,
|
|
474
|
+
total_byte_length: 0,
|
|
475
|
+
original_total_byte_length: 0,
|
|
476
|
+
llm_screenshot_count: 0,
|
|
477
|
+
llm_total_byte_length: 0,
|
|
478
|
+
llm_original_total_byte_length: 0,
|
|
479
|
+
llm_composition_error: null,
|
|
480
|
+
error_code: error?.code || (isStaleRecruitNodeError(error) ? "IMAGE_CAPTURE_STALE_NODE" : "IMAGE_CAPTURE_FAILED"),
|
|
481
|
+
error: error?.message || String(error || "Image capture failed"),
|
|
482
|
+
file_paths: filePaths,
|
|
483
|
+
llm_file_paths: []
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function createImageCaptureFailureScreening(candidate, error) {
|
|
488
|
+
return {
|
|
489
|
+
status: "fail",
|
|
490
|
+
passed: false,
|
|
491
|
+
score: 0,
|
|
492
|
+
reasons: ["image_capture_failed"],
|
|
493
|
+
error: compactError(error, "IMAGE_CAPTURE_FAILED"),
|
|
494
|
+
candidate
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function createRecoverableDetailFailureScreening(candidate, error) {
|
|
499
|
+
return {
|
|
500
|
+
status: "fail",
|
|
501
|
+
passed: false,
|
|
502
|
+
score: 0,
|
|
503
|
+
reasons: isStaleRecruitNodeError(error)
|
|
504
|
+
? ["detail_open_failed", "stale_node"]
|
|
505
|
+
: ["detail_open_failed"],
|
|
506
|
+
error: compactRecoverableDetailError(error),
|
|
507
|
+
candidate
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function countRecruitResultStatuses(results = [], {
|
|
512
|
+
greetCount = 0
|
|
513
|
+
} = {}) {
|
|
514
|
+
return {
|
|
515
|
+
processed: results.length,
|
|
516
|
+
screened: results.length,
|
|
517
|
+
detail_opened: results.filter((item) => item.detail).length,
|
|
518
|
+
passed: results.filter((item) => item.screening?.passed).length,
|
|
519
|
+
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
520
|
+
greet_count: greetCount,
|
|
521
|
+
post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
|
|
522
|
+
image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
|
|
523
|
+
detail_open_failed: results.filter((item) => (
|
|
524
|
+
item.error?.code === "DETAIL_STALE_NODE"
|
|
525
|
+
|| item.error?.code === "DETAIL_OPEN_FAILED"
|
|
526
|
+
)).length,
|
|
527
|
+
transient_recovered: results.filter((item) => (
|
|
528
|
+
item.error?.code === "DETAIL_STALE_NODE"
|
|
529
|
+
|| item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
|
|
530
|
+
|| item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
|
|
531
|
+
|| item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
|
|
532
|
+
)).length
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function runRecruitWorkflow({
|
|
537
|
+
client,
|
|
538
|
+
targetUrl = "",
|
|
539
|
+
criteria = "",
|
|
540
|
+
searchParams = {},
|
|
541
|
+
maxCandidates = 5,
|
|
542
|
+
detailLimit = null,
|
|
543
|
+
closeDetail = true,
|
|
544
|
+
delayMs = 0,
|
|
545
|
+
cardTimeoutMs = 90000,
|
|
546
|
+
resetBeforeSearch = true,
|
|
547
|
+
resetTimeoutMs = 180000,
|
|
548
|
+
cityOptionTimeoutMs = 30000,
|
|
549
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
550
|
+
imageWheelDeltaY = 650,
|
|
551
|
+
cvAcquisitionMode = "unknown",
|
|
552
|
+
listMaxScrolls = 20,
|
|
553
|
+
listStableSignatureLimit = 5,
|
|
554
|
+
listWheelDeltaY = 850,
|
|
555
|
+
listSettleMs = 2200,
|
|
556
|
+
listFallbackPoint = null,
|
|
557
|
+
refreshOnEnd = true,
|
|
558
|
+
maxRefreshRounds = 2,
|
|
559
|
+
refreshResetSettleMs = 5000,
|
|
560
|
+
screeningMode = "llm",
|
|
561
|
+
llmConfig = null,
|
|
562
|
+
llmTimeoutMs = 120000,
|
|
563
|
+
llmImageLimit = 8,
|
|
564
|
+
llmImageDetail = "high",
|
|
565
|
+
imageOutputDir = "",
|
|
566
|
+
humanRestEnabled = false,
|
|
567
|
+
humanBehavior = null,
|
|
568
|
+
postAction = "none",
|
|
569
|
+
maxGreetCount = null,
|
|
570
|
+
executePostAction = true,
|
|
571
|
+
actionTimeoutMs = 8000,
|
|
572
|
+
actionIntervalMs = 400,
|
|
573
|
+
actionAfterClickDelayMs = 900
|
|
574
|
+
} = {}, runControl) {
|
|
575
|
+
if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
|
|
576
|
+
const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
|
|
577
|
+
legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
|
|
578
|
+
});
|
|
579
|
+
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
580
|
+
configureHumanInteraction(client, {
|
|
581
|
+
enabled: effectiveHumanBehavior.enabled,
|
|
582
|
+
clickMovementEnabled: effectiveHumanBehavior.clickMovement,
|
|
583
|
+
textEntryEnabled: effectiveHumanBehavior.textEntry,
|
|
584
|
+
safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
|
|
585
|
+
actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
|
|
586
|
+
});
|
|
587
|
+
const humanRestController = createHumanRestController({
|
|
588
|
+
enabled: effectiveHumanRestEnabled,
|
|
589
|
+
shortRestEnabled: effectiveHumanBehavior.shortRest,
|
|
590
|
+
batchRestEnabled: effectiveHumanBehavior.batchRest,
|
|
591
|
+
restLevel: effectiveHumanBehavior.restLevel
|
|
592
|
+
});
|
|
593
|
+
const normalizedSearchParams = normalizeSearchParams(searchParams);
|
|
594
|
+
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
595
|
+
const normalizedPostAction = normalizeRecruitPostAction(postAction);
|
|
596
|
+
const postActionEnabled = normalizedPostAction !== "none";
|
|
597
|
+
const useLlmScreening = normalizedScreeningMode !== "deterministic";
|
|
598
|
+
const searchExchangeResumeFilterRequested = normalizedSearchParams.skip_recent_colleague_contacted === true;
|
|
599
|
+
let searchExchangeResumeFilterApplied = false;
|
|
600
|
+
const limit = Math.max(1, Number(maxCandidates) || 1);
|
|
601
|
+
const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
|
|
602
|
+
const networkRecorder = detailCountLimit > 0
|
|
603
|
+
? createRecruitDetailNetworkRecorder(client)
|
|
604
|
+
: null;
|
|
605
|
+
const cvAcquisitionState = createCvAcquisitionState({ mode: cvAcquisitionMode });
|
|
606
|
+
const listState = createInfiniteListState({
|
|
607
|
+
domain: "recruit",
|
|
608
|
+
listName: "search-results"
|
|
609
|
+
});
|
|
610
|
+
const viewportGuard = createViewportRunGuard({
|
|
611
|
+
client,
|
|
612
|
+
domain: "recruit",
|
|
613
|
+
root: "frame",
|
|
614
|
+
frameOwnerRoot: "frameOwner",
|
|
615
|
+
runControl,
|
|
616
|
+
getRoots: getRecruitRoots
|
|
617
|
+
});
|
|
618
|
+
async function ensureRecruitViewport(rootState, phase) {
|
|
619
|
+
const result = await viewportGuard.ensure(rootState, { phase });
|
|
620
|
+
return result.rootState || rootState;
|
|
621
|
+
}
|
|
622
|
+
const results = [];
|
|
623
|
+
const refreshAttempts = [];
|
|
624
|
+
let greetCount = 0;
|
|
625
|
+
let lastGreetQuotaAfterSpend = null;
|
|
626
|
+
let refreshRounds = 0;
|
|
627
|
+
let contextRecoveryAttempts = 0;
|
|
628
|
+
const candidateRecoveryCounts = new Map();
|
|
629
|
+
let rootState = null;
|
|
630
|
+
let cardNodeIds = [];
|
|
631
|
+
let listEndReason = "";
|
|
632
|
+
let lastHumanEvent = null;
|
|
633
|
+
const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
|
|
634
|
+
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
635
|
+
containerSelectors: RECRUIT_LIST_CONTAINER_SELECTORS,
|
|
636
|
+
itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
|
|
637
|
+
itemSelectors: [RECRUIT_CARD_SELECTOR],
|
|
638
|
+
viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
|
|
639
|
+
validateViewportPoint: true
|
|
640
|
+
}));
|
|
641
|
+
|
|
642
|
+
function recordHumanEvent(event = null) {
|
|
643
|
+
if (!event) return lastHumanEvent;
|
|
644
|
+
lastHumanEvent = {
|
|
645
|
+
at: new Date().toISOString(),
|
|
646
|
+
...event
|
|
647
|
+
};
|
|
648
|
+
return lastHumanEvent;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function maybeHumanActionCooldown(phase, timings = {}) {
|
|
652
|
+
if (!effectiveHumanBehavior.actionCooldown) return null;
|
|
653
|
+
const pauseMs = humanDelay(280, 90, {
|
|
654
|
+
minMs: 80,
|
|
655
|
+
maxMs: 720
|
|
656
|
+
});
|
|
657
|
+
if (pauseMs > 0) {
|
|
658
|
+
await runControl.sleep(pauseMs);
|
|
659
|
+
addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
|
|
660
|
+
}
|
|
661
|
+
return recordHumanEvent({
|
|
662
|
+
kind: "action_cooldown",
|
|
663
|
+
phase,
|
|
664
|
+
pause_ms: pauseMs
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function updateRecruitProgress(extra = {}) {
|
|
669
|
+
const counts = countRecruitResultStatuses(results, { greetCount });
|
|
670
|
+
const listSnapshot = compactInfiniteListState(listState);
|
|
671
|
+
const humanRestState = humanRestController.getState();
|
|
672
|
+
runControl.updateProgress({
|
|
673
|
+
card_count: cardNodeIds.length,
|
|
674
|
+
target_count: limit,
|
|
675
|
+
...counts,
|
|
676
|
+
screening_mode: normalizedScreeningMode,
|
|
677
|
+
unique_seen: listSnapshot.seen_count,
|
|
678
|
+
scroll_count: listSnapshot.scroll_count,
|
|
679
|
+
refresh_rounds: refreshRounds,
|
|
680
|
+
refresh_attempts: refreshAttempts.length,
|
|
681
|
+
context_recoveries: contextRecoveryAttempts,
|
|
682
|
+
list_end_reason: listEndReason || null,
|
|
683
|
+
viewport_checks: viewportGuard.getStats().checks,
|
|
684
|
+
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
685
|
+
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
686
|
+
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
687
|
+
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
688
|
+
human_rest_enabled: effectiveHumanRestEnabled,
|
|
689
|
+
human_rest_count: humanRestState.rest_count,
|
|
690
|
+
human_rest_ms: humanRestState.total_rest_ms,
|
|
691
|
+
search_exchange_resume_filter_requested: searchExchangeResumeFilterRequested ? 1 : 0,
|
|
692
|
+
search_exchange_resume_filter_applied: searchExchangeResumeFilterApplied ? 1 : 0,
|
|
693
|
+
last_human_event: lastHumanEvent,
|
|
694
|
+
...extra
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function checkpointInProgressCandidate({
|
|
699
|
+
index = results.length,
|
|
700
|
+
candidateKey = "",
|
|
701
|
+
cardNodeId = null,
|
|
702
|
+
detailStep = "",
|
|
703
|
+
error = null
|
|
704
|
+
} = {}) {
|
|
705
|
+
runControl.checkpoint({
|
|
706
|
+
in_progress_candidate: {
|
|
707
|
+
index,
|
|
708
|
+
key: candidateKey,
|
|
709
|
+
card_node_id: cardNodeId,
|
|
710
|
+
detail_step: detailStep || null,
|
|
711
|
+
counters: countRecruitResultStatuses(results, { greetCount }),
|
|
712
|
+
error: compactError(error, "RECRUIT_IN_PROGRESS_ERROR")
|
|
713
|
+
},
|
|
714
|
+
candidate_list: compactInfiniteListState(listState)
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function closeRecruitBlockingPanelsForRun(phase = "cleanup") {
|
|
719
|
+
const result = await closeRecruitBlockingPanels(client, {
|
|
720
|
+
attemptsLimit: 2,
|
|
721
|
+
rootState
|
|
722
|
+
});
|
|
723
|
+
if (!result?.closed) {
|
|
724
|
+
throw createRecruitBlockingPanelCloseFailureError(result, phase);
|
|
725
|
+
}
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async function recoverAndReapplyRecruitContext(reason = "context_recovery", error = null, {
|
|
730
|
+
forceRecentViewed = true
|
|
731
|
+
} = {}) {
|
|
732
|
+
await runControl.waitIfPaused();
|
|
733
|
+
runControl.throwIfCanceled();
|
|
734
|
+
const started = Date.now();
|
|
735
|
+
runControl.setPhase("recruit:recover-context");
|
|
736
|
+
contextRecoveryAttempts += 1;
|
|
737
|
+
const refreshResult = await refreshRecruitSearchAtEnd(client, {
|
|
738
|
+
searchParams: normalizedSearchParams,
|
|
739
|
+
requireCards: true,
|
|
740
|
+
searchTimeoutMs: cardTimeoutMs,
|
|
741
|
+
resetTimeoutMs,
|
|
742
|
+
resetSettleMs: refreshResetSettleMs,
|
|
743
|
+
cityOptionTimeoutMs,
|
|
744
|
+
forceRecentViewed
|
|
745
|
+
});
|
|
746
|
+
let blockingPanelClose = null;
|
|
747
|
+
if (refreshResult.ok) {
|
|
748
|
+
blockingPanelClose = await closeRecruitBlockingPanels(client, {
|
|
749
|
+
attemptsLimit: 2
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
const compactRefresh = {
|
|
753
|
+
...compactRefreshAttempt(refreshResult),
|
|
754
|
+
context_recovery: true,
|
|
755
|
+
recovery_reason: reason,
|
|
756
|
+
trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
|
|
757
|
+
account_rights_panel_close: compactCloseResult(blockingPanelClose),
|
|
758
|
+
elapsed_ms: Date.now() - started
|
|
759
|
+
};
|
|
760
|
+
refreshAttempts.push(compactRefresh);
|
|
761
|
+
runControl.checkpoint({
|
|
762
|
+
context_recovery: {
|
|
763
|
+
attempt: contextRecoveryAttempts,
|
|
764
|
+
reason,
|
|
765
|
+
trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
|
|
766
|
+
refresh: compactRefresh,
|
|
767
|
+
counters: countRecruitResultStatuses(results, { greetCount })
|
|
768
|
+
},
|
|
769
|
+
candidate_list: compactInfiniteListState(listState)
|
|
770
|
+
});
|
|
771
|
+
if (!refreshResult.ok) {
|
|
772
|
+
updateRecruitProgress({
|
|
773
|
+
refresh_method: refreshResult.method || null,
|
|
774
|
+
refresh_forced_recent_viewed: forceRecentViewed,
|
|
775
|
+
recovery_reason: reason
|
|
776
|
+
});
|
|
777
|
+
throw new Error(`Recruit context recovery failed after ${reason}: ${refreshResult.application?.reason || "refresh returned no cards"}`);
|
|
778
|
+
}
|
|
779
|
+
if (!blockingPanelClose?.closed) {
|
|
780
|
+
const panelError = createRecruitBlockingPanelCloseFailureError(blockingPanelClose, `recover:${reason}`);
|
|
781
|
+
panelError.refresh_attempt = compactRefresh;
|
|
782
|
+
throw panelError;
|
|
783
|
+
}
|
|
784
|
+
rootState = await getRecruitRoots(client);
|
|
785
|
+
rootState = await ensureRecruitViewport(rootState, "recover_after");
|
|
786
|
+
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
787
|
+
timeoutMs: cardTimeoutMs,
|
|
788
|
+
intervalMs: 300
|
|
789
|
+
});
|
|
790
|
+
resetInfiniteListForRefreshRound(listState, {
|
|
791
|
+
reason: `context_recovery:${reason}`,
|
|
792
|
+
round: contextRecoveryAttempts,
|
|
793
|
+
method: refreshResult.method,
|
|
794
|
+
metadata: {
|
|
795
|
+
card_count: cardNodeIds.length,
|
|
796
|
+
forced_recent_viewed: forceRecentViewed,
|
|
797
|
+
counters: countRecruitResultStatuses(results, { greetCount })
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
listEndReason = "";
|
|
801
|
+
updateRecruitProgress({
|
|
802
|
+
card_count: cardNodeIds.length,
|
|
803
|
+
refresh_method: refreshResult.method || null,
|
|
804
|
+
refresh_forced_recent_viewed: forceRecentViewed,
|
|
805
|
+
recovery_reason: reason
|
|
806
|
+
});
|
|
807
|
+
return refreshResult;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
runControl.setPhase("recruit:cleanup");
|
|
811
|
+
await closeRecruitDetail(client, { attemptsLimit: 2 });
|
|
812
|
+
await closeRecruitBlockingPanelsForRun("cleanup");
|
|
813
|
+
|
|
814
|
+
await runControl.waitIfPaused();
|
|
815
|
+
runControl.throwIfCanceled();
|
|
816
|
+
runControl.setPhase("recruit:roots");
|
|
817
|
+
rootState = await getRecruitRoots(client);
|
|
818
|
+
rootState = await ensureRecruitViewport(rootState, "roots");
|
|
819
|
+
runControl.checkpoint({
|
|
820
|
+
iframe_selector: rootState.iframe.selector,
|
|
821
|
+
iframe_document_node_id: rootState.iframe.documentNodeId,
|
|
822
|
+
search_params: normalizedSearchParams
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
if (hasRecruitSearchParams(normalizedSearchParams)) {
|
|
826
|
+
await runControl.waitIfPaused();
|
|
827
|
+
runControl.throwIfCanceled();
|
|
828
|
+
runControl.setPhase("recruit:search");
|
|
829
|
+
const searchResult = await applyRecruitSearchParams(client, {
|
|
830
|
+
searchParams: normalizedSearchParams,
|
|
831
|
+
requireCards: true,
|
|
832
|
+
resetBeforeApply: resetBeforeSearch,
|
|
833
|
+
searchTimeoutMs: cardTimeoutMs,
|
|
834
|
+
resetTimeoutMs,
|
|
835
|
+
cityOptionTimeoutMs
|
|
836
|
+
});
|
|
837
|
+
const exchangeResumeStep = searchResult.steps.find((step) => step.step === "exchange_resume");
|
|
838
|
+
searchExchangeResumeFilterApplied = Boolean(
|
|
839
|
+
searchExchangeResumeFilterRequested
|
|
840
|
+
&& exchangeResumeStep?.result?.applied
|
|
841
|
+
&& exchangeResumeStep?.result?.requested === true
|
|
842
|
+
);
|
|
843
|
+
runControl.checkpoint({
|
|
844
|
+
search: {
|
|
845
|
+
search_params: searchResult.search_params,
|
|
846
|
+
before_counts: searchResult.before_counts,
|
|
847
|
+
post_search_state: searchResult.post_search_state,
|
|
848
|
+
steps: searchResult.steps.map((step) => ({
|
|
849
|
+
step: step.step,
|
|
850
|
+
applied: step.result?.applied,
|
|
851
|
+
clicked: step.result?.clicked,
|
|
852
|
+
searched: step.result?.searched,
|
|
853
|
+
reason: step.result?.reason || null
|
|
854
|
+
}))
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
rootState = await getRecruitRoots(client);
|
|
858
|
+
rootState = await ensureRecruitViewport(rootState, "search");
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
await runControl.waitIfPaused();
|
|
862
|
+
runControl.throwIfCanceled();
|
|
863
|
+
runControl.setPhase("recruit:cards");
|
|
864
|
+
rootState = await ensureRecruitViewport(rootState, "cards");
|
|
865
|
+
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
866
|
+
timeoutMs: cardTimeoutMs,
|
|
867
|
+
intervalMs: 300
|
|
868
|
+
});
|
|
869
|
+
if (!cardNodeIds.length) {
|
|
870
|
+
throw new Error("No recruit/search candidate cards found for run service");
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
updateRecruitProgress({
|
|
874
|
+
list_end_reason: null
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
while (results.length < limit) {
|
|
878
|
+
const candidateStarted = Date.now();
|
|
879
|
+
const timings = {};
|
|
880
|
+
await runControl.waitIfPaused();
|
|
881
|
+
runControl.throwIfCanceled();
|
|
882
|
+
runControl.setPhase("recruit:candidate");
|
|
883
|
+
rootState = await ensureRecruitViewport(rootState, "candidate_loop");
|
|
884
|
+
|
|
885
|
+
const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
|
|
886
|
+
client,
|
|
887
|
+
state: listState,
|
|
888
|
+
maxScrolls: listMaxScrolls,
|
|
889
|
+
stableSignatureLimit: listStableSignatureLimit,
|
|
890
|
+
wheelDeltaY: listWheelDeltaY,
|
|
891
|
+
settleMs: listSettleMs,
|
|
892
|
+
listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
893
|
+
fallbackPoint: listFallbackResolver,
|
|
894
|
+
findNodeIds: async () => {
|
|
895
|
+
let currentRootState = await getRecruitRoots(client);
|
|
896
|
+
currentRootState = await ensureRecruitViewport(currentRootState, "candidate_find_nodes");
|
|
897
|
+
rootState = currentRootState;
|
|
898
|
+
const currentCardNodeIds = await waitForRecruitCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
899
|
+
timeoutMs: Math.min(cardTimeoutMs, 5000),
|
|
900
|
+
intervalMs: 300
|
|
901
|
+
});
|
|
902
|
+
cardNodeIds = currentCardNodeIds;
|
|
903
|
+
return currentCardNodeIds;
|
|
904
|
+
},
|
|
905
|
+
readCandidate: async (nodeId, { visibleIndex }) => readRecruitCardCandidate(client, nodeId, {
|
|
906
|
+
targetUrl,
|
|
907
|
+
source: "recruit-run-card",
|
|
908
|
+
metadata: {
|
|
909
|
+
run_candidate_index: results.length,
|
|
910
|
+
visible_index: visibleIndex,
|
|
911
|
+
search_params: normalizedSearchParams
|
|
912
|
+
}
|
|
913
|
+
}),
|
|
914
|
+
detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
|
|
915
|
+
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
916
|
+
markerSelectors: RECRUIT_BOTTOM_MARKER_SELECTORS,
|
|
917
|
+
refreshSelectors: RECRUIT_BOTTOM_REFRESH_SELECTORS,
|
|
918
|
+
textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
|
|
919
|
+
maxTextScanNodes: 500
|
|
920
|
+
})
|
|
921
|
+
}));
|
|
922
|
+
if (!nextCandidateResult.ok) {
|
|
923
|
+
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
924
|
+
if (
|
|
925
|
+
(nextCandidateResult.end_reached || isRefreshableListStall(nextCandidateResult.reason))
|
|
926
|
+
&& refreshOnEnd
|
|
927
|
+
&& results.length < limit
|
|
928
|
+
&& refreshRounds < Math.max(0, Number(maxRefreshRounds) || 0)
|
|
929
|
+
) {
|
|
930
|
+
await runControl.waitIfPaused();
|
|
931
|
+
runControl.throwIfCanceled();
|
|
932
|
+
runControl.setPhase("recruit:refresh");
|
|
933
|
+
refreshRounds += 1;
|
|
934
|
+
const refreshResult = await refreshRecruitSearchAtEnd(client, {
|
|
935
|
+
searchParams: normalizedSearchParams,
|
|
936
|
+
requireCards: true,
|
|
937
|
+
searchTimeoutMs: cardTimeoutMs,
|
|
938
|
+
resetTimeoutMs,
|
|
939
|
+
resetSettleMs: refreshResetSettleMs,
|
|
940
|
+
cityOptionTimeoutMs
|
|
941
|
+
});
|
|
942
|
+
const compactRefresh = compactRefreshAttempt(refreshResult);
|
|
943
|
+
refreshAttempts.push(compactRefresh);
|
|
944
|
+
runControl.checkpoint({
|
|
945
|
+
refresh_round: refreshRounds,
|
|
946
|
+
refresh: compactRefresh
|
|
947
|
+
});
|
|
948
|
+
updateRecruitProgress({
|
|
949
|
+
card_count: refreshResult.card_count || cardNodeIds.length,
|
|
950
|
+
refresh_method: refreshResult.method || null,
|
|
951
|
+
refresh_forced_recent_viewed: true,
|
|
952
|
+
list_end_reason: listEndReason
|
|
953
|
+
});
|
|
954
|
+
if (refreshResult.ok) {
|
|
955
|
+
rootState = await getRecruitRoots(client);
|
|
956
|
+
rootState = await ensureRecruitViewport(rootState, "refresh_after");
|
|
957
|
+
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
958
|
+
timeoutMs: cardTimeoutMs,
|
|
959
|
+
intervalMs: 300
|
|
960
|
+
});
|
|
961
|
+
resetInfiniteListForRefreshRound(listState, {
|
|
962
|
+
reason: listEndReason,
|
|
963
|
+
round: refreshRounds,
|
|
964
|
+
method: refreshResult.method,
|
|
965
|
+
metadata: {
|
|
966
|
+
card_count: cardNodeIds.length,
|
|
967
|
+
forced_recent_viewed: true
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
listEndReason = "";
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
throw createRecruitRefreshFailureError(compactRefresh, {
|
|
974
|
+
listEndReason,
|
|
975
|
+
targetCount: limit,
|
|
976
|
+
processedCount: results.length
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const index = results.length;
|
|
983
|
+
const cardNodeId = nextCandidateResult.item.node_id;
|
|
984
|
+
const candidateKey = nextCandidateResult.item.key;
|
|
985
|
+
const cardCandidate = nextCandidateResult.item.candidate;
|
|
986
|
+
|
|
987
|
+
let screeningCandidate = cardCandidate;
|
|
988
|
+
let detailResult = null;
|
|
989
|
+
let detailActionRootNodeIds = [];
|
|
990
|
+
let recoverableDetailError = null;
|
|
991
|
+
let detailStep = "not_started";
|
|
992
|
+
if (index < detailCountLimit) {
|
|
993
|
+
try {
|
|
994
|
+
await runControl.waitIfPaused();
|
|
995
|
+
runControl.throwIfCanceled();
|
|
996
|
+
runControl.setPhase("recruit:detail");
|
|
997
|
+
detailStep = "ensure_viewport";
|
|
998
|
+
rootState = await ensureRecruitViewport(rootState, "detail");
|
|
999
|
+
const blockingPanelClose = await closeRecruitBlockingPanels(client, {
|
|
1000
|
+
attemptsLimit: 2,
|
|
1001
|
+
rootState
|
|
1002
|
+
});
|
|
1003
|
+
if (!blockingPanelClose?.closed) {
|
|
1004
|
+
const panelError = createRecruitBlockingPanelCloseFailureError(
|
|
1005
|
+
blockingPanelClose,
|
|
1006
|
+
"before_detail_open"
|
|
1007
|
+
);
|
|
1008
|
+
timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
|
|
1009
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error: panelError });
|
|
1010
|
+
await recoverAndReapplyRecruitContext("account_rights_panel_before_detail", panelError, {
|
|
1011
|
+
forceRecentViewed: true
|
|
1012
|
+
});
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (blockingPanelClose.already_closed === false) {
|
|
1016
|
+
timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
|
|
1017
|
+
}
|
|
1018
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
1019
|
+
detailStep = "open_detail";
|
|
1020
|
+
networkRecorder.clear();
|
|
1021
|
+
await maybeHumanActionCooldown("before_detail_open", timings);
|
|
1022
|
+
const openedDetail = await openRecruitCardDetail(client, cardNodeId);
|
|
1023
|
+
detailActionRootNodeIds = (openedDetail.detail_state?.roots || []).map((root) => root.nodeId).filter(Boolean);
|
|
1024
|
+
addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
|
|
1025
|
+
addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
|
|
1026
|
+
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
1027
|
+
detailStep = "wait_network";
|
|
1028
|
+
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
1029
|
+
waitForRecruitDetailNetworkEvents,
|
|
1030
|
+
networkRecorder,
|
|
1031
|
+
{
|
|
1032
|
+
waitPlan,
|
|
1033
|
+
minCount: 1,
|
|
1034
|
+
requireLoaded: true,
|
|
1035
|
+
intervalMs: 120
|
|
1036
|
+
}
|
|
1037
|
+
));
|
|
1038
|
+
if (networkWait?.elapsed_ms != null) {
|
|
1039
|
+
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
1040
|
+
}
|
|
1041
|
+
detailStep = "extract_detail";
|
|
1042
|
+
detailResult = await extractRecruitDetailCandidate(client, {
|
|
1043
|
+
cardCandidate,
|
|
1044
|
+
cardNodeId,
|
|
1045
|
+
detailState: openedDetail.detail_state,
|
|
1046
|
+
networkEvents: networkRecorder.events,
|
|
1047
|
+
targetUrl,
|
|
1048
|
+
closeDetail: false,
|
|
1049
|
+
networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
|
|
1050
|
+
networkParseIntervalMs: 250
|
|
1051
|
+
});
|
|
1052
|
+
detailResult.card_box = openedDetail.card_box || null;
|
|
1053
|
+
detailResult.open_attempts = openedDetail.open_attempts || [];
|
|
1054
|
+
addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
|
|
1055
|
+
const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
1056
|
+
let source = "network";
|
|
1057
|
+
let imageEvidence = null;
|
|
1058
|
+
let captureTarget = null;
|
|
1059
|
+
let captureTargetWait = null;
|
|
1060
|
+
if (parsedNetworkProfileCount > 0) {
|
|
1061
|
+
recordCvNetworkHit(cvAcquisitionState, {
|
|
1062
|
+
parsedNetworkProfileCount,
|
|
1063
|
+
waitResult: networkWait
|
|
1064
|
+
});
|
|
1065
|
+
} else {
|
|
1066
|
+
detailStep = "wait_capture_target";
|
|
1067
|
+
captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
|
|
1068
|
+
domain: "recruit",
|
|
1069
|
+
timeoutMs: 6000,
|
|
1070
|
+
intervalMs: 250
|
|
1071
|
+
});
|
|
1072
|
+
captureTarget = captureTargetWait.target || null;
|
|
1073
|
+
const captureNodeId = captureTarget?.node_id || null;
|
|
1074
|
+
if (captureNodeId) {
|
|
1075
|
+
const imageEvidencePath = imageEvidenceFilePath({
|
|
1076
|
+
imageOutputDir,
|
|
1077
|
+
domain: "recruit",
|
|
1078
|
+
runId: runControl?.runId,
|
|
1079
|
+
index,
|
|
1080
|
+
extension: "jpg"
|
|
1081
|
+
});
|
|
1082
|
+
try {
|
|
1083
|
+
detailStep = "capture_image";
|
|
1084
|
+
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
1085
|
+
filePath: imageEvidencePath,
|
|
1086
|
+
format: "jpeg",
|
|
1087
|
+
quality: 72,
|
|
1088
|
+
optimize: true,
|
|
1089
|
+
resizeMaxWidth: 1100,
|
|
1090
|
+
captureViewport: false,
|
|
1091
|
+
padding: 0,
|
|
1092
|
+
maxScreenshots: maxImagePages,
|
|
1093
|
+
wheelDeltaY: imageWheelDeltaY,
|
|
1094
|
+
settleMs: 350,
|
|
1095
|
+
scrollMethod: "dom-anchor-fallback-input",
|
|
1096
|
+
scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
|
|
1097
|
+
stepTimeoutMs: 45000,
|
|
1098
|
+
totalTimeoutMs: 90000,
|
|
1099
|
+
duplicateStopCount: 1,
|
|
1100
|
+
skipDuplicateScreenshots: true,
|
|
1101
|
+
composeForLlm: true,
|
|
1102
|
+
llmPagesPerImage: 3,
|
|
1103
|
+
llmResizeMaxWidth: 1100,
|
|
1104
|
+
llmQuality: 72,
|
|
1105
|
+
metadata: {
|
|
1106
|
+
domain: "recruit",
|
|
1107
|
+
capture_mode: "scroll_sequence",
|
|
1108
|
+
acquisition_reason: "network_miss_image_fallback",
|
|
1109
|
+
run_candidate_index: index,
|
|
1110
|
+
candidate_key: candidateKey,
|
|
1111
|
+
capture_target: captureTarget,
|
|
1112
|
+
capture_target_wait: captureTargetWait
|
|
1113
|
+
}
|
|
1114
|
+
}));
|
|
1115
|
+
source = "image";
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
if (!isRecoverableRecruitImageCaptureError(error)) throw error;
|
|
1118
|
+
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
1119
|
+
if (recoveryCount < 1) {
|
|
1120
|
+
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
1121
|
+
timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
|
|
1122
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1123
|
+
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1124
|
+
await closeRecruitBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
1125
|
+
await recoverAndReapplyRecruitContext(`image_capture:${detailStep}`, error, {
|
|
1126
|
+
forceRecentViewed: true
|
|
1127
|
+
});
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
imageEvidence = createRecoverableRecruitImageCaptureEvidence(error, {
|
|
1131
|
+
elapsedMs: timings.screenshot_capture_ms,
|
|
1132
|
+
filePath: imageEvidencePath,
|
|
1133
|
+
extension: "jpg",
|
|
1134
|
+
maxScreenshots: maxImagePages
|
|
1135
|
+
});
|
|
1136
|
+
source = "image_capture_failed";
|
|
1137
|
+
}
|
|
1138
|
+
recordCvImageFallback(cvAcquisitionState, {
|
|
1139
|
+
reason: source === "image_capture_failed"
|
|
1140
|
+
? "network_miss_image_capture_failed"
|
|
1141
|
+
: "network_miss_image_fallback",
|
|
1142
|
+
parsedNetworkProfileCount,
|
|
1143
|
+
waitResult: networkWait,
|
|
1144
|
+
imageEvidence
|
|
1145
|
+
});
|
|
1146
|
+
} else {
|
|
1147
|
+
source = "missing_capture_node";
|
|
1148
|
+
recordCvNetworkMiss(cvAcquisitionState, {
|
|
1149
|
+
reason: "network_miss_no_capture_node",
|
|
1150
|
+
parsedNetworkProfileCount,
|
|
1151
|
+
waitResult: networkWait
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
detailResult.image_evidence = imageEvidence;
|
|
1157
|
+
detailResult.cv_acquisition = {
|
|
1158
|
+
source,
|
|
1159
|
+
mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
|
|
1160
|
+
wait_plan: waitPlan,
|
|
1161
|
+
network_wait: networkWait,
|
|
1162
|
+
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
1163
|
+
image_evidence: summarizeImageEvidence(imageEvidence),
|
|
1164
|
+
capture_target: captureTarget || null,
|
|
1165
|
+
capture_target_wait: captureTargetWait
|
|
1166
|
+
};
|
|
1167
|
+
screeningCandidate = detailResult.candidate;
|
|
1168
|
+
if (closeDetail && !postActionEnabled) {
|
|
1169
|
+
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
|
|
1170
|
+
await maybeHumanActionCooldown("after_detail_close", timings);
|
|
1171
|
+
if (!detailResult.close_result?.closed) {
|
|
1172
|
+
const closeError = createRecruitCloseFailureError(detailResult.close_result);
|
|
1173
|
+
const recovery = await recoverAndReapplyRecruitContext("detail_close_failed", closeError, {
|
|
1174
|
+
forceRecentViewed: true
|
|
1175
|
+
});
|
|
1176
|
+
detailResult.cv_acquisition = {
|
|
1177
|
+
...(detailResult.cv_acquisition || {}),
|
|
1178
|
+
close_recovery: {
|
|
1179
|
+
ok: Boolean(recovery.ok),
|
|
1180
|
+
method: recovery.method || "",
|
|
1181
|
+
forced_recent_viewed: Boolean(recovery.forced_recent_viewed),
|
|
1182
|
+
card_count: recovery.card_count || 0
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
} else {
|
|
1187
|
+
detailResult.close_result = null;
|
|
1188
|
+
}
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
if (!isRecoverableRecruitDetailError(error)) throw error;
|
|
1191
|
+
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
1192
|
+
if (recoveryCount < 1) {
|
|
1193
|
+
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
1194
|
+
timings.detail_recovery_trigger = compactRecoverableDetailError(error);
|
|
1195
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1196
|
+
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1197
|
+
await closeRecruitBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
1198
|
+
await recoverAndReapplyRecruitContext(`detail:${detailStep}`, error, {
|
|
1199
|
+
forceRecentViewed: true
|
|
1200
|
+
});
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
recoverableDetailError = error;
|
|
1204
|
+
detailResult = null;
|
|
1205
|
+
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
1206
|
+
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1207
|
+
await closeRecruitBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
await runControl.waitIfPaused();
|
|
1212
|
+
runControl.throwIfCanceled();
|
|
1213
|
+
runControl.setPhase("recruit:screening");
|
|
1214
|
+
let llmResult = null;
|
|
1215
|
+
if (useLlmScreening) {
|
|
1216
|
+
if (recoverableDetailError || detailResult?.image_evidence?.ok === false) {
|
|
1217
|
+
llmResult = null;
|
|
1218
|
+
} else if (!llmConfig) {
|
|
1219
|
+
llmResult = createMissingLlmConfigResult();
|
|
1220
|
+
} else {
|
|
1221
|
+
try {
|
|
1222
|
+
const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
|
|
1223
|
+
? "vision_model_ms"
|
|
1224
|
+
: "text_model_ms";
|
|
1225
|
+
llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
|
|
1226
|
+
candidate: screeningCandidate,
|
|
1227
|
+
criteria,
|
|
1228
|
+
config: llmConfig,
|
|
1229
|
+
timeoutMs: llmTimeoutMs,
|
|
1230
|
+
imageEvidence: detailResult?.image_evidence || null,
|
|
1231
|
+
maxImages: llmImageLimit,
|
|
1232
|
+
imageDetail: llmImageDetail
|
|
1233
|
+
}));
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
if (isFatalLlmProviderError(error)) {
|
|
1236
|
+
throw createFatalLlmRunError(error, {
|
|
1237
|
+
domain: "recruit",
|
|
1238
|
+
candidate: screeningCandidate
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
llmResult = createFailedLlmScreeningResult(error);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (detailResult) detailResult.llm_result = llmResult;
|
|
1245
|
+
}
|
|
1246
|
+
const screening = recoverableDetailError
|
|
1247
|
+
? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
|
|
1248
|
+
: detailResult?.image_evidence?.ok === false
|
|
1249
|
+
? createImageCaptureFailureScreening(screeningCandidate, {
|
|
1250
|
+
code: detailResult.image_evidence.error_code,
|
|
1251
|
+
message: detailResult.image_evidence.error
|
|
1252
|
+
})
|
|
1253
|
+
: useLlmScreening
|
|
1254
|
+
? llmResultToScreening(llmResult, screeningCandidate)
|
|
1255
|
+
: screenCandidate(screeningCandidate, { criteria });
|
|
1256
|
+
let actionDiscovery = null;
|
|
1257
|
+
let postActionResult = null;
|
|
1258
|
+
let closeFailureError = null;
|
|
1259
|
+
let closeRecoveryFailure = null;
|
|
1260
|
+
if (postActionEnabled && detailResult) {
|
|
1261
|
+
const postActionStarted = Date.now();
|
|
1262
|
+
await runControl.waitIfPaused();
|
|
1263
|
+
runControl.throwIfCanceled();
|
|
1264
|
+
runControl.setPhase("recruit:post-action");
|
|
1265
|
+
await maybeHumanActionCooldown("before_post_action", timings);
|
|
1266
|
+
actionDiscovery = await waitForRecruitDetailActionControls(client, {
|
|
1267
|
+
rootNodeIds: detailActionRootNodeIds,
|
|
1268
|
+
timeoutMs: actionTimeoutMs,
|
|
1269
|
+
intervalMs: actionIntervalMs,
|
|
1270
|
+
requireAny: true
|
|
1271
|
+
});
|
|
1272
|
+
postActionResult = await runRecruitPostAction({
|
|
1273
|
+
client,
|
|
1274
|
+
rootNodeIds: detailActionRootNodeIds,
|
|
1275
|
+
screening,
|
|
1276
|
+
actionDiscovery,
|
|
1277
|
+
postAction: normalizedPostAction,
|
|
1278
|
+
greetCount,
|
|
1279
|
+
maxGreetCount: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
|
|
1280
|
+
executePostAction,
|
|
1281
|
+
afterClickDelayMs: actionAfterClickDelayMs,
|
|
1282
|
+
lastGreetQuotaAfterSpend
|
|
1283
|
+
});
|
|
1284
|
+
if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
|
|
1285
|
+
greetCount += 1;
|
|
1286
|
+
}
|
|
1287
|
+
if (postActionResult.greet_quota_after_click?.found) {
|
|
1288
|
+
lastGreetQuotaAfterSpend = postActionResult.greet_quota_after_click;
|
|
1289
|
+
}
|
|
1290
|
+
addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
|
|
1291
|
+
}
|
|
1292
|
+
if (postActionEnabled && detailResult && closeDetail) {
|
|
1293
|
+
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
|
|
1294
|
+
await maybeHumanActionCooldown("after_detail_close", timings);
|
|
1295
|
+
if (!detailResult.close_result?.closed) {
|
|
1296
|
+
closeFailureError = createRecruitCloseFailureError(detailResult.close_result);
|
|
1297
|
+
try {
|
|
1298
|
+
const recovery = await recoverAndReapplyRecruitContext("detail_close_failed", closeFailureError, {
|
|
1299
|
+
forceRecentViewed: true
|
|
1300
|
+
});
|
|
1301
|
+
detailResult.cv_acquisition = {
|
|
1302
|
+
...(detailResult.cv_acquisition || {}),
|
|
1303
|
+
close_recovery: {
|
|
1304
|
+
ok: Boolean(recovery.ok),
|
|
1305
|
+
method: recovery.method || "",
|
|
1306
|
+
forced_recent_viewed: Boolean(recovery.forced_recent_viewed),
|
|
1307
|
+
card_count: recovery.card_count || 0
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
closeRecoveryFailure = error;
|
|
1312
|
+
detailResult.cv_acquisition = {
|
|
1313
|
+
...(detailResult.cv_acquisition || {}),
|
|
1314
|
+
close_recovery: {
|
|
1315
|
+
ok: false,
|
|
1316
|
+
reason: "context_recovery_failed",
|
|
1317
|
+
error: error?.message || String(error),
|
|
1318
|
+
forced_recent_viewed: true
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
timings.total_ms = Date.now() - candidateStarted;
|
|
1325
|
+
const compactResult = {
|
|
1326
|
+
index,
|
|
1327
|
+
candidate_key: candidateKey,
|
|
1328
|
+
card_node_id: cardNodeId,
|
|
1329
|
+
candidate: compactCandidate(screeningCandidate),
|
|
1330
|
+
detail: compactDetail(detailResult),
|
|
1331
|
+
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
1332
|
+
screening: compactScreening(screening),
|
|
1333
|
+
action_discovery: compactActionDiscovery(actionDiscovery),
|
|
1334
|
+
post_action: postActionResult,
|
|
1335
|
+
error: recoverableDetailError
|
|
1336
|
+
? compactRecoverableDetailError(recoverableDetailError)
|
|
1337
|
+
: closeRecoveryFailure
|
|
1338
|
+
? compactError(closeFailureError, "DETAIL_CLOSE_FAILED")
|
|
1339
|
+
: detailResult?.image_evidence?.ok === false
|
|
1340
|
+
? compactError({
|
|
1341
|
+
code: detailResult.image_evidence.error_code,
|
|
1342
|
+
message: detailResult.image_evidence.error
|
|
1343
|
+
}, "IMAGE_CAPTURE_FAILED")
|
|
1344
|
+
: null,
|
|
1345
|
+
timings
|
|
1346
|
+
};
|
|
1347
|
+
results.push(compactResult);
|
|
1348
|
+
markInfiniteListCandidateProcessed(listState, candidateKey, {
|
|
1349
|
+
metadata: {
|
|
1350
|
+
result_index: index,
|
|
1351
|
+
candidate_id: screeningCandidate.id || null
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
updateRecruitProgress({
|
|
1356
|
+
last_candidate_id: screeningCandidate.id || null,
|
|
1357
|
+
last_candidate_key: candidateKey,
|
|
1358
|
+
last_score: screening.score
|
|
1359
|
+
});
|
|
1360
|
+
const checkpointStarted = Date.now();
|
|
1361
|
+
runControl.checkpoint({
|
|
1362
|
+
results,
|
|
1363
|
+
last_candidate: {
|
|
1364
|
+
id: screeningCandidate.id || null,
|
|
1365
|
+
key: candidateKey,
|
|
1366
|
+
identity: screeningCandidate.identity || {},
|
|
1367
|
+
screening: {
|
|
1368
|
+
status: screening.status,
|
|
1369
|
+
passed: screening.passed,
|
|
1370
|
+
score: screening.score
|
|
1371
|
+
},
|
|
1372
|
+
llm_screening: compactScreeningLlmResult(llmResult),
|
|
1373
|
+
error: compactResult.error
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
1377
|
+
|
|
1378
|
+
if (effectiveHumanRestEnabled) {
|
|
1379
|
+
const restStarted = Date.now();
|
|
1380
|
+
const restResult = await humanRestController.takeBreakIfNeeded({
|
|
1381
|
+
sleepFn: (ms) => runControl.sleep(ms)
|
|
1382
|
+
});
|
|
1383
|
+
const restElapsed = Date.now() - restStarted;
|
|
1384
|
+
if (restResult.rested) {
|
|
1385
|
+
recordHumanEvent({
|
|
1386
|
+
kind: "rest",
|
|
1387
|
+
pause_ms: restResult.pause_ms || restElapsed,
|
|
1388
|
+
events: restResult.events || []
|
|
1389
|
+
});
|
|
1390
|
+
compactResult.human_rest = restResult;
|
|
1391
|
+
addTiming(compactResult.timings, "human_rest_ms", restElapsed);
|
|
1392
|
+
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
1393
|
+
updateRecruitProgress({
|
|
1394
|
+
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
1395
|
+
human_rest_last: restResult
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if (delayMs > 0) {
|
|
1401
|
+
const sleepStarted = Date.now();
|
|
1402
|
+
await runControl.sleep(delayMs);
|
|
1403
|
+
addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
|
|
1404
|
+
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
1405
|
+
}
|
|
1406
|
+
if (postActionResult?.stop_run) {
|
|
1407
|
+
listEndReason = postActionResult.reason || "post_action_stop";
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
runControl.setPhase("recruit:done");
|
|
1413
|
+
return {
|
|
1414
|
+
domain: "recruit",
|
|
1415
|
+
target_url: targetUrl,
|
|
1416
|
+
search_params: normalizedSearchParams,
|
|
1417
|
+
card_count: cardNodeIds.length,
|
|
1418
|
+
candidate_list: compactInfiniteListState(listState),
|
|
1419
|
+
viewport_health: {
|
|
1420
|
+
stats: viewportGuard.getStats(),
|
|
1421
|
+
events: viewportGuard.getEvents()
|
|
1422
|
+
},
|
|
1423
|
+
human_behavior: effectiveHumanBehavior,
|
|
1424
|
+
human_rest: humanRestController.getState(),
|
|
1425
|
+
last_human_event: lastHumanEvent,
|
|
1426
|
+
list_end_reason: listEndReason || null,
|
|
1427
|
+
search_exchange_resume_filter_requested: searchExchangeResumeFilterRequested ? 1 : 0,
|
|
1428
|
+
search_exchange_resume_filter_applied: searchExchangeResumeFilterApplied ? 1 : 0,
|
|
1429
|
+
last_greet_quota_after_spend: lastGreetQuotaAfterSpend,
|
|
1430
|
+
refresh_rounds: refreshRounds,
|
|
1431
|
+
refresh_attempts: refreshAttempts,
|
|
1432
|
+
context_recoveries: contextRecoveryAttempts,
|
|
1433
|
+
...countRecruitResultStatuses(results, { greetCount }),
|
|
1434
|
+
results
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
export function createRecruitRunService({
|
|
1439
|
+
lifecycle,
|
|
1440
|
+
idPrefix = "recruit",
|
|
1441
|
+
workflow = runRecruitWorkflow,
|
|
1442
|
+
onSnapshot = null
|
|
1443
|
+
} = {}) {
|
|
1444
|
+
const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
|
|
1445
|
+
|
|
1446
|
+
function startRecruitRun({
|
|
1447
|
+
runId = "",
|
|
1448
|
+
client,
|
|
1449
|
+
targetUrl = "",
|
|
1450
|
+
criteria = "",
|
|
1451
|
+
searchParams = {},
|
|
1452
|
+
maxCandidates = 5,
|
|
1453
|
+
detailLimit = null,
|
|
1454
|
+
closeDetail = true,
|
|
1455
|
+
delayMs = 0,
|
|
1456
|
+
cardTimeoutMs = 90000,
|
|
1457
|
+
resetBeforeSearch = true,
|
|
1458
|
+
resetTimeoutMs = 180000,
|
|
1459
|
+
cityOptionTimeoutMs = 30000,
|
|
1460
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
1461
|
+
imageWheelDeltaY = 650,
|
|
1462
|
+
cvAcquisitionMode = "unknown",
|
|
1463
|
+
listMaxScrolls = 20,
|
|
1464
|
+
listStableSignatureLimit = 5,
|
|
1465
|
+
listWheelDeltaY = 850,
|
|
1466
|
+
listSettleMs = 2200,
|
|
1467
|
+
listFallbackPoint = null,
|
|
1468
|
+
refreshOnEnd = true,
|
|
1469
|
+
maxRefreshRounds = 2,
|
|
1470
|
+
refreshResetSettleMs = 5000,
|
|
1471
|
+
screeningMode = "llm",
|
|
1472
|
+
llmConfig = null,
|
|
1473
|
+
llmTimeoutMs = 120000,
|
|
1474
|
+
llmImageLimit = 8,
|
|
1475
|
+
llmImageDetail = "high",
|
|
1476
|
+
imageOutputDir = "",
|
|
1477
|
+
humanRestEnabled = false,
|
|
1478
|
+
humanBehavior = null,
|
|
1479
|
+
postAction = "none",
|
|
1480
|
+
maxGreetCount = null,
|
|
1481
|
+
executePostAction = true,
|
|
1482
|
+
actionTimeoutMs = 8000,
|
|
1483
|
+
actionIntervalMs = 400,
|
|
1484
|
+
actionAfterClickDelayMs = 900,
|
|
1485
|
+
name = "recruit-domain-run"
|
|
1486
|
+
} = {}) {
|
|
1487
|
+
if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
|
|
1488
|
+
const normalizedSearchParams = normalizeSearchParams(searchParams);
|
|
1489
|
+
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
1490
|
+
const normalizedPostAction = normalizeRecruitPostAction(postAction);
|
|
1491
|
+
const searchExchangeResumeFilterRequested = normalizedSearchParams.skip_recent_colleague_contacted === true;
|
|
1492
|
+
const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
|
|
1493
|
+
const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
|
|
1494
|
+
const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
|
|
1495
|
+
legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
|
|
1496
|
+
});
|
|
1497
|
+
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
1498
|
+
return manager.startRun({
|
|
1499
|
+
runId,
|
|
1500
|
+
name,
|
|
1501
|
+
context: {
|
|
1502
|
+
domain: "recruit",
|
|
1503
|
+
target_url: targetUrl,
|
|
1504
|
+
criteria_present: Boolean(criteria),
|
|
1505
|
+
search_params: normalizedSearchParams,
|
|
1506
|
+
max_candidates: maxCandidates,
|
|
1507
|
+
detail_limit: normalizedDetailLimit,
|
|
1508
|
+
close_detail: closeDetail,
|
|
1509
|
+
reset_before_search: resetBeforeSearch,
|
|
1510
|
+
reset_timeout_ms: resetTimeoutMs,
|
|
1511
|
+
city_option_timeout_ms: cityOptionTimeoutMs,
|
|
1512
|
+
cv_acquisition_mode: cvAcquisitionMode,
|
|
1513
|
+
max_image_pages: maxImagePages,
|
|
1514
|
+
image_wheel_delta_y: imageWheelDeltaY,
|
|
1515
|
+
list_max_scrolls: listMaxScrolls,
|
|
1516
|
+
list_stable_signature_limit: listStableSignatureLimit,
|
|
1517
|
+
list_wheel_delta_y: listWheelDeltaY,
|
|
1518
|
+
list_settle_ms: listSettleMs,
|
|
1519
|
+
list_fallback_point: listFallbackPoint,
|
|
1520
|
+
refresh_on_end: refreshOnEnd,
|
|
1521
|
+
max_refresh_rounds: maxRefreshRounds,
|
|
1522
|
+
refresh_reset_settle_ms: refreshResetSettleMs,
|
|
1523
|
+
screening_mode: normalizedScreeningMode,
|
|
1524
|
+
llm_configured: Boolean(llmConfig),
|
|
1525
|
+
llm_timeout_ms: llmTimeoutMs,
|
|
1526
|
+
llm_image_limit: llmImageLimit,
|
|
1527
|
+
llm_image_detail: llmImageDetail,
|
|
1528
|
+
image_output_dir: imageOutputDir || "",
|
|
1529
|
+
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
1530
|
+
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1531
|
+
human_behavior: effectiveHumanBehavior,
|
|
1532
|
+
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
1533
|
+
human_rest_enabled: effectiveHumanRestEnabled,
|
|
1534
|
+
search_exchange_resume_filter_requested: searchExchangeResumeFilterRequested ? 1 : 0,
|
|
1535
|
+
search_exchange_resume_filter_applied: 0,
|
|
1536
|
+
post_action: normalizedPostAction,
|
|
1537
|
+
max_greet_count: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
|
|
1538
|
+
execute_post_action: Boolean(executePostAction),
|
|
1539
|
+
action_timeout_ms: actionTimeoutMs,
|
|
1540
|
+
action_interval_ms: actionIntervalMs,
|
|
1541
|
+
action_after_click_delay_ms: actionAfterClickDelayMs
|
|
1542
|
+
},
|
|
1543
|
+
progress: {
|
|
1544
|
+
card_count: 0,
|
|
1545
|
+
target_count: candidateLimit,
|
|
1546
|
+
processed: 0,
|
|
1547
|
+
screened: 0,
|
|
1548
|
+
detail_opened: 0,
|
|
1549
|
+
llm_screened: 0,
|
|
1550
|
+
passed: 0,
|
|
1551
|
+
image_capture_failed: 0,
|
|
1552
|
+
detail_open_failed: 0,
|
|
1553
|
+
transient_recovered: 0,
|
|
1554
|
+
context_recoveries: 0,
|
|
1555
|
+
human_behavior_enabled: effectiveHumanBehavior.enabled,
|
|
1556
|
+
human_behavior_profile: effectiveHumanBehavior.profile,
|
|
1557
|
+
human_rest_level: effectiveHumanBehavior.restLevel,
|
|
1558
|
+
human_rest_enabled: effectiveHumanRestEnabled,
|
|
1559
|
+
human_rest_count: 0,
|
|
1560
|
+
human_rest_ms: 0,
|
|
1561
|
+
greet_count: 0,
|
|
1562
|
+
post_action_clicked: 0,
|
|
1563
|
+
search_exchange_resume_filter_requested: searchExchangeResumeFilterRequested ? 1 : 0,
|
|
1564
|
+
search_exchange_resume_filter_applied: 0,
|
|
1565
|
+
last_human_event: null
|
|
1566
|
+
},
|
|
1567
|
+
checkpoint: {},
|
|
1568
|
+
task: (runControl) => workflow({
|
|
1569
|
+
client,
|
|
1570
|
+
targetUrl,
|
|
1571
|
+
criteria,
|
|
1572
|
+
searchParams: normalizedSearchParams,
|
|
1573
|
+
maxCandidates,
|
|
1574
|
+
detailLimit: normalizedDetailLimit,
|
|
1575
|
+
closeDetail,
|
|
1576
|
+
delayMs,
|
|
1577
|
+
cardTimeoutMs,
|
|
1578
|
+
resetBeforeSearch,
|
|
1579
|
+
resetTimeoutMs,
|
|
1580
|
+
cityOptionTimeoutMs,
|
|
1581
|
+
maxImagePages,
|
|
1582
|
+
imageWheelDeltaY,
|
|
1583
|
+
cvAcquisitionMode,
|
|
1584
|
+
listMaxScrolls,
|
|
1585
|
+
listStableSignatureLimit,
|
|
1586
|
+
listWheelDeltaY,
|
|
1587
|
+
listSettleMs,
|
|
1588
|
+
listFallbackPoint,
|
|
1589
|
+
refreshOnEnd,
|
|
1590
|
+
maxRefreshRounds,
|
|
1591
|
+
refreshResetSettleMs,
|
|
1592
|
+
screeningMode: normalizedScreeningMode,
|
|
1593
|
+
llmConfig,
|
|
1594
|
+
llmTimeoutMs,
|
|
1595
|
+
llmImageLimit,
|
|
1596
|
+
llmImageDetail,
|
|
1597
|
+
imageOutputDir,
|
|
1598
|
+
humanRestEnabled: effectiveHumanRestEnabled,
|
|
1599
|
+
humanBehavior: effectiveHumanBehavior,
|
|
1600
|
+
postAction: normalizedPostAction,
|
|
1601
|
+
maxGreetCount,
|
|
1602
|
+
executePostAction,
|
|
1603
|
+
actionTimeoutMs,
|
|
1604
|
+
actionIntervalMs,
|
|
1605
|
+
actionAfterClickDelayMs
|
|
1606
|
+
}, runControl)
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
return {
|
|
1611
|
+
startRecruitRun,
|
|
1612
|
+
getRecruitRun: manager.getRun,
|
|
1613
|
+
pauseRecruitRun: manager.pauseRun,
|
|
1614
|
+
resumeRecruitRun: manager.resumeRun,
|
|
1615
|
+
cancelRecruitRun: manager.cancelRun,
|
|
1616
|
+
waitForRecruitRun: manager.waitForRun,
|
|
1617
|
+
listRecruitRuns: manager.listRuns,
|
|
1618
|
+
manager
|
|
1619
|
+
};
|
|
1620
|
+
}
|