@reconcrap/boss-recommend-mcp 2.0.34 → 2.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { createRunLifecycleManager } from "../../core/run/index.js";
|
|
2
4
|
import {
|
|
3
5
|
addTiming,
|
|
@@ -137,6 +139,129 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
141
|
|
|
142
|
+
function compactError(error, fallbackCode = "RECRUIT_RUN_ERROR") {
|
|
143
|
+
if (!error) return null;
|
|
144
|
+
return {
|
|
145
|
+
code: error.code || fallbackCode,
|
|
146
|
+
message: error.message || String(error)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function createRecruitCloseFailureError(closeResult) {
|
|
151
|
+
const error = new Error(closeResult?.reason || "Recruit detail did not close before recovery");
|
|
152
|
+
error.code = "DETAIL_CLOSE_FAILED";
|
|
153
|
+
error.close_result = closeResult || null;
|
|
154
|
+
return error;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function isStaleRecruitNodeError(error) {
|
|
158
|
+
const message = String(error?.message || error || "");
|
|
159
|
+
return /Could not find node with given id|No node with given id|Node is detached|Cannot find node/i.test(message);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function isRecoverableRecruitImageCaptureError(error) {
|
|
163
|
+
const code = String(error?.code || "");
|
|
164
|
+
if (code === "IMAGE_CAPTURE_TIMEOUT" || code === "IMAGE_CAPTURE_TOTAL_TIMEOUT") return true;
|
|
165
|
+
if (isStaleRecruitNodeError(error)) return true;
|
|
166
|
+
return /Image fallback capture timed out/i.test(String(error?.message || error || ""));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function isRecoverableRecruitDetailError(error) {
|
|
170
|
+
return isStaleRecruitNodeError(error);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function compactRecoverableDetailError(error) {
|
|
174
|
+
return compactError(error, isStaleRecruitNodeError(error) ? "DETAIL_STALE_NODE" : "DETAIL_OPEN_FAILED");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function collectPartialImageEvidencePaths(basePath = "", extension = "jpg", maxCount = 12) {
|
|
178
|
+
const resolved = String(basePath || "").trim();
|
|
179
|
+
if (!resolved) return [];
|
|
180
|
+
const parsed = path.parse(resolved);
|
|
181
|
+
const ext = parsed.ext || `.${String(extension || "jpg").replace(/^\./, "") || "jpg"}`;
|
|
182
|
+
const files = [];
|
|
183
|
+
for (let index = 0; index < Math.max(1, Number(maxCount) || 1); index += 1) {
|
|
184
|
+
const page = String(index + 1).padStart(2, "0");
|
|
185
|
+
const candidatePath = path.join(parsed.dir, `${parsed.name}-page-${page}${ext}`);
|
|
186
|
+
if (fs.existsSync(candidatePath)) files.push(candidatePath);
|
|
187
|
+
}
|
|
188
|
+
return files;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function createRecoverableRecruitImageCaptureEvidence(error, {
|
|
192
|
+
elapsedMs = 0,
|
|
193
|
+
filePath = "",
|
|
194
|
+
extension = "jpg",
|
|
195
|
+
maxScreenshots = DEFAULT_MAX_IMAGE_PAGES
|
|
196
|
+
} = {}) {
|
|
197
|
+
const filePaths = collectPartialImageEvidencePaths(filePath, extension, maxScreenshots);
|
|
198
|
+
return {
|
|
199
|
+
schema_version: 1,
|
|
200
|
+
ok: false,
|
|
201
|
+
source: "image-scroll-sequence",
|
|
202
|
+
elapsed_ms: Math.max(0, Math.round(Number(error?.elapsed_ms ?? elapsedMs) || 0)),
|
|
203
|
+
capture_count: filePaths.length,
|
|
204
|
+
screenshot_count: filePaths.length,
|
|
205
|
+
unique_screenshot_count: filePaths.length,
|
|
206
|
+
dropped_duplicate_count: 0,
|
|
207
|
+
total_byte_length: 0,
|
|
208
|
+
original_total_byte_length: 0,
|
|
209
|
+
llm_screenshot_count: 0,
|
|
210
|
+
llm_total_byte_length: 0,
|
|
211
|
+
llm_original_total_byte_length: 0,
|
|
212
|
+
llm_composition_error: null,
|
|
213
|
+
error_code: error?.code || (isStaleRecruitNodeError(error) ? "IMAGE_CAPTURE_STALE_NODE" : "IMAGE_CAPTURE_FAILED"),
|
|
214
|
+
error: error?.message || String(error || "Image capture failed"),
|
|
215
|
+
file_paths: filePaths,
|
|
216
|
+
llm_file_paths: []
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function createImageCaptureFailureScreening(candidate, error) {
|
|
221
|
+
return {
|
|
222
|
+
status: "fail",
|
|
223
|
+
passed: false,
|
|
224
|
+
score: 0,
|
|
225
|
+
reasons: ["image_capture_failed"],
|
|
226
|
+
error: compactError(error, "IMAGE_CAPTURE_FAILED"),
|
|
227
|
+
candidate
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function createRecoverableDetailFailureScreening(candidate, error) {
|
|
232
|
+
return {
|
|
233
|
+
status: "fail",
|
|
234
|
+
passed: false,
|
|
235
|
+
score: 0,
|
|
236
|
+
reasons: isStaleRecruitNodeError(error)
|
|
237
|
+
? ["detail_open_failed", "stale_node"]
|
|
238
|
+
: ["detail_open_failed"],
|
|
239
|
+
error: compactRecoverableDetailError(error),
|
|
240
|
+
candidate
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function countRecruitResultStatuses(results = []) {
|
|
245
|
+
return {
|
|
246
|
+
processed: results.length,
|
|
247
|
+
screened: results.length,
|
|
248
|
+
detail_opened: results.filter((item) => item.detail).length,
|
|
249
|
+
passed: results.filter((item) => item.screening?.passed).length,
|
|
250
|
+
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
251
|
+
image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
|
|
252
|
+
detail_open_failed: results.filter((item) => (
|
|
253
|
+
item.error?.code === "DETAIL_STALE_NODE"
|
|
254
|
+
|| item.error?.code === "DETAIL_OPEN_FAILED"
|
|
255
|
+
)).length,
|
|
256
|
+
transient_recovered: results.filter((item) => (
|
|
257
|
+
item.error?.code === "DETAIL_STALE_NODE"
|
|
258
|
+
|| item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
|
|
259
|
+
|| item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
|
|
260
|
+
|| item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
|
|
261
|
+
)).length
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
140
265
|
export async function runRecruitWorkflow({
|
|
141
266
|
client,
|
|
142
267
|
targetUrl = "",
|
|
@@ -197,6 +322,8 @@ export async function runRecruitWorkflow({
|
|
|
197
322
|
const results = [];
|
|
198
323
|
const refreshAttempts = [];
|
|
199
324
|
let refreshRounds = 0;
|
|
325
|
+
let contextRecoveryAttempts = 0;
|
|
326
|
+
const candidateRecoveryCounts = new Map();
|
|
200
327
|
let cardNodeIds = [];
|
|
201
328
|
let listEndReason = "";
|
|
202
329
|
const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
|
|
@@ -208,6 +335,115 @@ export async function runRecruitWorkflow({
|
|
|
208
335
|
validateViewportPoint: true
|
|
209
336
|
}));
|
|
210
337
|
|
|
338
|
+
function updateRecruitProgress(extra = {}) {
|
|
339
|
+
const counts = countRecruitResultStatuses(results);
|
|
340
|
+
const listSnapshot = compactInfiniteListState(listState);
|
|
341
|
+
runControl.updateProgress({
|
|
342
|
+
card_count: cardNodeIds.length,
|
|
343
|
+
target_count: limit,
|
|
344
|
+
...counts,
|
|
345
|
+
screening_mode: normalizedScreeningMode,
|
|
346
|
+
unique_seen: listSnapshot.seen_count,
|
|
347
|
+
scroll_count: listSnapshot.scroll_count,
|
|
348
|
+
refresh_rounds: refreshRounds,
|
|
349
|
+
refresh_attempts: refreshAttempts.length,
|
|
350
|
+
context_recoveries: contextRecoveryAttempts,
|
|
351
|
+
list_end_reason: listEndReason || null,
|
|
352
|
+
viewport_checks: viewportGuard.getStats().checks,
|
|
353
|
+
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
354
|
+
...extra
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function checkpointInProgressCandidate({
|
|
359
|
+
index = results.length,
|
|
360
|
+
candidateKey = "",
|
|
361
|
+
cardNodeId = null,
|
|
362
|
+
detailStep = "",
|
|
363
|
+
error = null
|
|
364
|
+
} = {}) {
|
|
365
|
+
runControl.checkpoint({
|
|
366
|
+
in_progress_candidate: {
|
|
367
|
+
index,
|
|
368
|
+
key: candidateKey,
|
|
369
|
+
card_node_id: cardNodeId,
|
|
370
|
+
detail_step: detailStep || null,
|
|
371
|
+
counters: countRecruitResultStatuses(results),
|
|
372
|
+
error: compactError(error, "RECRUIT_IN_PROGRESS_ERROR")
|
|
373
|
+
},
|
|
374
|
+
candidate_list: compactInfiniteListState(listState)
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function recoverAndReapplyRecruitContext(reason = "context_recovery", error = null, {
|
|
379
|
+
forceRecentViewed = true
|
|
380
|
+
} = {}) {
|
|
381
|
+
await runControl.waitIfPaused();
|
|
382
|
+
runControl.throwIfCanceled();
|
|
383
|
+
const started = Date.now();
|
|
384
|
+
runControl.setPhase("recruit:recover-context");
|
|
385
|
+
contextRecoveryAttempts += 1;
|
|
386
|
+
const refreshResult = await refreshRecruitSearchAtEnd(client, {
|
|
387
|
+
searchParams: normalizedSearchParams,
|
|
388
|
+
requireCards: true,
|
|
389
|
+
searchTimeoutMs: cardTimeoutMs,
|
|
390
|
+
resetTimeoutMs,
|
|
391
|
+
resetSettleMs: refreshResetSettleMs,
|
|
392
|
+
cityOptionTimeoutMs,
|
|
393
|
+
forceRecentViewed
|
|
394
|
+
});
|
|
395
|
+
const compactRefresh = {
|
|
396
|
+
...compactRefreshAttempt(refreshResult),
|
|
397
|
+
context_recovery: true,
|
|
398
|
+
recovery_reason: reason,
|
|
399
|
+
trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
|
|
400
|
+
elapsed_ms: Date.now() - started
|
|
401
|
+
};
|
|
402
|
+
refreshAttempts.push(compactRefresh);
|
|
403
|
+
runControl.checkpoint({
|
|
404
|
+
context_recovery: {
|
|
405
|
+
attempt: contextRecoveryAttempts,
|
|
406
|
+
reason,
|
|
407
|
+
trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
|
|
408
|
+
refresh: compactRefresh,
|
|
409
|
+
counters: countRecruitResultStatuses(results)
|
|
410
|
+
},
|
|
411
|
+
candidate_list: compactInfiniteListState(listState)
|
|
412
|
+
});
|
|
413
|
+
if (!refreshResult.ok) {
|
|
414
|
+
updateRecruitProgress({
|
|
415
|
+
refresh_method: refreshResult.method || null,
|
|
416
|
+
refresh_forced_recent_viewed: forceRecentViewed,
|
|
417
|
+
recovery_reason: reason
|
|
418
|
+
});
|
|
419
|
+
throw new Error(`Recruit context recovery failed after ${reason}: ${refreshResult.application?.reason || "refresh returned no cards"}`);
|
|
420
|
+
}
|
|
421
|
+
rootState = await getRecruitRoots(client);
|
|
422
|
+
rootState = await ensureRecruitViewport(rootState, "recover_after");
|
|
423
|
+
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
424
|
+
timeoutMs: cardTimeoutMs,
|
|
425
|
+
intervalMs: 300
|
|
426
|
+
});
|
|
427
|
+
resetInfiniteListForRefreshRound(listState, {
|
|
428
|
+
reason: `context_recovery:${reason}`,
|
|
429
|
+
round: contextRecoveryAttempts,
|
|
430
|
+
method: refreshResult.method,
|
|
431
|
+
metadata: {
|
|
432
|
+
card_count: cardNodeIds.length,
|
|
433
|
+
forced_recent_viewed: forceRecentViewed,
|
|
434
|
+
counters: countRecruitResultStatuses(results)
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
listEndReason = "";
|
|
438
|
+
updateRecruitProgress({
|
|
439
|
+
card_count: cardNodeIds.length,
|
|
440
|
+
refresh_method: refreshResult.method || null,
|
|
441
|
+
refresh_forced_recent_viewed: forceRecentViewed,
|
|
442
|
+
recovery_reason: reason
|
|
443
|
+
});
|
|
444
|
+
return refreshResult;
|
|
445
|
+
}
|
|
446
|
+
|
|
211
447
|
runControl.setPhase("recruit:cleanup");
|
|
212
448
|
await closeRecruitDetail(client, { attemptsLimit: 2 });
|
|
213
449
|
|
|
@@ -264,21 +500,8 @@ export async function runRecruitWorkflow({
|
|
|
264
500
|
throw new Error("No recruit/search candidate cards found for run service");
|
|
265
501
|
}
|
|
266
502
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
target_count: limit,
|
|
270
|
-
processed: 0,
|
|
271
|
-
screened: 0,
|
|
272
|
-
detail_opened: 0,
|
|
273
|
-
passed: 0,
|
|
274
|
-
screening_mode: normalizedScreeningMode,
|
|
275
|
-
llm_screened: 0,
|
|
276
|
-
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
277
|
-
scroll_count: 0,
|
|
278
|
-
refresh_rounds: 0,
|
|
279
|
-
refresh_attempts: 0,
|
|
280
|
-
viewport_checks: viewportGuard.getStats().checks,
|
|
281
|
-
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
503
|
+
updateRecruitProgress({
|
|
504
|
+
list_end_reason: null
|
|
282
505
|
});
|
|
283
506
|
|
|
284
507
|
while (results.length < limit) {
|
|
@@ -351,23 +574,11 @@ export async function runRecruitWorkflow({
|
|
|
351
574
|
refresh_round: refreshRounds,
|
|
352
575
|
refresh: compactRefresh
|
|
353
576
|
});
|
|
354
|
-
|
|
577
|
+
updateRecruitProgress({
|
|
355
578
|
card_count: refreshResult.card_count || cardNodeIds.length,
|
|
356
|
-
target_count: limit,
|
|
357
|
-
processed: results.length,
|
|
358
|
-
screened: results.length,
|
|
359
|
-
detail_opened: results.filter((item) => item.detail).length,
|
|
360
|
-
passed: results.filter((item) => item.screening.passed).length,
|
|
361
|
-
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
362
|
-
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
363
|
-
scroll_count: compactInfiniteListState(listState).scroll_count,
|
|
364
|
-
refresh_rounds: refreshRounds,
|
|
365
|
-
refresh_attempts: refreshAttempts.length,
|
|
366
579
|
refresh_method: refreshResult.method || null,
|
|
367
580
|
refresh_forced_recent_viewed: true,
|
|
368
|
-
list_end_reason: listEndReason
|
|
369
|
-
viewport_checks: viewportGuard.getStats().checks,
|
|
370
|
-
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
581
|
+
list_end_reason: listEndReason
|
|
371
582
|
});
|
|
372
583
|
if (refreshResult.ok) {
|
|
373
584
|
rootState = await getRecruitRoots(client);
|
|
@@ -399,128 +610,197 @@ export async function runRecruitWorkflow({
|
|
|
399
610
|
|
|
400
611
|
let screeningCandidate = cardCandidate;
|
|
401
612
|
let detailResult = null;
|
|
613
|
+
let recoverableDetailError = null;
|
|
614
|
+
let detailStep = "not_started";
|
|
402
615
|
if (index < detailCountLimit) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
616
|
+
try {
|
|
617
|
+
await runControl.waitIfPaused();
|
|
618
|
+
runControl.throwIfCanceled();
|
|
619
|
+
runControl.setPhase("recruit:detail");
|
|
620
|
+
detailStep = "ensure_viewport";
|
|
621
|
+
rootState = await ensureRecruitViewport(rootState, "detail");
|
|
622
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
623
|
+
detailStep = "open_detail";
|
|
624
|
+
networkRecorder.clear();
|
|
625
|
+
const openedDetail = await openRecruitCardDetail(client, cardNodeId);
|
|
626
|
+
addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
|
|
627
|
+
addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
|
|
628
|
+
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
629
|
+
detailStep = "wait_network";
|
|
630
|
+
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
631
|
+
waitForRecruitDetailNetworkEvents,
|
|
632
|
+
networkRecorder,
|
|
633
|
+
{
|
|
634
|
+
waitPlan,
|
|
635
|
+
minCount: 1,
|
|
636
|
+
requireLoaded: true,
|
|
637
|
+
intervalMs: 120
|
|
638
|
+
}
|
|
639
|
+
));
|
|
640
|
+
if (networkWait?.elapsed_ms != null) {
|
|
641
|
+
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
420
642
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
closeDetail: false,
|
|
432
|
-
networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
|
|
433
|
-
networkParseIntervalMs: 250
|
|
434
|
-
});
|
|
435
|
-
addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
|
|
436
|
-
const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
437
|
-
let source = "network";
|
|
438
|
-
let imageEvidence = null;
|
|
439
|
-
let captureTarget = null;
|
|
440
|
-
let captureTargetWait = null;
|
|
441
|
-
if (parsedNetworkProfileCount > 0) {
|
|
442
|
-
recordCvNetworkHit(cvAcquisitionState, {
|
|
443
|
-
parsedNetworkProfileCount,
|
|
444
|
-
waitResult: networkWait
|
|
643
|
+
detailStep = "extract_detail";
|
|
644
|
+
detailResult = await extractRecruitDetailCandidate(client, {
|
|
645
|
+
cardCandidate,
|
|
646
|
+
cardNodeId,
|
|
647
|
+
detailState: openedDetail.detail_state,
|
|
648
|
+
networkEvents: networkRecorder.events,
|
|
649
|
+
targetUrl,
|
|
650
|
+
closeDetail: false,
|
|
651
|
+
networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
|
|
652
|
+
networkParseIntervalMs: 250
|
|
445
653
|
});
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
654
|
+
addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
|
|
655
|
+
const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
656
|
+
let source = "network";
|
|
657
|
+
let imageEvidence = null;
|
|
658
|
+
let captureTarget = null;
|
|
659
|
+
let captureTargetWait = null;
|
|
660
|
+
if (parsedNetworkProfileCount > 0) {
|
|
661
|
+
recordCvNetworkHit(cvAcquisitionState, {
|
|
662
|
+
parsedNetworkProfileCount,
|
|
663
|
+
waitResult: networkWait
|
|
664
|
+
});
|
|
665
|
+
} else {
|
|
666
|
+
detailStep = "wait_capture_target";
|
|
667
|
+
captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
|
|
668
|
+
domain: "recruit",
|
|
669
|
+
timeoutMs: 6000,
|
|
670
|
+
intervalMs: 250
|
|
671
|
+
});
|
|
672
|
+
captureTarget = captureTargetWait.target || null;
|
|
673
|
+
const captureNodeId = captureTarget?.node_id || null;
|
|
674
|
+
if (captureNodeId) {
|
|
675
|
+
const imageEvidencePath = imageEvidenceFilePath({
|
|
457
676
|
imageOutputDir,
|
|
458
677
|
domain: "recruit",
|
|
459
678
|
runId: runControl?.runId,
|
|
460
679
|
index,
|
|
461
680
|
extension: "jpg"
|
|
462
|
-
})
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
681
|
+
});
|
|
682
|
+
try {
|
|
683
|
+
detailStep = "capture_image";
|
|
684
|
+
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
685
|
+
filePath: imageEvidencePath,
|
|
686
|
+
format: "jpeg",
|
|
687
|
+
quality: 72,
|
|
688
|
+
optimize: true,
|
|
689
|
+
resizeMaxWidth: 1100,
|
|
690
|
+
captureViewport: false,
|
|
691
|
+
padding: 0,
|
|
692
|
+
maxScreenshots: maxImagePages,
|
|
693
|
+
wheelDeltaY: imageWheelDeltaY,
|
|
694
|
+
settleMs: 350,
|
|
695
|
+
scrollMethod: "dom-anchor-fallback-input",
|
|
696
|
+
stepTimeoutMs: 45000,
|
|
697
|
+
totalTimeoutMs: 90000,
|
|
698
|
+
duplicateStopCount: 1,
|
|
699
|
+
skipDuplicateScreenshots: true,
|
|
700
|
+
composeForLlm: true,
|
|
701
|
+
llmPagesPerImage: 3,
|
|
702
|
+
llmResizeMaxWidth: 1100,
|
|
703
|
+
llmQuality: 72,
|
|
704
|
+
metadata: {
|
|
705
|
+
domain: "recruit",
|
|
706
|
+
capture_mode: "scroll_sequence",
|
|
707
|
+
acquisition_reason: "network_miss_image_fallback",
|
|
708
|
+
run_candidate_index: index,
|
|
709
|
+
candidate_key: candidateKey,
|
|
710
|
+
capture_target: captureTarget,
|
|
711
|
+
capture_target_wait: captureTargetWait
|
|
712
|
+
}
|
|
713
|
+
}));
|
|
714
|
+
source = "image";
|
|
715
|
+
} catch (error) {
|
|
716
|
+
if (!isRecoverableRecruitImageCaptureError(error)) throw error;
|
|
717
|
+
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
718
|
+
if (recoveryCount < 1) {
|
|
719
|
+
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
720
|
+
timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
|
|
721
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
722
|
+
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
723
|
+
await recoverAndReapplyRecruitContext(`image_capture:${detailStep}`, error, {
|
|
724
|
+
forceRecentViewed: true
|
|
725
|
+
});
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
imageEvidence = createRecoverableRecruitImageCaptureEvidence(error, {
|
|
729
|
+
elapsedMs: timings.screenshot_capture_ms,
|
|
730
|
+
filePath: imageEvidencePath,
|
|
731
|
+
extension: "jpg",
|
|
732
|
+
maxScreenshots: maxImagePages
|
|
733
|
+
});
|
|
734
|
+
source = "image_capture_failed";
|
|
489
735
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
736
|
+
recordCvImageFallback(cvAcquisitionState, {
|
|
737
|
+
reason: source === "image_capture_failed"
|
|
738
|
+
? "network_miss_image_capture_failed"
|
|
739
|
+
: "network_miss_image_fallback",
|
|
740
|
+
parsedNetworkProfileCount,
|
|
741
|
+
waitResult: networkWait,
|
|
742
|
+
imageEvidence
|
|
743
|
+
});
|
|
744
|
+
} else {
|
|
745
|
+
source = "missing_capture_node";
|
|
746
|
+
recordCvNetworkMiss(cvAcquisitionState, {
|
|
747
|
+
reason: "network_miss_no_capture_node",
|
|
748
|
+
parsedNetworkProfileCount,
|
|
749
|
+
waitResult: networkWait
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
detailResult.image_evidence = imageEvidence;
|
|
755
|
+
detailResult.cv_acquisition = {
|
|
756
|
+
source,
|
|
757
|
+
mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
|
|
758
|
+
wait_plan: waitPlan,
|
|
759
|
+
network_wait: networkWait,
|
|
760
|
+
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
761
|
+
image_evidence: summarizeImageEvidence(imageEvidence),
|
|
762
|
+
capture_target: captureTarget || null,
|
|
763
|
+
capture_target_wait: captureTargetWait
|
|
764
|
+
};
|
|
765
|
+
screeningCandidate = detailResult.candidate;
|
|
766
|
+
if (closeDetail) {
|
|
767
|
+
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
|
|
768
|
+
if (!detailResult.close_result?.closed) {
|
|
769
|
+
const closeError = createRecruitCloseFailureError(detailResult.close_result);
|
|
770
|
+
const recovery = await recoverAndReapplyRecruitContext("detail_close_failed", closeError, {
|
|
771
|
+
forceRecentViewed: true
|
|
772
|
+
});
|
|
773
|
+
detailResult.cv_acquisition = {
|
|
774
|
+
...(detailResult.cv_acquisition || {}),
|
|
775
|
+
close_recovery: {
|
|
776
|
+
ok: Boolean(recovery.ok),
|
|
777
|
+
method: recovery.method || "",
|
|
778
|
+
forced_recent_viewed: Boolean(recovery.forced_recent_viewed),
|
|
779
|
+
card_count: recovery.card_count || 0
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
}
|
|
497
783
|
} else {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
784
|
+
detailResult.close_result = null;
|
|
785
|
+
}
|
|
786
|
+
} catch (error) {
|
|
787
|
+
if (!isRecoverableRecruitDetailError(error)) throw error;
|
|
788
|
+
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
789
|
+
if (recoveryCount < 1) {
|
|
790
|
+
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
791
|
+
timings.detail_recovery_trigger = compactRecoverableDetailError(error);
|
|
792
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
793
|
+
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
794
|
+
await recoverAndReapplyRecruitContext(`detail:${detailStep}`, error, {
|
|
795
|
+
forceRecentViewed: true
|
|
503
796
|
});
|
|
797
|
+
continue;
|
|
504
798
|
}
|
|
799
|
+
recoverableDetailError = error;
|
|
800
|
+
detailResult = null;
|
|
801
|
+
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
802
|
+
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
505
803
|
}
|
|
506
|
-
|
|
507
|
-
let closeResult = null;
|
|
508
|
-
if (closeDetail) {
|
|
509
|
-
closeResult = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
|
|
510
|
-
}
|
|
511
|
-
detailResult.close_result = closeResult;
|
|
512
|
-
detailResult.image_evidence = imageEvidence;
|
|
513
|
-
detailResult.cv_acquisition = {
|
|
514
|
-
source,
|
|
515
|
-
mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
|
|
516
|
-
wait_plan: waitPlan,
|
|
517
|
-
network_wait: networkWait,
|
|
518
|
-
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
519
|
-
image_evidence: summarizeImageEvidence(imageEvidence),
|
|
520
|
-
capture_target: captureTarget || null,
|
|
521
|
-
capture_target_wait: captureTargetWait
|
|
522
|
-
};
|
|
523
|
-
screeningCandidate = detailResult.candidate;
|
|
524
804
|
}
|
|
525
805
|
|
|
526
806
|
await runControl.waitIfPaused();
|
|
@@ -528,7 +808,9 @@ export async function runRecruitWorkflow({
|
|
|
528
808
|
runControl.setPhase("recruit:screening");
|
|
529
809
|
let llmResult = null;
|
|
530
810
|
if (useLlmScreening) {
|
|
531
|
-
if (
|
|
811
|
+
if (recoverableDetailError || detailResult?.image_evidence?.ok === false) {
|
|
812
|
+
llmResult = null;
|
|
813
|
+
} else if (!llmConfig) {
|
|
532
814
|
llmResult = createMissingLlmConfigResult();
|
|
533
815
|
} else {
|
|
534
816
|
try {
|
|
@@ -550,9 +832,16 @@ export async function runRecruitWorkflow({
|
|
|
550
832
|
}
|
|
551
833
|
if (detailResult) detailResult.llm_result = llmResult;
|
|
552
834
|
}
|
|
553
|
-
const screening =
|
|
554
|
-
?
|
|
555
|
-
:
|
|
835
|
+
const screening = recoverableDetailError
|
|
836
|
+
? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
|
|
837
|
+
: detailResult?.image_evidence?.ok === false
|
|
838
|
+
? createImageCaptureFailureScreening(screeningCandidate, {
|
|
839
|
+
code: detailResult.image_evidence.error_code,
|
|
840
|
+
message: detailResult.image_evidence.error
|
|
841
|
+
})
|
|
842
|
+
: useLlmScreening
|
|
843
|
+
? llmResultToScreening(llmResult, screeningCandidate)
|
|
844
|
+
: screenCandidate(screeningCandidate, { criteria });
|
|
556
845
|
timings.total_ms = Date.now() - candidateStarted;
|
|
557
846
|
const compactResult = {
|
|
558
847
|
index,
|
|
@@ -562,6 +851,14 @@ export async function runRecruitWorkflow({
|
|
|
562
851
|
detail: compactDetail(detailResult),
|
|
563
852
|
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
564
853
|
screening: compactScreening(screening),
|
|
854
|
+
error: recoverableDetailError
|
|
855
|
+
? compactRecoverableDetailError(recoverableDetailError)
|
|
856
|
+
: detailResult?.image_evidence?.ok === false
|
|
857
|
+
? compactError({
|
|
858
|
+
code: detailResult.image_evidence.error_code,
|
|
859
|
+
message: detailResult.image_evidence.error
|
|
860
|
+
}, "IMAGE_CAPTURE_FAILED")
|
|
861
|
+
: null,
|
|
565
862
|
timings
|
|
566
863
|
};
|
|
567
864
|
results.push(compactResult);
|
|
@@ -572,21 +869,7 @@ export async function runRecruitWorkflow({
|
|
|
572
869
|
}
|
|
573
870
|
});
|
|
574
871
|
|
|
575
|
-
|
|
576
|
-
card_count: cardNodeIds.length,
|
|
577
|
-
target_count: limit,
|
|
578
|
-
processed: results.length,
|
|
579
|
-
screened: results.length,
|
|
580
|
-
detail_opened: results.filter((item) => item.detail).length,
|
|
581
|
-
passed: results.filter((item) => item.screening.passed).length,
|
|
582
|
-
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
583
|
-
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
584
|
-
scroll_count: compactInfiniteListState(listState).scroll_count,
|
|
585
|
-
refresh_rounds: refreshRounds,
|
|
586
|
-
refresh_attempts: refreshAttempts.length,
|
|
587
|
-
list_end_reason: listEndReason || null,
|
|
588
|
-
viewport_checks: viewportGuard.getStats().checks,
|
|
589
|
-
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
872
|
+
updateRecruitProgress({
|
|
590
873
|
last_candidate_id: screeningCandidate.id || null,
|
|
591
874
|
last_candidate_key: candidateKey,
|
|
592
875
|
last_score: screening.score
|
|
@@ -603,7 +886,8 @@ export async function runRecruitWorkflow({
|
|
|
603
886
|
passed: screening.passed,
|
|
604
887
|
score: screening.score
|
|
605
888
|
},
|
|
606
|
-
llm_screening: compactScreeningLlmResult(llmResult)
|
|
889
|
+
llm_screening: compactScreeningLlmResult(llmResult),
|
|
890
|
+
error: compactResult.error
|
|
607
891
|
}
|
|
608
892
|
});
|
|
609
893
|
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
@@ -630,11 +914,8 @@ export async function runRecruitWorkflow({
|
|
|
630
914
|
list_end_reason: listEndReason || null,
|
|
631
915
|
refresh_rounds: refreshRounds,
|
|
632
916
|
refresh_attempts: refreshAttempts,
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
detail_opened: results.filter((item) => item.detail).length,
|
|
636
|
-
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
637
|
-
passed: results.filter((item) => item.screening.passed).length,
|
|
917
|
+
context_recoveries: contextRecoveryAttempts,
|
|
918
|
+
...countRecruitResultStatuses(results),
|
|
638
919
|
results
|
|
639
920
|
};
|
|
640
921
|
}
|
|
@@ -722,7 +1003,11 @@ export function createRecruitRunService({
|
|
|
722
1003
|
screened: 0,
|
|
723
1004
|
detail_opened: 0,
|
|
724
1005
|
llm_screened: 0,
|
|
725
|
-
passed: 0
|
|
1006
|
+
passed: 0,
|
|
1007
|
+
image_capture_failed: 0,
|
|
1008
|
+
detail_open_failed: 0,
|
|
1009
|
+
transient_recovered: 0,
|
|
1010
|
+
context_recoveries: 0
|
|
726
1011
|
},
|
|
727
1012
|
checkpoint: {},
|
|
728
1013
|
task: (runControl) => workflow({
|