@reconcrap/boss-recommend-mcp 2.0.52 → 2.0.54

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