@reconcrap/boss-recommend-mcp 2.0.10 → 2.0.12
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/core/capture/index.js +143 -1
- package/src/core/cv-acquisition/index.js +5 -0
- package/src/core/infinite-list/index.js +354 -1
- package/src/core/screening/index.js +58 -13
- package/src/domains/chat/constants.js +11 -0
- package/src/domains/chat/run-service.js +26 -10
- package/src/domains/recommend/constants.js +11 -0
- package/src/domains/recommend/refresh.js +104 -82
- package/src/domains/recommend/run-service.js +33 -8
- package/src/domains/recruit/constants.js +23 -0
- package/src/domains/recruit/run-service.js +20 -4
package/package.json
CHANGED
|
@@ -146,6 +146,14 @@ function filePathForSequence(basePath, index, extension) {
|
|
|
146
146
|
return path.join(parsed.dir, `${parsed.name}-page-${page}${parsed.ext || `.${extension}`}`);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
function filePathForLlmSequence(basePath, index) {
|
|
150
|
+
const resolved = resolveOutputPath(basePath);
|
|
151
|
+
if (!resolved) return null;
|
|
152
|
+
const parsed = path.parse(resolved);
|
|
153
|
+
const page = String(index + 1).padStart(2, "0");
|
|
154
|
+
return path.join(parsed.dir, `${parsed.name}-llm-${page}.jpg`);
|
|
155
|
+
}
|
|
156
|
+
|
|
149
157
|
function screenshotHash(buffer) {
|
|
150
158
|
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
151
159
|
}
|
|
@@ -207,6 +215,111 @@ async function optimizeScreenshotBuffer(buffer, {
|
|
|
207
215
|
}
|
|
208
216
|
}
|
|
209
217
|
|
|
218
|
+
async function composeScreenshotsForLlm(screenshots = [], {
|
|
219
|
+
basePath,
|
|
220
|
+
pagesPerImage = 3,
|
|
221
|
+
resizeMaxWidth = 1100,
|
|
222
|
+
quality = 72
|
|
223
|
+
} = {}) {
|
|
224
|
+
const fileScreenshots = screenshots.filter((item) => item?.file_path);
|
|
225
|
+
if (!basePath || fileScreenshots.length <= 1) {
|
|
226
|
+
return {
|
|
227
|
+
llm_file_paths: fileScreenshots.map((item) => item.file_path),
|
|
228
|
+
llm_screenshots: [],
|
|
229
|
+
llm_total_byte_length: 0,
|
|
230
|
+
llm_original_total_byte_length: 0,
|
|
231
|
+
llm_composition_error: null
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const safePagesPerImage = Math.max(1, Math.min(5, Number(pagesPerImage) || 3));
|
|
236
|
+
const safeWidth = Math.max(700, Math.min(1400, Number(resizeMaxWidth) || 1100));
|
|
237
|
+
const safeQuality = Math.max(45, Math.min(90, Number(quality) || 72));
|
|
238
|
+
const llmScreenshots = [];
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
for (let index = 0; index < fileScreenshots.length; index += safePagesPerImage) {
|
|
242
|
+
const group = fileScreenshots.slice(index, index + safePagesPerImage);
|
|
243
|
+
const prepared = [];
|
|
244
|
+
for (const item of group) {
|
|
245
|
+
const sourceBuffer = fs.readFileSync(item.file_path);
|
|
246
|
+
const { data, info } = await sharp(sourceBuffer, { failOn: "none" })
|
|
247
|
+
.resize({
|
|
248
|
+
width: safeWidth,
|
|
249
|
+
withoutEnlargement: true
|
|
250
|
+
})
|
|
251
|
+
.jpeg({
|
|
252
|
+
quality: safeQuality,
|
|
253
|
+
mozjpeg: true
|
|
254
|
+
})
|
|
255
|
+
.toBuffer({ resolveWithObject: true });
|
|
256
|
+
prepared.push({
|
|
257
|
+
input: data,
|
|
258
|
+
width: info.width,
|
|
259
|
+
height: info.height,
|
|
260
|
+
source_file_path: item.file_path
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const width = Math.max(...prepared.map((item) => item.width), 1);
|
|
265
|
+
const height = prepared.reduce((sum, item) => sum + item.height, 0);
|
|
266
|
+
let top = 0;
|
|
267
|
+
const composites = prepared.map((item) => {
|
|
268
|
+
const layer = {
|
|
269
|
+
input: item.input,
|
|
270
|
+
left: 0,
|
|
271
|
+
top
|
|
272
|
+
};
|
|
273
|
+
top += item.height;
|
|
274
|
+
return layer;
|
|
275
|
+
});
|
|
276
|
+
const outputBuffer = await sharp({
|
|
277
|
+
create: {
|
|
278
|
+
width,
|
|
279
|
+
height,
|
|
280
|
+
channels: 3,
|
|
281
|
+
background: "#ffffff"
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
.composite(composites)
|
|
285
|
+
.jpeg({
|
|
286
|
+
quality: safeQuality,
|
|
287
|
+
mozjpeg: true
|
|
288
|
+
})
|
|
289
|
+
.toBuffer();
|
|
290
|
+
const outputPath = filePathForLlmSequence(basePath, llmScreenshots.length);
|
|
291
|
+
fs.writeFileSync(outputPath, outputBuffer);
|
|
292
|
+
llmScreenshots.push({
|
|
293
|
+
index: llmScreenshots.length,
|
|
294
|
+
file_path: outputPath,
|
|
295
|
+
byte_length: outputBuffer.length,
|
|
296
|
+
source_file_paths: prepared.map((item) => item.source_file_path),
|
|
297
|
+
source_page_count: prepared.length,
|
|
298
|
+
width,
|
|
299
|
+
height,
|
|
300
|
+
format: "jpeg",
|
|
301
|
+
mime_type: "image/jpeg"
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return {
|
|
306
|
+
llm_file_paths: fileScreenshots.map((item) => item.file_path),
|
|
307
|
+
llm_screenshots: [],
|
|
308
|
+
llm_total_byte_length: 0,
|
|
309
|
+
llm_original_total_byte_length: fileScreenshots.reduce((sum, item) => sum + (Number(item.byte_length) || 0), 0),
|
|
310
|
+
llm_composition_error: error?.message || String(error)
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
llm_file_paths: llmScreenshots.map((item) => item.file_path),
|
|
316
|
+
llm_screenshots: llmScreenshots,
|
|
317
|
+
llm_total_byte_length: llmScreenshots.reduce((sum, item) => sum + (Number(item.byte_length) || 0), 0),
|
|
318
|
+
llm_original_total_byte_length: fileScreenshots.reduce((sum, item) => sum + (Number(item.byte_length) || 0), 0),
|
|
319
|
+
llm_composition_error: null
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
210
323
|
export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
211
324
|
filePath,
|
|
212
325
|
format = "png",
|
|
@@ -222,6 +335,10 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
222
335
|
skipDuplicateScreenshots = false,
|
|
223
336
|
optimize = false,
|
|
224
337
|
resizeMaxWidth = 0,
|
|
338
|
+
composeForLlm = false,
|
|
339
|
+
llmPagesPerImage = 3,
|
|
340
|
+
llmResizeMaxWidth = 1100,
|
|
341
|
+
llmQuality = 72,
|
|
225
342
|
metadata = {}
|
|
226
343
|
} = {}) {
|
|
227
344
|
if (!nodeId) throw new Error("captureScrolledNodeScreenshots requires nodeId");
|
|
@@ -322,6 +439,21 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
322
439
|
}
|
|
323
440
|
}
|
|
324
441
|
|
|
442
|
+
const llmComposition = composeForLlm
|
|
443
|
+
? await composeScreenshotsForLlm(screenshots, {
|
|
444
|
+
basePath: filePath,
|
|
445
|
+
pagesPerImage: llmPagesPerImage,
|
|
446
|
+
resizeMaxWidth: llmResizeMaxWidth,
|
|
447
|
+
quality: llmQuality
|
|
448
|
+
})
|
|
449
|
+
: {
|
|
450
|
+
llm_file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
|
|
451
|
+
llm_screenshots: [],
|
|
452
|
+
llm_total_byte_length: 0,
|
|
453
|
+
llm_original_total_byte_length: 0,
|
|
454
|
+
llm_composition_error: null
|
|
455
|
+
};
|
|
456
|
+
|
|
325
457
|
return {
|
|
326
458
|
schema_version: 1,
|
|
327
459
|
source: "image-scroll-sequence",
|
|
@@ -335,12 +467,22 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
335
467
|
dropped_duplicate_count: droppedDuplicateCount,
|
|
336
468
|
total_byte_length: screenshots.reduce((sum, item) => sum + (Number(item.byte_length) || 0), 0),
|
|
337
469
|
original_total_byte_length: screenshots.reduce((sum, item) => sum + (Number(item.original_byte_length) || 0), 0),
|
|
470
|
+
llm_file_paths: llmComposition.llm_file_paths,
|
|
471
|
+
llm_screenshot_count: llmComposition.llm_file_paths.length,
|
|
472
|
+
llm_total_byte_length: llmComposition.llm_total_byte_length,
|
|
473
|
+
llm_original_total_byte_length: llmComposition.llm_original_total_byte_length,
|
|
474
|
+
llm_composition_error: llmComposition.llm_composition_error,
|
|
475
|
+
llm_screenshots: llmComposition.llm_screenshots,
|
|
338
476
|
optimization: {
|
|
339
477
|
enabled: Boolean(optimize),
|
|
340
478
|
resize_max_width: Math.max(0, Number(resizeMaxWidth) || 0),
|
|
341
479
|
capture_viewport: Boolean(captureViewport),
|
|
342
480
|
format,
|
|
343
|
-
quality: quality ?? null
|
|
481
|
+
quality: quality ?? null,
|
|
482
|
+
llm_compose_enabled: Boolean(composeForLlm),
|
|
483
|
+
llm_pages_per_image: Math.max(1, Math.min(5, Number(llmPagesPerImage) || 3)),
|
|
484
|
+
llm_resize_max_width: Math.max(0, Number(llmResizeMaxWidth) || 0),
|
|
485
|
+
llm_quality: llmQuality ?? null
|
|
344
486
|
},
|
|
345
487
|
file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
|
|
346
488
|
screenshots,
|
|
@@ -132,8 +132,13 @@ export function summarizeImageEvidence(imageEvidence = null) {
|
|
|
132
132
|
dropped_duplicate_count: imageEvidence.dropped_duplicate_count || 0,
|
|
133
133
|
total_byte_length: imageEvidence.total_byte_length || 0,
|
|
134
134
|
original_total_byte_length: imageEvidence.original_total_byte_length || 0,
|
|
135
|
+
llm_screenshot_count: imageEvidence.llm_screenshot_count || 0,
|
|
136
|
+
llm_total_byte_length: imageEvidence.llm_total_byte_length || 0,
|
|
137
|
+
llm_original_total_byte_length: imageEvidence.llm_original_total_byte_length || 0,
|
|
138
|
+
llm_composition_error: imageEvidence.llm_composition_error || null,
|
|
135
139
|
optimization: imageEvidence.optimization || null,
|
|
136
140
|
file_paths: imageEvidence.file_paths || [],
|
|
141
|
+
llm_file_paths: imageEvidence.llm_file_paths || [],
|
|
137
142
|
first_clip: imageEvidence.screenshots?.[0]?.clip || imageEvidence.clip || null
|
|
138
143
|
};
|
|
139
144
|
}
|
|
@@ -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,
|
|
@@ -392,11 +392,18 @@ function normalizeImagePaths({ imageEvidence = null, imagePaths = [] } = {}) {
|
|
|
392
392
|
if (Array.isArray(imagePaths)) {
|
|
393
393
|
paths.push(...imagePaths);
|
|
394
394
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
paths.push(...
|
|
395
|
+
const evidenceLlmPaths = Array.isArray(imageEvidence?.llm_file_paths)
|
|
396
|
+
? imageEvidence.llm_file_paths
|
|
397
|
+
: [];
|
|
398
|
+
if (evidenceLlmPaths.length) {
|
|
399
|
+
paths.push(...evidenceLlmPaths);
|
|
400
|
+
} else {
|
|
401
|
+
if (Array.isArray(imageEvidence?.file_paths)) {
|
|
402
|
+
paths.push(...imageEvidence.file_paths);
|
|
403
|
+
}
|
|
404
|
+
if (Array.isArray(imageEvidence?.screenshots)) {
|
|
405
|
+
paths.push(...imageEvidence.screenshots.map((item) => item.file_path));
|
|
406
|
+
}
|
|
400
407
|
}
|
|
401
408
|
return unique(paths.map((filePath) => String(filePath || "").trim()).filter(Boolean));
|
|
402
409
|
}
|
|
@@ -1292,6 +1299,7 @@ export function compactScreeningLlmResult(llmResult) {
|
|
|
1292
1299
|
usage: llmResult.usage || null,
|
|
1293
1300
|
finish_reason: llmResult.finish_reason || null,
|
|
1294
1301
|
image_input_count: llmResult.image_input_count || 0,
|
|
1302
|
+
attempt_count: llmResult.attempt_count || 0,
|
|
1295
1303
|
error: llmResult.error || null,
|
|
1296
1304
|
screened_at: llmResult.screened_at || null
|
|
1297
1305
|
};
|
|
@@ -1324,6 +1332,7 @@ export function createFailedLlmScreeningResult(error) {
|
|
|
1324
1332
|
raw_model_output: "",
|
|
1325
1333
|
image_input_count: Number(error?.image_input_count) || 0,
|
|
1326
1334
|
image_inputs: Array.isArray(error?.image_inputs) ? error.image_inputs : [],
|
|
1335
|
+
attempt_count: Number(error?.llm_attempt_count) || 0,
|
|
1327
1336
|
error: error?.message || String(error || "unknown"),
|
|
1328
1337
|
screened_at: nowIso()
|
|
1329
1338
|
};
|
|
@@ -1352,7 +1361,7 @@ export function buildScreeningLlmMessages({
|
|
|
1352
1361
|
`请根据以下标准判断候选人是否通过筛选。\n\n筛选标准:\n${safeCriteria}\n\n`
|
|
1353
1362
|
+ `候选人信息:\n${safeText || "候选人的完整简历信息在后续截图中,请按截图顺序阅读。"}\n\n`
|
|
1354
1363
|
+ (images.length
|
|
1355
|
-
? `候选人简历截图共 ${images.length}
|
|
1364
|
+
? `候选人简历截图共 ${images.length} 张,按从上到下的滚动顺序排列。若截图是拼接长图,请按图内从上到下顺序完整阅读;不要跳过任何一段简历内容。\n\n`
|
|
1356
1365
|
: "")
|
|
1357
1366
|
+ "要求:\n"
|
|
1358
1367
|
+ "1) 只能依据候选人信息或截图中真实出现的内容判断。\n"
|
|
@@ -1383,6 +1392,24 @@ export function buildScreeningLlmMessages({
|
|
|
1383
1392
|
];
|
|
1384
1393
|
}
|
|
1385
1394
|
|
|
1395
|
+
function normalizeLlmMaxRetries(value) {
|
|
1396
|
+
if (value == null || value === "") return 1;
|
|
1397
|
+
const parsed = Number(value);
|
|
1398
|
+
if (!Number.isFinite(parsed) || parsed < 0) return 1;
|
|
1399
|
+
return Math.min(3, Math.floor(parsed));
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function isRetryableLlmRequestError(error) {
|
|
1403
|
+
const status = Number(error?.status);
|
|
1404
|
+
if ([408, 409, 425, 429].includes(status) || status >= 500) return true;
|
|
1405
|
+
return /(?:aborted|abort|timeout|timed out|fetch failed|socket|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN)/i
|
|
1406
|
+
.test(String(error?.message || error || ""));
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function sleepMs(ms) {
|
|
1410
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1386
1413
|
export async function callScreeningLlm({
|
|
1387
1414
|
candidate,
|
|
1388
1415
|
criteria,
|
|
@@ -1425,9 +1452,13 @@ export async function callScreeningLlm({
|
|
|
1425
1452
|
thinkingLevel: config.llmThinkingLevel || config.thinkingLevel || config.reasoningEffort || "low"
|
|
1426
1453
|
});
|
|
1427
1454
|
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1455
|
+
const maxRetries = normalizeLlmMaxRetries(config.llmMaxRetries ?? config.maxRetries);
|
|
1456
|
+
const maxAttempts = maxRetries + 1;
|
|
1457
|
+
let lastError = null;
|
|
1458
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1459
|
+
const controller = new AbortController();
|
|
1460
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1461
|
+
try {
|
|
1431
1462
|
const headers = {
|
|
1432
1463
|
"Content-Type": "application/json",
|
|
1433
1464
|
Authorization: `Bearer ${apiKey}`
|
|
@@ -1443,7 +1474,9 @@ export async function callScreeningLlm({
|
|
|
1443
1474
|
});
|
|
1444
1475
|
const responseText = await response.text();
|
|
1445
1476
|
if (!response.ok) {
|
|
1446
|
-
|
|
1477
|
+
const error = new Error(`LLM request failed: ${response.status} ${responseText.slice(0, 400)}`);
|
|
1478
|
+
error.status = response.status;
|
|
1479
|
+
throw error;
|
|
1447
1480
|
}
|
|
1448
1481
|
const json = tryParseJson(responseText);
|
|
1449
1482
|
if (!json) {
|
|
@@ -1485,13 +1518,25 @@ export async function callScreeningLlm({
|
|
|
1485
1518
|
raw_content_length: content.length,
|
|
1486
1519
|
image_input_count: imageInputs.length,
|
|
1487
1520
|
image_inputs: summarizeLlmImageInputs(imageInputs),
|
|
1521
|
+
attempt_count: attempt,
|
|
1488
1522
|
screened_at: nowIso()
|
|
1489
1523
|
};
|
|
1490
1524
|
} catch (error) {
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1525
|
+
lastError = error;
|
|
1526
|
+
if (attempt >= maxAttempts || !isRetryableLlmRequestError(error)) {
|
|
1527
|
+
error.image_input_count = imageInputs.length;
|
|
1528
|
+
error.image_inputs = summarizeLlmImageInputs(imageInputs);
|
|
1529
|
+
error.llm_attempt_count = attempt;
|
|
1530
|
+
throw error;
|
|
1531
|
+
}
|
|
1532
|
+
await sleepMs(Math.min(2500, 500 * attempt));
|
|
1494
1533
|
} finally {
|
|
1495
1534
|
clearTimeout(timer);
|
|
1496
1535
|
}
|
|
1536
|
+
}
|
|
1537
|
+
lastError = lastError || new Error("LLM request failed without response");
|
|
1538
|
+
lastError.image_input_count = imageInputs.length;
|
|
1539
|
+
lastError.image_inputs = summarizeLlmImageInputs(imageInputs);
|
|
1540
|
+
lastError.llm_attempt_count = maxAttempts;
|
|
1541
|
+
throw lastError;
|
|
1497
1542
|
}
|
|
@@ -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,
|
|
@@ -98,6 +102,7 @@ function compactLlmResult(llmResult) {
|
|
|
98
102
|
usage: llmResult.usage || null,
|
|
99
103
|
finish_reason: llmResult.finish_reason || null,
|
|
100
104
|
image_input_count: llmResult.image_input_count || 0,
|
|
105
|
+
attempt_count: llmResult.attempt_count || 0,
|
|
101
106
|
error: llmResult.error || null
|
|
102
107
|
};
|
|
103
108
|
}
|
|
@@ -371,9 +376,9 @@ export async function runChatWorkflow({
|
|
|
371
376
|
llmImageDetail = "high",
|
|
372
377
|
screeningMode = "llm",
|
|
373
378
|
listMaxScrolls = 20,
|
|
374
|
-
listStableSignatureLimit =
|
|
379
|
+
listStableSignatureLimit = 5,
|
|
375
380
|
listWheelDeltaY = 850,
|
|
376
|
-
listSettleMs =
|
|
381
|
+
listSettleMs = 2200,
|
|
377
382
|
listFallbackPoint = null,
|
|
378
383
|
imageOutputDir = ""
|
|
379
384
|
} = {}, runControl) {
|
|
@@ -630,6 +635,13 @@ export async function runChatWorkflow({
|
|
|
630
635
|
run_candidate_index: results.length,
|
|
631
636
|
visible_index: visibleIndex
|
|
632
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
|
|
633
645
|
})
|
|
634
646
|
}));
|
|
635
647
|
if (!nextCandidateResult.ok) {
|
|
@@ -826,11 +838,15 @@ export async function runChatWorkflow({
|
|
|
826
838
|
maxScreenshots: maxImagePages,
|
|
827
839
|
wheelDeltaY: imageWheelDeltaY,
|
|
828
840
|
settleMs: 350,
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
841
|
+
duplicateStopCount: 1,
|
|
842
|
+
skipDuplicateScreenshots: true,
|
|
843
|
+
composeForLlm: true,
|
|
844
|
+
llmPagesPerImage: 3,
|
|
845
|
+
llmResizeMaxWidth: 1100,
|
|
846
|
+
llmQuality: 72,
|
|
847
|
+
metadata: {
|
|
848
|
+
domain: "chat",
|
|
849
|
+
capture_mode: "scroll_sequence",
|
|
834
850
|
acquisition_reason: normalizedDetailSource === "image"
|
|
835
851
|
? "forced_image"
|
|
836
852
|
: "network_miss_image_fallback",
|
|
@@ -1132,9 +1148,9 @@ export function createChatRunService({
|
|
|
1132
1148
|
llmImageDetail = "high",
|
|
1133
1149
|
screeningMode = "llm",
|
|
1134
1150
|
listMaxScrolls = 20,
|
|
1135
|
-
listStableSignatureLimit =
|
|
1151
|
+
listStableSignatureLimit = 5,
|
|
1136
1152
|
listWheelDeltaY = 850,
|
|
1137
|
-
listSettleMs =
|
|
1153
|
+
listSettleMs = 2200,
|
|
1138
1154
|
listFallbackPoint = null,
|
|
1139
1155
|
imageOutputDir = "",
|
|
1140
1156
|
name = "chat-domain-run"
|
|
@@ -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",
|
|
@@ -110,95 +110,117 @@ export async function refreshRecommendListAtEnd(client, {
|
|
|
110
110
|
);
|
|
111
111
|
attempts.push(buttonResult);
|
|
112
112
|
if (buttonResult.ok) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
113
|
+
try {
|
|
114
|
+
currentRootState = await getRecommendRoots(client);
|
|
115
|
+
const pageScopeResult = await selectRecommendPageScope(
|
|
116
|
+
client,
|
|
117
|
+
currentRootState.iframe.documentNodeId,
|
|
118
|
+
{
|
|
119
|
+
pageScope,
|
|
120
|
+
fallbackScope: fallbackPageScope,
|
|
121
|
+
settleMs: buttonSettleMs > 10000 ? 3000 : 1200,
|
|
122
|
+
timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
if (!pageScopeResult.selected) {
|
|
126
|
+
throw new Error(`Recommend page scope was not selected after end refresh: ${pageScopeResult.reason || pageScope}`);
|
|
122
127
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
currentRootState = await getRecommendRoots(client);
|
|
129
|
+
const filterResult = await selectAndConfirmFirstSafeFilter(
|
|
130
|
+
client,
|
|
131
|
+
currentRootState.iframe.documentNodeId,
|
|
132
|
+
buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
|
|
133
|
+
);
|
|
134
|
+
const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
135
|
+
timeoutMs: cardTimeoutMs,
|
|
136
|
+
intervalMs: 500
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
ok: cardNodeIds.length > 0,
|
|
140
|
+
method: "end_refresh_button",
|
|
141
|
+
attempts,
|
|
142
|
+
page_scope: pageScopeResult,
|
|
143
|
+
filter: filterResult,
|
|
144
|
+
card_count: cardNodeIds.length,
|
|
145
|
+
root_state: currentRootState,
|
|
146
|
+
forced_recent_not_view: forceRecentNotView
|
|
147
|
+
};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
attempts.push({
|
|
150
|
+
ok: false,
|
|
151
|
+
method: "end_refresh_button_after_click",
|
|
152
|
+
reason: "end_refresh_reapply_failed",
|
|
153
|
+
error: error?.message || String(error)
|
|
154
|
+
});
|
|
126
155
|
}
|
|
127
|
-
currentRootState = await getRecommendRoots(client);
|
|
128
|
-
const filterResult = await selectAndConfirmFirstSafeFilter(
|
|
129
|
-
client,
|
|
130
|
-
currentRootState.iframe.documentNodeId,
|
|
131
|
-
buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
|
|
132
|
-
);
|
|
133
|
-
const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
134
|
-
timeoutMs: cardTimeoutMs,
|
|
135
|
-
intervalMs: 500
|
|
136
|
-
});
|
|
137
|
-
return {
|
|
138
|
-
ok: cardNodeIds.length > 0,
|
|
139
|
-
method: "end_refresh_button",
|
|
140
|
-
attempts,
|
|
141
|
-
page_scope: pageScopeResult,
|
|
142
|
-
filter: filterResult,
|
|
143
|
-
card_count: cardNodeIds.length,
|
|
144
|
-
root_state: currentRootState,
|
|
145
|
-
forced_recent_not_view: forceRecentNotView
|
|
146
|
-
};
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (!currentRootState?.iframe?.documentNodeId) {
|
|
157
|
-
throw new Error("Recommend iframe was not ready after refresh reload");
|
|
158
|
-
}
|
|
159
|
-
let jobSelection = null;
|
|
160
|
-
if (jobLabel) {
|
|
161
|
-
jobSelection = await selectRecommendJob(client, currentRootState.iframe.documentNodeId, {
|
|
162
|
-
jobLabel,
|
|
163
|
-
settleMs: reloadSettleMs > 10000 ? 12000 : 6000
|
|
159
|
+
try {
|
|
160
|
+
await client.Page.reload({ ignoreCache: true });
|
|
161
|
+
if (reloadSettleMs > 0) await sleep(reloadSettleMs);
|
|
162
|
+
currentRootState = await waitForRecommendRoots(client, {
|
|
163
|
+
timeoutMs: Math.max(30000, reloadSettleMs * 4),
|
|
164
|
+
intervalMs: 500
|
|
164
165
|
});
|
|
165
|
-
if (!
|
|
166
|
-
throw new Error(
|
|
166
|
+
if (!currentRootState?.iframe?.documentNodeId) {
|
|
167
|
+
throw new Error("Recommend iframe was not ready after refresh reload");
|
|
167
168
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
let jobSelection = null;
|
|
170
|
+
if (jobLabel) {
|
|
171
|
+
jobSelection = await selectRecommendJob(client, currentRootState.iframe.documentNodeId, {
|
|
172
|
+
jobLabel,
|
|
173
|
+
settleMs: reloadSettleMs > 10000 ? 12000 : 6000
|
|
174
|
+
});
|
|
175
|
+
if (!jobSelection.selected) {
|
|
176
|
+
throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
|
|
177
|
+
}
|
|
178
|
+
currentRootState = await getRecommendRoots(client);
|
|
178
179
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
const pageScopeResult = await selectRecommendPageScope(
|
|
181
|
+
client,
|
|
182
|
+
currentRootState.iframe.documentNodeId,
|
|
183
|
+
{
|
|
184
|
+
pageScope,
|
|
185
|
+
fallbackScope: fallbackPageScope,
|
|
186
|
+
settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
|
|
187
|
+
timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
if (!pageScopeResult.selected) {
|
|
191
|
+
throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
|
|
192
|
+
}
|
|
193
|
+
currentRootState = await getRecommendRoots(client);
|
|
194
|
+
const filterResult = await selectAndConfirmFirstSafeFilter(
|
|
195
|
+
client,
|
|
196
|
+
currentRootState.iframe.documentNodeId,
|
|
197
|
+
buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
|
|
198
|
+
);
|
|
199
|
+
const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
200
|
+
timeoutMs: cardTimeoutMs,
|
|
201
|
+
intervalMs: 500
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
ok: cardNodeIds.length > 0,
|
|
205
|
+
method: "page_reload",
|
|
206
|
+
attempts,
|
|
207
|
+
job_selection: jobSelection,
|
|
208
|
+
page_scope: pageScopeResult,
|
|
209
|
+
filter: filterResult,
|
|
210
|
+
card_count: cardNodeIds.length,
|
|
211
|
+
root_state: currentRootState,
|
|
212
|
+
forced_recent_not_view: forceRecentNotView
|
|
213
|
+
};
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
method: "page_reload",
|
|
218
|
+
reason: "page_reload_failed",
|
|
219
|
+
error: error?.message || String(error),
|
|
220
|
+
attempts,
|
|
221
|
+
card_count: 0,
|
|
222
|
+
root_state: currentRootState,
|
|
223
|
+
forced_recent_not_view: forceRecentNotView
|
|
224
|
+
};
|
|
182
225
|
}
|
|
183
|
-
currentRootState = await getRecommendRoots(client);
|
|
184
|
-
const filterResult = await selectAndConfirmFirstSafeFilter(
|
|
185
|
-
client,
|
|
186
|
-
currentRootState.iframe.documentNodeId,
|
|
187
|
-
buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
|
|
188
|
-
);
|
|
189
|
-
const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
190
|
-
timeoutMs: cardTimeoutMs,
|
|
191
|
-
intervalMs: 500
|
|
192
|
-
});
|
|
193
|
-
return {
|
|
194
|
-
ok: cardNodeIds.length > 0,
|
|
195
|
-
method: "page_reload",
|
|
196
|
-
attempts,
|
|
197
|
-
job_selection: jobSelection,
|
|
198
|
-
page_scope: pageScopeResult,
|
|
199
|
-
filter: filterResult,
|
|
200
|
-
card_count: cardNodeIds.length,
|
|
201
|
-
root_state: currentRootState,
|
|
202
|
-
forced_recent_not_view: forceRecentNotView
|
|
203
|
-
};
|
|
204
226
|
}
|
|
@@ -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
|
|
@@ -364,9 +378,9 @@ export async function runRecommendWorkflow({
|
|
|
364
378
|
imageWheelDeltaY = 650,
|
|
365
379
|
cvAcquisitionMode = "unknown",
|
|
366
380
|
listMaxScrolls = 20,
|
|
367
|
-
listStableSignatureLimit =
|
|
381
|
+
listStableSignatureLimit = 5,
|
|
368
382
|
listWheelDeltaY = 850,
|
|
369
|
-
listSettleMs =
|
|
383
|
+
listSettleMs = 2200,
|
|
370
384
|
listFallbackPoint = null,
|
|
371
385
|
refreshOnEnd = true,
|
|
372
386
|
maxRefreshRounds = 2,
|
|
@@ -559,15 +573,22 @@ export async function runRecommendWorkflow({
|
|
|
559
573
|
run_candidate_index: results.length,
|
|
560
574
|
visible_index: visibleIndex
|
|
561
575
|
}
|
|
576
|
+
}),
|
|
577
|
+
detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
|
|
578
|
+
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
579
|
+
markerSelectors: RECOMMEND_BOTTOM_MARKER_SELECTORS,
|
|
580
|
+
refreshSelectors: [RECOMMEND_END_REFRESH_SELECTOR],
|
|
581
|
+
textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
|
|
582
|
+
maxTextScanNodes: 500
|
|
562
583
|
})
|
|
563
584
|
}));
|
|
564
585
|
if (!nextCandidateResult.ok) {
|
|
565
586
|
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
566
587
|
if (
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
588
|
+
(nextCandidateResult.end_reached || isRefreshableListStall(nextCandidateResult.reason))
|
|
589
|
+
&& refreshOnEnd
|
|
590
|
+
&& results.length < limit
|
|
591
|
+
&& refreshRounds < Math.max(0, Number(maxRefreshRounds) || 0)
|
|
571
592
|
) {
|
|
572
593
|
await runControl.waitIfPaused();
|
|
573
594
|
runControl.throwIfCanceled();
|
|
@@ -715,6 +736,10 @@ export async function runRecommendWorkflow({
|
|
|
715
736
|
settleMs: 350,
|
|
716
737
|
duplicateStopCount: 1,
|
|
717
738
|
skipDuplicateScreenshots: true,
|
|
739
|
+
composeForLlm: true,
|
|
740
|
+
llmPagesPerImage: 3,
|
|
741
|
+
llmResizeMaxWidth: 1100,
|
|
742
|
+
llmQuality: 72,
|
|
718
743
|
metadata: {
|
|
719
744
|
domain: "recommend",
|
|
720
745
|
capture_mode: "scroll_sequence",
|
|
@@ -935,9 +960,9 @@ export function createRecommendRunService({
|
|
|
935
960
|
imageWheelDeltaY = 650,
|
|
936
961
|
cvAcquisitionMode = "unknown",
|
|
937
962
|
listMaxScrolls = 20,
|
|
938
|
-
listStableSignatureLimit =
|
|
963
|
+
listStableSignatureLimit = 5,
|
|
939
964
|
listWheelDeltaY = 850,
|
|
940
|
-
listSettleMs =
|
|
965
|
+
listSettleMs = 2200,
|
|
941
966
|
listFallbackPoint = null,
|
|
942
967
|
refreshOnEnd = true,
|
|
943
968
|
maxRefreshRounds = 2,
|
|
@@ -24,6 +24,29 @@ export const RECRUIT_NO_DATA_SELECTORS = Object.freeze([
|
|
|
24
24
|
'[class*="empty"]'
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
|
+
export const RECRUIT_BOTTOM_MARKER_SELECTORS = Object.freeze([
|
|
28
|
+
".finished-wrap",
|
|
29
|
+
".load-tips",
|
|
30
|
+
".tip-nodata",
|
|
31
|
+
".empty-tip",
|
|
32
|
+
".empty-text",
|
|
33
|
+
".no-data",
|
|
34
|
+
"[class*=\"finished\"]",
|
|
35
|
+
"[class*=\"load-tips\"]",
|
|
36
|
+
"[class*=\"empty\"]"
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export const RECRUIT_BOTTOM_REFRESH_SELECTORS = Object.freeze([
|
|
40
|
+
".finished-wrap .btn-refresh",
|
|
41
|
+
".finished-wrap .btn",
|
|
42
|
+
".no-data-refresh .btn-refresh",
|
|
43
|
+
".no-data-refresh .btn",
|
|
44
|
+
"[class*=\"refresh\"]",
|
|
45
|
+
"[ka*=\"refresh\"]",
|
|
46
|
+
"button",
|
|
47
|
+
"a"
|
|
48
|
+
]);
|
|
49
|
+
|
|
27
50
|
export const RECRUIT_SEARCH_SELECTORS = Object.freeze({
|
|
28
51
|
keywordInput: [
|
|
29
52
|
"input.search-input",
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
compactInfiniteListState,
|
|
21
21
|
createInfiniteListState,
|
|
22
|
+
detectInfiniteListBottomMarker,
|
|
22
23
|
getNextInfiniteListCandidate,
|
|
23
24
|
markInfiniteListCandidateProcessed,
|
|
24
25
|
resetInfiniteListForRefreshRound
|
|
@@ -49,6 +50,10 @@ import {
|
|
|
49
50
|
} from "./search.js";
|
|
50
51
|
import { refreshRecruitSearchAtEnd } from "./refresh.js";
|
|
51
52
|
import { getRecruitRoots } from "./roots.js";
|
|
53
|
+
import {
|
|
54
|
+
RECRUIT_BOTTOM_MARKER_SELECTORS,
|
|
55
|
+
RECRUIT_BOTTOM_REFRESH_SELECTORS
|
|
56
|
+
} from "./constants.js";
|
|
52
57
|
|
|
53
58
|
function compactScreening(screening) {
|
|
54
59
|
return {
|
|
@@ -144,9 +149,9 @@ export async function runRecruitWorkflow({
|
|
|
144
149
|
imageWheelDeltaY = 650,
|
|
145
150
|
cvAcquisitionMode = "unknown",
|
|
146
151
|
listMaxScrolls = 20,
|
|
147
|
-
listStableSignatureLimit =
|
|
152
|
+
listStableSignatureLimit = 5,
|
|
148
153
|
listWheelDeltaY = 850,
|
|
149
|
-
listSettleMs =
|
|
154
|
+
listSettleMs = 2200,
|
|
150
155
|
listFallbackPoint = null,
|
|
151
156
|
refreshOnEnd = true,
|
|
152
157
|
maxRefreshRounds = 2,
|
|
@@ -298,6 +303,13 @@ export async function runRecruitWorkflow({
|
|
|
298
303
|
visible_index: visibleIndex,
|
|
299
304
|
search_params: normalizedSearchParams
|
|
300
305
|
}
|
|
306
|
+
}),
|
|
307
|
+
detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
|
|
308
|
+
rootNodeId: rootState?.iframe?.documentNodeId,
|
|
309
|
+
markerSelectors: RECRUIT_BOTTOM_MARKER_SELECTORS,
|
|
310
|
+
refreshSelectors: RECRUIT_BOTTOM_REFRESH_SELECTORS,
|
|
311
|
+
textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
|
|
312
|
+
maxTextScanNodes: 500
|
|
301
313
|
})
|
|
302
314
|
}));
|
|
303
315
|
if (!nextCandidateResult.ok) {
|
|
@@ -440,6 +452,10 @@ export async function runRecruitWorkflow({
|
|
|
440
452
|
settleMs: 350,
|
|
441
453
|
duplicateStopCount: 1,
|
|
442
454
|
skipDuplicateScreenshots: true,
|
|
455
|
+
composeForLlm: true,
|
|
456
|
+
llmPagesPerImage: 3,
|
|
457
|
+
llmResizeMaxWidth: 1100,
|
|
458
|
+
llmQuality: 72,
|
|
443
459
|
metadata: {
|
|
444
460
|
domain: "recruit",
|
|
445
461
|
capture_mode: "scroll_sequence",
|
|
@@ -621,9 +637,9 @@ export function createRecruitRunService({
|
|
|
621
637
|
imageWheelDeltaY = 650,
|
|
622
638
|
cvAcquisitionMode = "unknown",
|
|
623
639
|
listMaxScrolls = 20,
|
|
624
|
-
listStableSignatureLimit =
|
|
640
|
+
listStableSignatureLimit = 5,
|
|
625
641
|
listWheelDeltaY = 850,
|
|
626
|
-
listSettleMs =
|
|
642
|
+
listSettleMs = 2200,
|
|
627
643
|
listFallbackPoint = null,
|
|
628
644
|
refreshOnEnd = true,
|
|
629
645
|
maxRefreshRounds = 2,
|