@reconcrap/boss-recommend-mcp 2.0.47 → 2.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/bin/boss-recommend-mcp.js +4 -4
  2. package/config/screening-config.example.json +27 -27
  3. package/package.json +1 -1
  4. package/scripts/postinstall.cjs +44 -44
  5. package/skills/boss-chat/README.md +39 -39
  6. package/skills/boss-chat/SKILL.md +93 -93
  7. package/skills/boss-recommend-pipeline/README.md +12 -12
  8. package/skills/boss-recommend-pipeline/SKILL.md +180 -180
  9. package/skills/boss-recruit-pipeline/README.md +17 -17
  10. package/skills/boss-recruit-pipeline/SKILL.md +58 -58
  11. package/src/chat-mcp.js +1780 -1780
  12. package/src/chat-runtime-config.js +749 -749
  13. package/src/cli.js +3054 -3054
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +1586 -1453
  16. package/src/core/capture/index.js +1201 -1201
  17. package/src/core/cv-acquisition/index.js +238 -238
  18. package/src/core/cv-capture-target/index.js +299 -299
  19. package/src/core/greet-quota/index.js +54 -54
  20. package/src/core/infinite-list/index.js +1326 -1326
  21. package/src/core/reporting/legacy-csv.js +341 -341
  22. package/src/core/run/timing.js +33 -33
  23. package/src/core/self-heal/index.js +973 -973
  24. package/src/core/self-heal/viewport.js +564 -564
  25. package/src/domains/chat/cards.js +137 -137
  26. package/src/domains/chat/constants.js +221 -221
  27. package/src/domains/chat/detail.js +1668 -1668
  28. package/src/domains/chat/index.js +7 -7
  29. package/src/domains/chat/jobs.js +592 -592
  30. package/src/domains/chat/page-guard.js +98 -98
  31. package/src/domains/chat/roots.js +56 -56
  32. package/src/domains/chat/run-service.js +1977 -1977
  33. package/src/domains/recommend/actions.js +457 -457
  34. package/src/domains/recommend/cards.js +243 -243
  35. package/src/domains/recommend/constants.js +165 -165
  36. package/src/domains/recommend/filters.js +610 -610
  37. package/src/domains/recommend/index.js +10 -10
  38. package/src/domains/recommend/jobs.js +316 -316
  39. package/src/domains/recommend/refresh.js +472 -472
  40. package/src/domains/recommend/roots.js +80 -80
  41. package/src/domains/recommend/scopes.js +246 -246
  42. package/src/domains/recruit/actions.js +277 -277
  43. package/src/domains/recruit/cards.js +74 -74
  44. package/src/domains/recruit/constants.js +167 -167
  45. package/src/domains/recruit/detail.js +461 -461
  46. package/src/domains/recruit/index.js +9 -9
  47. package/src/domains/recruit/instruction-parser.js +451 -451
  48. package/src/domains/recruit/refresh.js +44 -44
  49. package/src/domains/recruit/roots.js +68 -68
  50. package/src/domains/recruit/run-service.js +1207 -1207
  51. package/src/domains/recruit/search.js +1202 -1202
  52. package/src/recommend-mcp.js +22 -22
  53. package/src/recruit-mcp.js +1338 -1338
