@reconcrap/boss-recommend-mcp 2.0.17 → 2.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/chat-mcp.js +13 -1
- package/src/core/capture/index.js +264 -12
- package/src/core/cv-acquisition/index.js +4 -0
- package/src/domains/chat/jobs.js +170 -7
- package/src/domains/chat/run-service.js +222 -22
package/package.json
CHANGED
package/src/chat-mcp.js
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import {
|
|
35
35
|
CHAT_TARGET_URL,
|
|
36
36
|
closeChatResumeModal,
|
|
37
|
+
closeChatJobDropdown,
|
|
37
38
|
createChatRunService,
|
|
38
39
|
getChatRoots,
|
|
39
40
|
isForbiddenChatResumeTopLevelUrl,
|
|
@@ -833,7 +834,18 @@ async function connectChatChromeSession({
|
|
|
833
834
|
|
|
834
835
|
async function readChatJobOptionsFromSession(session) {
|
|
835
836
|
const roots = await getChatRoots(session.client);
|
|
836
|
-
|
|
837
|
+
const result = await readChatJobOptions(session.client, roots.rootNodes.top);
|
|
838
|
+
try {
|
|
839
|
+
result.menu_close = await closeChatJobDropdown(session.client, roots.rootNodes.top);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
result.menu_close = {
|
|
842
|
+
ok: false,
|
|
843
|
+
closed: false,
|
|
844
|
+
reason: "close_failed",
|
|
845
|
+
error: error?.message || String(error)
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
return result;
|
|
837
849
|
}
|
|
838
850
|
|
|
839
851
|
function normalizeChatStartInput(args = {}, configResolution = null) {
|
|
@@ -246,6 +246,190 @@ function pickEvenly(items = [], limit = 1) {
|
|
|
246
246
|
return Array.from(new Map(picked.map((item) => [item.node_id, item])).values());
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
function patternLabel(pattern) {
|
|
250
|
+
if (pattern instanceof RegExp) return pattern.source;
|
|
251
|
+
return normalizeText(pattern);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function stopBoundaryPatterns(patterns = []) {
|
|
255
|
+
return (Array.isArray(patterns) ? patterns : [patterns])
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
.map((pattern) => {
|
|
258
|
+
if (pattern instanceof RegExp) {
|
|
259
|
+
return {
|
|
260
|
+
raw: pattern,
|
|
261
|
+
label: pattern.source,
|
|
262
|
+
matches: (text) => pattern.test(text)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const normalized = normalizeText(pattern);
|
|
266
|
+
return {
|
|
267
|
+
raw: pattern,
|
|
268
|
+
label: normalized,
|
|
269
|
+
matches: (text) => normalized && text.includes(normalized)
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function collectStopBoundaryNodes(client, rootNodeId, {
|
|
275
|
+
selector = "",
|
|
276
|
+
textPatterns = [],
|
|
277
|
+
maxProbeNodes = 180,
|
|
278
|
+
maxTextLength = 700,
|
|
279
|
+
stepTimeoutMs = 45000
|
|
280
|
+
} = {}) {
|
|
281
|
+
const patterns = stopBoundaryPatterns(textPatterns);
|
|
282
|
+
const normalizedSelector = normalizeText(selector);
|
|
283
|
+
if (!normalizedSelector && !patterns.length) {
|
|
284
|
+
return {
|
|
285
|
+
enabled: false,
|
|
286
|
+
ok: false,
|
|
287
|
+
reason: "not_configured",
|
|
288
|
+
nodes: []
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const started = Date.now();
|
|
292
|
+
let nodeIds = [];
|
|
293
|
+
try {
|
|
294
|
+
nodeIds = uniqueNumbers(await querySelectorAll(
|
|
295
|
+
client,
|
|
296
|
+
rootNodeId,
|
|
297
|
+
normalizedSelector || DEFAULT_SCROLL_ANCHOR_SELECTOR
|
|
298
|
+
));
|
|
299
|
+
} catch (error) {
|
|
300
|
+
return {
|
|
301
|
+
enabled: true,
|
|
302
|
+
ok: false,
|
|
303
|
+
reason: "query_selector_all_failed",
|
|
304
|
+
selector: normalizedSelector || DEFAULT_SCROLL_ANCHOR_SELECTOR,
|
|
305
|
+
error: error?.message || String(error),
|
|
306
|
+
nodes: []
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const probeLimit = Math.max(1, Number(maxProbeNodes) || 180);
|
|
311
|
+
const maxStopTextLength = Math.max(40, Number(maxTextLength) || 700);
|
|
312
|
+
const perNodeTimeoutMs = Math.min(1000, Math.max(200, Math.floor((Number(stepTimeoutMs) || 45000) / 40)));
|
|
313
|
+
const nodes = [];
|
|
314
|
+
for (const nodeId of nodeIds.slice(0, probeLimit)) {
|
|
315
|
+
try {
|
|
316
|
+
let text = "";
|
|
317
|
+
let matchedPattern = null;
|
|
318
|
+
if (patterns.length) {
|
|
319
|
+
const outerHTML = await withCaptureTimeout(getOuterHTML(client, nodeId), {
|
|
320
|
+
label: `stop_boundary_html_${nodeId}`,
|
|
321
|
+
timeoutMs: perNodeTimeoutMs
|
|
322
|
+
});
|
|
323
|
+
text = normalizeText(htmlToText(outerHTML));
|
|
324
|
+
if (!text || text.length > maxStopTextLength) continue;
|
|
325
|
+
matchedPattern = patterns.find((pattern) => pattern.matches(text));
|
|
326
|
+
if (!matchedPattern) continue;
|
|
327
|
+
}
|
|
328
|
+
nodes.push({
|
|
329
|
+
node_id: nodeId,
|
|
330
|
+
text_preview: text.slice(0, 120),
|
|
331
|
+
matched_pattern: matchedPattern ? patternLabel(matchedPattern.raw) : null
|
|
332
|
+
});
|
|
333
|
+
} catch {}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
enabled: true,
|
|
338
|
+
ok: nodes.length > 0,
|
|
339
|
+
reason: nodes.length ? null : "no_matching_stop_boundary_nodes",
|
|
340
|
+
selector: normalizedSelector || DEFAULT_SCROLL_ANCHOR_SELECTOR,
|
|
341
|
+
elapsed_ms: Date.now() - started,
|
|
342
|
+
discovered_node_count: nodeIds.length,
|
|
343
|
+
probed_node_count: Math.min(nodeIds.length, probeLimit),
|
|
344
|
+
match_count: nodes.length,
|
|
345
|
+
pattern_labels: patterns.map((pattern) => pattern.label),
|
|
346
|
+
nodes
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function resolveVisibleStopBoundary(client, stopBoundaryPlan, clip, {
|
|
351
|
+
topPadding = 8,
|
|
352
|
+
minCaptureHeight = 180,
|
|
353
|
+
stepTimeoutMs = 45000
|
|
354
|
+
} = {}) {
|
|
355
|
+
if (!stopBoundaryPlan?.nodes?.length || !clip) return null;
|
|
356
|
+
const clipTop = Number(clip.y) || 0;
|
|
357
|
+
const clipBottom = clipTop + (Number(clip.height) || 0);
|
|
358
|
+
const safePadding = Math.max(0, Number(topPadding) || 0);
|
|
359
|
+
const safeMinHeight = Math.max(1, Number(minCaptureHeight) || 180);
|
|
360
|
+
const perNodeTimeoutMs = Math.min(900, Math.max(180, Math.floor((Number(stepTimeoutMs) || 45000) / 50)));
|
|
361
|
+
const visible = [];
|
|
362
|
+
|
|
363
|
+
for (const node of stopBoundaryPlan.nodes) {
|
|
364
|
+
try {
|
|
365
|
+
const box = await withCaptureTimeout(getNodeBox(client, node.node_id), {
|
|
366
|
+
label: `stop_boundary_box_${node.node_id}`,
|
|
367
|
+
timeoutMs: perNodeTimeoutMs
|
|
368
|
+
});
|
|
369
|
+
const rect = box?.rect || {};
|
|
370
|
+
const width = Number(rect.width) || 0;
|
|
371
|
+
const height = Number(rect.height) || 0;
|
|
372
|
+
if (width < 40 || height < 6) continue;
|
|
373
|
+
const top = Number(rect.y) || 0;
|
|
374
|
+
const bottom = top + height;
|
|
375
|
+
if (bottom <= clipTop + 1) {
|
|
376
|
+
return {
|
|
377
|
+
action: "stop_before_capture",
|
|
378
|
+
reason: "stop_boundary_above_clip",
|
|
379
|
+
node_id: node.node_id,
|
|
380
|
+
matched_pattern: node.matched_pattern,
|
|
381
|
+
text_preview: node.text_preview,
|
|
382
|
+
rect,
|
|
383
|
+
clip
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (top < clipBottom && bottom > clipTop) {
|
|
387
|
+
visible.push({
|
|
388
|
+
...node,
|
|
389
|
+
rect,
|
|
390
|
+
top,
|
|
391
|
+
bottom
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
} catch {}
|
|
395
|
+
}
|
|
396
|
+
if (!visible.length) return null;
|
|
397
|
+
|
|
398
|
+
visible.sort((a, b) => a.top - b.top);
|
|
399
|
+
const boundary = visible[0];
|
|
400
|
+
const boundaryY = Math.max(clipTop, boundary.top - safePadding);
|
|
401
|
+
const adjustedHeight = Math.max(0, boundaryY - clipTop);
|
|
402
|
+
if (adjustedHeight < safeMinHeight) {
|
|
403
|
+
return {
|
|
404
|
+
action: "stop_before_capture",
|
|
405
|
+
reason: "stop_boundary_near_clip_top",
|
|
406
|
+
node_id: boundary.node_id,
|
|
407
|
+
matched_pattern: boundary.matched_pattern,
|
|
408
|
+
text_preview: boundary.text_preview,
|
|
409
|
+
rect: boundary.rect,
|
|
410
|
+
clip,
|
|
411
|
+
adjusted_height: adjustedHeight,
|
|
412
|
+
min_capture_height: safeMinHeight
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
action: "capture_then_stop",
|
|
418
|
+
reason: "stop_boundary_visible",
|
|
419
|
+
node_id: boundary.node_id,
|
|
420
|
+
matched_pattern: boundary.matched_pattern,
|
|
421
|
+
text_preview: boundary.text_preview,
|
|
422
|
+
rect: boundary.rect,
|
|
423
|
+
clip,
|
|
424
|
+
adjusted_clip: {
|
|
425
|
+
...clip,
|
|
426
|
+
height: adjustedHeight
|
|
427
|
+
},
|
|
428
|
+
adjusted_height: adjustedHeight,
|
|
429
|
+
min_capture_height: safeMinHeight
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
249
433
|
async function collectDomScrollAnchors(client, rootNodeId, {
|
|
250
434
|
selector = DEFAULT_SCROLL_ANCHOR_SELECTOR,
|
|
251
435
|
maxScreenshots = 6,
|
|
@@ -525,6 +709,12 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
525
709
|
scrollAnchorSelector = DEFAULT_SCROLL_ANCHOR_SELECTOR,
|
|
526
710
|
scrollAnchorMaxProbeNodes = 260,
|
|
527
711
|
scrollAnchorMinGap = 180,
|
|
712
|
+
stopBoundarySelector = "",
|
|
713
|
+
stopBoundaryTextPatterns = [],
|
|
714
|
+
stopBoundaryMaxProbeNodes = 180,
|
|
715
|
+
stopBoundaryMaxTextLength = 700,
|
|
716
|
+
stopBoundaryTopPadding = 8,
|
|
717
|
+
stopBoundaryMinCaptureHeight = 180,
|
|
528
718
|
metadata = {}
|
|
529
719
|
} = {}) {
|
|
530
720
|
if (!nodeId) throw new Error("captureScrolledNodeScreenshots requires nodeId");
|
|
@@ -540,11 +730,26 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
540
730
|
stepTimeoutMs
|
|
541
731
|
})
|
|
542
732
|
: null;
|
|
733
|
+
const stopBoundaryEnabled = Boolean(
|
|
734
|
+
normalizeText(stopBoundarySelector)
|
|
735
|
+
|| (Array.isArray(stopBoundaryTextPatterns)
|
|
736
|
+
? stopBoundaryTextPatterns.length
|
|
737
|
+
: stopBoundaryTextPatterns)
|
|
738
|
+
);
|
|
739
|
+
let stopBoundaryPlan = {
|
|
740
|
+
enabled: false,
|
|
741
|
+
ok: false,
|
|
742
|
+
reason: "not_configured",
|
|
743
|
+
nodes: []
|
|
744
|
+
};
|
|
745
|
+
const stopBoundaryChecks = [];
|
|
543
746
|
const screenshots = [];
|
|
544
747
|
let consecutiveDuplicates = 0;
|
|
545
748
|
let previousHash = "";
|
|
546
749
|
let captureCount = 0;
|
|
547
750
|
let droppedDuplicateCount = 0;
|
|
751
|
+
let forceInputScrollAfterDuplicate = false;
|
|
752
|
+
let stopBoundaryResult = null;
|
|
548
753
|
let currentScrollMetadata = {
|
|
549
754
|
before_capture: "initial",
|
|
550
755
|
method: normalizedScrollMethod,
|
|
@@ -597,7 +802,37 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
597
802
|
timeoutMs: stepTimeoutMs
|
|
598
803
|
});
|
|
599
804
|
const clip = withPadding(box.rect, padding);
|
|
600
|
-
|
|
805
|
+
let visibleStopBoundary = null;
|
|
806
|
+
if (stopBoundaryEnabled) {
|
|
807
|
+
stopBoundaryPlan = await collectStopBoundaryNodes(client, nodeId, {
|
|
808
|
+
selector: stopBoundarySelector,
|
|
809
|
+
textPatterns: stopBoundaryTextPatterns,
|
|
810
|
+
maxProbeNodes: stopBoundaryMaxProbeNodes,
|
|
811
|
+
maxTextLength: stopBoundaryMaxTextLength,
|
|
812
|
+
stepTimeoutMs
|
|
813
|
+
});
|
|
814
|
+
stopBoundaryChecks.push({
|
|
815
|
+
capture_index: index,
|
|
816
|
+
ok: Boolean(stopBoundaryPlan.ok),
|
|
817
|
+
reason: stopBoundaryPlan.reason || null,
|
|
818
|
+
discovered_node_count: stopBoundaryPlan.discovered_node_count || 0,
|
|
819
|
+
probed_node_count: stopBoundaryPlan.probed_node_count || 0,
|
|
820
|
+
match_count: stopBoundaryPlan.match_count || 0,
|
|
821
|
+
elapsed_ms: stopBoundaryPlan.elapsed_ms || 0
|
|
822
|
+
});
|
|
823
|
+
visibleStopBoundary = await resolveVisibleStopBoundary(client, stopBoundaryPlan, clip, {
|
|
824
|
+
topPadding: stopBoundaryTopPadding,
|
|
825
|
+
minCaptureHeight: stopBoundaryMinCaptureHeight,
|
|
826
|
+
stepTimeoutMs
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
if (visibleStopBoundary?.action === "stop_before_capture") {
|
|
830
|
+
stopBoundaryResult = visibleStopBoundary;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
const effectiveClip = visibleStopBoundary?.adjusted_clip || clip;
|
|
834
|
+
const effectiveCaptureViewport = Boolean(captureViewport && !visibleStopBoundary?.adjusted_clip);
|
|
835
|
+
const captureOptions = effectiveCaptureViewport ? {
|
|
601
836
|
format,
|
|
602
837
|
fromSurface,
|
|
603
838
|
captureBeyondViewport: false
|
|
@@ -605,7 +840,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
605
840
|
format,
|
|
606
841
|
fromSurface,
|
|
607
842
|
captureBeyondViewport,
|
|
608
|
-
clip
|
|
843
|
+
clip: effectiveClip
|
|
609
844
|
};
|
|
610
845
|
if (quality != null) {
|
|
611
846
|
captureOptions.quality = quality;
|
|
@@ -658,16 +893,30 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
658
893
|
file_path: outputPath,
|
|
659
894
|
sha256: hash,
|
|
660
895
|
duplicate_of_previous: Boolean(duplicateOfPrevious),
|
|
661
|
-
clip,
|
|
662
|
-
capture_viewport:
|
|
896
|
+
clip: effectiveClip,
|
|
897
|
+
capture_viewport: effectiveCaptureViewport,
|
|
663
898
|
node_rect: box.rect,
|
|
664
899
|
scroll: currentScrollMetadata,
|
|
900
|
+
stop_boundary: visibleStopBoundary || null,
|
|
665
901
|
metadata
|
|
666
902
|
});
|
|
667
903
|
}
|
|
668
904
|
|
|
905
|
+
if (visibleStopBoundary?.action === "capture_then_stop") {
|
|
906
|
+
stopBoundaryResult = visibleStopBoundary;
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
|
|
669
910
|
previousHash = hash;
|
|
670
|
-
|
|
911
|
+
forceInputScrollAfterDuplicate = Boolean(
|
|
912
|
+
duplicateOfPrevious
|
|
913
|
+
&& normalizedScrollMethod === "dom-anchor-fallback-input"
|
|
914
|
+
&& currentScrollMetadata?.method === "DOM.scrollIntoViewIfNeeded"
|
|
915
|
+
);
|
|
916
|
+
if (
|
|
917
|
+
consecutiveDuplicates >= Math.max(1, Number(duplicateStopCount) || 1)
|
|
918
|
+
&& !forceInputScrollAfterDuplicate
|
|
919
|
+
) {
|
|
671
920
|
break;
|
|
672
921
|
}
|
|
673
922
|
|
|
@@ -675,7 +924,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
675
924
|
assertCaptureTotalBudget(sequenceStarted, totalTimeoutMs, `scroll_after_page_${index + 1}`);
|
|
676
925
|
let scrolledByDomAnchor = false;
|
|
677
926
|
const nextAnchor = anchorPlan?.anchors?.[index + 1] || null;
|
|
678
|
-
if (nextAnchor?.node_id && normalizedScrollMethod !== "input") {
|
|
927
|
+
if (nextAnchor?.node_id && normalizedScrollMethod !== "input" && !forceInputScrollAfterDuplicate) {
|
|
679
928
|
try {
|
|
680
929
|
await scrollDomAnchorIntoView(client, nextAnchor.node_id, {
|
|
681
930
|
label: `scroll_dom_anchor_${index + 1}`,
|
|
@@ -782,12 +1031,15 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
782
1031
|
step_timeout_ms: Math.max(0, Number(stepTimeoutMs) || 0),
|
|
783
1032
|
total_timeout_ms: Math.max(0, Number(totalTimeoutMs) || 0),
|
|
784
1033
|
scroll_method: normalizedScrollMethod,
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1034
|
+
scroll_anchor_selector: scrollAnchorSelector,
|
|
1035
|
+
scroll_anchor_max_probe_nodes: Math.max(1, Number(scrollAnchorMaxProbeNodes) || 260),
|
|
1036
|
+
scroll_anchor_min_gap: Math.max(0, Number(scrollAnchorMinGap) || 0)
|
|
1037
|
+
},
|
|
1038
|
+
scroll_anchor_plan: anchorPlan,
|
|
1039
|
+
stop_boundary_plan: stopBoundaryPlan,
|
|
1040
|
+
stop_boundary_checks: stopBoundaryChecks,
|
|
1041
|
+
stop_boundary_result: stopBoundaryResult,
|
|
1042
|
+
file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
|
|
791
1043
|
screenshots,
|
|
792
1044
|
metadata
|
|
793
1045
|
};
|
|
@@ -138,6 +138,10 @@ export function summarizeImageEvidence(imageEvidence = null) {
|
|
|
138
138
|
llm_original_total_byte_length: imageEvidence.llm_original_total_byte_length || 0,
|
|
139
139
|
llm_composition_error: imageEvidence.llm_composition_error || null,
|
|
140
140
|
optimization: imageEvidence.optimization || null,
|
|
141
|
+
scroll_anchor_plan: imageEvidence.scroll_anchor_plan || null,
|
|
142
|
+
stop_boundary_plan: imageEvidence.stop_boundary_plan || null,
|
|
143
|
+
stop_boundary_checks: imageEvidence.stop_boundary_checks || [],
|
|
144
|
+
stop_boundary_result: imageEvidence.stop_boundary_result || null,
|
|
141
145
|
error_code: imageEvidence.error_code || imageEvidence.code || null,
|
|
142
146
|
error: imageEvidence.error || null,
|
|
143
147
|
file_paths: imageEvidence.file_paths || [],
|
package/src/domains/chat/jobs.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getAttributesMap,
|
|
5
5
|
getNodeBox,
|
|
6
6
|
getOuterHTML,
|
|
7
|
+
pressKey,
|
|
7
8
|
querySelector,
|
|
8
9
|
querySelectorAll,
|
|
9
10
|
sleep
|
|
@@ -256,6 +257,80 @@ async function clickFirstVisible(client, rootNodeId, selectors = []) {
|
|
|
256
257
|
};
|
|
257
258
|
}
|
|
258
259
|
|
|
260
|
+
async function openChatJobDropdown(client, rootNodeId, {
|
|
261
|
+
timeoutMs = 12000,
|
|
262
|
+
intervalMs = 300,
|
|
263
|
+
settleMs = 800
|
|
264
|
+
} = {}) {
|
|
265
|
+
const started = Date.now();
|
|
266
|
+
const triedPoints = new Set();
|
|
267
|
+
const attempts = [];
|
|
268
|
+
for (const selector of CHAT_JOB_TRIGGER_SELECTORS) {
|
|
269
|
+
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
270
|
+
const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
|
|
271
|
+
for (const nodeId of nodeIds) {
|
|
272
|
+
try {
|
|
273
|
+
const box = await getNodeBox(client, nodeId);
|
|
274
|
+
if (box.rect.width <= 2 || box.rect.height <= 2) continue;
|
|
275
|
+
const y = box.center.y;
|
|
276
|
+
const xCandidates = [
|
|
277
|
+
["center", box.center.x],
|
|
278
|
+
["right_12", box.rect.x + box.rect.width - 12],
|
|
279
|
+
["right_44", box.rect.x + box.rect.width - 44],
|
|
280
|
+
["right_64", box.rect.x + box.rect.width - 64]
|
|
281
|
+
].filter(([, x]) => x > box.rect.x + 4 && x < box.rect.x + box.rect.width - 4);
|
|
282
|
+
for (const [pointName, x] of xCandidates) {
|
|
283
|
+
const pointKey = `${nodeId}:${Math.round(x)}:${Math.round(y)}`;
|
|
284
|
+
if (triedPoints.has(pointKey)) continue;
|
|
285
|
+
triedPoints.add(pointKey);
|
|
286
|
+
await clickPoint(client, x, y);
|
|
287
|
+
if (settleMs > 0) await sleep(Math.min(settleMs, 800));
|
|
288
|
+
const remaining = Math.max(300, timeoutMs - (Date.now() - started));
|
|
289
|
+
const optionsResult = await waitForChatJobOptions(client, currentRootNodeId, {
|
|
290
|
+
timeoutMs: Math.min(remaining, 1800),
|
|
291
|
+
intervalMs,
|
|
292
|
+
requireVisible: true
|
|
293
|
+
});
|
|
294
|
+
const visibleCount = (optionsResult.job_options || []).filter((option) => option.visible).length;
|
|
295
|
+
const attempt = {
|
|
296
|
+
clicked: true,
|
|
297
|
+
selector,
|
|
298
|
+
node_id: nodeId,
|
|
299
|
+
point: pointName,
|
|
300
|
+
center: { x, y },
|
|
301
|
+
visible_option_count: visibleCount
|
|
302
|
+
};
|
|
303
|
+
attempts.push(attempt);
|
|
304
|
+
if (visibleCount > 0) {
|
|
305
|
+
return {
|
|
306
|
+
...attempt,
|
|
307
|
+
attempts,
|
|
308
|
+
options_result: optionsResult
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (Date.now() - started > timeoutMs) break;
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
attempts.push({
|
|
315
|
+
clicked: false,
|
|
316
|
+
selector,
|
|
317
|
+
node_id: nodeId,
|
|
318
|
+
error: error?.message || String(error)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (Date.now() - started > timeoutMs) break;
|
|
322
|
+
}
|
|
323
|
+
if (Date.now() - started > timeoutMs) break;
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
clicked: attempts.some((attempt) => attempt.clicked),
|
|
327
|
+
selector: attempts.find((attempt) => attempt.clicked)?.selector || "",
|
|
328
|
+
node_id: attempts.find((attempt) => attempt.clicked)?.node_id || 0,
|
|
329
|
+
attempts,
|
|
330
|
+
options_result: null
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
259
334
|
async function waitForChatJobOptions(client, rootNodeId, {
|
|
260
335
|
timeoutMs = 12000,
|
|
261
336
|
intervalMs = 300,
|
|
@@ -312,6 +387,83 @@ async function waitForSelectedChatJob(client, rootNodeId, jobLabel = "", {
|
|
|
312
387
|
};
|
|
313
388
|
}
|
|
314
389
|
|
|
390
|
+
async function visibleChatJobOptions(client, rootNodeId) {
|
|
391
|
+
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
392
|
+
const visible = [];
|
|
393
|
+
for (const selector of CHAT_JOB_OPTION_SELECTORS) {
|
|
394
|
+
const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
|
|
395
|
+
for (const nodeId of nodeIds) {
|
|
396
|
+
try {
|
|
397
|
+
const box = await getNodeBox(client, nodeId);
|
|
398
|
+
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
399
|
+
visible.push({
|
|
400
|
+
selector,
|
|
401
|
+
node_id: nodeId,
|
|
402
|
+
center: box.center,
|
|
403
|
+
rect: box.rect
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
// Hidden job options are normal when the dropdown is closed.
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return visible;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function closeChatJobDropdown(client, rootNodeId, {
|
|
415
|
+
settleMs = 180
|
|
416
|
+
} = {}) {
|
|
417
|
+
const before = await visibleChatJobOptions(client, rootNodeId);
|
|
418
|
+
if (!before.length) {
|
|
419
|
+
return {
|
|
420
|
+
ok: true,
|
|
421
|
+
closed: false,
|
|
422
|
+
reason: "already_closed",
|
|
423
|
+
visible_before_count: 0,
|
|
424
|
+
visible_after_count: 0
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (typeof client?.Input?.dispatchKeyEvent !== "function") {
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
closed: false,
|
|
431
|
+
reason: "dispatch_key_unavailable",
|
|
432
|
+
visible_before_count: before.length,
|
|
433
|
+
visible_after_count: before.length
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
await pressKey(client, "Escape", {
|
|
437
|
+
code: "Escape",
|
|
438
|
+
windowsVirtualKeyCode: 27,
|
|
439
|
+
nativeVirtualKeyCode: 27
|
|
440
|
+
});
|
|
441
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
442
|
+
const after = await visibleChatJobOptions(client, rootNodeId);
|
|
443
|
+
return {
|
|
444
|
+
ok: after.length === 0,
|
|
445
|
+
closed: after.length === 0,
|
|
446
|
+
reason: after.length ? "still_visible_after_escape" : "escape",
|
|
447
|
+
visible_before_count: before.length,
|
|
448
|
+
visible_after_count: after.length,
|
|
449
|
+
first_visible_before: before[0] || null,
|
|
450
|
+
first_visible_after: after[0] || null
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function closeChatJobDropdownQuietly(client, rootNodeId, settleMs = 180) {
|
|
455
|
+
try {
|
|
456
|
+
return await closeChatJobDropdown(client, rootNodeId, { settleMs });
|
|
457
|
+
} catch (error) {
|
|
458
|
+
return {
|
|
459
|
+
ok: false,
|
|
460
|
+
closed: false,
|
|
461
|
+
reason: "close_failed",
|
|
462
|
+
error: error?.message || String(error)
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
315
467
|
export async function selectChatJob(client, rootNodeId, {
|
|
316
468
|
jobLabel = "",
|
|
317
469
|
timeoutMs = 12000,
|
|
@@ -340,6 +492,7 @@ export async function selectChatJob(client, rootNodeId, {
|
|
|
340
492
|
|| selectedLabelMatches(optionsResult.selected_label, requested)
|
|
341
493
|
)
|
|
342
494
|
) {
|
|
495
|
+
const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
|
|
343
496
|
return {
|
|
344
497
|
selected: true,
|
|
345
498
|
verified: true,
|
|
@@ -347,34 +500,41 @@ export async function selectChatJob(client, rootNodeId, {
|
|
|
347
500
|
requested,
|
|
348
501
|
selected_option: matched,
|
|
349
502
|
options: optionsResult.job_options || [],
|
|
350
|
-
selected_label: optionsResult.selected_label || matched.label
|
|
503
|
+
selected_label: optionsResult.selected_label || matched.label,
|
|
504
|
+
menu_close: menuClose
|
|
351
505
|
};
|
|
352
506
|
}
|
|
353
507
|
|
|
354
508
|
if (!matched || !matched.visible) {
|
|
355
509
|
const triggerRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
|
|
356
|
-
const trigger = await
|
|
357
|
-
|
|
510
|
+
const trigger = await openChatJobDropdown(client, triggerRootNodeId, {
|
|
511
|
+
timeoutMs,
|
|
512
|
+
intervalMs,
|
|
513
|
+
settleMs
|
|
514
|
+
});
|
|
358
515
|
currentRootNodeId = await freshTopRootNodeId(client, triggerRootNodeId);
|
|
359
|
-
optionsResult = await waitForChatJobOptions(client, currentRootNodeId, {
|
|
516
|
+
optionsResult = trigger.options_result || await waitForChatJobOptions(client, currentRootNodeId, {
|
|
360
517
|
timeoutMs,
|
|
361
518
|
intervalMs,
|
|
362
519
|
requireVisible: true
|
|
363
520
|
});
|
|
364
521
|
matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
|
|
365
522
|
if (!matched || !matched.visible) {
|
|
523
|
+
const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
|
|
366
524
|
return {
|
|
367
525
|
selected: false,
|
|
368
526
|
reason: matched ? "job_option_not_visible" : "job_option_not_found",
|
|
369
527
|
requested,
|
|
370
528
|
trigger,
|
|
371
529
|
options: optionsResult.job_options || [],
|
|
372
|
-
selected_label_before: optionsResult.selected_label || ""
|
|
530
|
+
selected_label_before: optionsResult.selected_label || "",
|
|
531
|
+
menu_close: menuClose
|
|
373
532
|
};
|
|
374
533
|
}
|
|
375
534
|
}
|
|
376
535
|
|
|
377
536
|
if (matched.active || normalizeJobText(optionsResult.selected_label).toLowerCase() === normalizeJobText(matched.label).toLowerCase()) {
|
|
537
|
+
const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
|
|
378
538
|
return {
|
|
379
539
|
selected: true,
|
|
380
540
|
verified: true,
|
|
@@ -382,7 +542,8 @@ export async function selectChatJob(client, rootNodeId, {
|
|
|
382
542
|
requested,
|
|
383
543
|
selected_option: matched,
|
|
384
544
|
options: optionsResult.job_options || [],
|
|
385
|
-
selected_label: optionsResult.selected_label || matched.label
|
|
545
|
+
selected_label: optionsResult.selected_label || matched.label,
|
|
546
|
+
menu_close: menuClose
|
|
386
547
|
};
|
|
387
548
|
}
|
|
388
549
|
|
|
@@ -408,6 +569,7 @@ export async function selectChatJob(client, rootNodeId, {
|
|
|
408
569
|
const selectedLabel = normalizeJobText(after.selected_label || "");
|
|
409
570
|
const activeMatch = activeMatchingJobOption(after.job_options || [], matched.label);
|
|
410
571
|
const verified = Boolean(verification.verified || selectedLabelMatches(selectedLabel, matched.label) || activeMatch);
|
|
572
|
+
const menuClose = await closeChatJobDropdownQuietly(client, afterRootNodeId, Math.min(settleMs, 300));
|
|
411
573
|
|
|
412
574
|
return {
|
|
413
575
|
selected: verified,
|
|
@@ -420,6 +582,7 @@ export async function selectChatJob(client, rootNodeId, {
|
|
|
420
582
|
options: after.job_options || optionsResult.job_options || [],
|
|
421
583
|
selected_label: selectedLabel,
|
|
422
584
|
before: optionsResult,
|
|
423
|
-
after
|
|
585
|
+
after,
|
|
586
|
+
menu_close: menuClose
|
|
424
587
|
};
|
|
425
588
|
}
|
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
createInfiniteListState,
|
|
22
22
|
detectInfiniteListBottomMarker,
|
|
23
23
|
getNextInfiniteListCandidate,
|
|
24
|
-
markInfiniteListCandidateProcessed
|
|
24
|
+
markInfiniteListCandidateProcessed,
|
|
25
|
+
resetInfiniteListForRefreshRound
|
|
25
26
|
} from "../../core/infinite-list/index.js";
|
|
26
27
|
import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
27
28
|
import { createRunLifecycleManager } from "../../core/run/index.js";
|
|
@@ -117,6 +118,22 @@ function compactCandidate(candidate) {
|
|
|
117
118
|
};
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
function compactChatJobGuard(result = null) {
|
|
122
|
+
if (!result || typeof result !== "object") return null;
|
|
123
|
+
return {
|
|
124
|
+
selected: Boolean(result.selected),
|
|
125
|
+
verified: Boolean(result.verified),
|
|
126
|
+
already_current: Boolean(result.already_current),
|
|
127
|
+
requested: result.requested || null,
|
|
128
|
+
reason: result.reason || null,
|
|
129
|
+
selected_label: result.selected_label || result.selected_option?.label || null,
|
|
130
|
+
selected_value: result.selected_option?.value || result.active_option?.value || null,
|
|
131
|
+
active_label: result.active_option?.label || null,
|
|
132
|
+
active_value: result.active_option?.value || null,
|
|
133
|
+
menu_close: result.menu_close || null
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
function compactDetail(detailResult) {
|
|
121
138
|
if (!detailResult) return null;
|
|
122
139
|
return {
|
|
@@ -136,6 +153,11 @@ function resultOpenedDetail(result) {
|
|
|
136
153
|
return Boolean(result?.detail && !result.detail?.cv_acquisition?.skipped);
|
|
137
154
|
}
|
|
138
155
|
|
|
156
|
+
export function chatDetailSkipReasonFromReadyState(state = {}) {
|
|
157
|
+
if (state?.attachment_resume_enabled) return "attachment_resume_already_available";
|
|
158
|
+
return "";
|
|
159
|
+
}
|
|
160
|
+
|
|
139
161
|
function llmToScreening(llmResult, candidate) {
|
|
140
162
|
return {
|
|
141
163
|
status: llmResult?.passed ? "pass" : "fail",
|
|
@@ -146,13 +168,71 @@ function llmToScreening(llmResult, candidate) {
|
|
|
146
168
|
};
|
|
147
169
|
}
|
|
148
170
|
|
|
149
|
-
function captureNodeIdFromResumeState(resumeState) {
|
|
150
|
-
return resumeState?.
|
|
151
|
-
|| resumeState?.
|
|
171
|
+
export function captureNodeIdFromResumeState(resumeState) {
|
|
172
|
+
return resumeState?.popup?.node_id
|
|
173
|
+
|| resumeState?.content?.node_id
|
|
152
174
|
|| resumeState?.resumeIframe?.node_id
|
|
153
175
|
|| null;
|
|
154
176
|
}
|
|
155
177
|
|
|
178
|
+
export function resolveChatDomFallbackWait({
|
|
179
|
+
normalizedDetailSource = "cascade",
|
|
180
|
+
parsedNetworkProfileCount = 0,
|
|
181
|
+
waitPlan = null,
|
|
182
|
+
resumeDomTimeoutMs = 60000
|
|
183
|
+
} = {}) {
|
|
184
|
+
const detailSource = normalizeDetailSource(normalizedDetailSource);
|
|
185
|
+
const configuredTimeoutMs = Math.max(0, Number(resumeDomTimeoutMs) || 0);
|
|
186
|
+
if (detailSource === "image") {
|
|
187
|
+
return {
|
|
188
|
+
skipped: false,
|
|
189
|
+
timeout_ms: Math.min(configuredTimeoutMs, 3500),
|
|
190
|
+
configured_timeout_ms: configuredTimeoutMs,
|
|
191
|
+
short_probe: true,
|
|
192
|
+
reason: "forced_image_modal_probe"
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (detailSource === "dom") {
|
|
196
|
+
return {
|
|
197
|
+
skipped: false,
|
|
198
|
+
timeout_ms: configuredTimeoutMs,
|
|
199
|
+
configured_timeout_ms: configuredTimeoutMs,
|
|
200
|
+
short_probe: false,
|
|
201
|
+
reason: "dom_source_full_wait"
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const profileCount = Math.max(0, Number(parsedNetworkProfileCount) || 0);
|
|
206
|
+
const previousImageMode = waitPlan?.mode_before === "image";
|
|
207
|
+
if (profileCount > 0) {
|
|
208
|
+
return {
|
|
209
|
+
skipped: false,
|
|
210
|
+
timeout_ms: Math.min(configuredTimeoutMs, previousImageMode ? 1500 : 3500),
|
|
211
|
+
configured_timeout_ms: configuredTimeoutMs,
|
|
212
|
+
short_probe: true,
|
|
213
|
+
reason: previousImageMode
|
|
214
|
+
? "previous_image_mode_profile_only_network_short_dom_probe"
|
|
215
|
+
: "profile_only_network_short_dom_probe"
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (previousImageMode) {
|
|
219
|
+
return {
|
|
220
|
+
skipped: false,
|
|
221
|
+
timeout_ms: Math.min(configuredTimeoutMs, 2500),
|
|
222
|
+
configured_timeout_ms: configuredTimeoutMs,
|
|
223
|
+
short_probe: true,
|
|
224
|
+
reason: "previous_image_mode_network_miss_short_dom_probe"
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
skipped: false,
|
|
229
|
+
timeout_ms: configuredTimeoutMs,
|
|
230
|
+
configured_timeout_ms: configuredTimeoutMs,
|
|
231
|
+
short_probe: false,
|
|
232
|
+
reason: "cascade_full_dom_wait"
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
156
236
|
function isRecoverableCdpNodeError(error) {
|
|
157
237
|
return /(?:Could not find node|No node with given id|Cannot find node|Could not compute box model)/i
|
|
158
238
|
.test(String(error?.message || error || ""));
|
|
@@ -208,6 +288,30 @@ const CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH = 500;
|
|
|
208
288
|
const CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH = 180;
|
|
209
289
|
const CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH = 650;
|
|
210
290
|
const CHAT_FULL_CV_NETWORK_MIN_RICH_ITEM_COUNT = 3;
|
|
291
|
+
const CHAT_RESUME_IMAGE_STOP_BOUNDARY_SELECTOR = [
|
|
292
|
+
"h1",
|
|
293
|
+
"h2",
|
|
294
|
+
"h3",
|
|
295
|
+
"h4",
|
|
296
|
+
"h5",
|
|
297
|
+
"p",
|
|
298
|
+
"span",
|
|
299
|
+
"section",
|
|
300
|
+
"article",
|
|
301
|
+
"div",
|
|
302
|
+
"[class*='privacy']",
|
|
303
|
+
"[class*='recommend']",
|
|
304
|
+
"[class*='similar']"
|
|
305
|
+
].join(",");
|
|
306
|
+
const CHAT_RESUME_IMAGE_STOP_BOUNDARY_TEXT = Object.freeze([
|
|
307
|
+
/其他名企大厂/,
|
|
308
|
+
/其他.*牛人/,
|
|
309
|
+
/毕业的牛人/,
|
|
310
|
+
/经历牛人/,
|
|
311
|
+
/为妥善保护/,
|
|
312
|
+
/查看全部.*项分析/,
|
|
313
|
+
/牛人分析器/
|
|
314
|
+
]);
|
|
211
315
|
const CHAT_FULL_CV_SECTION_PATTERNS = Object.freeze([
|
|
212
316
|
/教育(?:经历|背景|经验)?/i,
|
|
213
317
|
/工作(?:经历|经验)?/i,
|
|
@@ -738,6 +842,44 @@ export async function runChatWorkflow({
|
|
|
738
842
|
});
|
|
739
843
|
continue;
|
|
740
844
|
}
|
|
845
|
+
if (normalizeText(job)) {
|
|
846
|
+
const jobGuard = await selectChatJob(client, rootState.rootNodes.top, {
|
|
847
|
+
jobLabel: job,
|
|
848
|
+
timeoutMs: Math.min(readyTimeoutMs, 12000),
|
|
849
|
+
settleMs: Math.min(listSettleMs, 800)
|
|
850
|
+
});
|
|
851
|
+
if (!jobGuard.selected || jobGuard.verified !== true) {
|
|
852
|
+
const error = new Error(`CHAT_JOB_GUARD_FAILED: requested=${job}; selected=${jobGuard.selected_label || "unknown"}; reason=${jobGuard.reason || "unknown"}`);
|
|
853
|
+
error.code = "CHAT_JOB_GUARD_FAILED";
|
|
854
|
+
error.chat_job_guard = compactChatJobGuard(jobGuard);
|
|
855
|
+
throw error;
|
|
856
|
+
}
|
|
857
|
+
if (!jobGuard.already_current) {
|
|
858
|
+
runControl.checkpoint({
|
|
859
|
+
chat_context_step: "job_guard_reselected",
|
|
860
|
+
job_guard: compactChatJobGuard(jobGuard),
|
|
861
|
+
candidate_list: resetInfiniteListForRefreshRound(listState, {
|
|
862
|
+
reason: "chat_job_drift_repaired",
|
|
863
|
+
round: listState.ledger?.length || 0,
|
|
864
|
+
method: "selectChatJob",
|
|
865
|
+
metadata: {
|
|
866
|
+
requested_job: job,
|
|
867
|
+
selected_label: jobGuard.selected_label || "",
|
|
868
|
+
selected_value: jobGuard.selected_option?.value || ""
|
|
869
|
+
}
|
|
870
|
+
})
|
|
871
|
+
});
|
|
872
|
+
rootState = await ensureChatViewport(await getChatRoots(client), "candidate_job_guard_reselected");
|
|
873
|
+
await sleep(Math.min(listSettleMs, 1200));
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (jobGuard.menu_close?.closed) {
|
|
877
|
+
runControl.checkpoint({
|
|
878
|
+
chat_context_step: "job_guard_closed_dropdown",
|
|
879
|
+
job_guard: compactChatJobGuard(jobGuard)
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
741
883
|
|
|
742
884
|
const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
|
|
743
885
|
client,
|
|
@@ -825,20 +967,37 @@ export async function runChatWorkflow({
|
|
|
825
967
|
}
|
|
826
968
|
effectiveCardNodeId = selected.card_node_id || cardNodeId;
|
|
827
969
|
const selectionNetworkEvents = networkRecorder.events.slice();
|
|
970
|
+
try {
|
|
971
|
+
preActionState = await readChatConversationReadyState(client);
|
|
972
|
+
} catch (error) {
|
|
973
|
+
preActionState = {
|
|
974
|
+
error: error?.message || String(error)
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
const preDetailSkipReason = chatDetailSkipReasonFromReadyState(preActionState);
|
|
978
|
+
if (preDetailSkipReason) {
|
|
979
|
+
detailUnavailableReason = preDetailSkipReason;
|
|
980
|
+
detailResult = createSkippedDetailResult(cardCandidate, preDetailSkipReason);
|
|
981
|
+
detailResult.cv_acquisition.pre_detail_state = preActionState;
|
|
982
|
+
detailResult.cv_acquisition.selection_ready_state = selected.ready;
|
|
983
|
+
}
|
|
828
984
|
if (!selected.ready?.ok) {
|
|
829
|
-
if (
|
|
985
|
+
if (detailResult) {
|
|
986
|
+
// Already classified by the pre-detail conversation state.
|
|
987
|
+
} else if (selected.ready?.reason === "active_candidate_mismatch") {
|
|
830
988
|
detailUnavailableReason = "active_candidate_mismatch";
|
|
831
989
|
detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
|
|
832
990
|
detailResult.cv_acquisition.selection_ready_state = selected.ready;
|
|
833
991
|
} else {
|
|
834
992
|
detailStep = "read_conversation_ready_state";
|
|
835
|
-
preActionState = await readChatConversationReadyState(client);
|
|
836
993
|
if (preActionState.attachment_resume_enabled) {
|
|
837
994
|
detailUnavailableReason = "attachment_resume_already_available";
|
|
838
995
|
detailResult = createSkippedDetailResult(cardCandidate, "attachment_resume_already_available");
|
|
996
|
+
detailResult.cv_acquisition.pre_detail_state = preActionState;
|
|
839
997
|
} else {
|
|
840
998
|
detailUnavailableReason = "online_resume_button_unavailable";
|
|
841
999
|
detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
|
|
1000
|
+
detailResult.cv_acquisition.pre_detail_state = preActionState;
|
|
842
1001
|
}
|
|
843
1002
|
}
|
|
844
1003
|
}
|
|
@@ -962,10 +1121,40 @@ export async function runChatWorkflow({
|
|
|
962
1121
|
|
|
963
1122
|
if (!detailResult) {
|
|
964
1123
|
detailStep = "wait_resume_content";
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1124
|
+
const domFallbackPlan = resolveChatDomFallbackWait({
|
|
1125
|
+
normalizedDetailSource,
|
|
1126
|
+
parsedNetworkProfileCount,
|
|
1127
|
+
waitPlan,
|
|
1128
|
+
resumeDomTimeoutMs
|
|
1129
|
+
});
|
|
1130
|
+
if (domFallbackPlan.skipped || domFallbackPlan.timeout_ms <= 0) {
|
|
1131
|
+
contentWait = {
|
|
1132
|
+
ok: false,
|
|
1133
|
+
skipped: true,
|
|
1134
|
+
reason: domFallbackPlan.reason,
|
|
1135
|
+
elapsed_ms: 0,
|
|
1136
|
+
text_length: 0,
|
|
1137
|
+
resume_state: openedResume.resume_state,
|
|
1138
|
+
resume_html: null,
|
|
1139
|
+
dom_fallback_plan: domFallbackPlan,
|
|
1140
|
+
configured_timeout_ms: domFallbackPlan.configured_timeout_ms,
|
|
1141
|
+
timeout_ms: domFallbackPlan.timeout_ms,
|
|
1142
|
+
short_probe: Boolean(domFallbackPlan.short_probe)
|
|
1143
|
+
};
|
|
1144
|
+
addTiming(timings, "dom_fallback_ms", 0);
|
|
1145
|
+
} else {
|
|
1146
|
+
contentWait = await measureTiming(timings, "dom_fallback_ms", () => waitForChatResumeContent(client, {
|
|
1147
|
+
timeoutMs: domFallbackPlan.timeout_ms,
|
|
1148
|
+
intervalMs: 300
|
|
1149
|
+
}));
|
|
1150
|
+
contentWait.dom_fallback_plan = domFallbackPlan;
|
|
1151
|
+
contentWait.configured_timeout_ms = domFallbackPlan.configured_timeout_ms;
|
|
1152
|
+
contentWait.timeout_ms = domFallbackPlan.timeout_ms;
|
|
1153
|
+
contentWait.short_probe = Boolean(domFallbackPlan.short_probe);
|
|
1154
|
+
if (domFallbackPlan.short_probe && !contentWait.ok) {
|
|
1155
|
+
contentWait.reason = contentWait.reason || domFallbackPlan.reason;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
969
1158
|
resumeState = contentWait.resume_state || openedResume.resume_state;
|
|
970
1159
|
resumeHtml = contentWait.resume_html || null;
|
|
971
1160
|
resumeNetworkEvents = networkRecorder.events.slice();
|
|
@@ -1012,23 +1201,29 @@ export async function runChatWorkflow({
|
|
|
1012
1201
|
quality: 72,
|
|
1013
1202
|
optimize: true,
|
|
1014
1203
|
resizeMaxWidth: 1100,
|
|
1015
|
-
captureViewport:
|
|
1016
|
-
padding:
|
|
1204
|
+
captureViewport: false,
|
|
1205
|
+
padding: 0,
|
|
1017
1206
|
maxScreenshots: maxImagePages,
|
|
1018
1207
|
wheelDeltaY: imageWheelDeltaY,
|
|
1019
1208
|
settleMs: 350,
|
|
1020
1209
|
scrollMethod: "dom-anchor-fallback-input",
|
|
1021
1210
|
stepTimeoutMs: 45000,
|
|
1022
1211
|
totalTimeoutMs: 90000,
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1212
|
+
duplicateStopCount: 1,
|
|
1213
|
+
skipDuplicateScreenshots: true,
|
|
1214
|
+
composeForLlm: true,
|
|
1215
|
+
llmPagesPerImage: 3,
|
|
1216
|
+
llmResizeMaxWidth: 1100,
|
|
1217
|
+
llmQuality: 72,
|
|
1218
|
+
stopBoundarySelector: CHAT_RESUME_IMAGE_STOP_BOUNDARY_SELECTOR,
|
|
1219
|
+
stopBoundaryTextPatterns: CHAT_RESUME_IMAGE_STOP_BOUNDARY_TEXT,
|
|
1220
|
+
stopBoundaryMaxProbeNodes: 360,
|
|
1221
|
+
stopBoundaryTopPadding: 10,
|
|
1222
|
+
stopBoundaryMinCaptureHeight: 180,
|
|
1223
|
+
metadata: {
|
|
1224
|
+
domain: "chat",
|
|
1225
|
+
capture_mode: "scroll_sequence",
|
|
1226
|
+
capture_scope: "resume_modal_clip",
|
|
1032
1227
|
acquisition_reason: normalizedDetailSource === "image"
|
|
1033
1228
|
? "forced_image"
|
|
1034
1229
|
: "network_miss_image_fallback",
|
|
@@ -1165,7 +1360,12 @@ export async function runChatWorkflow({
|
|
|
1165
1360
|
skipped: Boolean(contentWait.skipped),
|
|
1166
1361
|
reason: contentWait.reason || null,
|
|
1167
1362
|
elapsed_ms: contentWait.elapsed_ms,
|
|
1168
|
-
text_length: contentWait.text_length
|
|
1363
|
+
text_length: contentWait.text_length,
|
|
1364
|
+
timeout_ms: contentWait.timeout_ms ?? contentWait.dom_fallback_plan?.timeout_ms ?? null,
|
|
1365
|
+
configured_timeout_ms: contentWait.configured_timeout_ms
|
|
1366
|
+
?? contentWait.dom_fallback_plan?.configured_timeout_ms
|
|
1367
|
+
?? null,
|
|
1368
|
+
short_probe: Boolean(contentWait.short_probe)
|
|
1169
1369
|
},
|
|
1170
1370
|
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
1171
1371
|
image_evidence: summarizeImageEvidence(imageEvidence),
|