@reconcrap/boss-recommend-mcp 2.0.11 → 2.0.13
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 +256 -23
- package/src/cli.js +2 -8
- package/src/core/browser/index.js +35 -8
- package/src/core/capture/index.js +323 -20
- package/src/core/cv-acquisition/index.js +3 -0
- package/src/core/infinite-list/index.js +354 -1
- package/src/domains/chat/constants.js +11 -0
- package/src/domains/chat/run-service.js +21 -5
- package/src/domains/recommend/constants.js +11 -0
- package/src/domains/recommend/run-service.js +57 -16
- package/src/domains/recruit/constants.js +23 -0
- package/src/domains/recruit/run-service.js +19 -4
|
@@ -1,10 +1,68 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import {
|
|
3
3
|
getNodeBox,
|
|
4
|
+
getOuterHTML,
|
|
5
|
+
querySelectorAll,
|
|
4
6
|
scrollNodeIntoView,
|
|
5
7
|
sleep
|
|
6
8
|
} from "../browser/index.js";
|
|
7
9
|
|
|
10
|
+
export const DEFAULT_BOTTOM_HINT_KEYWORDS = Object.freeze([
|
|
11
|
+
"没有更多",
|
|
12
|
+
"已显示全部",
|
|
13
|
+
"已经到底",
|
|
14
|
+
"暂无更多",
|
|
15
|
+
"推荐完了",
|
|
16
|
+
"没有更多人选",
|
|
17
|
+
"没有更多了",
|
|
18
|
+
"已到底"
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_LOAD_MORE_HINT_KEYWORDS = Object.freeze([
|
|
22
|
+
"滚动加载更多",
|
|
23
|
+
"下滑加载更多",
|
|
24
|
+
"继续下滑",
|
|
25
|
+
"继续滑动",
|
|
26
|
+
"滑动加载",
|
|
27
|
+
"正在加载",
|
|
28
|
+
"加载中"
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_BOTTOM_MARKER_SELECTORS = Object.freeze([
|
|
32
|
+
".finished-wrap",
|
|
33
|
+
".load-tips",
|
|
34
|
+
"div[role=\"tfoot\"] .load-tips",
|
|
35
|
+
".no-data-refresh",
|
|
36
|
+
".empty-tip",
|
|
37
|
+
".empty-text",
|
|
38
|
+
".no-data",
|
|
39
|
+
".tip-nodata",
|
|
40
|
+
"[class*=\"finished\"]",
|
|
41
|
+
"[class*=\"load-tips\"]",
|
|
42
|
+
"[class*=\"no-more\"]",
|
|
43
|
+
"[class*=\"no_more\"]"
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_BOTTOM_TEXT_SCAN_SELECTORS = Object.freeze([
|
|
47
|
+
"div",
|
|
48
|
+
"span",
|
|
49
|
+
"p",
|
|
50
|
+
"li",
|
|
51
|
+
"button",
|
|
52
|
+
"a"
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
export const DEFAULT_BOTTOM_REFRESH_SELECTORS = Object.freeze([
|
|
56
|
+
".finished-wrap .btn-refresh",
|
|
57
|
+
".finished-wrap .btn",
|
|
58
|
+
".no-data-refresh .btn-refresh",
|
|
59
|
+
".no-data-refresh .btn",
|
|
60
|
+
"[class*=\"refresh\"]",
|
|
61
|
+
"[ka*=\"refresh\"]",
|
|
62
|
+
"button",
|
|
63
|
+
"a"
|
|
64
|
+
]);
|
|
65
|
+
|
|
8
66
|
function nowIso() {
|
|
9
67
|
return new Date().toISOString();
|
|
10
68
|
}
|
|
@@ -13,6 +71,31 @@ function normalizeText(value) {
|
|
|
13
71
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
14
72
|
}
|
|
15
73
|
|
|
74
|
+
function uniqueValues(values = []) {
|
|
75
|
+
return Array.from(new Set(values.filter(Boolean)));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function decodeBasicHtmlEntities(value = "") {
|
|
79
|
+
return String(value || "")
|
|
80
|
+
.replace(/ | /gi, " ")
|
|
81
|
+
.replace(/&/gi, "&")
|
|
82
|
+
.replace(/</gi, "<")
|
|
83
|
+
.replace(/>/gi, ">")
|
|
84
|
+
.replace(/"/gi, "\"")
|
|
85
|
+
.replace(/'|'/gi, "'");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function plainTextFromHtml(html = "") {
|
|
89
|
+
return normalizeText(decodeBasicHtmlEntities(String(html || "")
|
|
90
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ")
|
|
91
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ")
|
|
92
|
+
.replace(/<[^>]+>/g, " ")));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isUsableBox(box) {
|
|
96
|
+
return Number(box?.rect?.width || 0) > 2 && Number(box?.rect?.height || 0) > 2;
|
|
97
|
+
}
|
|
98
|
+
|
|
16
99
|
function shortHash(value) {
|
|
17
100
|
return crypto.createHash("sha256").update(String(value || "")).digest("hex").slice(0, 16);
|
|
18
101
|
}
|
|
@@ -174,6 +257,220 @@ export function resetInfiniteListForRefreshRound(state, {
|
|
|
174
257
|
return compactInfiniteListState(state);
|
|
175
258
|
}
|
|
176
259
|
|
|
260
|
+
export function classifyInfiniteListBottomMarker({
|
|
261
|
+
text = "",
|
|
262
|
+
refreshButtonVisible = false,
|
|
263
|
+
bottomKeywords = DEFAULT_BOTTOM_HINT_KEYWORDS,
|
|
264
|
+
loadMoreKeywords = DEFAULT_LOAD_MORE_HINT_KEYWORDS
|
|
265
|
+
} = {}) {
|
|
266
|
+
const normalizedText = normalizeText(text);
|
|
267
|
+
const matchedBottomKeyword = bottomKeywords.find((keyword) => normalizedText.includes(keyword)) || null;
|
|
268
|
+
if (matchedBottomKeyword) {
|
|
269
|
+
return {
|
|
270
|
+
is_bottom: true,
|
|
271
|
+
reason: matchedBottomKeyword,
|
|
272
|
+
matched_bottom_keyword: matchedBottomKeyword,
|
|
273
|
+
matched_load_more_keyword: null
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const matchedLoadMoreKeyword = loadMoreKeywords.find((keyword) => normalizedText.includes(keyword)) || null;
|
|
278
|
+
if (matchedLoadMoreKeyword) {
|
|
279
|
+
return {
|
|
280
|
+
is_bottom: false,
|
|
281
|
+
reason: null,
|
|
282
|
+
matched_bottom_keyword: null,
|
|
283
|
+
matched_load_more_keyword: matchedLoadMoreKeyword
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (refreshButtonVisible) {
|
|
288
|
+
return {
|
|
289
|
+
is_bottom: true,
|
|
290
|
+
reason: "refresh_button_visible",
|
|
291
|
+
matched_bottom_keyword: null,
|
|
292
|
+
matched_load_more_keyword: null
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
is_bottom: false,
|
|
298
|
+
reason: null,
|
|
299
|
+
matched_bottom_keyword: null,
|
|
300
|
+
matched_load_more_keyword: null
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function safeQuerySelectorAll(client, rootNodeId, selector) {
|
|
305
|
+
try {
|
|
306
|
+
return await querySelectorAll(client, rootNodeId, selector);
|
|
307
|
+
} catch {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function readVisibleMarkerNode(client, nodeId) {
|
|
313
|
+
let box = null;
|
|
314
|
+
try {
|
|
315
|
+
box = await getNodeBox(client, nodeId);
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
if (!isUsableBox(box)) return null;
|
|
320
|
+
let outerHTML = "";
|
|
321
|
+
try {
|
|
322
|
+
outerHTML = await getOuterHTML(client, nodeId);
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
node_id: nodeId,
|
|
328
|
+
text: plainTextFromHtml(outerHTML),
|
|
329
|
+
box
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function looksLikeRefreshLabel(text = "") {
|
|
334
|
+
const normalized = normalizeText(text).replace(/\s+/g, "");
|
|
335
|
+
return Boolean(normalized) && normalized.length <= 80 && /刷新|refresh/i.test(normalized);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export async function detectInfiniteListBottomMarker(client, {
|
|
339
|
+
rootNodeId,
|
|
340
|
+
markerSelectors = DEFAULT_BOTTOM_MARKER_SELECTORS,
|
|
341
|
+
textScanSelectors = DEFAULT_BOTTOM_TEXT_SCAN_SELECTORS,
|
|
342
|
+
refreshSelectors = DEFAULT_BOTTOM_REFRESH_SELECTORS,
|
|
343
|
+
bottomKeywords = DEFAULT_BOTTOM_HINT_KEYWORDS,
|
|
344
|
+
loadMoreKeywords = DEFAULT_LOAD_MORE_HINT_KEYWORDS,
|
|
345
|
+
maxMarkerNodes = 300,
|
|
346
|
+
maxTextScanNodes = 800,
|
|
347
|
+
textMaxLength = 80
|
|
348
|
+
} = {}) {
|
|
349
|
+
if (!client || !rootNodeId) {
|
|
350
|
+
return {
|
|
351
|
+
found: false,
|
|
352
|
+
reason: "missing_client_or_root"
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const selectorCounts = {};
|
|
357
|
+
const markerNodeIds = [];
|
|
358
|
+
for (const selector of markerSelectors || []) {
|
|
359
|
+
const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
|
|
360
|
+
selectorCounts[selector] = nodeIds.length;
|
|
361
|
+
markerNodeIds.push(...nodeIds);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const visibleMarkers = [];
|
|
365
|
+
const markerIds = uniqueValues(markerNodeIds).slice(0, Math.max(0, Number(maxMarkerNodes) || 0));
|
|
366
|
+
for (const nodeId of markerIds) {
|
|
367
|
+
const marker = await readVisibleMarkerNode(client, nodeId);
|
|
368
|
+
if (!marker?.text) continue;
|
|
369
|
+
const classified = classifyInfiniteListBottomMarker({
|
|
370
|
+
text: marker.text,
|
|
371
|
+
bottomKeywords,
|
|
372
|
+
loadMoreKeywords
|
|
373
|
+
});
|
|
374
|
+
const summary = {
|
|
375
|
+
node_id: marker.node_id,
|
|
376
|
+
text: marker.text.slice(0, 160),
|
|
377
|
+
y: marker.box?.rect?.y || null,
|
|
378
|
+
matched_bottom_keyword: classified.matched_bottom_keyword,
|
|
379
|
+
matched_load_more_keyword: classified.matched_load_more_keyword
|
|
380
|
+
};
|
|
381
|
+
visibleMarkers.push(summary);
|
|
382
|
+
if (classified.is_bottom) {
|
|
383
|
+
return {
|
|
384
|
+
found: true,
|
|
385
|
+
reason: classified.reason,
|
|
386
|
+
source: "marker_selector",
|
|
387
|
+
marker: summary,
|
|
388
|
+
selector_counts: selectorCounts,
|
|
389
|
+
visible_marker_count: visibleMarkers.length,
|
|
390
|
+
refresh_button_visible: false
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const hasLoadMoreMarker = visibleMarkers.some((marker) => marker.matched_load_more_keyword);
|
|
396
|
+
|
|
397
|
+
const refreshNodeIds = [];
|
|
398
|
+
for (const selector of refreshSelectors || []) {
|
|
399
|
+
const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
|
|
400
|
+
selectorCounts[selector] = (selectorCounts[selector] || 0) + nodeIds.length;
|
|
401
|
+
refreshNodeIds.push(...nodeIds);
|
|
402
|
+
}
|
|
403
|
+
const refreshButtons = [];
|
|
404
|
+
for (const nodeId of uniqueValues(refreshNodeIds).slice(0, 300)) {
|
|
405
|
+
const marker = await readVisibleMarkerNode(client, nodeId);
|
|
406
|
+
if (!marker?.text || !looksLikeRefreshLabel(marker.text)) continue;
|
|
407
|
+
refreshButtons.push({
|
|
408
|
+
node_id: marker.node_id,
|
|
409
|
+
text: marker.text.slice(0, 120),
|
|
410
|
+
y: marker.box?.rect?.y || null
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (refreshButtons.length && !hasLoadMoreMarker) {
|
|
414
|
+
return {
|
|
415
|
+
found: true,
|
|
416
|
+
reason: "refresh_button_visible",
|
|
417
|
+
source: "refresh_button",
|
|
418
|
+
marker: refreshButtons[0],
|
|
419
|
+
selector_counts: selectorCounts,
|
|
420
|
+
visible_marker_count: visibleMarkers.length,
|
|
421
|
+
refresh_button_visible: true,
|
|
422
|
+
refresh_button_count: refreshButtons.length
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const scanNodeIds = [];
|
|
427
|
+
for (const selector of textScanSelectors || []) {
|
|
428
|
+
const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
|
|
429
|
+
selectorCounts[selector] = (selectorCounts[selector] || 0) + nodeIds.length;
|
|
430
|
+
scanNodeIds.push(...nodeIds);
|
|
431
|
+
}
|
|
432
|
+
let checkedTextNodeCount = 0;
|
|
433
|
+
for (const nodeId of uniqueValues(scanNodeIds).slice(0, Math.max(0, Number(maxTextScanNodes) || 0))) {
|
|
434
|
+
const marker = await readVisibleMarkerNode(client, nodeId);
|
|
435
|
+
if (!marker?.text || marker.text.length > textMaxLength) continue;
|
|
436
|
+
checkedTextNodeCount += 1;
|
|
437
|
+
const classified = classifyInfiniteListBottomMarker({
|
|
438
|
+
text: marker.text,
|
|
439
|
+
bottomKeywords,
|
|
440
|
+
loadMoreKeywords
|
|
441
|
+
});
|
|
442
|
+
if (classified.is_bottom) {
|
|
443
|
+
return {
|
|
444
|
+
found: true,
|
|
445
|
+
reason: classified.reason,
|
|
446
|
+
source: "text_scan",
|
|
447
|
+
marker: {
|
|
448
|
+
node_id: marker.node_id,
|
|
449
|
+
text: marker.text.slice(0, 160),
|
|
450
|
+
y: marker.box?.rect?.y || null,
|
|
451
|
+
matched_bottom_keyword: classified.matched_bottom_keyword
|
|
452
|
+
},
|
|
453
|
+
selector_counts: selectorCounts,
|
|
454
|
+
visible_marker_count: visibleMarkers.length,
|
|
455
|
+
checked_text_node_count: checkedTextNodeCount,
|
|
456
|
+
refresh_button_visible: refreshButtons.length > 0,
|
|
457
|
+
refresh_button_count: refreshButtons.length
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
found: false,
|
|
464
|
+
reason: hasLoadMoreMarker ? "load_more_marker_visible" : "bottom_marker_not_found",
|
|
465
|
+
selector_counts: selectorCounts,
|
|
466
|
+
visible_markers: visibleMarkers.slice(0, 20),
|
|
467
|
+
visible_marker_count: visibleMarkers.length,
|
|
468
|
+
checked_text_node_count: checkedTextNodeCount,
|
|
469
|
+
refresh_button_visible: refreshButtons.length > 0,
|
|
470
|
+
refresh_button_count: refreshButtons.length
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
177
474
|
export async function readVisibleInfiniteListItems({
|
|
178
475
|
nodeIds = [],
|
|
179
476
|
readCandidate,
|
|
@@ -333,9 +630,11 @@ export async function getNextInfiniteListCandidate({
|
|
|
333
630
|
state,
|
|
334
631
|
findNodeIds,
|
|
335
632
|
readCandidate,
|
|
633
|
+
detectBottomMarker = null,
|
|
336
634
|
keyForCandidate = candidateKeyFromProfile,
|
|
337
635
|
maxScrolls = 20,
|
|
338
636
|
stableSignatureLimit = 2,
|
|
637
|
+
minScrollsBeforeEnd = 3,
|
|
339
638
|
wheelDeltaY = 850,
|
|
340
639
|
settleMs = 1200,
|
|
341
640
|
fallbackPoint = null
|
|
@@ -383,6 +682,54 @@ export async function getNextInfiniteListCandidate({
|
|
|
383
682
|
return result;
|
|
384
683
|
}
|
|
385
684
|
|
|
685
|
+
if (typeof detectBottomMarker === "function") {
|
|
686
|
+
let bottomMarker = null;
|
|
687
|
+
try {
|
|
688
|
+
bottomMarker = await detectBottomMarker({
|
|
689
|
+
scrollAttempt,
|
|
690
|
+
items,
|
|
691
|
+
signature,
|
|
692
|
+
state: compactInfiniteListState(state)
|
|
693
|
+
});
|
|
694
|
+
} catch (error) {
|
|
695
|
+
bottomMarker = {
|
|
696
|
+
found: false,
|
|
697
|
+
reason: "bottom_marker_probe_failed",
|
|
698
|
+
error: error?.message || String(error)
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
attempts[attempts.length - 1].bottom_marker = bottomMarker;
|
|
702
|
+
if (bottomMarker?.found) {
|
|
703
|
+
state.ledger?.push({
|
|
704
|
+
at: nowIso(),
|
|
705
|
+
event: "bottom_marker_detected",
|
|
706
|
+
reason: bottomMarker.reason || "bottom_marker",
|
|
707
|
+
source: bottomMarker.source || "",
|
|
708
|
+
marker: bottomMarker.marker || null
|
|
709
|
+
});
|
|
710
|
+
const result = {
|
|
711
|
+
ok: false,
|
|
712
|
+
end_reached: true,
|
|
713
|
+
reason: "bottom_marker",
|
|
714
|
+
bottom_marker: bottomMarker,
|
|
715
|
+
attempts,
|
|
716
|
+
state: compactInfiniteListState(state)
|
|
717
|
+
};
|
|
718
|
+
state.last_result = {
|
|
719
|
+
at: nowIso(),
|
|
720
|
+
ok: false,
|
|
721
|
+
end_reached: true,
|
|
722
|
+
reason: result.reason,
|
|
723
|
+
bottom_marker: {
|
|
724
|
+
reason: bottomMarker.reason || null,
|
|
725
|
+
source: bottomMarker.source || null,
|
|
726
|
+
marker: bottomMarker.marker || null
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
return result;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
386
733
|
if (!items.length) {
|
|
387
734
|
const result = {
|
|
388
735
|
ok: false,
|
|
@@ -400,7 +747,9 @@ export async function getNextInfiniteListCandidate({
|
|
|
400
747
|
return result;
|
|
401
748
|
}
|
|
402
749
|
|
|
403
|
-
|
|
750
|
+
const stableLimit = Math.max(1, Number(stableSignatureLimit) || 1);
|
|
751
|
+
const minStableScrolls = Math.max(0, Number(minScrollsBeforeEnd) || 0);
|
|
752
|
+
if (signature.stable_signature_count >= stableLimit && scrollAttempt >= minStableScrolls) {
|
|
404
753
|
const result = {
|
|
405
754
|
ok: false,
|
|
406
755
|
end_reached: true,
|
|
@@ -416,6 +765,10 @@ export async function getNextInfiniteListCandidate({
|
|
|
416
765
|
};
|
|
417
766
|
return result;
|
|
418
767
|
}
|
|
768
|
+
if (signature.stable_signature_count >= stableLimit) {
|
|
769
|
+
attempts[attempts.length - 1].stable_end_deferred = true;
|
|
770
|
+
attempts[attempts.length - 1].min_scrolls_before_end = minStableScrolls;
|
|
771
|
+
}
|
|
419
772
|
|
|
420
773
|
const scrollResult = await scrollInfiniteListByVisibleItems(client, items, {
|
|
421
774
|
wheelDeltaY,
|
|
@@ -8,6 +8,17 @@ export const CHAT_CARD_SELECTORS = Object.freeze([
|
|
|
8
8
|
"div[role=\"listitem\"]"
|
|
9
9
|
]);
|
|
10
10
|
|
|
11
|
+
export const CHAT_BOTTOM_MARKER_SELECTORS = Object.freeze([
|
|
12
|
+
"div[role=\"tfoot\"] .load-tips",
|
|
13
|
+
"p.load-tips",
|
|
14
|
+
".load-tips",
|
|
15
|
+
".empty-tip",
|
|
16
|
+
".empty-text",
|
|
17
|
+
".no-data",
|
|
18
|
+
"[class*=\"load-tips\"]",
|
|
19
|
+
"[class*=\"empty\"]"
|
|
20
|
+
]);
|
|
21
|
+
|
|
11
22
|
export const CHAT_JOB_LABEL_SELECTORS = Object.freeze([
|
|
12
23
|
".chat-job .chat-select-job",
|
|
13
24
|
".chat-job .dropmenu-label",
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
compactInfiniteListState,
|
|
21
21
|
createInfiniteListState,
|
|
22
|
+
detectInfiniteListBottomMarker,
|
|
22
23
|
getNextInfiniteListCandidate,
|
|
23
24
|
markInfiniteListCandidateProcessed
|
|
24
25
|
} from "../../core/infinite-list/index.js";
|
|
@@ -34,7 +35,10 @@ import {
|
|
|
34
35
|
normalizeText,
|
|
35
36
|
screenCandidate
|
|
36
37
|
} from "../../core/screening/index.js";
|
|
37
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
CHAT_BOTTOM_MARKER_SELECTORS,
|
|
40
|
+
CHAT_TARGET_URL
|
|
41
|
+
} from "./constants.js";
|
|
38
42
|
import {
|
|
39
43
|
chatCandidateKeyFromProfile,
|
|
40
44
|
findChatCandidateNodeIdById,
|
|
@@ -372,9 +376,9 @@ export async function runChatWorkflow({
|
|
|
372
376
|
llmImageDetail = "high",
|
|
373
377
|
screeningMode = "llm",
|
|
374
378
|
listMaxScrolls = 20,
|
|
375
|
-
listStableSignatureLimit =
|
|
379
|
+
listStableSignatureLimit = 5,
|
|
376
380
|
listWheelDeltaY = 850,
|
|
377
|
-
listSettleMs =
|
|
381
|
+
listSettleMs = 2200,
|
|
378
382
|
listFallbackPoint = null,
|
|
379
383
|
imageOutputDir = ""
|
|
380
384
|
} = {}, runControl) {
|
|
@@ -631,6 +635,13 @@ export async function runChatWorkflow({
|
|
|
631
635
|
run_candidate_index: results.length,
|
|
632
636
|
visible_index: visibleIndex
|
|
633
637
|
}
|
|
638
|
+
}),
|
|
639
|
+
detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
|
|
640
|
+
rootNodeId: rootState?.rootNodes?.top,
|
|
641
|
+
markerSelectors: CHAT_BOTTOM_MARKER_SELECTORS,
|
|
642
|
+
refreshSelectors: [],
|
|
643
|
+
textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
|
|
644
|
+
maxTextScanNodes: 500
|
|
634
645
|
})
|
|
635
646
|
}));
|
|
636
647
|
if (!nextCandidateResult.ok) {
|
|
@@ -827,6 +838,9 @@ export async function runChatWorkflow({
|
|
|
827
838
|
maxScreenshots: maxImagePages,
|
|
828
839
|
wheelDeltaY: imageWheelDeltaY,
|
|
829
840
|
settleMs: 350,
|
|
841
|
+
scrollMethod: "dom-anchor-fallback-input",
|
|
842
|
+
stepTimeoutMs: 45000,
|
|
843
|
+
totalTimeoutMs: 90000,
|
|
830
844
|
duplicateStopCount: 1,
|
|
831
845
|
skipDuplicateScreenshots: true,
|
|
832
846
|
composeForLlm: true,
|
|
@@ -1137,9 +1151,9 @@ export function createChatRunService({
|
|
|
1137
1151
|
llmImageDetail = "high",
|
|
1138
1152
|
screeningMode = "llm",
|
|
1139
1153
|
listMaxScrolls = 20,
|
|
1140
|
-
listStableSignatureLimit =
|
|
1154
|
+
listStableSignatureLimit = 5,
|
|
1141
1155
|
listWheelDeltaY = 850,
|
|
1142
|
-
listSettleMs =
|
|
1156
|
+
listSettleMs = 2200,
|
|
1143
1157
|
listFallbackPoint = null,
|
|
1144
1158
|
imageOutputDir = "",
|
|
1145
1159
|
name = "chat-domain-run"
|
|
@@ -1163,6 +1177,8 @@ export function createChatRunService({
|
|
|
1163
1177
|
detail_limit: normalizedDetailLimit,
|
|
1164
1178
|
detail_source: normalizedDetailSource,
|
|
1165
1179
|
close_resume: closeResume,
|
|
1180
|
+
request_resume_for_passed: Boolean(requestResumeForPassed),
|
|
1181
|
+
dry_run_request_cv: Boolean(dryRunRequestCv),
|
|
1166
1182
|
cv_acquisition_mode: cvAcquisitionMode,
|
|
1167
1183
|
call_llm_on_image: Boolean(callLlmOnImage),
|
|
1168
1184
|
screening_mode: normalizedScreeningMode,
|
|
@@ -67,6 +67,17 @@ export const RECOMMEND_END_REFRESH_SELECTOR = [
|
|
|
67
67
|
"a"
|
|
68
68
|
].join(", ");
|
|
69
69
|
|
|
70
|
+
export const RECOMMEND_BOTTOM_MARKER_SELECTORS = Object.freeze([
|
|
71
|
+
".finished-wrap",
|
|
72
|
+
".no-data-refresh",
|
|
73
|
+
".load-tips",
|
|
74
|
+
".empty-tip",
|
|
75
|
+
".empty-text",
|
|
76
|
+
".no-data",
|
|
77
|
+
"[class*=\"finished\"]",
|
|
78
|
+
"[class*=\"load-tips\"]"
|
|
79
|
+
]);
|
|
80
|
+
|
|
70
81
|
export const DETAIL_POPUP_SELECTORS = Object.freeze([
|
|
71
82
|
".dialog-wrap.active",
|
|
72
83
|
".boss-popup__wrapper",
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
compactInfiniteListState,
|
|
23
23
|
createInfiniteListState,
|
|
24
|
+
detectInfiniteListBottomMarker,
|
|
24
25
|
getNextInfiniteListCandidate,
|
|
25
26
|
markInfiniteListCandidateProcessed,
|
|
26
27
|
resetInfiniteListForRefreshRound
|
|
@@ -54,6 +55,10 @@ import {
|
|
|
54
55
|
normalizeRecommendPageScope,
|
|
55
56
|
selectRecommendPageScope
|
|
56
57
|
} from "./scopes.js";
|
|
58
|
+
import {
|
|
59
|
+
RECOMMEND_BOTTOM_MARKER_SELECTORS,
|
|
60
|
+
RECOMMEND_END_REFRESH_SELECTOR
|
|
61
|
+
} from "./constants.js";
|
|
57
62
|
import {
|
|
58
63
|
clickRecommendActionControl,
|
|
59
64
|
normalizeRecommendPostAction,
|
|
@@ -66,6 +71,15 @@ function normalizeLabels(labels = []) {
|
|
|
66
71
|
return labels.map((label) => String(label || "").trim()).filter(Boolean);
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
function isRefreshableListStall(reason = "") {
|
|
75
|
+
return new Set([
|
|
76
|
+
"stable_visible_signature",
|
|
77
|
+
"max_scrolls_exhausted",
|
|
78
|
+
"scroll_failed",
|
|
79
|
+
"scroll_anchor_unavailable"
|
|
80
|
+
]).has(String(reason || ""));
|
|
81
|
+
}
|
|
82
|
+
|
|
69
83
|
function normalizeFilter(filter = {}) {
|
|
70
84
|
const filterGroups = Array.isArray(filter.filterGroups)
|
|
71
85
|
? filter.filterGroups
|
|
@@ -347,6 +361,18 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
347
361
|
};
|
|
348
362
|
}
|
|
349
363
|
|
|
364
|
+
function countPassedResults(results = []) {
|
|
365
|
+
return results.filter((item) => item?.screening?.passed).length;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
|
|
369
|
+
if (!error) return null;
|
|
370
|
+
return {
|
|
371
|
+
code: error.code || fallbackCode,
|
|
372
|
+
message: error.message || String(error)
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
350
376
|
export async function runRecommendWorkflow({
|
|
351
377
|
client,
|
|
352
378
|
targetUrl = "",
|
|
@@ -364,9 +390,9 @@ export async function runRecommendWorkflow({
|
|
|
364
390
|
imageWheelDeltaY = 650,
|
|
365
391
|
cvAcquisitionMode = "unknown",
|
|
366
392
|
listMaxScrolls = 20,
|
|
367
|
-
listStableSignatureLimit =
|
|
393
|
+
listStableSignatureLimit = 5,
|
|
368
394
|
listWheelDeltaY = 850,
|
|
369
|
-
listSettleMs =
|
|
395
|
+
listSettleMs = 2200,
|
|
370
396
|
listFallbackPoint = null,
|
|
371
397
|
refreshOnEnd = true,
|
|
372
398
|
maxRefreshRounds = 2,
|
|
@@ -393,9 +419,9 @@ export async function runRecommendWorkflow({
|
|
|
393
419
|
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
394
420
|
const useLlmScreening = normalizedScreeningMode !== "deterministic";
|
|
395
421
|
const postActionEnabled = normalizedPostAction !== "none";
|
|
396
|
-
const
|
|
397
|
-
const detailCountLimit = detailLimit == null ?
|
|
398
|
-
const effectiveDetailLimit = postActionEnabled ?
|
|
422
|
+
const targetPassCount = Math.max(1, Number(maxCandidates) || 1);
|
|
423
|
+
const detailCountLimit = detailLimit == null ? Number.POSITIVE_INFINITY : Math.max(0, Number(detailLimit) || 0);
|
|
424
|
+
const effectiveDetailLimit = postActionEnabled ? Number.POSITIVE_INFINITY : detailCountLimit;
|
|
399
425
|
const networkRecorder = effectiveDetailLimit > 0
|
|
400
426
|
? createRecommendDetailNetworkRecorder(client)
|
|
401
427
|
: null;
|
|
@@ -508,7 +534,8 @@ export async function runRecommendWorkflow({
|
|
|
508
534
|
|
|
509
535
|
runControl.updateProgress({
|
|
510
536
|
card_count: cardNodeIds.length,
|
|
511
|
-
target_count:
|
|
537
|
+
target_count: targetPassCount,
|
|
538
|
+
target_count_semantics: "passed_candidates",
|
|
512
539
|
processed: 0,
|
|
513
540
|
screened: 0,
|
|
514
541
|
detail_opened: 0,
|
|
@@ -525,7 +552,7 @@ export async function runRecommendWorkflow({
|
|
|
525
552
|
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
526
553
|
});
|
|
527
554
|
|
|
528
|
-
while (results
|
|
555
|
+
while (countPassedResults(results) < targetPassCount) {
|
|
529
556
|
const candidateStarted = Date.now();
|
|
530
557
|
const timings = {};
|
|
531
558
|
await runControl.waitIfPaused();
|
|
@@ -559,15 +586,22 @@ export async function runRecommendWorkflow({
|
|
|
559
586
|
run_candidate_index: results.length,
|
|
560
587
|
visible_index: visibleIndex
|
|
561
588
|
}
|
|
589
|
+
}),
|
|
590
|
+
detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
|
|
591
|
+
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
592
|
+
markerSelectors: RECOMMEND_BOTTOM_MARKER_SELECTORS,
|
|
593
|
+
refreshSelectors: [RECOMMEND_END_REFRESH_SELECTOR],
|
|
594
|
+
textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
|
|
595
|
+
maxTextScanNodes: 500
|
|
562
596
|
})
|
|
563
597
|
}));
|
|
564
598
|
if (!nextCandidateResult.ok) {
|
|
565
599
|
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
566
600
|
if (
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
601
|
+
(nextCandidateResult.end_reached || isRefreshableListStall(nextCandidateResult.reason))
|
|
602
|
+
&& refreshOnEnd
|
|
603
|
+
&& countPassedResults(results) < targetPassCount
|
|
604
|
+
&& refreshRounds < Math.max(0, Number(maxRefreshRounds) || 0)
|
|
571
605
|
) {
|
|
572
606
|
await runControl.waitIfPaused();
|
|
573
607
|
runControl.throwIfCanceled();
|
|
@@ -592,7 +626,8 @@ export async function runRecommendWorkflow({
|
|
|
592
626
|
});
|
|
593
627
|
runControl.updateProgress({
|
|
594
628
|
card_count: refreshResult.card_count || cardNodeIds.length,
|
|
595
|
-
target_count:
|
|
629
|
+
target_count: targetPassCount,
|
|
630
|
+
target_count_semantics: "passed_candidates",
|
|
596
631
|
processed: results.length,
|
|
597
632
|
screened: results.length,
|
|
598
633
|
detail_opened: results.filter((item) => item.detail).length,
|
|
@@ -713,6 +748,9 @@ export async function runRecommendWorkflow({
|
|
|
713
748
|
maxScreenshots: maxImagePages,
|
|
714
749
|
wheelDeltaY: imageWheelDeltaY,
|
|
715
750
|
settleMs: 350,
|
|
751
|
+
scrollMethod: "dom-anchor-fallback-input",
|
|
752
|
+
stepTimeoutMs: 45000,
|
|
753
|
+
totalTimeoutMs: 90000,
|
|
716
754
|
duplicateStopCount: 1,
|
|
717
755
|
skipDuplicateScreenshots: true,
|
|
718
756
|
composeForLlm: true,
|
|
@@ -838,7 +876,8 @@ export async function runRecommendWorkflow({
|
|
|
838
876
|
|
|
839
877
|
runControl.updateProgress({
|
|
840
878
|
card_count: cardNodeIds.length,
|
|
841
|
-
target_count:
|
|
879
|
+
target_count: targetPassCount,
|
|
880
|
+
target_count_semantics: "passed_candidates",
|
|
842
881
|
processed: results.length,
|
|
843
882
|
screened: results.length,
|
|
844
883
|
detail_opened: results.filter((item) => item.detail).length,
|
|
@@ -939,9 +978,9 @@ export function createRecommendRunService({
|
|
|
939
978
|
imageWheelDeltaY = 650,
|
|
940
979
|
cvAcquisitionMode = "unknown",
|
|
941
980
|
listMaxScrolls = 20,
|
|
942
|
-
listStableSignatureLimit =
|
|
981
|
+
listStableSignatureLimit = 5,
|
|
943
982
|
listWheelDeltaY = 850,
|
|
944
|
-
listSettleMs =
|
|
983
|
+
listSettleMs = 2200,
|
|
945
984
|
listFallbackPoint = null,
|
|
946
985
|
refreshOnEnd = true,
|
|
947
986
|
maxRefreshRounds = 2,
|
|
@@ -968,7 +1007,7 @@ export function createRecommendRunService({
|
|
|
968
1007
|
const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
|
|
969
1008
|
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
970
1009
|
const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
|
|
971
|
-
const normalizedDetailLimit = detailLimit == null ?
|
|
1010
|
+
const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
|
|
972
1011
|
return manager.startRun({
|
|
973
1012
|
name,
|
|
974
1013
|
context: {
|
|
@@ -980,6 +1019,7 @@ export function createRecommendRunService({
|
|
|
980
1019
|
fallback_page_scope: normalizedFallbackPageScope,
|
|
981
1020
|
filter: normalizedFilter,
|
|
982
1021
|
max_candidates: maxCandidates,
|
|
1022
|
+
max_candidates_semantics: "passed_candidates",
|
|
983
1023
|
detail_limit: normalizedDetailLimit,
|
|
984
1024
|
close_detail: closeDetail,
|
|
985
1025
|
cv_acquisition_mode: cvAcquisitionMode,
|
|
@@ -1008,6 +1048,7 @@ export function createRecommendRunService({
|
|
|
1008
1048
|
progress: {
|
|
1009
1049
|
card_count: 0,
|
|
1010
1050
|
target_count: candidateLimit,
|
|
1051
|
+
target_count_semantics: "passed_candidates",
|
|
1011
1052
|
processed: 0,
|
|
1012
1053
|
screened: 0,
|
|
1013
1054
|
detail_opened: 0,
|