@reconcrap/boss-recommend-mcp 2.0.34 → 2.0.35
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
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
waitForRecommendCardNodeIds
|
|
5
5
|
} from "./cards.js";
|
|
6
6
|
import {
|
|
7
|
-
RECOMMEND_RECENT_NOT_VIEW_LABEL
|
|
7
|
+
RECOMMEND_RECENT_NOT_VIEW_LABEL,
|
|
8
|
+
RECOMMEND_TARGET_URL
|
|
8
9
|
} from "./constants.js";
|
|
9
10
|
import { selectAndConfirmFirstSafeFilter } from "./filters.js";
|
|
10
11
|
import { selectRecommendJob } from "./jobs.js";
|
|
@@ -94,6 +95,8 @@ export async function refreshRecommendListAtEnd(client, {
|
|
|
94
95
|
fallbackPageScope = "recommend",
|
|
95
96
|
filter = {},
|
|
96
97
|
preferEndRefreshButton = true,
|
|
98
|
+
forceNavigate = false,
|
|
99
|
+
targetUrl = RECOMMEND_TARGET_URL,
|
|
97
100
|
forceRecentNotView = true,
|
|
98
101
|
cardTimeoutMs = 30000,
|
|
99
102
|
buttonSettleMs = 8000,
|
|
@@ -156,8 +159,17 @@ export async function refreshRecommendListAtEnd(client, {
|
|
|
156
159
|
}
|
|
157
160
|
}
|
|
158
161
|
|
|
162
|
+
let fallbackMethod = "page_reload";
|
|
159
163
|
try {
|
|
160
|
-
|
|
164
|
+
let method = "page_reload";
|
|
165
|
+
if (forceNavigate && typeof client?.Page?.navigate === "function") {
|
|
166
|
+
await client.Page.navigate({ url: targetUrl || RECOMMEND_TARGET_URL });
|
|
167
|
+
method = "page_navigate";
|
|
168
|
+
fallbackMethod = method;
|
|
169
|
+
} else {
|
|
170
|
+
await client.Page.reload({ ignoreCache: true });
|
|
171
|
+
fallbackMethod = method;
|
|
172
|
+
}
|
|
161
173
|
if (reloadSettleMs > 0) await sleep(reloadSettleMs);
|
|
162
174
|
currentRootState = await waitForRecommendRoots(client, {
|
|
163
175
|
timeoutMs: Math.max(30000, reloadSettleMs * 4),
|
|
@@ -202,8 +214,9 @@ export async function refreshRecommendListAtEnd(client, {
|
|
|
202
214
|
});
|
|
203
215
|
return {
|
|
204
216
|
ok: cardNodeIds.length > 0,
|
|
205
|
-
method
|
|
217
|
+
method,
|
|
206
218
|
attempts,
|
|
219
|
+
target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
207
220
|
job_selection: jobSelection,
|
|
208
221
|
page_scope: pageScopeResult,
|
|
209
222
|
filter: filterResult,
|
|
@@ -214,10 +227,11 @@ export async function refreshRecommendListAtEnd(client, {
|
|
|
214
227
|
} catch (error) {
|
|
215
228
|
return {
|
|
216
229
|
ok: false,
|
|
217
|
-
method:
|
|
218
|
-
reason: "page_reload_failed",
|
|
230
|
+
method: fallbackMethod,
|
|
231
|
+
reason: fallbackMethod === "page_navigate" ? "page_navigate_failed" : "page_reload_failed",
|
|
219
232
|
error: error?.message || String(error),
|
|
220
233
|
attempts,
|
|
234
|
+
target_url: fallbackMethod === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
221
235
|
card_count: 0,
|
|
222
236
|
root_state: currentRootState,
|
|
223
237
|
forced_recent_not_view: forceRecentNotView
|
|
@@ -65,7 +65,8 @@ import {
|
|
|
65
65
|
RECOMMEND_BOTTOM_MARKER_SELECTORS,
|
|
66
66
|
RECOMMEND_CARD_SELECTOR,
|
|
67
67
|
RECOMMEND_END_REFRESH_SELECTOR,
|
|
68
|
-
RECOMMEND_LIST_CONTAINER_SELECTORS
|
|
68
|
+
RECOMMEND_LIST_CONTAINER_SELECTORS,
|
|
69
|
+
RECOMMEND_TARGET_URL
|
|
69
70
|
} from "./constants.js";
|
|
70
71
|
import {
|
|
71
72
|
clickRecommendActionControl,
|
|
@@ -355,6 +356,7 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
355
356
|
ok: Boolean(refreshAttempt.ok),
|
|
356
357
|
method: refreshAttempt.method || "",
|
|
357
358
|
forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
|
|
359
|
+
target_url: refreshAttempt.target_url || null,
|
|
358
360
|
card_count: refreshAttempt.card_count || 0,
|
|
359
361
|
attempts: (refreshAttempt.attempts || []).map((attempt) => ({
|
|
360
362
|
ok: Boolean(attempt.ok),
|
|
@@ -364,13 +366,39 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
364
366
|
before_card_count: attempt.before_card_count || 0,
|
|
365
367
|
after_card_count: attempt.after_card_count || 0
|
|
366
368
|
})),
|
|
369
|
+
job_selection: compactJobSelection(refreshAttempt.job_selection),
|
|
367
370
|
page_scope: compactPageScopeSelection(refreshAttempt.page_scope),
|
|
368
371
|
filter: compactFilterResult(refreshAttempt.filter)
|
|
369
372
|
};
|
|
370
373
|
}
|
|
371
374
|
|
|
375
|
+
export function countRecommendResultStatuses(results = [], {
|
|
376
|
+
greetCount = 0
|
|
377
|
+
} = {}) {
|
|
378
|
+
return {
|
|
379
|
+
processed: results.length,
|
|
380
|
+
screened: results.length,
|
|
381
|
+
detail_opened: results.filter((item) => item.detail).length,
|
|
382
|
+
passed: results.filter((item) => item.screening?.passed).length,
|
|
383
|
+
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
384
|
+
greet_count: greetCount,
|
|
385
|
+
post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
|
|
386
|
+
image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
|
|
387
|
+
detail_open_failed: results.filter((item) => (
|
|
388
|
+
item.error?.code === "DETAIL_STALE_NODE"
|
|
389
|
+
|| item.error?.code === "DETAIL_OPEN_FAILED"
|
|
390
|
+
)).length,
|
|
391
|
+
transient_recovered: results.filter((item) => (
|
|
392
|
+
item.error?.code === "DETAIL_STALE_NODE"
|
|
393
|
+
|| item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
|
|
394
|
+
|| item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
|
|
395
|
+
|| item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
|
|
396
|
+
)).length
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
372
400
|
function countPassedResults(results = []) {
|
|
373
|
-
return results.
|
|
401
|
+
return countRecommendResultStatuses(results).passed;
|
|
374
402
|
}
|
|
375
403
|
|
|
376
404
|
function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
|
|
@@ -381,6 +409,13 @@ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
|
|
|
381
409
|
};
|
|
382
410
|
}
|
|
383
411
|
|
|
412
|
+
function createRecommendCloseFailureError(closeResult) {
|
|
413
|
+
const error = new Error(closeResult?.reason || "Recommend detail did not close before recovery");
|
|
414
|
+
error.code = "DETAIL_CLOSE_FAILED";
|
|
415
|
+
error.close_result = closeResult || null;
|
|
416
|
+
return error;
|
|
417
|
+
}
|
|
418
|
+
|
|
384
419
|
export function isRecoverableImageCaptureError(error) {
|
|
385
420
|
const code = String(error?.code || "");
|
|
386
421
|
if (code === "IMAGE_CAPTURE_TIMEOUT" || code === "IMAGE_CAPTURE_TOTAL_TIMEOUT") return true;
|
|
@@ -535,7 +570,9 @@ export async function runRecommendWorkflow({
|
|
|
535
570
|
const results = [];
|
|
536
571
|
const refreshAttempts = [];
|
|
537
572
|
let refreshRounds = 0;
|
|
573
|
+
let contextRecoveryAttempts = 0;
|
|
538
574
|
let greetCount = 0;
|
|
575
|
+
const candidateRecoveryCounts = new Map();
|
|
539
576
|
let jobSelection = null;
|
|
540
577
|
let pageScopeSelection = null;
|
|
541
578
|
let filterResult = null;
|
|
@@ -550,6 +587,121 @@ export async function runRecommendWorkflow({
|
|
|
550
587
|
validateViewportPoint: true
|
|
551
588
|
}));
|
|
552
589
|
|
|
590
|
+
function updateRecommendProgress(extra = {}) {
|
|
591
|
+
const counts = countRecommendResultStatuses(results, { greetCount });
|
|
592
|
+
const listSnapshot = compactInfiniteListState(listState);
|
|
593
|
+
runControl.updateProgress({
|
|
594
|
+
card_count: cardNodeIds.length,
|
|
595
|
+
target_count: targetPassCount,
|
|
596
|
+
target_count_semantics: "passed_candidates",
|
|
597
|
+
...counts,
|
|
598
|
+
screening_mode: normalizedScreeningMode,
|
|
599
|
+
unique_seen: listSnapshot.seen_count,
|
|
600
|
+
scroll_count: listSnapshot.scroll_count,
|
|
601
|
+
refresh_rounds: refreshRounds,
|
|
602
|
+
refresh_attempts: refreshAttempts.length,
|
|
603
|
+
context_recoveries: contextRecoveryAttempts,
|
|
604
|
+
list_end_reason: listEndReason || null,
|
|
605
|
+
viewport_checks: viewportGuard.getStats().checks,
|
|
606
|
+
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
607
|
+
...extra
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function checkpointInProgressCandidate({
|
|
612
|
+
index = results.length,
|
|
613
|
+
candidateKey = "",
|
|
614
|
+
cardNodeId = null,
|
|
615
|
+
detailStep = "",
|
|
616
|
+
error = null
|
|
617
|
+
} = {}) {
|
|
618
|
+
runControl.checkpoint({
|
|
619
|
+
in_progress_candidate: {
|
|
620
|
+
index,
|
|
621
|
+
key: candidateKey,
|
|
622
|
+
card_node_id: cardNodeId,
|
|
623
|
+
detail_step: detailStep || null,
|
|
624
|
+
counters: countRecommendResultStatuses(results, { greetCount }),
|
|
625
|
+
error: compactError(error, "RECOMMEND_IN_PROGRESS_ERROR")
|
|
626
|
+
},
|
|
627
|
+
candidate_list: compactInfiniteListState(listState)
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function recoverAndReapplyRecommendContext(reason = "context_recovery", error = null, {
|
|
632
|
+
forceRecentNotView = true
|
|
633
|
+
} = {}) {
|
|
634
|
+
await runControl.waitIfPaused();
|
|
635
|
+
runControl.throwIfCanceled();
|
|
636
|
+
const started = Date.now();
|
|
637
|
+
runControl.setPhase("recommend:recover-context");
|
|
638
|
+
contextRecoveryAttempts += 1;
|
|
639
|
+
const refreshResult = await refreshRecommendListAtEnd(client, {
|
|
640
|
+
rootState,
|
|
641
|
+
jobLabel,
|
|
642
|
+
pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
|
|
643
|
+
fallbackPageScope: normalizedFallbackPageScope,
|
|
644
|
+
filter: normalizedFilter,
|
|
645
|
+
preferEndRefreshButton: false,
|
|
646
|
+
forceNavigate: true,
|
|
647
|
+
targetUrl: targetUrl || RECOMMEND_TARGET_URL,
|
|
648
|
+
forceRecentNotView,
|
|
649
|
+
cardTimeoutMs,
|
|
650
|
+
buttonSettleMs: refreshButtonSettleMs,
|
|
651
|
+
reloadSettleMs: refreshReloadSettleMs
|
|
652
|
+
});
|
|
653
|
+
const compactRefresh = {
|
|
654
|
+
...compactRefreshAttempt(refreshResult),
|
|
655
|
+
context_recovery: true,
|
|
656
|
+
recovery_reason: reason,
|
|
657
|
+
trigger_error: compactError(error, "RECOMMEND_CONTEXT_RECOVERY_TRIGGER"),
|
|
658
|
+
elapsed_ms: Date.now() - started
|
|
659
|
+
};
|
|
660
|
+
refreshAttempts.push(compactRefresh);
|
|
661
|
+
runControl.checkpoint({
|
|
662
|
+
context_recovery: {
|
|
663
|
+
attempt: contextRecoveryAttempts,
|
|
664
|
+
reason,
|
|
665
|
+
trigger_error: compactError(error, "RECOMMEND_CONTEXT_RECOVERY_TRIGGER"),
|
|
666
|
+
refresh: compactRefresh,
|
|
667
|
+
counters: countRecommendResultStatuses(results, { greetCount })
|
|
668
|
+
},
|
|
669
|
+
candidate_list: compactInfiniteListState(listState)
|
|
670
|
+
});
|
|
671
|
+
if (!refreshResult.ok) {
|
|
672
|
+
updateRecommendProgress({
|
|
673
|
+
refresh_method: refreshResult.method || null,
|
|
674
|
+
refresh_forced_recent_not_view: forceRecentNotView,
|
|
675
|
+
recovery_reason: reason
|
|
676
|
+
});
|
|
677
|
+
throw new Error(`Recommend context recovery failed after ${reason}: ${refreshResult.reason || refreshResult.error || "refresh returned no cards"}`);
|
|
678
|
+
}
|
|
679
|
+
rootState = refreshResult.root_state || await getRecommendRoots(client);
|
|
680
|
+
rootState = await ensureRecommendViewport(rootState, "recover_after");
|
|
681
|
+
cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
682
|
+
timeoutMs: cardTimeoutMs,
|
|
683
|
+
intervalMs: 300
|
|
684
|
+
});
|
|
685
|
+
resetInfiniteListForRefreshRound(listState, {
|
|
686
|
+
reason: `context_recovery:${reason}`,
|
|
687
|
+
round: contextRecoveryAttempts,
|
|
688
|
+
method: refreshResult.method,
|
|
689
|
+
metadata: {
|
|
690
|
+
card_count: cardNodeIds.length,
|
|
691
|
+
forced_recent_not_view: forceRecentNotView,
|
|
692
|
+
counters: countRecommendResultStatuses(results, { greetCount })
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
listEndReason = "";
|
|
696
|
+
updateRecommendProgress({
|
|
697
|
+
card_count: cardNodeIds.length,
|
|
698
|
+
refresh_method: refreshResult.method || null,
|
|
699
|
+
refresh_forced_recent_not_view: forceRecentNotView,
|
|
700
|
+
recovery_reason: reason
|
|
701
|
+
});
|
|
702
|
+
return refreshResult;
|
|
703
|
+
}
|
|
704
|
+
|
|
553
705
|
runControl.setPhase("recommend:cleanup");
|
|
554
706
|
await closeRecommendDetail(client, { attemptsLimit: 2 });
|
|
555
707
|
|
|
@@ -630,24 +782,8 @@ export async function runRecommendWorkflow({
|
|
|
630
782
|
throw new Error("No recommend candidate cards found for run service");
|
|
631
783
|
}
|
|
632
784
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
target_count: targetPassCount,
|
|
636
|
-
target_count_semantics: "passed_candidates",
|
|
637
|
-
processed: 0,
|
|
638
|
-
screened: 0,
|
|
639
|
-
detail_opened: 0,
|
|
640
|
-
passed: 0,
|
|
641
|
-
greet_count: 0,
|
|
642
|
-
post_action_clicked: 0,
|
|
643
|
-
screening_mode: normalizedScreeningMode,
|
|
644
|
-
llm_screened: 0,
|
|
645
|
-
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
646
|
-
scroll_count: 0,
|
|
647
|
-
refresh_rounds: 0,
|
|
648
|
-
refresh_attempts: 0,
|
|
649
|
-
viewport_checks: viewportGuard.getStats().checks,
|
|
650
|
-
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
785
|
+
updateRecommendProgress({
|
|
786
|
+
list_end_reason: null
|
|
651
787
|
});
|
|
652
788
|
|
|
653
789
|
while (countPassedResults(results) < targetPassCount) {
|
|
@@ -722,24 +858,11 @@ export async function runRecommendWorkflow({
|
|
|
722
858
|
refresh_round: refreshRounds,
|
|
723
859
|
refresh: compactRefresh
|
|
724
860
|
});
|
|
725
|
-
|
|
861
|
+
updateRecommendProgress({
|
|
726
862
|
card_count: refreshResult.card_count || cardNodeIds.length,
|
|
727
|
-
target_count: targetPassCount,
|
|
728
|
-
target_count_semantics: "passed_candidates",
|
|
729
|
-
processed: results.length,
|
|
730
|
-
screened: results.length,
|
|
731
|
-
detail_opened: results.filter((item) => item.detail).length,
|
|
732
|
-
passed: results.filter((item) => item.screening.passed).length,
|
|
733
|
-
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
734
|
-
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
735
|
-
scroll_count: compactInfiniteListState(listState).scroll_count,
|
|
736
|
-
refresh_rounds: refreshRounds,
|
|
737
|
-
refresh_attempts: refreshAttempts.length,
|
|
738
863
|
refresh_method: refreshResult.method || null,
|
|
739
864
|
refresh_forced_recent_not_view: true,
|
|
740
|
-
list_end_reason: listEndReason
|
|
741
|
-
viewport_checks: viewportGuard.getStats().checks,
|
|
742
|
-
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
865
|
+
list_end_reason: listEndReason
|
|
743
866
|
});
|
|
744
867
|
if (refreshResult.ok) {
|
|
745
868
|
rootState = refreshResult.root_state || await getRecommendRoots(client);
|
|
@@ -772,12 +895,16 @@ export async function runRecommendWorkflow({
|
|
|
772
895
|
let screeningCandidate = cardCandidate;
|
|
773
896
|
let detailResult = null;
|
|
774
897
|
let recoverableDetailError = null;
|
|
898
|
+
let detailStep = "not_started";
|
|
775
899
|
if (index < effectiveDetailLimit) {
|
|
776
900
|
try {
|
|
777
901
|
await runControl.waitIfPaused();
|
|
778
902
|
runControl.throwIfCanceled();
|
|
779
903
|
runControl.setPhase("recommend:detail");
|
|
904
|
+
detailStep = "ensure_viewport";
|
|
780
905
|
rootState = await ensureRecommendViewport(rootState, "detail");
|
|
906
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
907
|
+
detailStep = "open_detail";
|
|
781
908
|
networkRecorder.clear();
|
|
782
909
|
const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
|
|
783
910
|
cardNodeId,
|
|
@@ -794,6 +921,7 @@ export async function runRecommendWorkflow({
|
|
|
794
921
|
cardCandidate = openedDetail.card_candidate || cardCandidate;
|
|
795
922
|
screeningCandidate = cardCandidate;
|
|
796
923
|
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
924
|
+
detailStep = "wait_network";
|
|
797
925
|
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
798
926
|
waitForRecommendDetailNetworkEvents,
|
|
799
927
|
networkRecorder,
|
|
@@ -807,6 +935,7 @@ export async function runRecommendWorkflow({
|
|
|
807
935
|
if (networkWait?.elapsed_ms != null) {
|
|
808
936
|
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
809
937
|
}
|
|
938
|
+
detailStep = "extract_detail";
|
|
810
939
|
detailResult = await extractRecommendDetailCandidate(client, {
|
|
811
940
|
cardCandidate,
|
|
812
941
|
cardNodeId,
|
|
@@ -830,6 +959,7 @@ export async function runRecommendWorkflow({
|
|
|
830
959
|
waitResult: networkWait
|
|
831
960
|
});
|
|
832
961
|
} else {
|
|
962
|
+
detailStep = "wait_capture_target";
|
|
833
963
|
captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
|
|
834
964
|
domain: "recommend",
|
|
835
965
|
timeoutMs: 6000,
|
|
@@ -846,6 +976,7 @@ export async function runRecommendWorkflow({
|
|
|
846
976
|
extension: "jpg"
|
|
847
977
|
});
|
|
848
978
|
try {
|
|
979
|
+
detailStep = "capture_image";
|
|
849
980
|
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
850
981
|
filePath: imageEvidencePath,
|
|
851
982
|
format: "jpeg",
|
|
@@ -879,6 +1010,17 @@ export async function runRecommendWorkflow({
|
|
|
879
1010
|
source = "image";
|
|
880
1011
|
} catch (error) {
|
|
881
1012
|
if (!isRecoverableImageCaptureError(error)) throw error;
|
|
1013
|
+
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
1014
|
+
if (recoveryCount < 1) {
|
|
1015
|
+
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
1016
|
+
timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
|
|
1017
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1018
|
+
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1019
|
+
await recoverAndReapplyRecommendContext(`image_capture:${detailStep}`, error, {
|
|
1020
|
+
forceRecentNotView: true
|
|
1021
|
+
});
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
882
1024
|
imageEvidence = createRecoverableImageCaptureEvidence(error, {
|
|
883
1025
|
elapsedMs: timings.screenshot_capture_ms,
|
|
884
1026
|
filePath: imageEvidencePath,
|
|
@@ -919,6 +1061,17 @@ export async function runRecommendWorkflow({
|
|
|
919
1061
|
screeningCandidate = detailResult.candidate;
|
|
920
1062
|
} catch (error) {
|
|
921
1063
|
if (!isRecoverableRecommendDetailError(error)) throw error;
|
|
1064
|
+
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
1065
|
+
if (recoveryCount < 1) {
|
|
1066
|
+
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
1067
|
+
timings.detail_recovery_trigger = compactRecoverableDetailError(error);
|
|
1068
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1069
|
+
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1070
|
+
await recoverAndReapplyRecommendContext(`detail:${detailStep}`, error, {
|
|
1071
|
+
forceRecentNotView: true
|
|
1072
|
+
});
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
922
1075
|
recoverableDetailError = error;
|
|
923
1076
|
detailResult = null;
|
|
924
1077
|
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
@@ -994,6 +1147,21 @@ export async function runRecommendWorkflow({
|
|
|
994
1147
|
}
|
|
995
1148
|
if (detailResult && closeDetail) {
|
|
996
1149
|
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
|
|
1150
|
+
if (!detailResult.close_result?.closed) {
|
|
1151
|
+
const closeError = createRecommendCloseFailureError(detailResult.close_result);
|
|
1152
|
+
const recovery = await recoverAndReapplyRecommendContext("detail_close_failed", closeError, {
|
|
1153
|
+
forceRecentNotView: true
|
|
1154
|
+
});
|
|
1155
|
+
detailResult.cv_acquisition = {
|
|
1156
|
+
...(detailResult.cv_acquisition || {}),
|
|
1157
|
+
close_recovery: {
|
|
1158
|
+
ok: Boolean(recovery.ok),
|
|
1159
|
+
method: recovery.method || "",
|
|
1160
|
+
forced_recent_not_view: Boolean(recovery.forced_recent_not_view),
|
|
1161
|
+
card_count: recovery.card_count || 0
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
997
1165
|
}
|
|
998
1166
|
timings.total_ms = Date.now() - candidateStarted;
|
|
999
1167
|
const compactResult = {
|
|
@@ -1024,27 +1192,7 @@ export async function runRecommendWorkflow({
|
|
|
1024
1192
|
}
|
|
1025
1193
|
});
|
|
1026
1194
|
|
|
1027
|
-
|
|
1028
|
-
card_count: cardNodeIds.length,
|
|
1029
|
-
target_count: targetPassCount,
|
|
1030
|
-
target_count_semantics: "passed_candidates",
|
|
1031
|
-
processed: results.length,
|
|
1032
|
-
screened: results.length,
|
|
1033
|
-
detail_opened: results.filter((item) => item.detail).length,
|
|
1034
|
-
passed: results.filter((item) => item.screening.passed).length,
|
|
1035
|
-
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
1036
|
-
greet_count: greetCount,
|
|
1037
|
-
post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
|
|
1038
|
-
image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
|
|
1039
|
-
detail_open_failed: results.filter((item) => item.error?.code === "DETAIL_STALE_NODE" || item.error?.code === "DETAIL_OPEN_FAILED").length,
|
|
1040
|
-
transient_recovered: results.filter((item) => item.error?.code === "DETAIL_STALE_NODE" || item.error?.code === "IMAGE_CAPTURE_STALE_NODE" || item.error?.code === "IMAGE_CAPTURE_TIMEOUT" || item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT").length,
|
|
1041
|
-
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
1042
|
-
scroll_count: compactInfiniteListState(listState).scroll_count,
|
|
1043
|
-
refresh_rounds: refreshRounds,
|
|
1044
|
-
refresh_attempts: refreshAttempts.length,
|
|
1045
|
-
list_end_reason: listEndReason || null,
|
|
1046
|
-
viewport_checks: viewportGuard.getStats().checks,
|
|
1047
|
-
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
1195
|
+
updateRecommendProgress({
|
|
1048
1196
|
last_candidate_id: screeningCandidate.id || null,
|
|
1049
1197
|
last_candidate_key: candidateKey,
|
|
1050
1198
|
last_score: screening.score
|
|
@@ -1097,16 +1245,8 @@ export async function runRecommendWorkflow({
|
|
|
1097
1245
|
list_end_reason: listEndReason || null,
|
|
1098
1246
|
refresh_rounds: refreshRounds,
|
|
1099
1247
|
refresh_attempts: refreshAttempts,
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
detail_opened: results.filter((item) => item.detail).length,
|
|
1103
|
-
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
1104
|
-
detail_open_failed: results.filter((item) => item.error?.code === "DETAIL_STALE_NODE" || item.error?.code === "DETAIL_OPEN_FAILED").length,
|
|
1105
|
-
image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
|
|
1106
|
-
transient_recovered: results.filter((item) => item.error?.code === "DETAIL_STALE_NODE" || item.error?.code === "IMAGE_CAPTURE_STALE_NODE" || item.error?.code === "IMAGE_CAPTURE_TIMEOUT" || item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT").length,
|
|
1107
|
-
passed: results.filter((item) => item.screening.passed).length,
|
|
1108
|
-
greet_count: greetCount,
|
|
1109
|
-
post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
|
|
1248
|
+
context_recoveries: contextRecoveryAttempts,
|
|
1249
|
+
...countRecommendResultStatuses(results, { greetCount }),
|
|
1110
1250
|
results
|
|
1111
1251
|
};
|
|
1112
1252
|
}
|
|
@@ -1213,7 +1353,11 @@ export function createRecommendRunService({
|
|
|
1213
1353
|
llm_screened: 0,
|
|
1214
1354
|
passed: 0,
|
|
1215
1355
|
greet_count: 0,
|
|
1216
|
-
post_action_clicked: 0
|
|
1356
|
+
post_action_clicked: 0,
|
|
1357
|
+
image_capture_failed: 0,
|
|
1358
|
+
detail_open_failed: 0,
|
|
1359
|
+
transient_recovered: 0,
|
|
1360
|
+
context_recoveries: 0
|
|
1217
1361
|
},
|
|
1218
1362
|
checkpoint: {},
|
|
1219
1363
|
task: (runControl) => workflow({
|
|
@@ -3,10 +3,13 @@ import {
|
|
|
3
3
|
normalizeRecruitSearchParams
|
|
4
4
|
} from "./search.js";
|
|
5
5
|
|
|
6
|
-
export function buildRecruitRefreshSearchParams(searchParams = {}
|
|
6
|
+
export function buildRecruitRefreshSearchParams(searchParams = {}, {
|
|
7
|
+
forceRecentViewed = true
|
|
8
|
+
} = {}) {
|
|
9
|
+
const normalizedSearchParams = normalizeRecruitSearchParams(searchParams);
|
|
7
10
|
return {
|
|
8
|
-
...
|
|
9
|
-
filter_recent_viewed: true
|
|
11
|
+
...normalizedSearchParams,
|
|
12
|
+
filter_recent_viewed: forceRecentViewed ? true : normalizedSearchParams.filter_recent_viewed
|
|
10
13
|
};
|
|
11
14
|
}
|
|
12
15
|
|
|
@@ -16,9 +19,10 @@ export async function refreshRecruitSearchAtEnd(client, {
|
|
|
16
19
|
searchTimeoutMs = 90000,
|
|
17
20
|
resetTimeoutMs = 180000,
|
|
18
21
|
resetSettleMs = 5000,
|
|
19
|
-
cityOptionTimeoutMs = 30000
|
|
22
|
+
cityOptionTimeoutMs = 30000,
|
|
23
|
+
forceRecentViewed = true
|
|
20
24
|
} = {}) {
|
|
21
|
-
const refreshSearchParams = buildRecruitRefreshSearchParams(searchParams);
|
|
25
|
+
const refreshSearchParams = buildRecruitRefreshSearchParams(searchParams, { forceRecentViewed });
|
|
22
26
|
const application = await applyRecruitSearchParams(client, {
|
|
23
27
|
searchParams: refreshSearchParams,
|
|
24
28
|
requireCards,
|
|
@@ -32,7 +36,7 @@ export async function refreshRecruitSearchAtEnd(client, {
|
|
|
32
36
|
return {
|
|
33
37
|
ok: !requireCards || cardCount > 0,
|
|
34
38
|
method: "page_reload_search",
|
|
35
|
-
forced_recent_viewed:
|
|
39
|
+
forced_recent_viewed: Boolean(forceRecentViewed),
|
|
36
40
|
search_params: refreshSearchParams,
|
|
37
41
|
card_count: cardCount,
|
|
38
42
|
application
|
|
@@ -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({
|