@reconcrap/boss-recommend-mcp 2.0.33 → 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.
@@ -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.filter((item) => item?.screening?.passed).length;
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
- runControl.updateProgress({
634
- card_count: cardNodeIds.length,
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
- runControl.updateProgress({
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
- runControl.updateProgress({
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
- processed: results.length,
1101
- screened: results.length,
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
- ...normalizeRecruitSearchParams(searchParams),
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: true,
39
+ forced_recent_viewed: Boolean(forceRecentViewed),
36
40
  search_params: refreshSearchParams,
37
41
  card_count: cardCount,
38
42
  application