@@ -1,1977 +1,1977 @@
1
- import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
2
- import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
3
- import {
4
- clickPoint,
5
- configureHumanInteraction,
6
- createHumanRestController,
7
- getNodeBox,
8
- humanDelay,
9
- normalizeHumanBehaviorOptions,
10
- scrollNodeIntoView,
11
- sleep
12
- } from "../../core/browser/index.js";
13
- import {
14
- compactCvAcquisitionState,
15
- countParsedNetworkProfiles,
16
- createCvAcquisitionState,
17
- DEFAULT_MAX_IMAGE_PAGES,
18
- getCvNetworkWaitPlan,
19
- recordCvImageFallback,
20
- recordCvNetworkHit,
21
- recordCvNetworkMiss,
22
- summarizeImageEvidence,
23
- waitForCvNetworkEvents
24
- } from "../../core/cv-acquisition/index.js";
25
- import {
26
- compactInfiniteListState,
27
- createInfiniteListState,
28
- detectInfiniteListBottomMarker,
29
- getNextInfiniteListCandidate,
30
- markInfiniteListCandidateProcessed,
31
- resetInfiniteListForRefreshRound,
32
- resolveInfiniteListFallbackPoint
33
- } from "../../core/infinite-list/index.js";
34
- import { createViewportRunGuard } from "../../core/self-heal/index.js";
35
- import { createRunLifecycleManager } from "../../core/run/index.js";
36
- import {
37
- addTiming,
38
- imageEvidenceFilePath,
39
- measureTiming
40
- } from "../../core/run/timing.js";
41
- import {
42
- callScreeningLlm,
43
- normalizeText,
44
- screenCandidate
45
- } from "../../core/screening/index.js";
46
- import {
47
- CHAT_BOTTOM_MARKER_SELECTORS,
48
- CHAT_CARD_SELECTORS,
49
- CHAT_LIST_CONTAINER_SELECTORS,
50
- CHAT_TARGET_URL
51
- } from "./constants.js";
52
- import {
53
- chatCandidateKeyFromProfile,
54
- findChatCandidateNodeIdById,
55
- readChatCardCandidate,
56
- waitForChatCandidateNodeIds
57
- } from "./cards.js";
58
- import {
59
- closeChatResumeModal,
60
- createChatProfileNetworkRecorder,
61
- extractChatProfileCandidate,
62
- isUnsafeChatOnlineResumeLinkError,
63
- openChatOnlineResume,
64
- quickChatResumeModalOpenProbe,
65
- readChatConversationReadyState,
66
- requestChatResumeForPassedCandidate,
67
- selectChatMessageFilter,
68
- selectChatPrimaryLabel,
69
- waitForChatOnlineResumeButton,
70
- waitForChatProfileNetworkEvents,
71
- waitForChatResumeContent
72
- } from "./detail.js";
73
- import { selectChatJob } from "./jobs.js";
74
- import {
75
- getChatTopLevelState,
76
- isForbiddenChatResumeNavigationError,
77
- makeForbiddenChatResumeNavigationError,
78
- recoverChatShell
79
- } from "./page-guard.js";
80
- import { getChatRoots } from "./roots.js";
81
-
82
- const DETAIL_SOURCES = new Set(["cascade", "network", "dom", "image"]);
83
-
84
- function normalizeDetailSource(value) {
85
- const normalized = String(value || "").trim().toLowerCase();
86
- return DETAIL_SOURCES.has(normalized) ? normalized : "cascade";
87
- }
88
-
89
- function compactScreening(screening) {
90
- return {
91
- status: screening.status,
92
- passed: screening.passed,
93
- score: screening.score,
94
- reasons: screening.reasons,
95
- candidate: {
96
- domain: screening.candidate?.domain || "chat",
97
- source: screening.candidate?.source || "",
98
- id: screening.candidate?.id || null,
99
- identity: screening.candidate?.identity || {}
100
- }
101
- };
102
- }
103
-
104
- function compactLlmResult(llmResult) {
105
- if (!llmResult) return null;
106
- return {
107
- ok: Boolean(llmResult.ok),
108
- provider: llmResult.provider || null,
109
- passed: llmResult.passed,
110
- cot: llmResult.cot || llmResult.decision_cot || "",
111
- reasoning_content: llmResult.reasoning_content || "",
112
- raw_model_output: llmResult.raw_model_output || "",
113
- evidence_count: llmResult.evidence?.length || 0,
114
- usage: llmResult.usage || null,
115
- finish_reason: llmResult.finish_reason || null,
116
- image_input_count: llmResult.image_input_count || 0,
117
- attempt_count: llmResult.attempt_count || 0,
118
- fallback_count: llmResult.fallback_count || 0,
119
- llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
120
- error: llmResult.error || null
121
- };
122
- }
123
-
124
- function compactCandidate(candidate) {
125
- return {
126
- id: candidate?.id || null,
127
- identity: candidate?.identity || {},
128
- text_length: candidate?.text?.raw?.length || 0,
129
- tag_count: candidate?.tags?.length || 0
130
- };
131
- }
132
-
133
- function compactChatJobGuard(result = null) {
134
- if (!result || typeof result !== "object") return null;
135
- return {
136
- selected: Boolean(result.selected),
137
- verified: Boolean(result.verified),
138
- already_current: Boolean(result.already_current),
139
- requested: result.requested || null,
140
- reason: result.reason || null,
141
- selected_label: result.selected_label || result.selected_option?.label || null,
142
- selected_value: result.selected_option?.value || result.active_option?.value || null,
143
- active_label: result.active_option?.label || null,
144
- active_value: result.active_option?.value || null,
145
- menu_close: result.menu_close || null
146
- };
147
- }
148
-
149
- function compactDetail(detailResult) {
150
- if (!detailResult) return null;
151
- return {
152
- popup_text_length: detailResult.detail?.popup_text?.length || 0,
153
- content_text_length: detailResult.detail?.content_text?.length || 0,
154
- resume_iframe_text_length: detailResult.detail?.resume_iframe_text?.length || 0,
155
- network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
156
- parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
157
- cv_acquisition: detailResult.cv_acquisition || null,
158
- image_evidence: summarizeImageEvidence(detailResult.image_evidence),
159
- llm_screening: compactLlmResult(detailResult.llm_result),
160
- close_result: detailResult.close_result
161
- };
162
- }
163
-
164
- function resultOpenedDetail(result) {
165
- return Boolean(result?.detail && !result.detail?.cv_acquisition?.skipped);
166
- }
167
-
168
- export function countChatResultStatuses(results = []) {
169
- return {
170
- processed: results.length,
171
- screened: results.length,
172
- detail_opened: results.filter(resultOpenedDetail).length,
173
- llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
174
- passed: results.filter((item) => item.screening?.passed).length,
175
- skipped: results.filter((item) => item.screening?.status === "skip").length
176
- };
177
- }
178
-
179
- export function chatDetailSkipReasonFromReadyState(state = {}) {
180
- if (state?.attachment_resume_enabled) return "attachment_resume_already_available";
181
- return "";
182
- }
183
-
184
- export function makeChatResumeModalOpenBeforeCandidateClickError(closeResult = null) {
185
- const error = new Error("CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK");
186
- error.code = "CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK";
187
- error.close_result = closeResult || null;
188
- return error;
189
- }
190
-
191
- export function isChatResumeModalCloseFailureError(error) {
192
- return error?.code === "CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK"
193
- || /CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK/i.test(String(error?.message || error || ""));
194
- }
195
-
196
- export async function ensureNoOpenChatResumeModalBeforeCandidateClick(client, {
197
- closeAttempts = 3
198
- } = {}) {
199
- const probe = await quickChatResumeModalOpenProbe(client);
200
- if (!probe.open) {
201
- return {
202
- closed: true,
203
- already_closed: true,
204
- probe
205
- };
206
- }
207
- const closeResult = await closeChatResumeModal(client, { attemptsLimit: closeAttempts });
208
- if (closeResult?.closed) {
209
- return {
210
- closed: true,
211
- already_closed: false,
212
- probe,
213
- close_result: closeResult
214
- };
215
- }
216
- throw makeChatResumeModalOpenBeforeCandidateClickError(closeResult);
217
- }
218
-
219
- function llmToScreening(llmResult, candidate) {
220
- return {
221
- status: llmResult?.passed ? "pass" : "fail",
222
- passed: Boolean(llmResult?.passed),
223
- score: llmResult?.passed ? 100 : 0,
224
- reasons: llmResult?.error ? ["llm_invalid_response"] : [],
225
- candidate
226
- };
227
- }
228
-
229
- export function captureNodeIdFromResumeState(resumeState) {
230
- return resumeState?.content?.node_id
231
- || resumeState?.resumeIframe?.node_id
232
- || resumeState?.popup?.node_id
233
- || null;
234
- }
235
-
236
- export function resolveChatDomFallbackWait({
237
- normalizedDetailSource = "cascade",
238
- parsedNetworkProfileCount = 0,
239
- waitPlan = null,
240
- resumeDomTimeoutMs = 60000
241
- } = {}) {
242
- const detailSource = normalizeDetailSource(normalizedDetailSource);
243
- const configuredTimeoutMs = Math.max(0, Number(resumeDomTimeoutMs) || 0);
244
- if (detailSource === "image") {
245
- return {
246
- skipped: false,
247
- timeout_ms: Math.min(configuredTimeoutMs, 3500),
248
- configured_timeout_ms: configuredTimeoutMs,
249
- short_probe: true,
250
- reason: "forced_image_modal_probe"
251
- };
252
- }
253
- if (detailSource === "dom") {
254
- return {
255
- skipped: false,
256
- timeout_ms: configuredTimeoutMs,
257
- configured_timeout_ms: configuredTimeoutMs,
258
- short_probe: false,
259
- reason: "dom_source_full_wait"
260
- };
261
- }
262
-
263
- const profileCount = Math.max(0, Number(parsedNetworkProfileCount) || 0);
264
- const previousImageMode = waitPlan?.mode_before === "image";
265
- if (profileCount > 0) {
266
- return {
267
- skipped: false,
268
- timeout_ms: Math.min(configuredTimeoutMs, previousImageMode ? 1500 : 3500),
269
- configured_timeout_ms: configuredTimeoutMs,
270
- short_probe: true,
271
- reason: previousImageMode
272
- ? "previous_image_mode_profile_only_network_short_dom_probe"
273
- : "profile_only_network_short_dom_probe"
274
- };
275
- }
276
- if (previousImageMode) {
277
- return {
278
- skipped: false,
279
- timeout_ms: Math.min(configuredTimeoutMs, 2500),
280
- configured_timeout_ms: configuredTimeoutMs,
281
- short_probe: true,
282
- reason: "previous_image_mode_network_miss_short_dom_probe"
283
- };
284
- }
285
- return {
286
- skipped: false,
287
- timeout_ms: configuredTimeoutMs,
288
- configured_timeout_ms: configuredTimeoutMs,
289
- short_probe: false,
290
- reason: "cascade_full_dom_wait"
291
- };
292
- }
293
-
294
- function isRecoverableCdpNodeError(error) {
295
- return /(?:Could not find node|No node with given id|Cannot find node|Could not compute box model)/i
296
- .test(String(error?.message || error || ""));
297
- }
298
-
299
- function isRecoverableLlmScreeningError(error) {
300
- return /(?:LLM response missing boolean passed decision|LLM response was not valid JSON)/i
301
- .test(String(error?.message || error || ""));
302
- }
303
-
304
- function createFailedLlmResult(error) {
305
- return {
306
- ok: false,
307
- passed: false,
308
- reason: "",
309
- evidence: [],
310
- cot: "",
311
- decision_cot: "",
312
- reasoning_content: "",
313
- raw_model_output: "",
314
- attempt_count: Number(error?.llm_attempt_count) || 0,
315
- fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
316
- llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
317
- error: error?.message || String(error || "unknown"),
318
- screened_at: new Date().toISOString()
319
- };
320
- }
321
-
322
- function normalizeScreeningMode(value) {
323
- const normalized = String(value || "llm").trim().toLowerCase();
324
- return ["deterministic", "local", "local_scorer"].includes(normalized)
325
- ? "deterministic"
326
- : "llm";
327
- }
328
-
329
- function createMissingLlmConfigResult() {
330
- return createFailedLlmResult(new Error("LLM screening config is required for production chat runs"));
331
- }
332
-
333
- function createSkippedDetailResult(cardCandidate, reason, error = null) {
334
- return {
335
- candidate: cardCandidate,
336
- parsed_network_profiles: [],
337
- network_bodies: [],
338
- detail: {},
339
- cv_acquisition: {
340
- source: reason,
341
- skipped: true,
342
- error: error?.message || null,
343
- error_code: error?.code || null
344
- },
345
- close_result: null
346
- };
347
- }
348
-
349
- function compactChatRuntimeError(error) {
350
- if (!error) return null;
351
- return {
352
- name: error.name || "Error",
353
- code: error.code || null,
354
- message: error.message || String(error),
355
- close_result: error.close_result || null,
356
- page_state: error.page_state || null
357
- };
358
- }
359
-
360
- const CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH = 500;
361
- const CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH = 180;
362
- const CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH = 650;
363
- const CHAT_FULL_CV_NETWORK_MIN_RICH_ITEM_COUNT = 3;
364
- const CHAT_RESUME_IMAGE_STOP_BOUNDARY_SELECTOR = [
365
- "h1",
366
- "h2",
367
- "h3",
368
- "h4",
369
- "h5",
370
- "p",
371
- "span",
372
- "section",
373
- "article",
374
- "div",
375
- "[class*='privacy']",
376
- "[class*='recommend']",
377
- "[class*='similar']"
378
- ].join(",");
379
- const CHAT_RESUME_IMAGE_STOP_BOUNDARY_TEXT = Object.freeze([
380
- /其他名企大厂/,
381
- /其他.*牛人/,
382
- /毕业的牛人/,
383
- /经历牛人/,
384
- /为妥善保护/,
385
- /查看全部.*项分析/,
386
- /牛人分析器/
387
- ]);
388
- const CHAT_FULL_CV_SECTION_PATTERNS = Object.freeze([
389
- /教育(?:经历|背景|经验)?/i,
390
- /工作(?:经历|经验)?/i,
391
- /项目(?:经历|经验)?/i,
392
- /实习(?:经历|经验)?/i,
393
- /科研(?:经历|经验)?/i,
394
- /论文|会议|专利/i,
395
- /个人(?:优势|总结|介绍|评价)/i,
396
- /专业技能|技能(?:特长|标签)?/i,
397
- /求职(?:期望|意向)/i,
398
- /校园经历|在校经历|竞赛|证书/i
399
- ]);
400
-
401
- function detailTextForFullCvCheck(detailResult = {}) {
402
- return [
403
- detailResult?.detail?.popup_text,
404
- detailResult?.detail?.content_text,
405
- detailResult?.detail?.resume_iframe_text
406
- ].filter(Boolean).join("\n\n");
407
- }
408
-
409
- function resumeSectionMatchCount(text = "") {
410
- const normalized = normalizeText(text);
411
- if (!normalized) return 0;
412
- return CHAT_FULL_CV_SECTION_PATTERNS
413
- .filter((pattern) => pattern.test(normalized))
414
- .length;
415
- }
416
-
417
- function hasResumeLikeDomText(text = "") {
418
- const normalized = normalizeText(text);
419
- if (normalized.length >= CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH) return true;
420
- return normalized.length >= CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH
421
- && resumeSectionMatchCount(normalized) >= 2;
422
- }
423
-
424
- function networkProfileTextLength(profileResult = {}) {
425
- return normalizeText(profileResult?.profile?.text || "").length;
426
- }
427
-
428
- function isFullCvNetworkProfile(profileResult = {}) {
429
- if (!profileResult?.ok) return false;
430
- const sourceKeys = profileResult.profile?.source_keys || {};
431
- const textLength = networkProfileTextLength(profileResult);
432
- const sectionCount = resumeSectionMatchCount(profileResult.profile?.text || "");
433
- const richItemCount = [
434
- "education_count",
435
- "work_count",
436
- "project_count",
437
- "expectation_count",
438
- "certification_count"
439
- ].reduce((sum, key) => sum + (Number(sourceKeys[key]) || 0), 0);
440
- const hasResumeSections = sectionCount >= 3 || (sectionCount >= 2 && richItemCount >= 2);
441
- const hasEnoughNetworkText = textLength >= CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH;
442
-
443
- if (sourceKeys.geek_detail_info || sourceKeys.geek_detail) {
444
- return hasEnoughNetworkText && (
445
- hasResumeSections
446
- || richItemCount >= CHAT_FULL_CV_NETWORK_MIN_RICH_ITEM_COUNT
447
- );
448
- }
449
- if (sourceKeys.network_html_text) {
450
- return textLength >= CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH
451
- && sectionCount >= 2;
452
- }
453
- if (sourceKeys.chat_history_resume) {
454
- const educationCount = Number(sourceKeys.education_count) || 0;
455
- const workCount = Number(sourceKeys.work_count) || 0;
456
- return (educationCount + workCount) >= 2
457
- && textLength >= CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH
458
- && sectionCount >= 2;
459
- }
460
- return false;
461
- }
462
-
463
- function hasUsableImageEvidence(imageEvidence = null) {
464
- if (!imageEvidence || imageEvidence.ok === false) return false;
465
- return Boolean(
466
- (Array.isArray(imageEvidence.llm_file_paths) && imageEvidence.llm_file_paths.length)
467
- || (Array.isArray(imageEvidence.file_paths) && imageEvidence.file_paths.length)
468
- || Number(imageEvidence.llm_screenshot_count) > 0
469
- || Number(imageEvidence.unique_screenshot_count) > 0
470
- || Number(imageEvidence.screenshot_count) > 0
471
- || Number(imageEvidence.capture_count) > 0
472
- );
473
- }
474
-
475
- export function summarizeChatFullCvEvidence({
476
- detailResult = null,
477
- contentWait = null,
478
- imageEvidence = null
479
- } = {}) {
480
- const parsedProfiles = (detailResult?.parsed_network_profiles || []).filter((item) => item?.ok);
481
- const fullNetworkProfiles = parsedProfiles.filter(isFullCvNetworkProfile);
482
- const profileOnlyCount = Math.max(0, parsedProfiles.length - fullNetworkProfiles.length);
483
- const detailText = detailTextForFullCvCheck(detailResult);
484
- const domTextLength = detailText.length;
485
- const domSectionCount = resumeSectionMatchCount(detailText);
486
- const domFullCv = Boolean(contentWait?.ok) && hasResumeLikeDomText(detailText);
487
- const imageFullCv = hasUsableImageEvidence(imageEvidence);
488
- const source = fullNetworkProfiles.length
489
- ? "network"
490
- : domFullCv
491
- ? "dom"
492
- : imageFullCv
493
- ? "image"
494
- : null;
495
- return {
496
- full_cv_acquired: Boolean(source),
497
- source,
498
- network_full_cv_count: fullNetworkProfiles.length,
499
- network_profile_only_count: profileOnlyCount,
500
- parsed_network_profile_count: parsedProfiles.length,
501
- dom_full_cv: domFullCv,
502
- dom_text_length: domTextLength,
503
- dom_section_count: domSectionCount,
504
- content_wait_ok: Boolean(contentWait?.ok),
505
- image_full_cv: imageFullCv,
506
- image_summary: summarizeImageEvidence(imageEvidence)
507
- };
508
- }
509
-
510
- async function resolveFreshChatCardNodeId(client, {
511
- fallbackNodeId,
512
- candidate,
513
- rootNodeId = null
514
- } = {}) {
515
- const candidateId = candidate?.id || "";
516
- if (!candidateId) return fallbackNodeId;
517
- let currentRootNodeId = rootNodeId;
518
- if (!currentRootNodeId) {
519
- const rootState = await getChatRoots(client);
520
- currentRootNodeId = rootState.rootNodes.top;
521
- }
522
- const freshNodeId = await findChatCandidateNodeIdById(client, currentRootNodeId, candidateId);
523
- return freshNodeId || fallbackNodeId;
524
- }
525
-
526
- async function selectFreshChatCandidate(client, {
527
- cardNodeId,
528
- candidate,
529
- timeoutMs,
530
- settleMs = 1200
531
- } = {}) {
532
- let lastError = null;
533
- for (let attempt = 0; attempt < 3; attempt += 1) {
534
- const modalGuard = await ensureNoOpenChatResumeModalBeforeCandidateClick(client);
535
- const rootState = await getChatRoots(client);
536
- const freshNodeId = await resolveFreshChatCardNodeId(client, {
537
- fallbackNodeId: cardNodeId,
538
- candidate,
539
- rootNodeId: rootState.rootNodes.top
540
- });
541
- try {
542
- await scrollNodeIntoView(client, freshNodeId);
543
- await sleep(250);
544
- const box = await getNodeBox(client, freshNodeId);
545
- await clickPoint(client, box.center.x, box.center.y);
546
- if (settleMs > 0) await sleep(settleMs);
547
- const ready = await waitForChatOnlineResumeButton(client, {
548
- timeoutMs,
549
- expectedCandidateId: candidate?.id || ""
550
- });
551
- return {
552
- card_box: box,
553
- ready,
554
- card_node_id: freshNodeId,
555
- refreshed_node: freshNodeId !== cardNodeId,
556
- modal_guard: modalGuard,
557
- attempt: attempt + 1
558
- };
559
- } catch (error) {
560
- lastError = error;
561
- if (!isRecoverableCdpNodeError(error)) throw error;
562
- await sleep(350);
563
- }
564
- }
565
- throw lastError || new Error("Chat candidate selection failed");
566
- }
567
-
568
- function selectedDetailNetworkEvents(detailSource, selectionEvents, resumeEvents) {
569
- if (detailSource !== "network" && detailSource !== "cascade") return [];
570
- return [
571
- ...(selectionEvents || []),
572
- ...(resumeEvents || [])
573
- ];
574
- }
575
-
576
- async function setupChatRunContext(client, {
577
- job,
578
- normalizedStartFrom,
579
- readyTimeoutMs,
580
- listSettleMs,
581
- runControl,
582
- ensureViewport = null
583
- } = {}) {
584
- let rootState = await getChatRoots(client);
585
- if (ensureViewport) {
586
- rootState = await ensureViewport(rootState, "context_roots");
587
- }
588
- runControl.checkpoint({
589
- top_document_node_id: rootState.rootNodes.top
590
- });
591
-
592
- const primaryLabel = await selectChatPrimaryLabel(client, {
593
- label: "全部",
594
- timeoutMs: readyTimeoutMs,
595
- settleMs: listSettleMs
596
- });
597
- runControl.checkpoint({
598
- chat_context_step: "primary_label",
599
- primary_label: primaryLabel
600
- });
601
-
602
- const jobSelection = normalizeText(job)
603
- ? await selectChatJob(client, rootState.rootNodes.top, {
604
- jobLabel: job,
605
- timeoutMs: readyTimeoutMs,
606
- settleMs: listSettleMs
607
- })
608
- : {
609
- selected: false,
610
- reason: "job_not_requested"
611
- };
612
- if (normalizeText(job) && !jobSelection.selected) {
613
- throw new Error(`Chat job selection failed: ${jobSelection.reason || "unknown"}`);
614
- }
615
- if (normalizeText(job) && jobSelection.verified !== true) {
616
- throw new Error(`Chat job selection was not verified: requested=${jobSelection.requested || job}; selected=${jobSelection.selected_label || "unknown"}`);
617
- }
618
- rootState = await getChatRoots(client);
619
- if (ensureViewport) {
620
- rootState = await ensureViewport(rootState, "context_job");
621
- }
622
- runControl.checkpoint({
623
- chat_context_step: "job_selection",
624
- primary_label: primaryLabel,
625
- job_selection: jobSelection
626
- });
627
-
628
- const startFilter = await selectChatMessageFilter(client, {
629
- startFrom: normalizedStartFrom,
630
- timeoutMs: readyTimeoutMs,
631
- settleMs: listSettleMs
632
- });
633
- if (!startFilter.ok) {
634
- throw new Error(`Chat start filter selection failed: ${startFilter.error || "unknown"}`);
635
- }
636
- rootState = await getChatRoots(client);
637
- if (ensureViewport) {
638
- rootState = await ensureViewport(rootState, "context_start_filter");
639
- }
640
- runControl.checkpoint({
641
- chat_context_step: "start_filter",
642
- primary_label: primaryLabel,
643
- job_selection: jobSelection,
644
- start_filter: startFilter
645
- });
646
-
647
- return {
648
- rootState,
649
- contextSetup: {
650
- primary_label: primaryLabel,
651
- job_selection: jobSelection,
652
- start_filter: startFilter,
653
- requested_start_from: normalizedStartFrom
654
- }
655
- };
656
- }
657
-
658
- export async function runChatWorkflow({
659
- client,
660
- targetUrl = CHAT_TARGET_URL,
661
- job = "",
662
- startFrom = "all",
663
- criteria = "",
664
- maxCandidates = 5,
665
- targetPassCount = null,
666
- processUntilListEnd = false,
667
- detailLimit = null,
668
- detailSource = "cascade",
669
- closeResume = true,
670
- requestResumeForPassed = false,
671
- dryRunRequestCv = false,
672
- greetingText = "Hi同学,能麻烦发下简历吗?",
673
- delayMs = 0,
674
- cardTimeoutMs = 90000,
675
- readyTimeoutMs = 60000,
676
- onlineResumeButtonTimeoutMs = 30000,
677
- resumeDomTimeoutMs = 60000,
678
- maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
679
- imageWheelDeltaY = 650,
680
- cvAcquisitionMode = "unknown",
681
- callLlmOnImage = false,
682
- llmConfig = null,
683
- llmTimeoutMs = 120000,
684
- llmImageLimit = 8,
685
- llmImageDetail = "high",
686
- screeningMode = "llm",
687
- listMaxScrolls = 20,
688
- listStableSignatureLimit = 5,
689
- listWheelDeltaY = 850,
690
- listSettleMs = 2200,
691
- listFallbackPoint = null,
692
- imageOutputDir = "",
693
- humanRestEnabled = false,
694
- humanBehavior = null
695
- } = {}, runControl) {
696
- if (!client) throw new Error("runChatWorkflow requires a guarded CDP client");
697
- const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
698
- legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
699
- });
700
- const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
701
- configureHumanInteraction(client, {
702
- enabled: effectiveHumanBehavior.enabled,
703
- clickMovementEnabled: effectiveHumanBehavior.clickMovement,
704
- textEntryEnabled: effectiveHumanBehavior.textEntry,
705
- safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
706
- actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
707
- });
708
- const humanRestController = createHumanRestController({
709
- enabled: effectiveHumanRestEnabled,
710
- shortRestEnabled: effectiveHumanBehavior.shortRest,
711
- batchRestEnabled: effectiveHumanBehavior.batchRest
712
- });
713
- const normalizedDetailSource = normalizeDetailSource(detailSource);
714
- const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
715
- const useLlmScreening = normalizedScreeningMode !== "deterministic";
716
- const processedLimit = Math.max(1, Number(maxCandidates) || 1);
717
- const passTarget = Number.isFinite(Number(targetPassCount)) && Number(targetPassCount) > 0
718
- ? Number(targetPassCount)
719
- : null;
720
- const normalizedStartFrom = normalizeText(startFrom).toLowerCase() === "unread" ? "unread" : "all";
721
- const detailCountLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
722
- const networkRecorder = detailCountLimit > 0
723
- ? createChatProfileNetworkRecorder(client)
724
- : null;
725
- const cvAcquisitionState = createCvAcquisitionState({ mode: cvAcquisitionMode });
726
- const listState = createInfiniteListState({
727
- domain: "chat",
728
- listName: "chat-candidates"
729
- });
730
- const viewportGuard = createViewportRunGuard({
731
- client,
732
- domain: "chat",
733
- root: "top",
734
- frameOwnerRoot: "top",
735
- runControl,
736
- getRoots: getChatRoots
737
- });
738
- async function ensureChatViewport(rootState, phase) {
739
- const result = await viewportGuard.ensure(rootState, { phase });
740
- return result.rootState || rootState;
741
- }
742
- const results = [];
743
- let cardNodeIds = [];
744
- let listEndReason = "";
745
- const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
746
- rootNodeId: rootState?.rootNodes?.top,
747
- containerSelectors: CHAT_LIST_CONTAINER_SELECTORS,
748
- itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
749
- itemSelectors: CHAT_CARD_SELECTORS,
750
- viewportPoint: { xRatio: 0.16, yRatio: 0.4 },
751
- validateViewportPoint: true
752
- }));
753
- let requestedCount = 0;
754
- let requestSatisfiedCount = 0;
755
- let requestSkippedCount = 0;
756
- let contextSetup = {};
757
- let contextRecoveryAttempts = 0;
758
- let lastHumanEvent = null;
759
-
760
- function recordHumanEvent(event = null) {
761
- if (!event) return lastHumanEvent;
762
- lastHumanEvent = {
763
- at: new Date().toISOString(),
764
- ...event
765
- };
766
- return lastHumanEvent;
767
- }
768
-
769
- async function maybeHumanActionCooldown(phase, timings = {}) {
770
- if (!effectiveHumanBehavior.actionCooldown) return null;
771
- const pauseMs = humanDelay(280, 90, {
772
- minMs: 80,
773
- maxMs: 720
774
- });
775
- if (pauseMs > 0) {
776
- await runControl.sleep(pauseMs);
777
- addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
778
- }
779
- return recordHumanEvent({
780
- kind: "action_cooldown",
781
- phase,
782
- pause_ms: pauseMs
783
- });
784
- }
785
-
786
- runControl.setPhase("chat:cleanup");
787
- let initialTopLevelState = await getChatTopLevelState(client);
788
- if (!initialTopLevelState.is_chat_shell) {
789
- const recovery = await recoverChatShell(client, {
790
- targetUrl,
791
- timeoutMs: readyTimeoutMs
792
- });
793
- runControl.checkpoint({
794
- chat_shell_recovery: {
795
- reason: "initial_non_chat_shell",
796
- ...recovery
797
- }
798
- });
799
- if (!recovery.recovered) {
800
- throw new Error(`Chat shell recovery failed before run setup: ${recovery.after?.url || recovery.before?.url || "unknown"}`);
801
- }
802
- initialTopLevelState = recovery.after;
803
- }
804
- await closeChatResumeModal(client, { attemptsLimit: 2 });
805
-
806
- await runControl.waitIfPaused();
807
- runControl.throwIfCanceled();
808
- runControl.setPhase("chat:context");
809
- const setup = await setupChatRunContext(client, {
810
- job,
811
- normalizedStartFrom,
812
- readyTimeoutMs,
813
- listSettleMs,
814
- runControl,
815
- ensureViewport: ensureChatViewport
816
- });
817
- let rootState = setup.rootState;
818
- contextSetup = {
819
- ...setup.contextSetup,
820
- initial_top_level_state: initialTopLevelState
821
- };
822
- runControl.checkpoint({
823
- chat_context: contextSetup
824
- });
825
-
826
- async function recoverAndReapplyChatContext(reason, error = null, {
827
- forceRefresh = false
828
- } = {}) {
829
- runControl.setPhase("chat:recover_shell");
830
- contextRecoveryAttempts += 1;
831
- const shellRecovery = await recoverChatShell(client, {
832
- targetUrl,
833
- timeoutMs: readyTimeoutMs,
834
- forceNavigate: forceRefresh
835
- });
836
- runControl.checkpoint({
837
- chat_shell_recovery: {
838
- reason,
839
- error: error?.message || null,
840
- total_refresh: Boolean(forceRefresh),
841
- ...shellRecovery
842
- }
843
- });
844
- if (!shellRecovery.recovered && !shellRecovery.after?.is_chat_shell) {
845
- throw new Error(`Chat shell recovery failed after ${reason}: ${shellRecovery.after?.url || shellRecovery.before?.url || "unknown"}`);
846
- }
847
- await closeChatResumeModal(client, { attemptsLimit: 2 });
848
- const recoveredSetup = await setupChatRunContext(client, {
849
- job,
850
- normalizedStartFrom,
851
- readyTimeoutMs,
852
- listSettleMs,
853
- runControl,
854
- ensureViewport: ensureChatViewport
855
- });
856
- rootState = recoveredSetup.rootState;
857
- const counters = countChatResultStatuses(results);
858
- const candidateList = resetInfiniteListForRefreshRound(listState, {
859
- reason,
860
- round: listState.ledger?.length || 0,
861
- method: forceRefresh ? "total_refresh_reapply_chat_context" : "reapply_chat_context",
862
- metadata: {
863
- processed: counters.processed,
864
- passed: counters.passed,
865
- skipped: counters.skipped
866
- }
867
- });
868
- const recovery = {
869
- reason,
870
- total_refresh: Boolean(forceRefresh),
871
- attempt: contextRecoveryAttempts,
872
- shell: shellRecovery,
873
- candidate_list: candidateList,
874
- counters
875
- };
876
- contextSetup = {
877
- ...recoveredSetup.contextSetup,
878
- recovered_from: reason,
879
- recovery,
880
- previous_context: contextSetup
881
- };
882
- runControl.checkpoint({
883
- chat_context: contextSetup,
884
- candidate_list: candidateList
885
- });
886
- return recovery;
887
- }
888
-
889
- await runControl.waitIfPaused();
890
- runControl.throwIfCanceled();
891
- runControl.setPhase("chat:cards");
892
- const cardRootState = await ensureChatViewport(await getChatRoots(client), "cards");
893
- const initialCards = await waitForChatCandidateNodeIds(client, cardRootState.rootNodes.top, {
894
- timeoutMs: cardTimeoutMs,
895
- intervalMs: 500
896
- });
897
- cardNodeIds = initialCards.nodeIds || [];
898
- if (!cardNodeIds.length) {
899
- runControl.checkpoint({
900
- empty_list_state: {
901
- method: "cdp_dom_selector_count",
902
- candidate_count: 0,
903
- requested_start_from: normalizedStartFrom
904
- }
905
- });
906
- listEndReason = "no_chat_candidates_found";
907
- runControl.updateProgress({
908
- card_count: 0,
909
- target_count: passTarget || (processUntilListEnd ? "all" : processedLimit),
910
- target_pass_count: passTarget,
911
- processed_limit: processedLimit,
912
- processed: 0,
913
- screened: 0,
914
- detail_opened: 0,
915
- llm_screened: 0,
916
- passed: 0,
917
- skipped: 0,
918
- requested: 0,
919
- request_satisfied: 0,
920
- request_skipped: 0,
921
- unique_seen: compactInfiniteListState(listState).seen_count,
922
- scroll_count: compactInfiniteListState(listState).scroll_count,
923
- context_recoveries: contextRecoveryAttempts,
924
- list_end_reason: listEndReason,
925
- viewport_checks: viewportGuard.getStats().checks,
926
- viewport_recoveries: viewportGuard.getStats().recoveries,
927
- human_behavior_enabled: effectiveHumanBehavior.enabled,
928
- human_behavior_profile: effectiveHumanBehavior.profile,
929
- human_rest_enabled: effectiveHumanRestEnabled,
930
- human_rest_count: humanRestController.getState().rest_count,
931
- human_rest_ms: humanRestController.getState().total_rest_ms,
932
- last_human_event: lastHumanEvent
933
- });
934
- runControl.setPhase("chat:done");
935
- return {
936
- domain: "chat",
937
- target_url: targetUrl,
938
- card_count: 0,
939
- context_setup: contextSetup,
940
- empty_list_state: {
941
- method: "cdp_dom_selector_count",
942
- candidate_count: 0,
943
- requested_start_from: normalizedStartFrom
944
- },
945
- candidate_list: compactInfiniteListState(listState),
946
- viewport_health: {
947
- stats: viewportGuard.getStats(),
948
- events: viewportGuard.getEvents()
949
- },
950
- human_behavior: effectiveHumanBehavior,
951
- human_rest: humanRestController.getState(),
952
- last_human_event: lastHumanEvent,
953
- list_end_reason: listEndReason,
954
- target_pass_count: passTarget,
955
- process_until_list_end: Boolean(processUntilListEnd),
956
- processed_limit: processedLimit,
957
- detail_source: normalizedDetailSource,
958
- processed: 0,
959
- screened: 0,
960
- detail_opened: 0,
961
- llm_screened: 0,
962
- passed: 0,
963
- skipped: 0,
964
- requested: requestedCount,
965
- request_satisfied: requestSatisfiedCount,
966
- request_skipped: requestSkippedCount,
967
- context_recoveries: contextRecoveryAttempts,
968
- results
969
- };
970
- }
971
-
972
- runControl.updateProgress({
973
- card_count: cardNodeIds.length,
974
- target_count: passTarget || (processUntilListEnd ? "all" : processedLimit),
975
- target_pass_count: passTarget,
976
- processed_limit: processedLimit,
977
- processed: 0,
978
- screened: 0,
979
- detail_opened: 0,
980
- llm_screened: 0,
981
- passed: 0,
982
- skipped: 0,
983
- requested: 0,
984
- request_satisfied: 0,
985
- request_skipped: 0,
986
- screening_mode: normalizedScreeningMode,
987
- unique_seen: compactInfiniteListState(listState).seen_count,
988
- scroll_count: 0,
989
- context_recoveries: contextRecoveryAttempts,
990
- viewport_checks: viewportGuard.getStats().checks,
991
- viewport_recoveries: viewportGuard.getStats().recoveries
992
- });
993
-
994
- while (
995
- results.length < processedLimit
996
- && (
997
- !passTarget
998
- || results.filter((item) => item.screening?.passed).length < passTarget
999
- )
1000
- ) {
1001
- const candidateStarted = Date.now();
1002
- const timings = {};
1003
- await runControl.waitIfPaused();
1004
- runControl.throwIfCanceled();
1005
- runControl.setPhase("chat:candidate");
1006
- rootState = await ensureChatViewport(rootState, "candidate_loop");
1007
- const loopTopLevelState = await getChatTopLevelState(client);
1008
- if (!loopTopLevelState.is_chat_shell) {
1009
- await recoverAndReapplyChatContext("candidate_loop_non_chat_shell", {
1010
- message: `Unexpected chat top-level URL: ${loopTopLevelState.url}`
1011
- });
1012
- continue;
1013
- }
1014
- if (normalizeText(job)) {
1015
- const jobGuard = await selectChatJob(client, rootState.rootNodes.top, {
1016
- jobLabel: job,
1017
- timeoutMs: Math.min(readyTimeoutMs, 12000),
1018
- settleMs: Math.min(listSettleMs, 800)
1019
- });
1020
- if (!jobGuard.selected || jobGuard.verified !== true) {
1021
- const error = new Error(`CHAT_JOB_GUARD_FAILED: requested=${job}; selected=${jobGuard.selected_label || "unknown"}; reason=${jobGuard.reason || "unknown"}`);
1022
- error.code = "CHAT_JOB_GUARD_FAILED";
1023
- error.chat_job_guard = compactChatJobGuard(jobGuard);
1024
- runControl.checkpoint({
1025
- chat_context_step: "job_guard_failed",
1026
- job_guard: compactChatJobGuard(jobGuard),
1027
- error: {
1028
- code: error.code,
1029
- message: error.message
1030
- }
1031
- });
1032
- if (contextRecoveryAttempts < 2) {
1033
- await recoverAndReapplyChatContext("job_guard_failed", error, { forceRefresh: true });
1034
- continue;
1035
- }
1036
- throw error;
1037
- }
1038
- if (!jobGuard.already_current) {
1039
- runControl.checkpoint({
1040
- chat_context_step: "job_guard_reselected",
1041
- job_guard: compactChatJobGuard(jobGuard),
1042
- candidate_list: resetInfiniteListForRefreshRound(listState, {
1043
- reason: "chat_job_drift_repaired",
1044
- round: listState.ledger?.length || 0,
1045
- method: "selectChatJob",
1046
- metadata: {
1047
- requested_job: job,
1048
- selected_label: jobGuard.selected_label || "",
1049
- selected_value: jobGuard.selected_option?.value || ""
1050
- }
1051
- })
1052
- });
1053
- rootState = await ensureChatViewport(await getChatRoots(client), "candidate_job_guard_reselected");
1054
- await sleep(Math.min(listSettleMs, 1200));
1055
- continue;
1056
- }
1057
- if (jobGuard.menu_close?.closed) {
1058
- runControl.checkpoint({
1059
- chat_context_step: "job_guard_closed_dropdown",
1060
- job_guard: compactChatJobGuard(jobGuard)
1061
- });
1062
- }
1063
- }
1064
-
1065
- const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
1066
- client,
1067
- state: listState,
1068
- maxScrolls: listMaxScrolls,
1069
- stableSignatureLimit: listStableSignatureLimit,
1070
- wheelDeltaY: listWheelDeltaY,
1071
- settleMs: listSettleMs,
1072
- listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1073
- fallbackPoint: listFallbackResolver,
1074
- findNodeIds: async () => {
1075
- const currentRootState = await ensureChatViewport(await getChatRoots(client), "candidate_find_nodes");
1076
- rootState = currentRootState;
1077
- const currentCards = await waitForChatCandidateNodeIds(client, currentRootState.rootNodes.top, {
1078
- timeoutMs: Math.min(cardTimeoutMs, 8000),
1079
- intervalMs: 500
1080
- });
1081
- cardNodeIds = currentCards.nodeIds || [];
1082
- return cardNodeIds;
1083
- },
1084
- keyForCandidate: chatCandidateKeyFromProfile,
1085
- readCandidate: async (nodeId, { visibleIndex }) => readChatCardCandidate(client, nodeId, {
1086
- targetUrl,
1087
- source: "chat-run-card",
1088
- metadata: {
1089
- run_candidate_index: results.length,
1090
- visible_index: visibleIndex
1091
- }
1092
- }),
1093
- detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
1094
- rootNodeId: rootState?.rootNodes?.top,
1095
- markerSelectors: CHAT_BOTTOM_MARKER_SELECTORS,
1096
- refreshSelectors: [],
1097
- textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
1098
- maxTextScanNodes: 500
1099
- })
1100
- }));
1101
- if (!nextCandidateResult.ok) {
1102
- const endTopLevelState = await getChatTopLevelState(client);
1103
- if (!endTopLevelState.is_chat_shell) {
1104
- await recoverAndReapplyChatContext("candidate_list_end_non_chat_shell", {
1105
- message: `Unexpected chat top-level URL at list end: ${endTopLevelState.url}`
1106
- });
1107
- continue;
1108
- }
1109
- if (nextCandidateResult.reason === "empty_visible_list") {
1110
- runControl.checkpoint({
1111
- terminal_empty_list_state: {
1112
- method: "cdp_dom_selector_count",
1113
- reason: nextCandidateResult.reason,
1114
- requested_start_from: normalizedStartFrom
1115
- }
1116
- });
1117
- }
1118
- listEndReason = nextCandidateResult.reason || "list_exhausted";
1119
- break;
1120
- }
1121
-
1122
- const index = results.length;
1123
- const cardNodeId = nextCandidateResult.item.node_id;
1124
- let effectiveCardNodeId = cardNodeId;
1125
- const candidateKey = nextCandidateResult.item.key;
1126
- const cardCandidate = nextCandidateResult.item.candidate;
1127
-
1128
- let screeningCandidate = cardCandidate;
1129
- let detailResult = null;
1130
- let preActionState = null;
1131
- let detailUnavailableReason = "";
1132
- if (index < detailCountLimit) {
1133
- let detailStep = "start";
1134
- const checkpointInProgressCandidate = (patch = {}) => runControl.checkpoint({
1135
- in_progress_candidate: {
1136
- index,
1137
- key: candidateKey,
1138
- card_node_id: effectiveCardNodeId || cardNodeId,
1139
- candidate: compactCandidate(cardCandidate),
1140
- detail_step: detailStep,
1141
- counters: countChatResultStatuses(results),
1142
- ...patch
1143
- }
1144
- });
1145
- try {
1146
- await runControl.waitIfPaused();
1147
- runControl.throwIfCanceled();
1148
- runControl.setPhase("chat:detail");
1149
- rootState = await ensureChatViewport(rootState, "detail");
1150
- checkpointInProgressCandidate({ event: "detail_start" });
1151
-
1152
- detailStep = "select_candidate";
1153
- networkRecorder.clear();
1154
- await maybeHumanActionCooldown("before_detail_open", timings);
1155
- const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1156
- cardNodeId,
1157
- candidate: cardCandidate,
1158
- timeoutMs: onlineResumeButtonTimeoutMs
1159
- }));
1160
- if (selected.ready?.forbidden_top_level_navigation) {
1161
- throw makeForbiddenChatResumeNavigationError(selected.ready.top_level_state);
1162
- }
1163
- effectiveCardNodeId = selected.card_node_id || cardNodeId;
1164
- const selectionNetworkEvents = networkRecorder.events.slice();
1165
- try {
1166
- preActionState = await readChatConversationReadyState(client);
1167
- } catch (error) {
1168
- preActionState = {
1169
- error: error?.message || String(error)
1170
- };
1171
- }
1172
- const preDetailSkipReason = chatDetailSkipReasonFromReadyState(preActionState);
1173
- if (preDetailSkipReason) {
1174
- detailUnavailableReason = preDetailSkipReason;
1175
- detailResult = createSkippedDetailResult(cardCandidate, preDetailSkipReason);
1176
- detailResult.cv_acquisition.pre_detail_state = preActionState;
1177
- detailResult.cv_acquisition.selection_ready_state = selected.ready;
1178
- }
1179
- if (!selected.ready?.ok) {
1180
- if (detailResult) {
1181
- // Already classified by the pre-detail conversation state.
1182
- } else if (selected.ready?.reason === "active_candidate_mismatch") {
1183
- detailUnavailableReason = "active_candidate_mismatch";
1184
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
1185
- detailResult.cv_acquisition.selection_ready_state = selected.ready;
1186
- } else {
1187
- detailStep = "read_conversation_ready_state";
1188
- if (preActionState.attachment_resume_enabled) {
1189
- detailUnavailableReason = "attachment_resume_already_available";
1190
- detailResult = createSkippedDetailResult(cardCandidate, "attachment_resume_already_available");
1191
- detailResult.cv_acquisition.pre_detail_state = preActionState;
1192
- } else {
1193
- detailUnavailableReason = "online_resume_button_unavailable";
1194
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
1195
- detailResult.cv_acquisition.pre_detail_state = preActionState;
1196
- }
1197
- }
1198
- }
1199
-
1200
- if (!detailResult) {
1201
- const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1202
- let networkWait = null;
1203
- let contentWait = {
1204
- ok: false,
1205
- skipped: false,
1206
- reason: "not_started",
1207
- elapsed_ms: 0,
1208
- text_length: 0
1209
- };
1210
- let resumeState = null;
1211
- let resumeHtml = null;
1212
- let resumeNetworkEvents = [];
1213
- let parsedNetworkProfileCount = 0;
1214
-
1215
- if (
1216
- ["network", "cascade"].includes(normalizedDetailSource)
1217
- && selectionNetworkEvents.length > 0
1218
- ) {
1219
- detailStep = "extract_selection_network_profile";
1220
- detailResult = await extractChatProfileCandidate(client, {
1221
- cardCandidate,
1222
- cardNodeId: effectiveCardNodeId,
1223
- resumeState: null,
1224
- resumeHtml: null,
1225
- networkEvents: selectionNetworkEvents,
1226
- targetUrl,
1227
- closeResume: false,
1228
- networkParseRetryMs: waitPlan.mode_before === "image" ? 250 : 900,
1229
- networkParseIntervalMs: 150
1230
- });
1231
- addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1232
- parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1233
- const selectionNetworkEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
1234
- if (selectionNetworkEvidence.network_full_cv_count > 0) {
1235
- networkWait = {
1236
- ok: true,
1237
- skipped: true,
1238
- reason: "selection_network_full_cv_before_online_resume_click",
1239
- elapsed_ms: detailResult.network_parse_retry_elapsed_ms,
1240
- count: selectionNetworkEvents.length,
1241
- total_event_count: selectionNetworkEvents.length,
1242
- wait_plan: waitPlan
1243
- };
1244
- contentWait = {
1245
- ok: true,
1246
- skipped: true,
1247
- reason: "selection_network_full_cv_before_online_resume_click",
1248
- elapsed_ms: 0,
1249
- text_length: 0
1250
- };
1251
- } else {
1252
- detailResult = null;
1253
- }
1254
- }
1255
-
1256
- if (!detailResult) {
1257
- detailStep = "open_online_resume";
1258
- networkRecorder.clear();
1259
- await maybeHumanActionCooldown("before_resume_open", timings);
1260
- const openedResume = await measureTiming(timings, "detail_open_ms", () => openChatOnlineResume(client, {
1261
- timeoutMs: readyTimeoutMs
1262
- }));
1263
- resumeState = openedResume.resume_state;
1264
- detailStep = "wait_network";
1265
- networkWait = ["network", "cascade"].includes(normalizedDetailSource)
1266
- ? await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
1267
- waitForChatProfileNetworkEvents,
1268
- networkRecorder,
1269
- {
1270
- waitPlan,
1271
- minCount: 1,
1272
- requireLoaded: true,
1273
- intervalMs: 200
1274
- }
1275
- ))
1276
- : null;
1277
- if (networkWait?.elapsed_ms != null) {
1278
- timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
1279
- }
1280
- resumeNetworkEvents = networkRecorder.events.slice();
1281
-
1282
- if (
1283
- ["network", "cascade"].includes(normalizedDetailSource)
1284
- && networkWait?.count > 0
1285
- ) {
1286
- detailStep = "extract_network_profile";
1287
- detailResult = await extractChatProfileCandidate(client, {
1288
- cardCandidate,
1289
- cardNodeId: effectiveCardNodeId,
1290
- resumeState,
1291
- resumeHtml,
1292
- networkEvents: selectedDetailNetworkEvents(
1293
- normalizedDetailSource,
1294
- selectionNetworkEvents,
1295
- resumeNetworkEvents
1296
- ),
1297
- targetUrl,
1298
- closeResume: false,
1299
- networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
1300
- networkParseIntervalMs: 250
1301
- });
1302
- addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1303
- parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1304
- const networkEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
1305
- if (networkEvidence.network_full_cv_count > 0) {
1306
- contentWait = {
1307
- ok: true,
1308
- skipped: true,
1309
- reason: "network_full_cv_parsed_before_dom_wait",
1310
- elapsed_ms: 0,
1311
- text_length: 0
1312
- };
1313
- } else {
1314
- detailResult = null;
1315
- }
1316
- }
1317
-
1318
- if (!detailResult) {
1319
- detailStep = "wait_resume_content";
1320
- const domFallbackPlan = resolveChatDomFallbackWait({
1321
- normalizedDetailSource,
1322
- parsedNetworkProfileCount,
1323
- waitPlan,
1324
- resumeDomTimeoutMs
1325
- });
1326
- if (domFallbackPlan.skipped || domFallbackPlan.timeout_ms <= 0) {
1327
- contentWait = {
1328
- ok: false,
1329
- skipped: true,
1330
- reason: domFallbackPlan.reason,
1331
- elapsed_ms: 0,
1332
- text_length: 0,
1333
- resume_state: openedResume.resume_state,
1334
- resume_html: null,
1335
- dom_fallback_plan: domFallbackPlan,
1336
- configured_timeout_ms: domFallbackPlan.configured_timeout_ms,
1337
- timeout_ms: domFallbackPlan.timeout_ms,
1338
- short_probe: Boolean(domFallbackPlan.short_probe)
1339
- };
1340
- addTiming(timings, "dom_fallback_ms", 0);
1341
- } else {
1342
- contentWait = await measureTiming(timings, "dom_fallback_ms", () => waitForChatResumeContent(client, {
1343
- timeoutMs: domFallbackPlan.timeout_ms,
1344
- intervalMs: 300
1345
- }));
1346
- contentWait.dom_fallback_plan = domFallbackPlan;
1347
- contentWait.configured_timeout_ms = domFallbackPlan.configured_timeout_ms;
1348
- contentWait.timeout_ms = domFallbackPlan.timeout_ms;
1349
- contentWait.short_probe = Boolean(domFallbackPlan.short_probe);
1350
- if (domFallbackPlan.short_probe && !contentWait.ok) {
1351
- contentWait.reason = contentWait.reason || domFallbackPlan.reason;
1352
- }
1353
- }
1354
- resumeState = contentWait.resume_state || openedResume.resume_state;
1355
- resumeHtml = contentWait.resume_html || null;
1356
- resumeNetworkEvents = networkRecorder.events.slice();
1357
- detailStep = "extract_resume_content";
1358
- detailResult = await extractChatProfileCandidate(client, {
1359
- cardCandidate,
1360
- cardNodeId: effectiveCardNodeId,
1361
- resumeState,
1362
- resumeHtml,
1363
- networkEvents: selectedDetailNetworkEvents(
1364
- normalizedDetailSource,
1365
- selectionNetworkEvents,
1366
- resumeNetworkEvents
1367
- ),
1368
- targetUrl,
1369
- closeResume: false,
1370
- networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
1371
- networkParseIntervalMs: 250
1372
- });
1373
- addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1374
- parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1375
- }
1376
- }
1377
-
1378
- let source = normalizedDetailSource === "dom" ? "dom" : "network";
1379
- let imageEvidence = null;
1380
- let llmResult = null;
1381
- let captureTarget = null;
1382
- let captureTargetWait = null;
1383
- let fullCvEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
1384
- const shouldCaptureImage = normalizedDetailSource === "image"
1385
- || (normalizedDetailSource === "cascade" && !fullCvEvidence.full_cv_acquired);
1386
- if (shouldCaptureImage) {
1387
- captureTargetWait = await waitForCvCaptureTarget(client, resumeState, {
1388
- domain: "chat",
1389
- timeoutMs: 6000,
1390
- intervalMs: 250
1391
- });
1392
- captureTarget = captureTargetWait.target || null;
1393
- const captureNodeId = captureTarget?.node_id || null;
1394
- if (captureNodeId) {
1395
- detailStep = "capture_image_fallback";
1396
- imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
1397
- filePath: imageEvidenceFilePath({
1398
- imageOutputDir,
1399
- domain: "chat",
1400
- runId: runControl?.runId,
1401
- index,
1402
- extension: "jpg"
1403
- }),
1404
- format: "jpeg",
1405
- quality: 72,
1406
- optimize: true,
1407
- resizeMaxWidth: 1100,
1408
- captureViewport: false,
1409
- padding: 0,
1410
- maxScreenshots: maxImagePages,
1411
- wheelDeltaY: imageWheelDeltaY,
1412
- settleMs: 350,
1413
- scrollMethod: "dom-anchor-fallback-input",
1414
- scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1415
- stepTimeoutMs: 45000,
1416
- totalTimeoutMs: 90000,
1417
- duplicateStopCount: 1,
1418
- skipDuplicateScreenshots: true,
1419
- composeForLlm: true,
1420
- llmPagesPerImage: 3,
1421
- llmResizeMaxWidth: 1100,
1422
- llmQuality: 72,
1423
- stopBoundarySelector: CHAT_RESUME_IMAGE_STOP_BOUNDARY_SELECTOR,
1424
- stopBoundaryTextPatterns: CHAT_RESUME_IMAGE_STOP_BOUNDARY_TEXT,
1425
- stopBoundaryMaxProbeNodes: 360,
1426
- stopBoundaryTopPadding: 10,
1427
- stopBoundaryMinCaptureHeight: 180,
1428
- metadata: {
1429
- domain: "chat",
1430
- capture_mode: "scroll_sequence",
1431
- capture_scope: "resume_modal_clip",
1432
- acquisition_reason: normalizedDetailSource === "image"
1433
- ? "forced_image"
1434
- : "network_miss_image_fallback",
1435
- run_candidate_index: index,
1436
- candidate_key: candidateKey,
1437
- capture_target: captureTarget,
1438
- capture_target_wait: captureTargetWait
1439
- }
1440
- }));
1441
- source = "image";
1442
- fullCvEvidence = summarizeChatFullCvEvidence({
1443
- detailResult,
1444
- contentWait,
1445
- imageEvidence
1446
- });
1447
- recordCvImageFallback(cvAcquisitionState, {
1448
- reason: fullCvEvidence.network_profile_only_count > 0
1449
- ? "profile_only_network_image_fallback"
1450
- : "network_miss_image_fallback",
1451
- parsedNetworkProfileCount,
1452
- waitResult: networkWait,
1453
- imageEvidence
1454
- });
1455
- if (callLlmOnImage && fullCvEvidence.full_cv_acquired) {
1456
- detailStep = "llm_image_screening";
1457
- if (!llmConfig) {
1458
- llmResult = createMissingLlmConfigResult();
1459
- } else {
1460
- try {
1461
- llmResult = await measureTiming(timings, "vision_model_ms", () => callScreeningLlm({
1462
- candidate: detailResult.candidate,
1463
- criteria,
1464
- config: llmConfig,
1465
- timeoutMs: llmTimeoutMs,
1466
- imageEvidence,
1467
- maxImages: llmImageLimit,
1468
- imageDetail: llmImageDetail
1469
- }));
1470
- } catch (error) {
1471
- llmResult = createFailedLlmResult(error);
1472
- }
1473
- }
1474
- }
1475
- } else {
1476
- source = "missing_capture_node";
1477
- fullCvEvidence = summarizeChatFullCvEvidence({
1478
- detailResult,
1479
- contentWait,
1480
- imageEvidence
1481
- });
1482
- recordCvNetworkMiss(cvAcquisitionState, {
1483
- reason: "network_miss_no_capture_node",
1484
- parsedNetworkProfileCount,
1485
- waitResult: networkWait
1486
- });
1487
- }
1488
- } else if (fullCvEvidence.network_full_cv_count > 0) {
1489
- source = "network";
1490
- recordCvNetworkHit(cvAcquisitionState, {
1491
- reason: "full_cv_network_profile",
1492
- parsedNetworkProfileCount,
1493
- waitResult: networkWait
1494
- });
1495
- } else if (fullCvEvidence.dom_full_cv) {
1496
- source = "dom";
1497
- if (normalizedDetailSource !== "dom") {
1498
- recordCvNetworkMiss(cvAcquisitionState, {
1499
- reason: parsedNetworkProfileCount > 0
1500
- ? "profile_only_network_dom_fallback"
1501
- : "network_miss_dom_fallback",
1502
- parsedNetworkProfileCount,
1503
- waitResult: networkWait
1504
- });
1505
- }
1506
- } else if (parsedNetworkProfileCount > 0) {
1507
- source = "profile_only_network";
1508
- recordCvNetworkMiss(cvAcquisitionState, {
1509
- reason: "profile_only_network_not_full_cv",
1510
- parsedNetworkProfileCount,
1511
- waitResult: networkWait
1512
- });
1513
- } else if (normalizedDetailSource !== "dom") {
1514
- source = "network_miss";
1515
- recordCvNetworkMiss(cvAcquisitionState, {
1516
- reason: "network_miss_without_image_fallback",
1517
- parsedNetworkProfileCount,
1518
- waitResult: networkWait
1519
- });
1520
- }
1521
-
1522
- if (useLlmScreening && !llmResult) {
1523
- if (!fullCvEvidence.full_cv_acquired) {
1524
- detailUnavailableReason = "full_cv_not_acquired";
1525
- } else {
1526
- detailStep = "llm_screening";
1527
- if (!llmConfig) {
1528
- llmResult = createMissingLlmConfigResult();
1529
- } else {
1530
- try {
1531
- const llmTimingKey = imageEvidence?.file_paths?.length
1532
- ? "vision_model_ms"
1533
- : "text_model_ms";
1534
- llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1535
- candidate: detailResult.candidate,
1536
- criteria,
1537
- config: llmConfig,
1538
- timeoutMs: llmTimeoutMs,
1539
- imageEvidence,
1540
- maxImages: llmImageLimit,
1541
- imageDetail: llmImageDetail
1542
- }));
1543
- } catch (error) {
1544
- llmResult = createFailedLlmResult(error);
1545
- }
1546
- }
1547
- }
1548
- }
1549
-
1550
- let closeResult = null;
1551
- let closeRecovery = null;
1552
- if (closeResume) {
1553
- detailStep = "close_resume_modal";
1554
- checkpointInProgressCandidate({
1555
- event: "before_close_resume_modal",
1556
- source,
1557
- image_evidence: summarizeImageEvidence(imageEvidence),
1558
- llm_screening: compactLlmResult(llmResult),
1559
- full_cv_evidence: fullCvEvidence
1560
- });
1561
- closeResult = await measureTiming(timings, "close_detail_ms", () => closeChatResumeModal(client));
1562
- await maybeHumanActionCooldown("after_detail_close", timings);
1563
- if (!closeResult?.closed) {
1564
- closeRecovery = await recoverAndReapplyChatContext(
1565
- "resume_modal_close_failed:close_resume_modal",
1566
- makeChatResumeModalOpenBeforeCandidateClickError(closeResult),
1567
- { forceRefresh: true }
1568
- );
1569
- }
1570
- }
1571
- detailResult.close_result = closeResult;
1572
- detailResult.image_evidence = imageEvidence;
1573
- detailResult.llm_result = llmResult;
1574
- detailResult.cv_acquisition = {
1575
- source,
1576
- mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
1577
- wait_plan: waitPlan,
1578
- network_wait: networkWait,
1579
- selection_network_event_count: selectionNetworkEvents.length,
1580
- resume_network_event_count: resumeNetworkEvents.length,
1581
- content_wait: {
1582
- ok: contentWait.ok,
1583
- skipped: Boolean(contentWait.skipped),
1584
- reason: contentWait.reason || null,
1585
- elapsed_ms: contentWait.elapsed_ms,
1586
- text_length: contentWait.text_length,
1587
- timeout_ms: contentWait.timeout_ms ?? contentWait.dom_fallback_plan?.timeout_ms ?? null,
1588
- configured_timeout_ms: contentWait.configured_timeout_ms
1589
- ?? contentWait.dom_fallback_plan?.configured_timeout_ms
1590
- ?? null,
1591
- short_probe: Boolean(contentWait.short_probe)
1592
- },
1593
- parsed_network_profile_count: parsedNetworkProfileCount,
1594
- image_evidence: summarizeImageEvidence(imageEvidence),
1595
- capture_target: captureTarget || null,
1596
- capture_target_wait: captureTargetWait,
1597
- full_cv_evidence: fullCvEvidence,
1598
- close_recovery: closeRecovery
1599
- };
1600
- }
1601
- } catch (error) {
1602
- checkpointInProgressCandidate({
1603
- event: "detail_error",
1604
- error: compactChatRuntimeError(error)
1605
- });
1606
- if (isForbiddenChatResumeNavigationError(error)) {
1607
- detailUnavailableReason = "forbidden_top_level_resume_navigation";
1608
- const recovery = await recoverAndReapplyChatContext(detailUnavailableReason, error);
1609
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1610
- detailResult.cv_acquisition.recovery = recovery;
1611
- } else if (isChatResumeModalCloseFailureError(error)) {
1612
- const recoveryReason = `resume_modal_close_failed:${detailStep}`;
1613
- const recovery = await recoverAndReapplyChatContext(recoveryReason, error, { forceRefresh: true });
1614
- checkpointInProgressCandidate({
1615
- event: "retry_after_modal_recovery",
1616
- recovery
1617
- });
1618
- continue;
1619
- } else if (isUnsafeChatOnlineResumeLinkError(error)) {
1620
- detailUnavailableReason = "unsafe_online_resume_navigation_link";
1621
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1622
- detailResult.cv_acquisition.blocked_pre_click = true;
1623
- detailResult.cv_acquisition.button_href = error.href || null;
1624
- detailResult.cv_acquisition.button_selector = error.button_selector || null;
1625
- detailResult.cv_acquisition.attempts = error.attempts || null;
1626
- } else {
1627
- if (!isRecoverableCdpNodeError(error)) throw error;
1628
- detailUnavailableReason = `recoverable_cdp_node_stale:${detailStep}`;
1629
- detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1630
- await closeChatResumeModal(client, { attemptsLimit: 2 });
1631
- }
1632
- }
1633
- screeningCandidate = detailResult.candidate;
1634
- }
1635
-
1636
- await runControl.waitIfPaused();
1637
- runControl.throwIfCanceled();
1638
- runControl.setPhase("chat:screening");
1639
- let cardOnlyLlmResult = null;
1640
- if (useLlmScreening && !detailUnavailableReason && !detailResult?.llm_result) {
1641
- detailUnavailableReason = detailResult
1642
- ? "full_cv_not_acquired"
1643
- : "detail_not_opened_full_cv_required";
1644
- }
1645
- const effectiveLlmResult = detailResult?.llm_result || cardOnlyLlmResult;
1646
- const screening = detailUnavailableReason
1647
- ? {
1648
- status: "skip",
1649
- passed: false,
1650
- score: 0,
1651
- reasons: [detailUnavailableReason],
1652
- candidate: screeningCandidate
1653
- }
1654
- : useLlmScreening
1655
- ? llmToScreening(effectiveLlmResult, screeningCandidate)
1656
- : screenCandidate(screeningCandidate, { criteria });
1657
- let postAction = null;
1658
- if (requestResumeForPassed && screening.passed) {
1659
- await maybeHumanActionCooldown("before_post_action", timings);
1660
- postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
1661
- greetingText,
1662
- dryRun: dryRunRequestCv
1663
- }));
1664
- if (postAction?.requested) requestSatisfiedCount += 1;
1665
- if (postAction?.skipped) requestSkippedCount += 1;
1666
- if (postAction?.requested && !postAction?.skipped) requestedCount += 1;
1667
- if (!postAction?.requested && !postAction?.skipped && !dryRunRequestCv) {
1668
- throw new Error(`REQUEST_CV_NOT_VERIFIED:${postAction?.reason || "unknown"}`);
1669
- }
1670
- }
1671
- timings.total_ms = Date.now() - candidateStarted;
1672
- const compactResult = {
1673
- index,
1674
- candidate_key: candidateKey,
1675
- card_node_id: effectiveCardNodeId,
1676
- candidate: compactCandidate(screeningCandidate),
1677
- detail: compactDetail(detailResult),
1678
- llm_screening: detailResult ? null : compactLlmResult(cardOnlyLlmResult),
1679
- screening: compactScreening(screening),
1680
- post_action: postAction,
1681
- pre_action_state: preActionState,
1682
- timings
1683
- };
1684
- results.push(compactResult);
1685
- markInfiniteListCandidateProcessed(listState, candidateKey, {
1686
- metadata: {
1687
- result_index: index,
1688
- candidate_id: screeningCandidate.id || null
1689
- }
1690
- });
1691
-
1692
- const counters = countChatResultStatuses(results);
1693
- runControl.updateProgress({
1694
- card_count: cardNodeIds.length,
1695
- target_count: passTarget || (processUntilListEnd ? "all" : processedLimit),
1696
- target_pass_count: passTarget,
1697
- processed_limit: processedLimit,
1698
- processed: counters.processed,
1699
- screened: counters.screened,
1700
- detail_opened: counters.detail_opened,
1701
- llm_screened: counters.llm_screened,
1702
- passed: counters.passed,
1703
- skipped: counters.skipped,
1704
- requested: requestedCount,
1705
- request_satisfied: requestSatisfiedCount,
1706
- request_skipped: requestSkippedCount,
1707
- unique_seen: compactInfiniteListState(listState).seen_count,
1708
- scroll_count: compactInfiniteListState(listState).scroll_count,
1709
- context_recoveries: contextRecoveryAttempts,
1710
- list_end_reason: listEndReason || null,
1711
- viewport_checks: viewportGuard.getStats().checks,
1712
- viewport_recoveries: viewportGuard.getStats().recoveries,
1713
- human_behavior_enabled: effectiveHumanBehavior.enabled,
1714
- human_behavior_profile: effectiveHumanBehavior.profile,
1715
- human_rest_enabled: effectiveHumanRestEnabled,
1716
- human_rest_count: humanRestController.getState().rest_count,
1717
- human_rest_ms: humanRestController.getState().total_rest_ms,
1718
- last_human_event: lastHumanEvent,
1719
- last_candidate_id: screeningCandidate.id || null,
1720
- last_candidate_key: candidateKey,
1721
- last_score: screening.score
1722
- });
1723
- const checkpointStarted = Date.now();
1724
- runControl.checkpoint({
1725
- results,
1726
- in_progress_candidate: null,
1727
- last_candidate: {
1728
- id: screeningCandidate.id || null,
1729
- key: candidateKey,
1730
- identity: screeningCandidate.identity || {},
1731
- screening: {
1732
- status: screening.status,
1733
- passed: screening.passed,
1734
- score: screening.score
1735
- },
1736
- llm_screening: compactLlmResult(effectiveLlmResult)
1737
- }
1738
- });
1739
- addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
1740
-
1741
- if (effectiveHumanRestEnabled) {
1742
- const restStarted = Date.now();
1743
- const restResult = await humanRestController.takeBreakIfNeeded({
1744
- sleepFn: (ms) => runControl.sleep(ms)
1745
- });
1746
- const restElapsed = Date.now() - restStarted;
1747
- if (restResult.rested) {
1748
- recordHumanEvent({
1749
- kind: "rest",
1750
- pause_ms: restResult.pause_ms || restElapsed,
1751
- events: restResult.events || []
1752
- });
1753
- compactResult.human_rest = restResult;
1754
- addTiming(compactResult.timings, "human_rest_ms", restElapsed);
1755
- compactResult.timings.total_ms = Date.now() - candidateStarted;
1756
- runControl.updateProgress({
1757
- human_rest_enabled: effectiveHumanRestEnabled,
1758
- human_rest_count: humanRestController.getState().rest_count,
1759
- human_rest_ms: humanRestController.getState().total_rest_ms,
1760
- human_rest_last: restResult,
1761
- context_recoveries: contextRecoveryAttempts,
1762
- last_human_event: lastHumanEvent
1763
- });
1764
- }
1765
- }
1766
-
1767
- if (delayMs > 0) {
1768
- const sleepStarted = Date.now();
1769
- await runControl.sleep(delayMs);
1770
- addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
1771
- compactResult.timings.total_ms = Date.now() - candidateStarted;
1772
- }
1773
- }
1774
-
1775
- runControl.setPhase("chat:done");
1776
- const finalCounters = countChatResultStatuses(results);
1777
- return {
1778
- domain: "chat",
1779
- target_url: targetUrl,
1780
- card_count: cardNodeIds.length,
1781
- context_setup: contextSetup,
1782
- candidate_list: compactInfiniteListState(listState),
1783
- viewport_health: {
1784
- stats: viewportGuard.getStats(),
1785
- events: viewportGuard.getEvents()
1786
- },
1787
- human_behavior: effectiveHumanBehavior,
1788
- human_rest: humanRestController.getState(),
1789
- last_human_event: lastHumanEvent,
1790
- list_end_reason: listEndReason || null,
1791
- target_pass_count: passTarget,
1792
- process_until_list_end: Boolean(processUntilListEnd),
1793
- processed_limit: processedLimit,
1794
- detail_source: normalizedDetailSource,
1795
- processed: finalCounters.processed,
1796
- screened: finalCounters.screened,
1797
- detail_opened: finalCounters.detail_opened,
1798
- llm_screened: finalCounters.llm_screened,
1799
- passed: finalCounters.passed,
1800
- skipped: finalCounters.skipped,
1801
- requested: requestedCount,
1802
- request_satisfied: requestSatisfiedCount,
1803
- request_skipped: requestSkippedCount,
1804
- context_recoveries: contextRecoveryAttempts,
1805
- results
1806
- };
1807
- }
1808
-
1809
- export function createChatRunService({
1810
- lifecycle,
1811
- idPrefix = "chat",
1812
- workflow = runChatWorkflow,
1813
- onSnapshot = null
1814
- } = {}) {
1815
- const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
1816
-
1817
- function startChatRun({
1818
- client,
1819
- targetUrl = CHAT_TARGET_URL,
1820
- job = "",
1821
- startFrom = "all",
1822
- criteria = "",
1823
- maxCandidates = 5,
1824
- targetPassCount = null,
1825
- processUntilListEnd = false,
1826
- detailLimit = null,
1827
- detailSource = "cascade",
1828
- closeResume = true,
1829
- requestResumeForPassed = false,
1830
- dryRunRequestCv = false,
1831
- greetingText = "Hi同学,能麻烦发下简历吗?",
1832
- delayMs = 0,
1833
- cardTimeoutMs = 90000,
1834
- readyTimeoutMs = 60000,
1835
- onlineResumeButtonTimeoutMs = 30000,
1836
- resumeDomTimeoutMs = 60000,
1837
- maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
1838
- imageWheelDeltaY = 650,
1839
- cvAcquisitionMode = "unknown",
1840
- callLlmOnImage = false,
1841
- llmConfig = null,
1842
- llmTimeoutMs = 120000,
1843
- llmImageLimit = 8,
1844
- llmImageDetail = "high",
1845
- screeningMode = "llm",
1846
- listMaxScrolls = 20,
1847
- listStableSignatureLimit = 5,
1848
- listWheelDeltaY = 850,
1849
- listSettleMs = 2200,
1850
- listFallbackPoint = null,
1851
- imageOutputDir = "",
1852
- humanRestEnabled = false,
1853
- humanBehavior = null,
1854
- name = "chat-domain-run"
1855
- } = {}) {
1856
- if (!client) throw new Error("startChatRun requires a guarded CDP client");
1857
- const normalizedDetailSource = normalizeDetailSource(detailSource);
1858
- const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1859
- const processedLimit = Math.max(1, Number(maxCandidates) || 1);
1860
- const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
1861
- const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1862
- legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1863
- });
1864
- const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1865
- return manager.startRun({
1866
- name,
1867
- context: {
1868
- domain: "chat",
1869
- target_url: targetUrl,
1870
- criteria_present: Boolean(criteria),
1871
- job,
1872
- start_from: startFrom,
1873
- max_candidates: maxCandidates,
1874
- target_pass_count: targetPassCount,
1875
- process_until_list_end: Boolean(processUntilListEnd),
1876
- detail_limit: normalizedDetailLimit,
1877
- detail_source: normalizedDetailSource,
1878
- close_resume: closeResume,
1879
- request_resume_for_passed: Boolean(requestResumeForPassed),
1880
- dry_run_request_cv: Boolean(dryRunRequestCv),
1881
- greeting_text: greetingText,
1882
- cv_acquisition_mode: cvAcquisitionMode,
1883
- call_llm_on_image: Boolean(callLlmOnImage),
1884
- screening_mode: normalizedScreeningMode,
1885
- llm_configured: Boolean(llmConfig),
1886
- llm_timeout_ms: llmTimeoutMs,
1887
- llm_image_limit: llmImageLimit,
1888
- llm_image_detail: llmImageDetail,
1889
- max_image_pages: maxImagePages,
1890
- image_wheel_delta_y: imageWheelDeltaY,
1891
- list_max_scrolls: listMaxScrolls,
1892
- list_stable_signature_limit: listStableSignatureLimit,
1893
- list_wheel_delta_y: listWheelDeltaY,
1894
- list_settle_ms: listSettleMs,
1895
- list_fallback_point: listFallbackPoint,
1896
- online_resume_button_timeout_ms: onlineResumeButtonTimeoutMs,
1897
- image_output_dir: imageOutputDir || "",
1898
- human_behavior_enabled: effectiveHumanBehavior.enabled,
1899
- human_behavior_profile: effectiveHumanBehavior.profile,
1900
- human_behavior: effectiveHumanBehavior,
1901
- human_rest_enabled: effectiveHumanRestEnabled
1902
- },
1903
- progress: {
1904
- card_count: 0,
1905
- target_count: targetPassCount || (processUntilListEnd ? "all" : processedLimit),
1906
- target_pass_count: targetPassCount,
1907
- processed_limit: processedLimit,
1908
- processed: 0,
1909
- screened: 0,
1910
- detail_opened: 0,
1911
- llm_screened: 0,
1912
- passed: 0,
1913
- skipped: 0,
1914
- requested: 0,
1915
- request_satisfied: 0,
1916
- request_skipped: 0,
1917
- context_recoveries: 0,
1918
- human_behavior_enabled: effectiveHumanBehavior.enabled,
1919
- human_behavior_profile: effectiveHumanBehavior.profile,
1920
- human_rest_enabled: effectiveHumanRestEnabled,
1921
- human_rest_count: 0,
1922
- human_rest_ms: 0,
1923
- last_human_event: null
1924
- },
1925
- checkpoint: {},
1926
- task: (runControl) => workflow({
1927
- client,
1928
- targetUrl,
1929
- job,
1930
- startFrom,
1931
- criteria,
1932
- maxCandidates,
1933
- targetPassCount,
1934
- processUntilListEnd,
1935
- detailLimit: normalizedDetailLimit,
1936
- detailSource: normalizedDetailSource,
1937
- closeResume,
1938
- requestResumeForPassed,
1939
- dryRunRequestCv,
1940
- greetingText,
1941
- delayMs,
1942
- cardTimeoutMs,
1943
- readyTimeoutMs,
1944
- onlineResumeButtonTimeoutMs,
1945
- resumeDomTimeoutMs,
1946
- maxImagePages,
1947
- imageWheelDeltaY,
1948
- cvAcquisitionMode,
1949
- callLlmOnImage,
1950
- llmConfig,
1951
- llmTimeoutMs,
1952
- llmImageLimit,
1953
- llmImageDetail,
1954
- screeningMode: normalizedScreeningMode,
1955
- listMaxScrolls,
1956
- listStableSignatureLimit,
1957
- listWheelDeltaY,
1958
- listSettleMs,
1959
- listFallbackPoint,
1960
- imageOutputDir,
1961
- humanRestEnabled: effectiveHumanRestEnabled,
1962
- humanBehavior: effectiveHumanBehavior
1963
- }, runControl)
1964
- });
1965
- }
1966
-
1967
- return {
1968
- startChatRun,
1969
- getChatRun: manager.getRun,
1970
- pauseChatRun: manager.pauseRun,
1971
- resumeChatRun: manager.resumeRun,
1972
- cancelChatRun: manager.cancelRun,
1973
- waitForChatRun: manager.waitForRun,
1974
- listChatRuns: manager.listRuns,
1975
- manager
1976
- };
1977
- }
1
+ import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
2
+ import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
3
+ import {
4
+ clickPoint,
5
+ configureHumanInteraction,
6
+ createHumanRestController,
7
+ getNodeBox,
8
+ humanDelay,
9
+ normalizeHumanBehaviorOptions,
10
+ scrollNodeIntoView,
11
+ sleep
12
+ } from "../../core/browser/index.js";
13
+ import {
14
+ compactCvAcquisitionState,
15
+ countParsedNetworkProfiles,
16
+ createCvAcquisitionState,
17
+ DEFAULT_MAX_IMAGE_PAGES,
18
+ getCvNetworkWaitPlan,
19
+ recordCvImageFallback,
20
+ recordCvNetworkHit,
21
+ recordCvNetworkMiss,
22
+ summarizeImageEvidence,
23
+ waitForCvNetworkEvents
24
+ } from "../../core/cv-acquisition/index.js";
25
+ import {
26
+ compactInfiniteListState,
27
+ createInfiniteListState,
28
+ detectInfiniteListBottomMarker,
29
+ getNextInfiniteListCandidate,
30
+ markInfiniteListCandidateProcessed,
31
+ resetInfiniteListForRefreshRound,
32
+ resolveInfiniteListFallbackPoint
33
+ } from "../../core/infinite-list/index.js";
34
+ import { createViewportRunGuard } from "../../core/self-heal/index.js";
35
+ import { createRunLifecycleManager } from "../../core/run/index.js";
36
+ import {
37
+ addTiming,
38
+ imageEvidenceFilePath,
39
+ measureTiming
40
+ } from "../../core/run/timing.js";
41
+ import {
42
+ callScreeningLlm,
43
+ normalizeText,
44
+ screenCandidate
45
+ } from "../../core/screening/index.js";
46
+ import {
47
+ CHAT_BOTTOM_MARKER_SELECTORS,
48
+ CHAT_CARD_SELECTORS,
49
+ CHAT_LIST_CONTAINER_SELECTORS,
50
+ CHAT_TARGET_URL
51
+ } from "./constants.js";
52
+ import {
53
+ chatCandidateKeyFromProfile,
54
+ findChatCandidateNodeIdById,
55
+ readChatCardCandidate,
56
+ waitForChatCandidateNodeIds
57
+ } from "./cards.js";
58
+ import {
59
+ closeChatResumeModal,
60
+ createChatProfileNetworkRecorder,
61
+ extractChatProfileCandidate,
62
+ isUnsafeChatOnlineResumeLinkError,
63
+ openChatOnlineResume,
64
+ quickChatResumeModalOpenProbe,
65
+ readChatConversationReadyState,
66
+ requestChatResumeForPassedCandidate,
67
+ selectChatMessageFilter,
68
+ selectChatPrimaryLabel,
69
+ waitForChatOnlineResumeButton,
70
+ waitForChatProfileNetworkEvents,
71
+ waitForChatResumeContent
72
+ } from "./detail.js";
73
+ import { selectChatJob } from "./jobs.js";
74
+ import {
75
+ getChatTopLevelState,
76
+ isForbiddenChatResumeNavigationError,
77
+ makeForbiddenChatResumeNavigationError,
78
+ recoverChatShell
79
+ } from "./page-guard.js";
80
+ import { getChatRoots } from "./roots.js";
81
+
82
+ const DETAIL_SOURCES = new Set(["cascade", "network", "dom", "image"]);
83
+
84
+ function normalizeDetailSource(value) {
85
+ const normalized = String(value || "").trim().toLowerCase();
86
+ return DETAIL_SOURCES.has(normalized) ? normalized : "cascade";
87
+ }
88
+
89
+ function compactScreening(screening) {
90
+ return {
91
+ status: screening.status,
92
+ passed: screening.passed,
93
+ score: screening.score,
94
+ reasons: screening.reasons,
95
+ candidate: {
96
+ domain: screening.candidate?.domain || "chat",
97
+ source: screening.candidate?.source || "",
98
+ id: screening.candidate?.id || null,
99
+ identity: screening.candidate?.identity || {}
100
+ }
101
+ };
102
+ }
103
+
104
+ function compactLlmResult(llmResult) {
105
+ if (!llmResult) return null;
106
+ return {
107
+ ok: Boolean(llmResult.ok),
108
+ provider: llmResult.provider || null,
109
+ passed: llmResult.passed,
110
+ cot: llmResult.cot || llmResult.decision_cot || "",
111
+ reasoning_content: llmResult.reasoning_content || "",
112
+ raw_model_output: llmResult.raw_model_output || "",
113
+ evidence_count: llmResult.evidence?.length || 0,
114
+ usage: llmResult.usage || null,
115
+ finish_reason: llmResult.finish_reason || null,
116
+ image_input_count: llmResult.image_input_count || 0,
117
+ attempt_count: llmResult.attempt_count || 0,
118
+ fallback_count: llmResult.fallback_count || 0,
119
+ llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
120
+ error: llmResult.error || null
121
+ };
122
+ }
123
+
124
+ function compactCandidate(candidate) {
125
+ return {
126
+ id: candidate?.id || null,
127
+ identity: candidate?.identity || {},
128
+ text_length: candidate?.text?.raw?.length || 0,
129
+ tag_count: candidate?.tags?.length || 0
130
+ };
131
+ }
132
+
133
+ function compactChatJobGuard(result = null) {
134
+ if (!result || typeof result !== "object") return null;
135
+ return {
136
+ selected: Boolean(result.selected),
137
+ verified: Boolean(result.verified),
138
+ already_current: Boolean(result.already_current),
139
+ requested: result.requested || null,
140
+ reason: result.reason || null,
141
+ selected_label: result.selected_label || result.selected_option?.label || null,
142
+ selected_value: result.selected_option?.value || result.active_option?.value || null,
143
+ active_label: result.active_option?.label || null,
144
+ active_value: result.active_option?.value || null,
145
+ menu_close: result.menu_close || null
146
+ };
147
+ }
148
+
149
+ function compactDetail(detailResult) {
150
+ if (!detailResult) return null;
151
+ return {
152
+ popup_text_length: detailResult.detail?.popup_text?.length || 0,
153
+ content_text_length: detailResult.detail?.content_text?.length || 0,
154
+ resume_iframe_text_length: detailResult.detail?.resume_iframe_text?.length || 0,
155
+ network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
156
+ parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
157
+ cv_acquisition: detailResult.cv_acquisition || null,
158
+ image_evidence: summarizeImageEvidence(detailResult.image_evidence),
159
+ llm_screening: compactLlmResult(detailResult.llm_result),
160
+ close_result: detailResult.close_result
161
+ };
162
+ }
163
+
164
+ function resultOpenedDetail(result) {
165
+ return Boolean(result?.detail && !result.detail?.cv_acquisition?.skipped);
166
+ }
167
+
168
+ export function countChatResultStatuses(results = []) {
169
+ return {
170
+ processed: results.length,
171
+ screened: results.length,
172
+ detail_opened: results.filter(resultOpenedDetail).length,
173
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
174
+ passed: results.filter((item) => item.screening?.passed).length,
175
+ skipped: results.filter((item) => item.screening?.status === "skip").length
176
+ };
177
+ }
178
+
179
+ export function chatDetailSkipReasonFromReadyState(state = {}) {
180
+ if (state?.attachment_resume_enabled) return "attachment_resume_already_available";
181
+ return "";
182
+ }
183
+
184
+ export function makeChatResumeModalOpenBeforeCandidateClickError(closeResult = null) {
185
+ const error = new Error("CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK");
186
+ error.code = "CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK";
187
+ error.close_result = closeResult || null;
188
+ return error;
189
+ }
190
+
191
+ export function isChatResumeModalCloseFailureError(error) {
192
+ return error?.code === "CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK"
193
+ || /CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK/i.test(String(error?.message || error || ""));
194
+ }
195
+
196
+ export async function ensureNoOpenChatResumeModalBeforeCandidateClick(client, {
197
+ closeAttempts = 3
198
+ } = {}) {
199
+ const probe = await quickChatResumeModalOpenProbe(client);
200
+ if (!probe.open) {
201
+ return {
202
+ closed: true,
203
+ already_closed: true,
204
+ probe
205
+ };
206
+ }
207
+ const closeResult = await closeChatResumeModal(client, { attemptsLimit: closeAttempts });
208
+ if (closeResult?.closed) {
209
+ return {
210
+ closed: true,
211
+ already_closed: false,
212
+ probe,
213
+ close_result: closeResult
214
+ };
215
+ }
216
+ throw makeChatResumeModalOpenBeforeCandidateClickError(closeResult);
217
+ }
218
+
219
+ function llmToScreening(llmResult, candidate) {
220
+ return {
221
+ status: llmResult?.passed ? "pass" : "fail",
222
+ passed: Boolean(llmResult?.passed),
223
+ score: llmResult?.passed ? 100 : 0,
224
+ reasons: llmResult?.error ? ["llm_invalid_response"] : [],
225
+ candidate
226
+ };
227
+ }
228
+
229
+ export function captureNodeIdFromResumeState(resumeState) {
230
+ return resumeState?.content?.node_id
231
+ || resumeState?.resumeIframe?.node_id
232
+ || resumeState?.popup?.node_id
233
+ || null;
234
+ }
235
+
236
+ export function resolveChatDomFallbackWait({
237
+ normalizedDetailSource = "cascade",
238
+ parsedNetworkProfileCount = 0,
239
+ waitPlan = null,
240
+ resumeDomTimeoutMs = 60000
241
+ } = {}) {
242
+ const detailSource = normalizeDetailSource(normalizedDetailSource);
243
+ const configuredTimeoutMs = Math.max(0, Number(resumeDomTimeoutMs) || 0);
244
+ if (detailSource === "image") {
245
+ return {
246
+ skipped: false,
247
+ timeout_ms: Math.min(configuredTimeoutMs, 3500),
248
+ configured_timeout_ms: configuredTimeoutMs,
249
+ short_probe: true,
250
+ reason: "forced_image_modal_probe"
251
+ };
252
+ }
253
+ if (detailSource === "dom") {
254
+ return {
255
+ skipped: false,
256
+ timeout_ms: configuredTimeoutMs,
257
+ configured_timeout_ms: configuredTimeoutMs,
258
+ short_probe: false,
259
+ reason: "dom_source_full_wait"
260
+ };
261
+ }
262
+
263
+ const profileCount = Math.max(0, Number(parsedNetworkProfileCount) || 0);
264
+ const previousImageMode = waitPlan?.mode_before === "image";
265
+ if (profileCount > 0) {
266
+ return {
267
+ skipped: false,
268
+ timeout_ms: Math.min(configuredTimeoutMs, previousImageMode ? 1500 : 3500),
269
+ configured_timeout_ms: configuredTimeoutMs,
270
+ short_probe: true,
271
+ reason: previousImageMode
272
+ ? "previous_image_mode_profile_only_network_short_dom_probe"
273
+ : "profile_only_network_short_dom_probe"
274
+ };
275
+ }
276
+ if (previousImageMode) {
277
+ return {
278
+ skipped: false,
279
+ timeout_ms: Math.min(configuredTimeoutMs, 2500),
280
+ configured_timeout_ms: configuredTimeoutMs,
281
+ short_probe: true,
282
+ reason: "previous_image_mode_network_miss_short_dom_probe"
283
+ };
284
+ }
285
+ return {
286
+ skipped: false,
287
+ timeout_ms: configuredTimeoutMs,
288
+ configured_timeout_ms: configuredTimeoutMs,
289
+ short_probe: false,
290
+ reason: "cascade_full_dom_wait"
291
+ };
292
+ }
293
+
294
+ function isRecoverableCdpNodeError(error) {
295
+ return /(?:Could not find node|No node with given id|Cannot find node|Could not compute box model)/i
296
+ .test(String(error?.message || error || ""));
297
+ }
298
+
299
+ function isRecoverableLlmScreeningError(error) {
300
+ return /(?:LLM response missing boolean passed decision|LLM response was not valid JSON)/i
301
+ .test(String(error?.message || error || ""));
302
+ }
303
+
304
+ function createFailedLlmResult(error) {
305
+ return {
306
+ ok: false,
307
+ passed: false,
308
+ reason: "",
309
+ evidence: [],
310
+ cot: "",
311
+ decision_cot: "",
312
+ reasoning_content: "",
313
+ raw_model_output: "",
314
+ attempt_count: Number(error?.llm_attempt_count) || 0,
315
+ fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
316
+ llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
317
+ error: error?.message || String(error || "unknown"),
318
+ screened_at: new Date().toISOString()
319
+ };
320
+ }
321
+
322
+ function normalizeScreeningMode(value) {
323
+ const normalized = String(value || "llm").trim().toLowerCase();
324
+ return ["deterministic", "local", "local_scorer"].includes(normalized)
325
+ ? "deterministic"
326
+ : "llm";
327
+ }
328
+
329
+ function createMissingLlmConfigResult() {
330
+ return createFailedLlmResult(new Error("LLM screening config is required for production chat runs"));
331
+ }
332
+
333
+ function createSkippedDetailResult(cardCandidate, reason, error = null) {
334
+ return {
335
+ candidate: cardCandidate,
336
+ parsed_network_profiles: [],
337
+ network_bodies: [],
338
+ detail: {},
339
+ cv_acquisition: {
340
+ source: reason,
341
+ skipped: true,
342
+ error: error?.message || null,
343
+ error_code: error?.code || null
344
+ },
345
+ close_result: null
346
+ };
347
+ }
348
+
349
+ function compactChatRuntimeError(error) {
350
+ if (!error) return null;
351
+ return {
352
+ name: error.name || "Error",
353
+ code: error.code || null,
354
+ message: error.message || String(error),
355
+ close_result: error.close_result || null,
356
+ page_state: error.page_state || null
357
+ };
358
+ }
359
+
360
+ const CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH = 500;
361
+ const CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH = 180;
362
+ const CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH = 650;
363
+ const CHAT_FULL_CV_NETWORK_MIN_RICH_ITEM_COUNT = 3;
364
+ const CHAT_RESUME_IMAGE_STOP_BOUNDARY_SELECTOR = [
365
+ "h1",
366
+ "h2",
367
+ "h3",
368
+ "h4",
369
+ "h5",
370
+ "p",
371
+ "span",
372
+ "section",
373
+ "article",
374
+ "div",
375
+ "[class*='privacy']",
376
+ "[class*='recommend']",
377
+ "[class*='similar']"
378
+ ].join(",");
379
+ const CHAT_RESUME_IMAGE_STOP_BOUNDARY_TEXT = Object.freeze([
380
+ /其他名企大厂/,
381
+ /其他.*牛人/,
382
+ /毕业的牛人/,
383
+ /经历牛人/,
384
+ /为妥善保护/,
385
+ /查看全部.*项分析/,
386
+ /牛人分析器/
387
+ ]);
388
+ const CHAT_FULL_CV_SECTION_PATTERNS = Object.freeze([
389
+ /教育(?:经历|背景|经验)?/i,
390
+ /工作(?:经历|经验)?/i,
391
+ /项目(?:经历|经验)?/i,
392
+ /实习(?:经历|经验)?/i,
393
+ /科研(?:经历|经验)?/i,
394
+ /论文|会议|专利/i,
395
+ /个人(?:优势|总结|介绍|评价)/i,
396
+ /专业技能|技能(?:特长|标签)?/i,
397
+ /求职(?:期望|意向)/i,
398
+ /校园经历|在校经历|竞赛|证书/i
399
+ ]);
400
+
401
+ function detailTextForFullCvCheck(detailResult = {}) {
402
+ return [
403
+ detailResult?.detail?.popup_text,
404
+ detailResult?.detail?.content_text,
405
+ detailResult?.detail?.resume_iframe_text
406
+ ].filter(Boolean).join("\n\n");
407
+ }
408
+
409
+ function resumeSectionMatchCount(text = "") {
410
+ const normalized = normalizeText(text);
411
+ if (!normalized) return 0;
412
+ return CHAT_FULL_CV_SECTION_PATTERNS
413
+ .filter((pattern) => pattern.test(normalized))
414
+ .length;
415
+ }
416
+
417
+ function hasResumeLikeDomText(text = "") {
418
+ const normalized = normalizeText(text);
419
+ if (normalized.length >= CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH) return true;
420
+ return normalized.length >= CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH
421
+ && resumeSectionMatchCount(normalized) >= 2;
422
+ }
423
+
424
+ function networkProfileTextLength(profileResult = {}) {
425
+ return normalizeText(profileResult?.profile?.text || "").length;
426
+ }
427
+
428
+ function isFullCvNetworkProfile(profileResult = {}) {
429
+ if (!profileResult?.ok) return false;
430
+ const sourceKeys = profileResult.profile?.source_keys || {};
431
+ const textLength = networkProfileTextLength(profileResult);
432
+ const sectionCount = resumeSectionMatchCount(profileResult.profile?.text || "");
433
+ const richItemCount = [
434
+ "education_count",
435
+ "work_count",
436
+ "project_count",
437
+ "expectation_count",
438
+ "certification_count"
439
+ ].reduce((sum, key) => sum + (Number(sourceKeys[key]) || 0), 0);
440
+ const hasResumeSections = sectionCount >= 3 || (sectionCount >= 2 && richItemCount >= 2);
441
+ const hasEnoughNetworkText = textLength >= CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH;
442
+
443
+ if (sourceKeys.geek_detail_info || sourceKeys.geek_detail) {
444
+ return hasEnoughNetworkText && (
445
+ hasResumeSections
446
+ || richItemCount >= CHAT_FULL_CV_NETWORK_MIN_RICH_ITEM_COUNT
447
+ );
448
+ }
449
+ if (sourceKeys.network_html_text) {
450
+ return textLength >= CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH
451
+ && sectionCount >= 2;
452
+ }
453
+ if (sourceKeys.chat_history_resume) {
454
+ const educationCount = Number(sourceKeys.education_count) || 0;
455
+ const workCount = Number(sourceKeys.work_count) || 0;
456
+ return (educationCount + workCount) >= 2
457
+ && textLength >= CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH
458
+ && sectionCount >= 2;
459
+ }
460
+ return false;
461
+ }
462
+
463
+ function hasUsableImageEvidence(imageEvidence = null) {
464
+ if (!imageEvidence || imageEvidence.ok === false) return false;
465
+ return Boolean(
466
+ (Array.isArray(imageEvidence.llm_file_paths) && imageEvidence.llm_file_paths.length)
467
+ || (Array.isArray(imageEvidence.file_paths) && imageEvidence.file_paths.length)
468
+ || Number(imageEvidence.llm_screenshot_count) > 0
469
+ || Number(imageEvidence.unique_screenshot_count) > 0
470
+ || Number(imageEvidence.screenshot_count) > 0
471
+ || Number(imageEvidence.capture_count) > 0
472
+ );
473
+ }
474
+
475
+ export function summarizeChatFullCvEvidence({
476
+ detailResult = null,
477
+ contentWait = null,
478
+ imageEvidence = null
479
+ } = {}) {
480
+ const parsedProfiles = (detailResult?.parsed_network_profiles || []).filter((item) => item?.ok);
481
+ const fullNetworkProfiles = parsedProfiles.filter(isFullCvNetworkProfile);
482
+ const profileOnlyCount = Math.max(0, parsedProfiles.length - fullNetworkProfiles.length);
483
+ const detailText = detailTextForFullCvCheck(detailResult);
484
+ const domTextLength = detailText.length;
485
+ const domSectionCount = resumeSectionMatchCount(detailText);
486
+ const domFullCv = Boolean(contentWait?.ok) && hasResumeLikeDomText(detailText);
487
+ const imageFullCv = hasUsableImageEvidence(imageEvidence);
488
+ const source = fullNetworkProfiles.length
489
+ ? "network"
490
+ : domFullCv
491
+ ? "dom"
492
+ : imageFullCv
493
+ ? "image"
494
+ : null;
495
+ return {
496
+ full_cv_acquired: Boolean(source),
497
+ source,
498
+ network_full_cv_count: fullNetworkProfiles.length,
499
+ network_profile_only_count: profileOnlyCount,
500
+ parsed_network_profile_count: parsedProfiles.length,
501
+ dom_full_cv: domFullCv,
502
+ dom_text_length: domTextLength,
503
+ dom_section_count: domSectionCount,
504
+ content_wait_ok: Boolean(contentWait?.ok),
505
+ image_full_cv: imageFullCv,
506
+ image_summary: summarizeImageEvidence(imageEvidence)
507
+ };
508
+ }
509
+
510
+ async function resolveFreshChatCardNodeId(client, {
511
+ fallbackNodeId,
512
+ candidate,
513
+ rootNodeId = null
514
+ } = {}) {
515
+ const candidateId = candidate?.id || "";
516
+ if (!candidateId) return fallbackNodeId;
517
+ let currentRootNodeId = rootNodeId;
518
+ if (!currentRootNodeId) {
519
+ const rootState = await getChatRoots(client);
520
+ currentRootNodeId = rootState.rootNodes.top;
521
+ }
522
+ const freshNodeId = await findChatCandidateNodeIdById(client, currentRootNodeId, candidateId);
523
+ return freshNodeId || fallbackNodeId;
524
+ }
525
+
526
+ async function selectFreshChatCandidate(client, {
527
+ cardNodeId,
528
+ candidate,
529
+ timeoutMs,
530
+ settleMs = 1200
531
+ } = {}) {
532
+ let lastError = null;
533
+ for (let attempt = 0; attempt < 3; attempt += 1) {
534
+ const modalGuard = await ensureNoOpenChatResumeModalBeforeCandidateClick(client);
535
+ const rootState = await getChatRoots(client);
536
+ const freshNodeId = await resolveFreshChatCardNodeId(client, {
537
+ fallbackNodeId: cardNodeId,
538
+ candidate,
539
+ rootNodeId: rootState.rootNodes.top
540
+ });
541
+ try {
542
+ await scrollNodeIntoView(client, freshNodeId);
543
+ await sleep(250);
544
+ const box = await getNodeBox(client, freshNodeId);
545
+ await clickPoint(client, box.center.x, box.center.y);
546
+ if (settleMs > 0) await sleep(settleMs);
547
+ const ready = await waitForChatOnlineResumeButton(client, {
548
+ timeoutMs,
549
+ expectedCandidateId: candidate?.id || ""
550
+ });
551
+ return {
552
+ card_box: box,
553
+ ready,
554
+ card_node_id: freshNodeId,
555
+ refreshed_node: freshNodeId !== cardNodeId,
556
+ modal_guard: modalGuard,
557
+ attempt: attempt + 1
558
+ };
559
+ } catch (error) {
560
+ lastError = error;
561
+ if (!isRecoverableCdpNodeError(error)) throw error;
562
+ await sleep(350);
563
+ }
564
+ }
565
+ throw lastError || new Error("Chat candidate selection failed");
566
+ }
567
+
568
+ function selectedDetailNetworkEvents(detailSource, selectionEvents, resumeEvents) {
569
+ if (detailSource !== "network" && detailSource !== "cascade") return [];
570
+ return [
571
+ ...(selectionEvents || []),
572
+ ...(resumeEvents || [])
573
+ ];
574
+ }
575
+
576
+ async function setupChatRunContext(client, {
577
+ job,
578
+ normalizedStartFrom,
579
+ readyTimeoutMs,
580
+ listSettleMs,
581
+ runControl,
582
+ ensureViewport = null
583
+ } = {}) {
584
+ let rootState = await getChatRoots(client);
585
+ if (ensureViewport) {
586
+ rootState = await ensureViewport(rootState, "context_roots");
587
+ }
588
+ runControl.checkpoint({
589
+ top_document_node_id: rootState.rootNodes.top
590
+ });
591
+
592
+ const primaryLabel = await selectChatPrimaryLabel(client, {
593
+ label: "全部",
594
+ timeoutMs: readyTimeoutMs,
595
+ settleMs: listSettleMs
596
+ });
597
+ runControl.checkpoint({
598
+ chat_context_step: "primary_label",
599
+ primary_label: primaryLabel
600
+ });
601
+
602
+ const jobSelection = normalizeText(job)
603
+ ? await selectChatJob(client, rootState.rootNodes.top, {
604
+ jobLabel: job,
605
+ timeoutMs: readyTimeoutMs,
606
+ settleMs: listSettleMs
607
+ })
608
+ : {
609
+ selected: false,
610
+ reason: "job_not_requested"
611
+ };
612
+ if (normalizeText(job) && !jobSelection.selected) {
613
+ throw new Error(`Chat job selection failed: ${jobSelection.reason || "unknown"}`);
614
+ }
615
+ if (normalizeText(job) && jobSelection.verified !== true) {
616
+ throw new Error(`Chat job selection was not verified: requested=${jobSelection.requested || job}; selected=${jobSelection.selected_label || "unknown"}`);
617
+ }
618
+ rootState = await getChatRoots(client);
619
+ if (ensureViewport) {
620
+ rootState = await ensureViewport(rootState, "context_job");
621
+ }
622
+ runControl.checkpoint({
623
+ chat_context_step: "job_selection",
624
+ primary_label: primaryLabel,
625
+ job_selection: jobSelection
626
+ });
627
+
628
+ const startFilter = await selectChatMessageFilter(client, {
629
+ startFrom: normalizedStartFrom,
630
+ timeoutMs: readyTimeoutMs,
631
+ settleMs: listSettleMs
632
+ });
633
+ if (!startFilter.ok) {
634
+ throw new Error(`Chat start filter selection failed: ${startFilter.error || "unknown"}`);
635
+ }
636
+ rootState = await getChatRoots(client);
637
+ if (ensureViewport) {
638
+ rootState = await ensureViewport(rootState, "context_start_filter");
639
+ }
640
+ runControl.checkpoint({
641
+ chat_context_step: "start_filter",
642
+ primary_label: primaryLabel,
643
+ job_selection: jobSelection,
644
+ start_filter: startFilter
645
+ });
646
+
647
+ return {
648
+ rootState,
649
+ contextSetup: {
650
+ primary_label: primaryLabel,
651
+ job_selection: jobSelection,
652
+ start_filter: startFilter,
653
+ requested_start_from: normalizedStartFrom
654
+ }
655
+ };
656
+ }
657
+
658
+ export async function runChatWorkflow({
659
+ client,
660
+ targetUrl = CHAT_TARGET_URL,
661
+ job = "",
662
+ startFrom = "all",
663
+ criteria = "",
664
+ maxCandidates = 5,
665
+ targetPassCount = null,
666
+ processUntilListEnd = false,
667
+ detailLimit = null,
668
+ detailSource = "cascade",
669
+ closeResume = true,
670
+ requestResumeForPassed = false,
671
+ dryRunRequestCv = false,
672
+ greetingText = "Hi同学,能麻烦发下简历吗?",
673
+ delayMs = 0,
674
+ cardTimeoutMs = 90000,
675
+ readyTimeoutMs = 60000,
676
+ onlineResumeButtonTimeoutMs = 30000,
677
+ resumeDomTimeoutMs = 60000,
678
+ maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
679
+ imageWheelDeltaY = 650,
680
+ cvAcquisitionMode = "unknown",
681
+ callLlmOnImage = false,
682
+ llmConfig = null,
683
+ llmTimeoutMs = 120000,
684
+ llmImageLimit = 8,
685
+ llmImageDetail = "high",
686
+ screeningMode = "llm",
687
+ listMaxScrolls = 20,
688
+ listStableSignatureLimit = 5,
689
+ listWheelDeltaY = 850,
690
+ listSettleMs = 2200,
691
+ listFallbackPoint = null,
692
+ imageOutputDir = "",
693
+ humanRestEnabled = false,
694
+ humanBehavior = null
695
+ } = {}, runControl) {
696
+ if (!client) throw new Error("runChatWorkflow requires a guarded CDP client");
697
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
698
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
699
+ });
700
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
701
+ configureHumanInteraction(client, {
702
+ enabled: effectiveHumanBehavior.enabled,
703
+ clickMovementEnabled: effectiveHumanBehavior.clickMovement,
704
+ textEntryEnabled: effectiveHumanBehavior.textEntry,
705
+ safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
706
+ actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
707
+ });
708
+ const humanRestController = createHumanRestController({
709
+ enabled: effectiveHumanRestEnabled,
710
+ shortRestEnabled: effectiveHumanBehavior.shortRest,
711
+ batchRestEnabled: effectiveHumanBehavior.batchRest
712
+ });
713
+ const normalizedDetailSource = normalizeDetailSource(detailSource);
714
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
715
+ const useLlmScreening = normalizedScreeningMode !== "deterministic";
716
+ const processedLimit = Math.max(1, Number(maxCandidates) || 1);
717
+ const passTarget = Number.isFinite(Number(targetPassCount)) && Number(targetPassCount) > 0
718
+ ? Number(targetPassCount)
719
+ : null;
720
+ const normalizedStartFrom = normalizeText(startFrom).toLowerCase() === "unread" ? "unread" : "all";
721
+ const detailCountLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
722
+ const networkRecorder = detailCountLimit > 0
723
+ ? createChatProfileNetworkRecorder(client)
724
+ : null;
725
+ const cvAcquisitionState = createCvAcquisitionState({ mode: cvAcquisitionMode });
726
+ const listState = createInfiniteListState({
727
+ domain: "chat",
728
+ listName: "chat-candidates"
729
+ });
730
+ const viewportGuard = createViewportRunGuard({
731
+ client,
732
+ domain: "chat",
733
+ root: "top",
734
+ frameOwnerRoot: "top",
735
+ runControl,
736
+ getRoots: getChatRoots
737
+ });
738
+ async function ensureChatViewport(rootState, phase) {
739
+ const result = await viewportGuard.ensure(rootState, { phase });
740
+ return result.rootState || rootState;
741
+ }
742
+ const results = [];
743
+ let cardNodeIds = [];
744
+ let listEndReason = "";
745
+ const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
746
+ rootNodeId: rootState?.rootNodes?.top,
747
+ containerSelectors: CHAT_LIST_CONTAINER_SELECTORS,
748
+ itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
749
+ itemSelectors: CHAT_CARD_SELECTORS,
750
+ viewportPoint: { xRatio: 0.16, yRatio: 0.4 },
751
+ validateViewportPoint: true
752
+ }));
753
+ let requestedCount = 0;
754
+ let requestSatisfiedCount = 0;
755
+ let requestSkippedCount = 0;
756
+ let contextSetup = {};
757
+ let contextRecoveryAttempts = 0;
758
+ let lastHumanEvent = null;
759
+
760
+ function recordHumanEvent(event = null) {
761
+ if (!event) return lastHumanEvent;
762
+ lastHumanEvent = {
763
+ at: new Date().toISOString(),
764
+ ...event
765
+ };
766
+ return lastHumanEvent;
767
+ }
768
+
769
+ async function maybeHumanActionCooldown(phase, timings = {}) {
770
+ if (!effectiveHumanBehavior.actionCooldown) return null;
771
+ const pauseMs = humanDelay(280, 90, {
772
+ minMs: 80,
773
+ maxMs: 720
774
+ });
775
+ if (pauseMs > 0) {
776
+ await runControl.sleep(pauseMs);
777
+ addTiming(timings, `human_${phase}_pause_ms`, pauseMs);
778
+ }
779
+ return recordHumanEvent({
780
+ kind: "action_cooldown",
781
+ phase,
782
+ pause_ms: pauseMs
783
+ });
784
+ }
785
+
786
+ runControl.setPhase("chat:cleanup");
787
+ let initialTopLevelState = await getChatTopLevelState(client);
788
+ if (!initialTopLevelState.is_chat_shell) {
789
+ const recovery = await recoverChatShell(client, {
790
+ targetUrl,
791
+ timeoutMs: readyTimeoutMs
792
+ });
793
+ runControl.checkpoint({
794
+ chat_shell_recovery: {
795
+ reason: "initial_non_chat_shell",
796
+ ...recovery
797
+ }
798
+ });
799
+ if (!recovery.recovered) {
800
+ throw new Error(`Chat shell recovery failed before run setup: ${recovery.after?.url || recovery.before?.url || "unknown"}`);
801
+ }
802
+ initialTopLevelState = recovery.after;
803
+ }
804
+ await closeChatResumeModal(client, { attemptsLimit: 2 });
805
+
806
+ await runControl.waitIfPaused();
807
+ runControl.throwIfCanceled();
808
+ runControl.setPhase("chat:context");
809
+ const setup = await setupChatRunContext(client, {
810
+ job,
811
+ normalizedStartFrom,
812
+ readyTimeoutMs,
813
+ listSettleMs,
814
+ runControl,
815
+ ensureViewport: ensureChatViewport
816
+ });
817
+ let rootState = setup.rootState;
818
+ contextSetup = {
819
+ ...setup.contextSetup,
820
+ initial_top_level_state: initialTopLevelState
821
+ };
822
+ runControl.checkpoint({
823
+ chat_context: contextSetup
824
+ });
825
+
826
+ async function recoverAndReapplyChatContext(reason, error = null, {
827
+ forceRefresh = false
828
+ } = {}) {
829
+ runControl.setPhase("chat:recover_shell");
830
+ contextRecoveryAttempts += 1;
831
+ const shellRecovery = await recoverChatShell(client, {
832
+ targetUrl,
833
+ timeoutMs: readyTimeoutMs,
834
+ forceNavigate: forceRefresh
835
+ });
836
+ runControl.checkpoint({
837
+ chat_shell_recovery: {
838
+ reason,
839
+ error: error?.message || null,
840
+ total_refresh: Boolean(forceRefresh),
841
+ ...shellRecovery
842
+ }
843
+ });
844
+ if (!shellRecovery.recovered && !shellRecovery.after?.is_chat_shell) {
845
+ throw new Error(`Chat shell recovery failed after ${reason}: ${shellRecovery.after?.url || shellRecovery.before?.url || "unknown"}`);
846
+ }
847
+ await closeChatResumeModal(client, { attemptsLimit: 2 });
848
+ const recoveredSetup = await setupChatRunContext(client, {
849
+ job,
850
+ normalizedStartFrom,
851
+ readyTimeoutMs,
852
+ listSettleMs,
853
+ runControl,
854
+ ensureViewport: ensureChatViewport
855
+ });
856
+ rootState = recoveredSetup.rootState;
857
+ const counters = countChatResultStatuses(results);
858
+ const candidateList = resetInfiniteListForRefreshRound(listState, {
859
+ reason,
860
+ round: listState.ledger?.length || 0,
861
+ method: forceRefresh ? "total_refresh_reapply_chat_context" : "reapply_chat_context",
862
+ metadata: {
863
+ processed: counters.processed,
864
+ passed: counters.passed,
865
+ skipped: counters.skipped
866
+ }
867
+ });
868
+ const recovery = {
869
+ reason,
870
+ total_refresh: Boolean(forceRefresh),
871
+ attempt: contextRecoveryAttempts,
872
+ shell: shellRecovery,
873
+ candidate_list: candidateList,
874
+ counters
875
+ };
876
+ contextSetup = {
877
+ ...recoveredSetup.contextSetup,
878
+ recovered_from: reason,
879
+ recovery,
880
+ previous_context: contextSetup
881
+ };
882
+ runControl.checkpoint({
883
+ chat_context: contextSetup,
884
+ candidate_list: candidateList
885
+ });
886
+ return recovery;
887
+ }
888
+
889
+ await runControl.waitIfPaused();
890
+ runControl.throwIfCanceled();
891
+ runControl.setPhase("chat:cards");
892
+ const cardRootState = await ensureChatViewport(await getChatRoots(client), "cards");
893
+ const initialCards = await waitForChatCandidateNodeIds(client, cardRootState.rootNodes.top, {
894
+ timeoutMs: cardTimeoutMs,
895
+ intervalMs: 500
896
+ });
897
+ cardNodeIds = initialCards.nodeIds || [];
898
+ if (!cardNodeIds.length) {
899
+ runControl.checkpoint({
900
+ empty_list_state: {
901
+ method: "cdp_dom_selector_count",
902
+ candidate_count: 0,
903
+ requested_start_from: normalizedStartFrom
904
+ }
905
+ });
906
+ listEndReason = "no_chat_candidates_found";
907
+ runControl.updateProgress({
908
+ card_count: 0,
909
+ target_count: passTarget || (processUntilListEnd ? "all" : processedLimit),
910
+ target_pass_count: passTarget,
911
+ processed_limit: processedLimit,
912
+ processed: 0,
913
+ screened: 0,
914
+ detail_opened: 0,
915
+ llm_screened: 0,
916
+ passed: 0,
917
+ skipped: 0,
918
+ requested: 0,
919
+ request_satisfied: 0,
920
+ request_skipped: 0,
921
+ unique_seen: compactInfiniteListState(listState).seen_count,
922
+ scroll_count: compactInfiniteListState(listState).scroll_count,
923
+ context_recoveries: contextRecoveryAttempts,
924
+ list_end_reason: listEndReason,
925
+ viewport_checks: viewportGuard.getStats().checks,
926
+ viewport_recoveries: viewportGuard.getStats().recoveries,
927
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
928
+ human_behavior_profile: effectiveHumanBehavior.profile,
929
+ human_rest_enabled: effectiveHumanRestEnabled,
930
+ human_rest_count: humanRestController.getState().rest_count,
931
+ human_rest_ms: humanRestController.getState().total_rest_ms,
932
+ last_human_event: lastHumanEvent
933
+ });
934
+ runControl.setPhase("chat:done");
935
+ return {
936
+ domain: "chat",
937
+ target_url: targetUrl,
938
+ card_count: 0,
939
+ context_setup: contextSetup,
940
+ empty_list_state: {
941
+ method: "cdp_dom_selector_count",
942
+ candidate_count: 0,
943
+ requested_start_from: normalizedStartFrom
944
+ },
945
+ candidate_list: compactInfiniteListState(listState),
946
+ viewport_health: {
947
+ stats: viewportGuard.getStats(),
948
+ events: viewportGuard.getEvents()
949
+ },
950
+ human_behavior: effectiveHumanBehavior,
951
+ human_rest: humanRestController.getState(),
952
+ last_human_event: lastHumanEvent,
953
+ list_end_reason: listEndReason,
954
+ target_pass_count: passTarget,
955
+ process_until_list_end: Boolean(processUntilListEnd),
956
+ processed_limit: processedLimit,
957
+ detail_source: normalizedDetailSource,
958
+ processed: 0,
959
+ screened: 0,
960
+ detail_opened: 0,
961
+ llm_screened: 0,
962
+ passed: 0,
963
+ skipped: 0,
964
+ requested: requestedCount,
965
+ request_satisfied: requestSatisfiedCount,
966
+ request_skipped: requestSkippedCount,
967
+ context_recoveries: contextRecoveryAttempts,
968
+ results
969
+ };
970
+ }
971
+
972
+ runControl.updateProgress({
973
+ card_count: cardNodeIds.length,
974
+ target_count: passTarget || (processUntilListEnd ? "all" : processedLimit),
975
+ target_pass_count: passTarget,
976
+ processed_limit: processedLimit,
977
+ processed: 0,
978
+ screened: 0,
979
+ detail_opened: 0,
980
+ llm_screened: 0,
981
+ passed: 0,
982
+ skipped: 0,
983
+ requested: 0,
984
+ request_satisfied: 0,
985
+ request_skipped: 0,
986
+ screening_mode: normalizedScreeningMode,
987
+ unique_seen: compactInfiniteListState(listState).seen_count,
988
+ scroll_count: 0,
989
+ context_recoveries: contextRecoveryAttempts,
990
+ viewport_checks: viewportGuard.getStats().checks,
991
+ viewport_recoveries: viewportGuard.getStats().recoveries
992
+ });
993
+
994
+ while (
995
+ results.length < processedLimit
996
+ && (
997
+ !passTarget
998
+ || results.filter((item) => item.screening?.passed).length < passTarget
999
+ )
1000
+ ) {
1001
+ const candidateStarted = Date.now();
1002
+ const timings = {};
1003
+ await runControl.waitIfPaused();
1004
+ runControl.throwIfCanceled();
1005
+ runControl.setPhase("chat:candidate");
1006
+ rootState = await ensureChatViewport(rootState, "candidate_loop");
1007
+ const loopTopLevelState = await getChatTopLevelState(client);
1008
+ if (!loopTopLevelState.is_chat_shell) {
1009
+ await recoverAndReapplyChatContext("candidate_loop_non_chat_shell", {
1010
+ message: `Unexpected chat top-level URL: ${loopTopLevelState.url}`
1011
+ });
1012
+ continue;
1013
+ }
1014
+ if (normalizeText(job)) {
1015
+ const jobGuard = await selectChatJob(client, rootState.rootNodes.top, {
1016
+ jobLabel: job,
1017
+ timeoutMs: Math.min(readyTimeoutMs, 12000),
1018
+ settleMs: Math.min(listSettleMs, 800)
1019
+ });
1020
+ if (!jobGuard.selected || jobGuard.verified !== true) {
1021
+ const error = new Error(`CHAT_JOB_GUARD_FAILED: requested=${job}; selected=${jobGuard.selected_label || "unknown"}; reason=${jobGuard.reason || "unknown"}`);
1022
+ error.code = "CHAT_JOB_GUARD_FAILED";
1023
+ error.chat_job_guard = compactChatJobGuard(jobGuard);
1024
+ runControl.checkpoint({
1025
+ chat_context_step: "job_guard_failed",
1026
+ job_guard: compactChatJobGuard(jobGuard),
1027
+ error: {
1028
+ code: error.code,
1029
+ message: error.message
1030
+ }
1031
+ });
1032
+ if (contextRecoveryAttempts < 2) {
1033
+ await recoverAndReapplyChatContext("job_guard_failed", error, { forceRefresh: true });
1034
+ continue;
1035
+ }
1036
+ throw error;
1037
+ }
1038
+ if (!jobGuard.already_current) {
1039
+ runControl.checkpoint({
1040
+ chat_context_step: "job_guard_reselected",
1041
+ job_guard: compactChatJobGuard(jobGuard),
1042
+ candidate_list: resetInfiniteListForRefreshRound(listState, {
1043
+ reason: "chat_job_drift_repaired",
1044
+ round: listState.ledger?.length || 0,
1045
+ method: "selectChatJob",
1046
+ metadata: {
1047
+ requested_job: job,
1048
+ selected_label: jobGuard.selected_label || "",
1049
+ selected_value: jobGuard.selected_option?.value || ""
1050
+ }
1051
+ })
1052
+ });
1053
+ rootState = await ensureChatViewport(await getChatRoots(client), "candidate_job_guard_reselected");
1054
+ await sleep(Math.min(listSettleMs, 1200));
1055
+ continue;
1056
+ }
1057
+ if (jobGuard.menu_close?.closed) {
1058
+ runControl.checkpoint({
1059
+ chat_context_step: "job_guard_closed_dropdown",
1060
+ job_guard: compactChatJobGuard(jobGuard)
1061
+ });
1062
+ }
1063
+ }
1064
+
1065
+ const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
1066
+ client,
1067
+ state: listState,
1068
+ maxScrolls: listMaxScrolls,
1069
+ stableSignatureLimit: listStableSignatureLimit,
1070
+ wheelDeltaY: listWheelDeltaY,
1071
+ settleMs: listSettleMs,
1072
+ listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1073
+ fallbackPoint: listFallbackResolver,
1074
+ findNodeIds: async () => {
1075
+ const currentRootState = await ensureChatViewport(await getChatRoots(client), "candidate_find_nodes");
1076
+ rootState = currentRootState;
1077
+ const currentCards = await waitForChatCandidateNodeIds(client, currentRootState.rootNodes.top, {
1078
+ timeoutMs: Math.min(cardTimeoutMs, 8000),
1079
+ intervalMs: 500
1080
+ });
1081
+ cardNodeIds = currentCards.nodeIds || [];
1082
+ return cardNodeIds;
1083
+ },
1084
+ keyForCandidate: chatCandidateKeyFromProfile,
1085
+ readCandidate: async (nodeId, { visibleIndex }) => readChatCardCandidate(client, nodeId, {
1086
+ targetUrl,
1087
+ source: "chat-run-card",
1088
+ metadata: {
1089
+ run_candidate_index: results.length,
1090
+ visible_index: visibleIndex
1091
+ }
1092
+ }),
1093
+ detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
1094
+ rootNodeId: rootState?.rootNodes?.top,
1095
+ markerSelectors: CHAT_BOTTOM_MARKER_SELECTORS,
1096
+ refreshSelectors: [],
1097
+ textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
1098
+ maxTextScanNodes: 500
1099
+ })
1100
+ }));
1101
+ if (!nextCandidateResult.ok) {
1102
+ const endTopLevelState = await getChatTopLevelState(client);
1103
+ if (!endTopLevelState.is_chat_shell) {
1104
+ await recoverAndReapplyChatContext("candidate_list_end_non_chat_shell", {
1105
+ message: `Unexpected chat top-level URL at list end: ${endTopLevelState.url}`
1106
+ });
1107
+ continue;
1108
+ }
1109
+ if (nextCandidateResult.reason === "empty_visible_list") {
1110
+ runControl.checkpoint({
1111
+ terminal_empty_list_state: {
1112
+ method: "cdp_dom_selector_count",
1113
+ reason: nextCandidateResult.reason,
1114
+ requested_start_from: normalizedStartFrom
1115
+ }
1116
+ });
1117
+ }
1118
+ listEndReason = nextCandidateResult.reason || "list_exhausted";
1119
+ break;
1120
+ }
1121
+
1122
+ const index = results.length;
1123
+ const cardNodeId = nextCandidateResult.item.node_id;
1124
+ let effectiveCardNodeId = cardNodeId;
1125
+ const candidateKey = nextCandidateResult.item.key;
1126
+ const cardCandidate = nextCandidateResult.item.candidate;
1127
+
1128
+ let screeningCandidate = cardCandidate;
1129
+ let detailResult = null;
1130
+ let preActionState = null;
1131
+ let detailUnavailableReason = "";
1132
+ if (index < detailCountLimit) {
1133
+ let detailStep = "start";
1134
+ const checkpointInProgressCandidate = (patch = {}) => runControl.checkpoint({
1135
+ in_progress_candidate: {
1136
+ index,
1137
+ key: candidateKey,
1138
+ card_node_id: effectiveCardNodeId || cardNodeId,
1139
+ candidate: compactCandidate(cardCandidate),
1140
+ detail_step: detailStep,
1141
+ counters: countChatResultStatuses(results),
1142
+ ...patch
1143
+ }
1144
+ });
1145
+ try {
1146
+ await runControl.waitIfPaused();
1147
+ runControl.throwIfCanceled();
1148
+ runControl.setPhase("chat:detail");
1149
+ rootState = await ensureChatViewport(rootState, "detail");
1150
+ checkpointInProgressCandidate({ event: "detail_start" });
1151
+
1152
+ detailStep = "select_candidate";
1153
+ networkRecorder.clear();
1154
+ await maybeHumanActionCooldown("before_detail_open", timings);
1155
+ const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
1156
+ cardNodeId,
1157
+ candidate: cardCandidate,
1158
+ timeoutMs: onlineResumeButtonTimeoutMs
1159
+ }));
1160
+ if (selected.ready?.forbidden_top_level_navigation) {
1161
+ throw makeForbiddenChatResumeNavigationError(selected.ready.top_level_state);
1162
+ }
1163
+ effectiveCardNodeId = selected.card_node_id || cardNodeId;
1164
+ const selectionNetworkEvents = networkRecorder.events.slice();
1165
+ try {
1166
+ preActionState = await readChatConversationReadyState(client);
1167
+ } catch (error) {
1168
+ preActionState = {
1169
+ error: error?.message || String(error)
1170
+ };
1171
+ }
1172
+ const preDetailSkipReason = chatDetailSkipReasonFromReadyState(preActionState);
1173
+ if (preDetailSkipReason) {
1174
+ detailUnavailableReason = preDetailSkipReason;
1175
+ detailResult = createSkippedDetailResult(cardCandidate, preDetailSkipReason);
1176
+ detailResult.cv_acquisition.pre_detail_state = preActionState;
1177
+ detailResult.cv_acquisition.selection_ready_state = selected.ready;
1178
+ }
1179
+ if (!selected.ready?.ok) {
1180
+ if (detailResult) {
1181
+ // Already classified by the pre-detail conversation state.
1182
+ } else if (selected.ready?.reason === "active_candidate_mismatch") {
1183
+ detailUnavailableReason = "active_candidate_mismatch";
1184
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
1185
+ detailResult.cv_acquisition.selection_ready_state = selected.ready;
1186
+ } else {
1187
+ detailStep = "read_conversation_ready_state";
1188
+ if (preActionState.attachment_resume_enabled) {
1189
+ detailUnavailableReason = "attachment_resume_already_available";
1190
+ detailResult = createSkippedDetailResult(cardCandidate, "attachment_resume_already_available");
1191
+ detailResult.cv_acquisition.pre_detail_state = preActionState;
1192
+ } else {
1193
+ detailUnavailableReason = "online_resume_button_unavailable";
1194
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
1195
+ detailResult.cv_acquisition.pre_detail_state = preActionState;
1196
+ }
1197
+ }
1198
+ }
1199
+
1200
+ if (!detailResult) {
1201
+ const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1202
+ let networkWait = null;
1203
+ let contentWait = {
1204
+ ok: false,
1205
+ skipped: false,
1206
+ reason: "not_started",
1207
+ elapsed_ms: 0,
1208
+ text_length: 0
1209
+ };
1210
+ let resumeState = null;
1211
+ let resumeHtml = null;
1212
+ let resumeNetworkEvents = [];
1213
+ let parsedNetworkProfileCount = 0;
1214
+
1215
+ if (
1216
+ ["network", "cascade"].includes(normalizedDetailSource)
1217
+ && selectionNetworkEvents.length > 0
1218
+ ) {
1219
+ detailStep = "extract_selection_network_profile";
1220
+ detailResult = await extractChatProfileCandidate(client, {
1221
+ cardCandidate,
1222
+ cardNodeId: effectiveCardNodeId,
1223
+ resumeState: null,
1224
+ resumeHtml: null,
1225
+ networkEvents: selectionNetworkEvents,
1226
+ targetUrl,
1227
+ closeResume: false,
1228
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 250 : 900,
1229
+ networkParseIntervalMs: 150
1230
+ });
1231
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1232
+ parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1233
+ const selectionNetworkEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
1234
+ if (selectionNetworkEvidence.network_full_cv_count > 0) {
1235
+ networkWait = {
1236
+ ok: true,
1237
+ skipped: true,
1238
+ reason: "selection_network_full_cv_before_online_resume_click",
1239
+ elapsed_ms: detailResult.network_parse_retry_elapsed_ms,
1240
+ count: selectionNetworkEvents.length,
1241
+ total_event_count: selectionNetworkEvents.length,
1242
+ wait_plan: waitPlan
1243
+ };
1244
+ contentWait = {
1245
+ ok: true,
1246
+ skipped: true,
1247
+ reason: "selection_network_full_cv_before_online_resume_click",
1248
+ elapsed_ms: 0,
1249
+ text_length: 0
1250
+ };
1251
+ } else {
1252
+ detailResult = null;
1253
+ }
1254
+ }
1255
+
1256
+ if (!detailResult) {
1257
+ detailStep = "open_online_resume";
1258
+ networkRecorder.clear();
1259
+ await maybeHumanActionCooldown("before_resume_open", timings);
1260
+ const openedResume = await measureTiming(timings, "detail_open_ms", () => openChatOnlineResume(client, {
1261
+ timeoutMs: readyTimeoutMs
1262
+ }));
1263
+ resumeState = openedResume.resume_state;
1264
+ detailStep = "wait_network";
1265
+ networkWait = ["network", "cascade"].includes(normalizedDetailSource)
1266
+ ? await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
1267
+ waitForChatProfileNetworkEvents,
1268
+ networkRecorder,
1269
+ {
1270
+ waitPlan,
1271
+ minCount: 1,
1272
+ requireLoaded: true,
1273
+ intervalMs: 200
1274
+ }
1275
+ ))
1276
+ : null;
1277
+ if (networkWait?.elapsed_ms != null) {
1278
+ timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
1279
+ }
1280
+ resumeNetworkEvents = networkRecorder.events.slice();
1281
+
1282
+ if (
1283
+ ["network", "cascade"].includes(normalizedDetailSource)
1284
+ && networkWait?.count > 0
1285
+ ) {
1286
+ detailStep = "extract_network_profile";
1287
+ detailResult = await extractChatProfileCandidate(client, {
1288
+ cardCandidate,
1289
+ cardNodeId: effectiveCardNodeId,
1290
+ resumeState,
1291
+ resumeHtml,
1292
+ networkEvents: selectedDetailNetworkEvents(
1293
+ normalizedDetailSource,
1294
+ selectionNetworkEvents,
1295
+ resumeNetworkEvents
1296
+ ),
1297
+ targetUrl,
1298
+ closeResume: false,
1299
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
1300
+ networkParseIntervalMs: 250
1301
+ });
1302
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1303
+ parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1304
+ const networkEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
1305
+ if (networkEvidence.network_full_cv_count > 0) {
1306
+ contentWait = {
1307
+ ok: true,
1308
+ skipped: true,
1309
+ reason: "network_full_cv_parsed_before_dom_wait",
1310
+ elapsed_ms: 0,
1311
+ text_length: 0
1312
+ };
1313
+ } else {
1314
+ detailResult = null;
1315
+ }
1316
+ }
1317
+
1318
+ if (!detailResult) {
1319
+ detailStep = "wait_resume_content";
1320
+ const domFallbackPlan = resolveChatDomFallbackWait({
1321
+ normalizedDetailSource,
1322
+ parsedNetworkProfileCount,
1323
+ waitPlan,
1324
+ resumeDomTimeoutMs
1325
+ });
1326
+ if (domFallbackPlan.skipped || domFallbackPlan.timeout_ms <= 0) {
1327
+ contentWait = {
1328
+ ok: false,
1329
+ skipped: true,
1330
+ reason: domFallbackPlan.reason,
1331
+ elapsed_ms: 0,
1332
+ text_length: 0,
1333
+ resume_state: openedResume.resume_state,
1334
+ resume_html: null,
1335
+ dom_fallback_plan: domFallbackPlan,
1336
+ configured_timeout_ms: domFallbackPlan.configured_timeout_ms,
1337
+ timeout_ms: domFallbackPlan.timeout_ms,
1338
+ short_probe: Boolean(domFallbackPlan.short_probe)
1339
+ };
1340
+ addTiming(timings, "dom_fallback_ms", 0);
1341
+ } else {
1342
+ contentWait = await measureTiming(timings, "dom_fallback_ms", () => waitForChatResumeContent(client, {
1343
+ timeoutMs: domFallbackPlan.timeout_ms,
1344
+ intervalMs: 300
1345
+ }));
1346
+ contentWait.dom_fallback_plan = domFallbackPlan;
1347
+ contentWait.configured_timeout_ms = domFallbackPlan.configured_timeout_ms;
1348
+ contentWait.timeout_ms = domFallbackPlan.timeout_ms;
1349
+ contentWait.short_probe = Boolean(domFallbackPlan.short_probe);
1350
+ if (domFallbackPlan.short_probe && !contentWait.ok) {
1351
+ contentWait.reason = contentWait.reason || domFallbackPlan.reason;
1352
+ }
1353
+ }
1354
+ resumeState = contentWait.resume_state || openedResume.resume_state;
1355
+ resumeHtml = contentWait.resume_html || null;
1356
+ resumeNetworkEvents = networkRecorder.events.slice();
1357
+ detailStep = "extract_resume_content";
1358
+ detailResult = await extractChatProfileCandidate(client, {
1359
+ cardCandidate,
1360
+ cardNodeId: effectiveCardNodeId,
1361
+ resumeState,
1362
+ resumeHtml,
1363
+ networkEvents: selectedDetailNetworkEvents(
1364
+ normalizedDetailSource,
1365
+ selectionNetworkEvents,
1366
+ resumeNetworkEvents
1367
+ ),
1368
+ targetUrl,
1369
+ closeResume: false,
1370
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
1371
+ networkParseIntervalMs: 250
1372
+ });
1373
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1374
+ parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1375
+ }
1376
+ }
1377
+
1378
+ let source = normalizedDetailSource === "dom" ? "dom" : "network";
1379
+ let imageEvidence = null;
1380
+ let llmResult = null;
1381
+ let captureTarget = null;
1382
+ let captureTargetWait = null;
1383
+ let fullCvEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
1384
+ const shouldCaptureImage = normalizedDetailSource === "image"
1385
+ || (normalizedDetailSource === "cascade" && !fullCvEvidence.full_cv_acquired);
1386
+ if (shouldCaptureImage) {
1387
+ captureTargetWait = await waitForCvCaptureTarget(client, resumeState, {
1388
+ domain: "chat",
1389
+ timeoutMs: 6000,
1390
+ intervalMs: 250
1391
+ });
1392
+ captureTarget = captureTargetWait.target || null;
1393
+ const captureNodeId = captureTarget?.node_id || null;
1394
+ if (captureNodeId) {
1395
+ detailStep = "capture_image_fallback";
1396
+ imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
1397
+ filePath: imageEvidenceFilePath({
1398
+ imageOutputDir,
1399
+ domain: "chat",
1400
+ runId: runControl?.runId,
1401
+ index,
1402
+ extension: "jpg"
1403
+ }),
1404
+ format: "jpeg",
1405
+ quality: 72,
1406
+ optimize: true,
1407
+ resizeMaxWidth: 1100,
1408
+ captureViewport: false,
1409
+ padding: 0,
1410
+ maxScreenshots: maxImagePages,
1411
+ wheelDeltaY: imageWheelDeltaY,
1412
+ settleMs: 350,
1413
+ scrollMethod: "dom-anchor-fallback-input",
1414
+ scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1415
+ stepTimeoutMs: 45000,
1416
+ totalTimeoutMs: 90000,
1417
+ duplicateStopCount: 1,
1418
+ skipDuplicateScreenshots: true,
1419
+ composeForLlm: true,
1420
+ llmPagesPerImage: 3,
1421
+ llmResizeMaxWidth: 1100,
1422
+ llmQuality: 72,
1423
+ stopBoundarySelector: CHAT_RESUME_IMAGE_STOP_BOUNDARY_SELECTOR,
1424
+ stopBoundaryTextPatterns: CHAT_RESUME_IMAGE_STOP_BOUNDARY_TEXT,
1425
+ stopBoundaryMaxProbeNodes: 360,
1426
+ stopBoundaryTopPadding: 10,
1427
+ stopBoundaryMinCaptureHeight: 180,
1428
+ metadata: {
1429
+ domain: "chat",
1430
+ capture_mode: "scroll_sequence",
1431
+ capture_scope: "resume_modal_clip",
1432
+ acquisition_reason: normalizedDetailSource === "image"
1433
+ ? "forced_image"
1434
+ : "network_miss_image_fallback",
1435
+ run_candidate_index: index,
1436
+ candidate_key: candidateKey,
1437
+ capture_target: captureTarget,
1438
+ capture_target_wait: captureTargetWait
1439
+ }
1440
+ }));
1441
+ source = "image";
1442
+ fullCvEvidence = summarizeChatFullCvEvidence({
1443
+ detailResult,
1444
+ contentWait,
1445
+ imageEvidence
1446
+ });
1447
+ recordCvImageFallback(cvAcquisitionState, {
1448
+ reason: fullCvEvidence.network_profile_only_count > 0
1449
+ ? "profile_only_network_image_fallback"
1450
+ : "network_miss_image_fallback",
1451
+ parsedNetworkProfileCount,
1452
+ waitResult: networkWait,
1453
+ imageEvidence
1454
+ });
1455
+ if (callLlmOnImage && fullCvEvidence.full_cv_acquired) {
1456
+ detailStep = "llm_image_screening";
1457
+ if (!llmConfig) {
1458
+ llmResult = createMissingLlmConfigResult();
1459
+ } else {
1460
+ try {
1461
+ llmResult = await measureTiming(timings, "vision_model_ms", () => callScreeningLlm({
1462
+ candidate: detailResult.candidate,
1463
+ criteria,
1464
+ config: llmConfig,
1465
+ timeoutMs: llmTimeoutMs,
1466
+ imageEvidence,
1467
+ maxImages: llmImageLimit,
1468
+ imageDetail: llmImageDetail
1469
+ }));
1470
+ } catch (error) {
1471
+ llmResult = createFailedLlmResult(error);
1472
+ }
1473
+ }
1474
+ }
1475
+ } else {
1476
+ source = "missing_capture_node";
1477
+ fullCvEvidence = summarizeChatFullCvEvidence({
1478
+ detailResult,
1479
+ contentWait,
1480
+ imageEvidence
1481
+ });
1482
+ recordCvNetworkMiss(cvAcquisitionState, {
1483
+ reason: "network_miss_no_capture_node",
1484
+ parsedNetworkProfileCount,
1485
+ waitResult: networkWait
1486
+ });
1487
+ }
1488
+ } else if (fullCvEvidence.network_full_cv_count > 0) {
1489
+ source = "network";
1490
+ recordCvNetworkHit(cvAcquisitionState, {
1491
+ reason: "full_cv_network_profile",
1492
+ parsedNetworkProfileCount,
1493
+ waitResult: networkWait
1494
+ });
1495
+ } else if (fullCvEvidence.dom_full_cv) {
1496
+ source = "dom";
1497
+ if (normalizedDetailSource !== "dom") {
1498
+ recordCvNetworkMiss(cvAcquisitionState, {
1499
+ reason: parsedNetworkProfileCount > 0
1500
+ ? "profile_only_network_dom_fallback"
1501
+ : "network_miss_dom_fallback",
1502
+ parsedNetworkProfileCount,
1503
+ waitResult: networkWait
1504
+ });
1505
+ }
1506
+ } else if (parsedNetworkProfileCount > 0) {
1507
+ source = "profile_only_network";
1508
+ recordCvNetworkMiss(cvAcquisitionState, {
1509
+ reason: "profile_only_network_not_full_cv",
1510
+ parsedNetworkProfileCount,
1511
+ waitResult: networkWait
1512
+ });
1513
+ } else if (normalizedDetailSource !== "dom") {
1514
+ source = "network_miss";
1515
+ recordCvNetworkMiss(cvAcquisitionState, {
1516
+ reason: "network_miss_without_image_fallback",
1517
+ parsedNetworkProfileCount,
1518
+ waitResult: networkWait
1519
+ });
1520
+ }
1521
+
1522
+ if (useLlmScreening && !llmResult) {
1523
+ if (!fullCvEvidence.full_cv_acquired) {
1524
+ detailUnavailableReason = "full_cv_not_acquired";
1525
+ } else {
1526
+ detailStep = "llm_screening";
1527
+ if (!llmConfig) {
1528
+ llmResult = createMissingLlmConfigResult();
1529
+ } else {
1530
+ try {
1531
+ const llmTimingKey = imageEvidence?.file_paths?.length
1532
+ ? "vision_model_ms"
1533
+ : "text_model_ms";
1534
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1535
+ candidate: detailResult.candidate,
1536
+ criteria,
1537
+ config: llmConfig,
1538
+ timeoutMs: llmTimeoutMs,
1539
+ imageEvidence,
1540
+ maxImages: llmImageLimit,
1541
+ imageDetail: llmImageDetail
1542
+ }));
1543
+ } catch (error) {
1544
+ llmResult = createFailedLlmResult(error);
1545
+ }
1546
+ }
1547
+ }
1548
+ }
1549
+
1550
+ let closeResult = null;
1551
+ let closeRecovery = null;
1552
+ if (closeResume) {
1553
+ detailStep = "close_resume_modal";
1554
+ checkpointInProgressCandidate({
1555
+ event: "before_close_resume_modal",
1556
+ source,
1557
+ image_evidence: summarizeImageEvidence(imageEvidence),
1558
+ llm_screening: compactLlmResult(llmResult),
1559
+ full_cv_evidence: fullCvEvidence
1560
+ });
1561
+ closeResult = await measureTiming(timings, "close_detail_ms", () => closeChatResumeModal(client));
1562
+ await maybeHumanActionCooldown("after_detail_close", timings);
1563
+ if (!closeResult?.closed) {
1564
+ closeRecovery = await recoverAndReapplyChatContext(
1565
+ "resume_modal_close_failed:close_resume_modal",
1566
+ makeChatResumeModalOpenBeforeCandidateClickError(closeResult),
1567
+ { forceRefresh: true }
1568
+ );
1569
+ }
1570
+ }
1571
+ detailResult.close_result = closeResult;
1572
+ detailResult.image_evidence = imageEvidence;
1573
+ detailResult.llm_result = llmResult;
1574
+ detailResult.cv_acquisition = {
1575
+ source,
1576
+ mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
1577
+ wait_plan: waitPlan,
1578
+ network_wait: networkWait,
1579
+ selection_network_event_count: selectionNetworkEvents.length,
1580
+ resume_network_event_count: resumeNetworkEvents.length,
1581
+ content_wait: {
1582
+ ok: contentWait.ok,
1583
+ skipped: Boolean(contentWait.skipped),
1584
+ reason: contentWait.reason || null,
1585
+ elapsed_ms: contentWait.elapsed_ms,
1586
+ text_length: contentWait.text_length,
1587
+ timeout_ms: contentWait.timeout_ms ?? contentWait.dom_fallback_plan?.timeout_ms ?? null,
1588
+ configured_timeout_ms: contentWait.configured_timeout_ms
1589
+ ?? contentWait.dom_fallback_plan?.configured_timeout_ms
1590
+ ?? null,
1591
+ short_probe: Boolean(contentWait.short_probe)
1592
+ },
1593
+ parsed_network_profile_count: parsedNetworkProfileCount,
1594
+ image_evidence: summarizeImageEvidence(imageEvidence),
1595
+ capture_target: captureTarget || null,
1596
+ capture_target_wait: captureTargetWait,
1597
+ full_cv_evidence: fullCvEvidence,
1598
+ close_recovery: closeRecovery
1599
+ };
1600
+ }
1601
+ } catch (error) {
1602
+ checkpointInProgressCandidate({
1603
+ event: "detail_error",
1604
+ error: compactChatRuntimeError(error)
1605
+ });
1606
+ if (isForbiddenChatResumeNavigationError(error)) {
1607
+ detailUnavailableReason = "forbidden_top_level_resume_navigation";
1608
+ const recovery = await recoverAndReapplyChatContext(detailUnavailableReason, error);
1609
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1610
+ detailResult.cv_acquisition.recovery = recovery;
1611
+ } else if (isChatResumeModalCloseFailureError(error)) {
1612
+ const recoveryReason = `resume_modal_close_failed:${detailStep}`;
1613
+ const recovery = await recoverAndReapplyChatContext(recoveryReason, error, { forceRefresh: true });
1614
+ checkpointInProgressCandidate({
1615
+ event: "retry_after_modal_recovery",
1616
+ recovery
1617
+ });
1618
+ continue;
1619
+ } else if (isUnsafeChatOnlineResumeLinkError(error)) {
1620
+ detailUnavailableReason = "unsafe_online_resume_navigation_link";
1621
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1622
+ detailResult.cv_acquisition.blocked_pre_click = true;
1623
+ detailResult.cv_acquisition.button_href = error.href || null;
1624
+ detailResult.cv_acquisition.button_selector = error.button_selector || null;
1625
+ detailResult.cv_acquisition.attempts = error.attempts || null;
1626
+ } else {
1627
+ if (!isRecoverableCdpNodeError(error)) throw error;
1628
+ detailUnavailableReason = `recoverable_cdp_node_stale:${detailStep}`;
1629
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1630
+ await closeChatResumeModal(client, { attemptsLimit: 2 });
1631
+ }
1632
+ }
1633
+ screeningCandidate = detailResult.candidate;
1634
+ }
1635
+
1636
+ await runControl.waitIfPaused();
1637
+ runControl.throwIfCanceled();
1638
+ runControl.setPhase("chat:screening");
1639
+ let cardOnlyLlmResult = null;
1640
+ if (useLlmScreening && !detailUnavailableReason && !detailResult?.llm_result) {
1641
+ detailUnavailableReason = detailResult
1642
+ ? "full_cv_not_acquired"
1643
+ : "detail_not_opened_full_cv_required";
1644
+ }
1645
+ const effectiveLlmResult = detailResult?.llm_result || cardOnlyLlmResult;
1646
+ const screening = detailUnavailableReason
1647
+ ? {
1648
+ status: "skip",
1649
+ passed: false,
1650
+ score: 0,
1651
+ reasons: [detailUnavailableReason],
1652
+ candidate: screeningCandidate
1653
+ }
1654
+ : useLlmScreening
1655
+ ? llmToScreening(effectiveLlmResult, screeningCandidate)
1656
+ : screenCandidate(screeningCandidate, { criteria });
1657
+ let postAction = null;
1658
+ if (requestResumeForPassed && screening.passed) {
1659
+ await maybeHumanActionCooldown("before_post_action", timings);
1660
+ postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
1661
+ greetingText,
1662
+ dryRun: dryRunRequestCv
1663
+ }));
1664
+ if (postAction?.requested) requestSatisfiedCount += 1;
1665
+ if (postAction?.skipped) requestSkippedCount += 1;
1666
+ if (postAction?.requested && !postAction?.skipped) requestedCount += 1;
1667
+ if (!postAction?.requested && !postAction?.skipped && !dryRunRequestCv) {
1668
+ throw new Error(`REQUEST_CV_NOT_VERIFIED:${postAction?.reason || "unknown"}`);
1669
+ }
1670
+ }
1671
+ timings.total_ms = Date.now() - candidateStarted;
1672
+ const compactResult = {
1673
+ index,
1674
+ candidate_key: candidateKey,
1675
+ card_node_id: effectiveCardNodeId,
1676
+ candidate: compactCandidate(screeningCandidate),
1677
+ detail: compactDetail(detailResult),
1678
+ llm_screening: detailResult ? null : compactLlmResult(cardOnlyLlmResult),
1679
+ screening: compactScreening(screening),
1680
+ post_action: postAction,
1681
+ pre_action_state: preActionState,
1682
+ timings
1683
+ };
1684
+ results.push(compactResult);
1685
+ markInfiniteListCandidateProcessed(listState, candidateKey, {
1686
+ metadata: {
1687
+ result_index: index,
1688
+ candidate_id: screeningCandidate.id || null
1689
+ }
1690
+ });
1691
+
1692
+ const counters = countChatResultStatuses(results);
1693
+ runControl.updateProgress({
1694
+ card_count: cardNodeIds.length,
1695
+ target_count: passTarget || (processUntilListEnd ? "all" : processedLimit),
1696
+ target_pass_count: passTarget,
1697
+ processed_limit: processedLimit,
1698
+ processed: counters.processed,
1699
+ screened: counters.screened,
1700
+ detail_opened: counters.detail_opened,
1701
+ llm_screened: counters.llm_screened,
1702
+ passed: counters.passed,
1703
+ skipped: counters.skipped,
1704
+ requested: requestedCount,
1705
+ request_satisfied: requestSatisfiedCount,
1706
+ request_skipped: requestSkippedCount,
1707
+ unique_seen: compactInfiniteListState(listState).seen_count,
1708
+ scroll_count: compactInfiniteListState(listState).scroll_count,
1709
+ context_recoveries: contextRecoveryAttempts,
1710
+ list_end_reason: listEndReason || null,
1711
+ viewport_checks: viewportGuard.getStats().checks,
1712
+ viewport_recoveries: viewportGuard.getStats().recoveries,
1713
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1714
+ human_behavior_profile: effectiveHumanBehavior.profile,
1715
+ human_rest_enabled: effectiveHumanRestEnabled,
1716
+ human_rest_count: humanRestController.getState().rest_count,
1717
+ human_rest_ms: humanRestController.getState().total_rest_ms,
1718
+ last_human_event: lastHumanEvent,
1719
+ last_candidate_id: screeningCandidate.id || null,
1720
+ last_candidate_key: candidateKey,
1721
+ last_score: screening.score
1722
+ });
1723
+ const checkpointStarted = Date.now();
1724
+ runControl.checkpoint({
1725
+ results,
1726
+ in_progress_candidate: null,
1727
+ last_candidate: {
1728
+ id: screeningCandidate.id || null,
1729
+ key: candidateKey,
1730
+ identity: screeningCandidate.identity || {},
1731
+ screening: {
1732
+ status: screening.status,
1733
+ passed: screening.passed,
1734
+ score: screening.score
1735
+ },
1736
+ llm_screening: compactLlmResult(effectiveLlmResult)
1737
+ }
1738
+ });
1739
+ addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
1740
+
1741
+ if (effectiveHumanRestEnabled) {
1742
+ const restStarted = Date.now();
1743
+ const restResult = await humanRestController.takeBreakIfNeeded({
1744
+ sleepFn: (ms) => runControl.sleep(ms)
1745
+ });
1746
+ const restElapsed = Date.now() - restStarted;
1747
+ if (restResult.rested) {
1748
+ recordHumanEvent({
1749
+ kind: "rest",
1750
+ pause_ms: restResult.pause_ms || restElapsed,
1751
+ events: restResult.events || []
1752
+ });
1753
+ compactResult.human_rest = restResult;
1754
+ addTiming(compactResult.timings, "human_rest_ms", restElapsed);
1755
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
1756
+ runControl.updateProgress({
1757
+ human_rest_enabled: effectiveHumanRestEnabled,
1758
+ human_rest_count: humanRestController.getState().rest_count,
1759
+ human_rest_ms: humanRestController.getState().total_rest_ms,
1760
+ human_rest_last: restResult,
1761
+ context_recoveries: contextRecoveryAttempts,
1762
+ last_human_event: lastHumanEvent
1763
+ });
1764
+ }
1765
+ }
1766
+
1767
+ if (delayMs > 0) {
1768
+ const sleepStarted = Date.now();
1769
+ await runControl.sleep(delayMs);
1770
+ addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
1771
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
1772
+ }
1773
+ }
1774
+
1775
+ runControl.setPhase("chat:done");
1776
+ const finalCounters = countChatResultStatuses(results);
1777
+ return {
1778
+ domain: "chat",
1779
+ target_url: targetUrl,
1780
+ card_count: cardNodeIds.length,
1781
+ context_setup: contextSetup,
1782
+ candidate_list: compactInfiniteListState(listState),
1783
+ viewport_health: {
1784
+ stats: viewportGuard.getStats(),
1785
+ events: viewportGuard.getEvents()
1786
+ },
1787
+ human_behavior: effectiveHumanBehavior,
1788
+ human_rest: humanRestController.getState(),
1789
+ last_human_event: lastHumanEvent,
1790
+ list_end_reason: listEndReason || null,
1791
+ target_pass_count: passTarget,
1792
+ process_until_list_end: Boolean(processUntilListEnd),
1793
+ processed_limit: processedLimit,
1794
+ detail_source: normalizedDetailSource,
1795
+ processed: finalCounters.processed,
1796
+ screened: finalCounters.screened,
1797
+ detail_opened: finalCounters.detail_opened,
1798
+ llm_screened: finalCounters.llm_screened,
1799
+ passed: finalCounters.passed,
1800
+ skipped: finalCounters.skipped,
1801
+ requested: requestedCount,
1802
+ request_satisfied: requestSatisfiedCount,
1803
+ request_skipped: requestSkippedCount,
1804
+ context_recoveries: contextRecoveryAttempts,
1805
+ results
1806
+ };
1807
+ }
1808
+
1809
+ export function createChatRunService({
1810
+ lifecycle,
1811
+ idPrefix = "chat",
1812
+ workflow = runChatWorkflow,
1813
+ onSnapshot = null
1814
+ } = {}) {
1815
+ const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
1816
+
1817
+ function startChatRun({
1818
+ client,
1819
+ targetUrl = CHAT_TARGET_URL,
1820
+ job = "",
1821
+ startFrom = "all",
1822
+ criteria = "",
1823
+ maxCandidates = 5,
1824
+ targetPassCount = null,
1825
+ processUntilListEnd = false,
1826
+ detailLimit = null,
1827
+ detailSource = "cascade",
1828
+ closeResume = true,
1829
+ requestResumeForPassed = false,
1830
+ dryRunRequestCv = false,
1831
+ greetingText = "Hi同学,能麻烦发下简历吗?",
1832
+ delayMs = 0,
1833
+ cardTimeoutMs = 90000,
1834
+ readyTimeoutMs = 60000,
1835
+ onlineResumeButtonTimeoutMs = 30000,
1836
+ resumeDomTimeoutMs = 60000,
1837
+ maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
1838
+ imageWheelDeltaY = 650,
1839
+ cvAcquisitionMode = "unknown",
1840
+ callLlmOnImage = false,
1841
+ llmConfig = null,
1842
+ llmTimeoutMs = 120000,
1843
+ llmImageLimit = 8,
1844
+ llmImageDetail = "high",
1845
+ screeningMode = "llm",
1846
+ listMaxScrolls = 20,
1847
+ listStableSignatureLimit = 5,
1848
+ listWheelDeltaY = 850,
1849
+ listSettleMs = 2200,
1850
+ listFallbackPoint = null,
1851
+ imageOutputDir = "",
1852
+ humanRestEnabled = false,
1853
+ humanBehavior = null,
1854
+ name = "chat-domain-run"
1855
+ } = {}) {
1856
+ if (!client) throw new Error("startChatRun requires a guarded CDP client");
1857
+ const normalizedDetailSource = normalizeDetailSource(detailSource);
1858
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1859
+ const processedLimit = Math.max(1, Number(maxCandidates) || 1);
1860
+ const normalizedDetailLimit = detailLimit == null ? processedLimit : Math.max(0, Number(detailLimit) || 0);
1861
+ const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1862
+ legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1863
+ });
1864
+ const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1865
+ return manager.startRun({
1866
+ name,
1867
+ context: {
1868
+ domain: "chat",
1869
+ target_url: targetUrl,
1870
+ criteria_present: Boolean(criteria),
1871
+ job,
1872
+ start_from: startFrom,
1873
+ max_candidates: maxCandidates,
1874
+ target_pass_count: targetPassCount,
1875
+ process_until_list_end: Boolean(processUntilListEnd),
1876
+ detail_limit: normalizedDetailLimit,
1877
+ detail_source: normalizedDetailSource,
1878
+ close_resume: closeResume,
1879
+ request_resume_for_passed: Boolean(requestResumeForPassed),
1880
+ dry_run_request_cv: Boolean(dryRunRequestCv),
1881
+ greeting_text: greetingText,
1882
+ cv_acquisition_mode: cvAcquisitionMode,
1883
+ call_llm_on_image: Boolean(callLlmOnImage),
1884
+ screening_mode: normalizedScreeningMode,
1885
+ llm_configured: Boolean(llmConfig),
1886
+ llm_timeout_ms: llmTimeoutMs,
1887
+ llm_image_limit: llmImageLimit,
1888
+ llm_image_detail: llmImageDetail,
1889
+ max_image_pages: maxImagePages,
1890
+ image_wheel_delta_y: imageWheelDeltaY,
1891
+ list_max_scrolls: listMaxScrolls,
1892
+ list_stable_signature_limit: listStableSignatureLimit,
1893
+ list_wheel_delta_y: listWheelDeltaY,
1894
+ list_settle_ms: listSettleMs,
1895
+ list_fallback_point: listFallbackPoint,
1896
+ online_resume_button_timeout_ms: onlineResumeButtonTimeoutMs,
1897
+ image_output_dir: imageOutputDir || "",
1898
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1899
+ human_behavior_profile: effectiveHumanBehavior.profile,
1900
+ human_behavior: effectiveHumanBehavior,
1901
+ human_rest_enabled: effectiveHumanRestEnabled
1902
+ },
1903
+ progress: {
1904
+ card_count: 0,
1905
+ target_count: targetPassCount || (processUntilListEnd ? "all" : processedLimit),
1906
+ target_pass_count: targetPassCount,
1907
+ processed_limit: processedLimit,
1908
+ processed: 0,
1909
+ screened: 0,
1910
+ detail_opened: 0,
1911
+ llm_screened: 0,
1912
+ passed: 0,
1913
+ skipped: 0,
1914
+ requested: 0,
1915
+ request_satisfied: 0,
1916
+ request_skipped: 0,
1917
+ context_recoveries: 0,
1918
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1919
+ human_behavior_profile: effectiveHumanBehavior.profile,
1920
+ human_rest_enabled: effectiveHumanRestEnabled,
1921
+ human_rest_count: 0,
1922
+ human_rest_ms: 0,
1923
+ last_human_event: null
1924
+ },
1925
+ checkpoint: {},
1926
+ task: (runControl) => workflow({
1927
+ client,
1928
+ targetUrl,
1929
+ job,
1930
+ startFrom,
1931
+ criteria,
1932
+ maxCandidates,
1933
+ targetPassCount,
1934
+ processUntilListEnd,
1935
+ detailLimit: normalizedDetailLimit,
1936
+ detailSource: normalizedDetailSource,
1937
+ closeResume,
1938
+ requestResumeForPassed,
1939
+ dryRunRequestCv,
1940
+ greetingText,
1941
+ delayMs,
1942
+ cardTimeoutMs,
1943
+ readyTimeoutMs,
1944
+ onlineResumeButtonTimeoutMs,
1945
+ resumeDomTimeoutMs,
1946
+ maxImagePages,
1947
+ imageWheelDeltaY,
1948
+ cvAcquisitionMode,
1949
+ callLlmOnImage,
1950
+ llmConfig,
1951
+ llmTimeoutMs,
1952
+ llmImageLimit,
1953
+ llmImageDetail,
1954
+ screeningMode: normalizedScreeningMode,
1955
+ listMaxScrolls,
1956
+ listStableSignatureLimit,
1957
+ listWheelDeltaY,
1958
+ listSettleMs,
1959
+ listFallbackPoint,
1960
+ imageOutputDir,
1961
+ humanRestEnabled: effectiveHumanRestEnabled,
1962
+ humanBehavior: effectiveHumanBehavior
1963
+ }, runControl)
1964
+ });
1965
+ }
1966
+
1967
+ return {
1968
+ startChatRun,
1969
+ getChatRun: manager.getRun,
1970
+ pauseChatRun: manager.pauseRun,
1971
+ resumeChatRun: manager.resumeRun,
1972
+ cancelChatRun: manager.cancelRun,
1973
+ waitForChatRun: manager.waitForRun,
1974
+ listChatRuns: manager.listRuns,
1975
+ manager
1976
+ };
1977
+ }