@reconcrap/boss-recommend-mcp 2.0.29 → 2.0.30

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.0.29",
3
+ "version": "2.0.30",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -41,6 +41,7 @@ import {
41
41
  closeRecommendDetail,
42
42
  createRecommendDetailNetworkRecorder,
43
43
  extractRecommendDetailCandidate,
44
+ isStaleRecommendNodeError,
44
45
  openRecommendCardDetailWithFreshRetry,
45
46
  waitForRecommendDetailNetworkEvents
46
47
  } from "./detail.js";
@@ -381,6 +382,7 @@ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
381
382
  export function isRecoverableImageCaptureError(error) {
382
383
  const code = String(error?.code || "");
383
384
  if (code === "IMAGE_CAPTURE_TIMEOUT" || code === "IMAGE_CAPTURE_TOTAL_TIMEOUT") return true;
385
+ if (isStaleRecommendNodeError(error)) return true;
384
386
  return /Image fallback capture timed out/i.test(String(error?.message || error || ""));
385
387
  }
386
388
 
@@ -420,7 +422,7 @@ export function createRecoverableImageCaptureEvidence(error, {
420
422
  llm_total_byte_length: 0,
421
423
  llm_original_total_byte_length: 0,
422
424
  llm_composition_error: null,
423
- error_code: error?.code || "IMAGE_CAPTURE_FAILED",
425
+ error_code: error?.code || (isStaleRecommendNodeError(error) ? "IMAGE_CAPTURE_STALE_NODE" : "IMAGE_CAPTURE_FAILED"),
424
426
  error: error?.message || String(error || "Image capture failed"),
425
427
  file_paths: filePaths,
426
428
  llm_file_paths: []
@@ -438,6 +440,27 @@ function createImageCaptureFailureScreening(candidate, error) {
438
440
  };
439
441
  }
440
442
 
443
+ export function isRecoverableRecommendDetailError(error) {
444
+ return isStaleRecommendNodeError(error);
445
+ }
446
+
447
+ function compactRecoverableDetailError(error) {
448
+ return compactError(error, isStaleRecommendNodeError(error) ? "DETAIL_STALE_NODE" : "DETAIL_OPEN_FAILED");
449
+ }
450
+
451
+ function createRecoverableDetailFailureScreening(candidate, error) {
452
+ return {
453
+ status: "fail",
454
+ passed: false,
455
+ score: 0,
456
+ reasons: isStaleRecommendNodeError(error)
457
+ ? ["detail_open_failed", "stale_node"]
458
+ : ["detail_open_failed"],
459
+ error: compactRecoverableDetailError(error),
460
+ candidate
461
+ };
462
+ }
463
+
441
464
  export async function runRecommendWorkflow({
442
465
  client,
443
466
  targetUrl = "",
@@ -746,139 +769,149 @@ export async function runRecommendWorkflow({
746
769
 
747
770
  let screeningCandidate = cardCandidate;
748
771
  let detailResult = null;
772
+ let recoverableDetailError = null;
749
773
  if (index < effectiveDetailLimit) {
750
- await runControl.waitIfPaused();
751
- runControl.throwIfCanceled();
752
- runControl.setPhase("recommend:detail");
753
- rootState = await ensureRecommendViewport(rootState, "detail");
754
- networkRecorder.clear();
755
- const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
756
- cardNodeId,
757
- candidateKey,
758
- cardCandidate,
759
- rootState,
760
- targetUrl,
761
- maxAttempts: 2
762
- });
763
- addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
764
- addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
765
- cardNodeId = openedDetail.card_node_id || cardNodeId;
766
- cardCandidate = openedDetail.card_candidate || cardCandidate;
767
- screeningCandidate = cardCandidate;
768
- const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
769
- const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
770
- waitForRecommendDetailNetworkEvents,
771
- networkRecorder,
772
- {
773
- waitPlan,
774
- minCount: 1,
775
- requireLoaded: true,
776
- intervalMs: 120
777
- }
778
- ));
779
- if (networkWait?.elapsed_ms != null) {
780
- timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
781
- }
782
- detailResult = await extractRecommendDetailCandidate(client, {
783
- cardCandidate,
784
- cardNodeId,
785
- detailState: openedDetail.detail_state,
786
- networkEvents: networkRecorder.events,
787
- targetUrl,
788
- closeDetail: false,
789
- networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
790
- networkParseIntervalMs: 250
791
- });
792
- addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
793
-
794
- const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
795
- let source = "network";
796
- let imageEvidence = null;
797
- if (parsedNetworkProfileCount > 0) {
798
- recordCvNetworkHit(cvAcquisitionState, {
799
- parsedNetworkProfileCount,
800
- waitResult: networkWait
774
+ try {
775
+ await runControl.waitIfPaused();
776
+ runControl.throwIfCanceled();
777
+ runControl.setPhase("recommend:detail");
778
+ rootState = await ensureRecommendViewport(rootState, "detail");
779
+ networkRecorder.clear();
780
+ const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
781
+ cardNodeId,
782
+ candidateKey,
783
+ cardCandidate,
784
+ rootState,
785
+ targetUrl,
786
+ retryTimeoutMs: 8000,
787
+ maxAttempts: 3
801
788
  });
802
- } else {
803
- const captureNodeId = openedDetail.detail_state?.popup?.node_id
804
- || openedDetail.detail_state?.resumeIframe?.node_id
805
- || null;
806
- if (captureNodeId) {
807
- const imageEvidencePath = imageEvidenceFilePath({
808
- imageOutputDir,
809
- domain: "recommend",
810
- runId: runControl?.runId,
811
- index,
812
- extension: "jpg"
813
- });
814
- try {
815
- imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
816
- filePath: imageEvidencePath,
817
- format: "jpeg",
818
- quality: 72,
819
- optimize: true,
820
- resizeMaxWidth: 1100,
821
- captureViewport: true,
822
- padding: 4,
823
- maxScreenshots: maxImagePages,
824
- wheelDeltaY: imageWheelDeltaY,
825
- settleMs: 350,
826
- scrollMethod: "dom-anchor-fallback-input",
827
- stepTimeoutMs: 45000,
828
- totalTimeoutMs: 90000,
829
- duplicateStopCount: 1,
830
- skipDuplicateScreenshots: true,
831
- composeForLlm: true,
832
- llmPagesPerImage: 3,
833
- llmResizeMaxWidth: 1100,
834
- llmQuality: 72,
835
- metadata: {
836
- domain: "recommend",
837
- capture_mode: "scroll_sequence",
838
- acquisition_reason: "network_miss_image_fallback",
839
- run_candidate_index: index,
840
- candidate_key: candidateKey
841
- }
842
- }));
843
- source = "image";
844
- } catch (error) {
845
- if (!isRecoverableImageCaptureError(error)) throw error;
846
- imageEvidence = createRecoverableImageCaptureEvidence(error, {
847
- elapsedMs: timings.screenshot_capture_ms,
848
- filePath: imageEvidencePath,
849
- extension: "jpg",
850
- maxScreenshots: maxImagePages
851
- });
852
- source = "image_capture_failed";
789
+ addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
790
+ addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
791
+ cardNodeId = openedDetail.card_node_id || cardNodeId;
792
+ cardCandidate = openedDetail.card_candidate || cardCandidate;
793
+ screeningCandidate = cardCandidate;
794
+ const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
795
+ const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
796
+ waitForRecommendDetailNetworkEvents,
797
+ networkRecorder,
798
+ {
799
+ waitPlan,
800
+ minCount: 1,
801
+ requireLoaded: true,
802
+ intervalMs: 120
853
803
  }
854
- recordCvImageFallback(cvAcquisitionState, {
855
- reason: source === "image_capture_failed"
856
- ? "network_miss_image_capture_failed"
857
- : "network_miss_image_fallback",
858
- parsedNetworkProfileCount,
859
- waitResult: networkWait,
860
- imageEvidence
861
- });
862
- } else {
863
- source = "missing_capture_node";
864
- recordCvNetworkMiss(cvAcquisitionState, {
865
- reason: "network_miss_no_capture_node",
804
+ ));
805
+ if (networkWait?.elapsed_ms != null) {
806
+ timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
807
+ }
808
+ detailResult = await extractRecommendDetailCandidate(client, {
809
+ cardCandidate,
810
+ cardNodeId,
811
+ detailState: openedDetail.detail_state,
812
+ networkEvents: networkRecorder.events,
813
+ targetUrl,
814
+ closeDetail: false,
815
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
816
+ networkParseIntervalMs: 250
817
+ });
818
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
819
+
820
+ const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
821
+ let source = "network";
822
+ let imageEvidence = null;
823
+ if (parsedNetworkProfileCount > 0) {
824
+ recordCvNetworkHit(cvAcquisitionState, {
866
825
  parsedNetworkProfileCount,
867
826
  waitResult: networkWait
868
827
  });
828
+ } else {
829
+ const captureNodeId = openedDetail.detail_state?.popup?.node_id
830
+ || openedDetail.detail_state?.resumeIframe?.node_id
831
+ || null;
832
+ if (captureNodeId) {
833
+ const imageEvidencePath = imageEvidenceFilePath({
834
+ imageOutputDir,
835
+ domain: "recommend",
836
+ runId: runControl?.runId,
837
+ index,
838
+ extension: "jpg"
839
+ });
840
+ try {
841
+ imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
842
+ filePath: imageEvidencePath,
843
+ format: "jpeg",
844
+ quality: 72,
845
+ optimize: true,
846
+ resizeMaxWidth: 1100,
847
+ captureViewport: true,
848
+ padding: 4,
849
+ maxScreenshots: maxImagePages,
850
+ wheelDeltaY: imageWheelDeltaY,
851
+ settleMs: 350,
852
+ scrollMethod: "dom-anchor-fallback-input",
853
+ stepTimeoutMs: 45000,
854
+ totalTimeoutMs: 90000,
855
+ duplicateStopCount: 1,
856
+ skipDuplicateScreenshots: true,
857
+ composeForLlm: true,
858
+ llmPagesPerImage: 3,
859
+ llmResizeMaxWidth: 1100,
860
+ llmQuality: 72,
861
+ metadata: {
862
+ domain: "recommend",
863
+ capture_mode: "scroll_sequence",
864
+ acquisition_reason: "network_miss_image_fallback",
865
+ run_candidate_index: index,
866
+ candidate_key: candidateKey
867
+ }
868
+ }));
869
+ source = "image";
870
+ } catch (error) {
871
+ if (!isRecoverableImageCaptureError(error)) throw error;
872
+ imageEvidence = createRecoverableImageCaptureEvidence(error, {
873
+ elapsedMs: timings.screenshot_capture_ms,
874
+ filePath: imageEvidencePath,
875
+ extension: "jpg",
876
+ maxScreenshots: maxImagePages
877
+ });
878
+ source = "image_capture_failed";
879
+ }
880
+ recordCvImageFallback(cvAcquisitionState, {
881
+ reason: source === "image_capture_failed"
882
+ ? "network_miss_image_capture_failed"
883
+ : "network_miss_image_fallback",
884
+ parsedNetworkProfileCount,
885
+ waitResult: networkWait,
886
+ imageEvidence
887
+ });
888
+ } else {
889
+ source = "missing_capture_node";
890
+ recordCvNetworkMiss(cvAcquisitionState, {
891
+ reason: "network_miss_no_capture_node",
892
+ parsedNetworkProfileCount,
893
+ waitResult: networkWait
894
+ });
895
+ }
869
896
  }
870
- }
871
897
 
872
- detailResult.image_evidence = imageEvidence;
873
- detailResult.cv_acquisition = {
874
- source,
875
- mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
876
- wait_plan: waitPlan,
877
- network_wait: networkWait,
878
- parsed_network_profile_count: parsedNetworkProfileCount,
879
- image_evidence: summarizeImageEvidence(imageEvidence)
880
- };
881
- screeningCandidate = detailResult.candidate;
898
+ detailResult.image_evidence = imageEvidence;
899
+ detailResult.cv_acquisition = {
900
+ source,
901
+ mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
902
+ wait_plan: waitPlan,
903
+ network_wait: networkWait,
904
+ parsed_network_profile_count: parsedNetworkProfileCount,
905
+ image_evidence: summarizeImageEvidence(imageEvidence)
906
+ };
907
+ screeningCandidate = detailResult.candidate;
908
+ } catch (error) {
909
+ if (!isRecoverableRecommendDetailError(error)) throw error;
910
+ recoverableDetailError = error;
911
+ detailResult = null;
912
+ timings.detail_recovered_error = compactRecoverableDetailError(error);
913
+ await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
914
+ }
882
915
  }
883
916
 
884
917
  await runControl.waitIfPaused();
@@ -886,7 +919,7 @@ export async function runRecommendWorkflow({
886
919
  runControl.setPhase("recommend:screening");
887
920
  let llmResult = null;
888
921
  if (useLlmScreening) {
889
- if (detailResult?.image_evidence?.ok === false) {
922
+ if (recoverableDetailError || detailResult?.image_evidence?.ok === false) {
890
923
  llmResult = null;
891
924
  } else if (!llmConfig) {
892
925
  llmResult = createMissingLlmConfigResult();
@@ -910,7 +943,9 @@ export async function runRecommendWorkflow({
910
943
  }
911
944
  if (detailResult) detailResult.llm_result = llmResult;
912
945
  }
913
- const screening = detailResult?.image_evidence?.ok === false
946
+ const screening = recoverableDetailError
947
+ ? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
948
+ : detailResult?.image_evidence?.ok === false
914
949
  ? createImageCaptureFailureScreening(screeningCandidate, {
915
950
  code: detailResult.image_evidence.error_code,
916
951
  message: detailResult.image_evidence.error
@@ -959,7 +994,9 @@ export async function runRecommendWorkflow({
959
994
  screening: compactScreening(screening),
960
995
  action_discovery: compactActionDiscovery(actionDiscovery),
961
996
  post_action: postActionResult,
962
- error: detailResult?.image_evidence?.ok === false
997
+ error: recoverableDetailError
998
+ ? compactRecoverableDetailError(recoverableDetailError)
999
+ : detailResult?.image_evidence?.ok === false
963
1000
  ? compactError({
964
1001
  code: detailResult.image_evidence.error_code,
965
1002
  message: detailResult.image_evidence.error
@@ -987,6 +1024,8 @@ export async function runRecommendWorkflow({
987
1024
  greet_count: greetCount,
988
1025
  post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
989
1026
  image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
1027
+ detail_open_failed: results.filter((item) => item.error?.code === "DETAIL_STALE_NODE" || item.error?.code === "DETAIL_OPEN_FAILED").length,
1028
+ 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,
990
1029
  unique_seen: compactInfiniteListState(listState).seen_count,
991
1030
  scroll_count: compactInfiniteListState(listState).scroll_count,
992
1031
  refresh_rounds: refreshRounds,
@@ -1050,6 +1089,9 @@ export async function runRecommendWorkflow({
1050
1089
  screened: results.length,
1051
1090
  detail_opened: results.filter((item) => item.detail).length,
1052
1091
  llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
1092
+ detail_open_failed: results.filter((item) => item.error?.code === "DETAIL_STALE_NODE" || item.error?.code === "DETAIL_OPEN_FAILED").length,
1093
+ image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
1094
+ 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,
1053
1095
  passed: results.filter((item) => item.screening.passed).length,
1054
1096
  greet_count: greetCount,
1055
1097
  post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
@@ -288,6 +288,66 @@ function completionReason(status) {
288
288
  return null;
289
289
  }
290
290
 
291
+ function normalizeErrorText(error = {}) {
292
+ return normalizeText([
293
+ error?.code || "",
294
+ error?.message || error || ""
295
+ ].join(" "));
296
+ }
297
+
298
+ function classifyRecommendRecovery(error = {}) {
299
+ const text = normalizeErrorText(error);
300
+ if (!text) return null;
301
+ if (/BOSS_LOGIN_REQUIRED/i.test(text)) return "login_required";
302
+ if (/Could not find node with given id|No node with given id|Node is detached|Cannot find node|DETAIL_STALE_NODE|IMAGE_CAPTURE_STALE_NODE/i.test(text)) {
303
+ return "transient_stale_dom";
304
+ }
305
+ if (/IMAGE_CAPTURE_TIMEOUT|IMAGE_CAPTURE_TOTAL_TIMEOUT|Image fallback capture timed out/i.test(text)) {
306
+ return "transient_image_capture";
307
+ }
308
+ if (/(?:aborted|abort|timeout|timed out|fetch failed|socket|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN)/i.test(text)) {
309
+ return "transient_network_or_llm";
310
+ }
311
+ return null;
312
+ }
313
+
314
+ function buildConstrainedAgentRecovery(snapshot = {}, meta = {}, artifacts = null) {
315
+ const error = snapshot?.error || snapshot?.result?.error || null;
316
+ const classification = classifyRecommendRecovery(error);
317
+ if (!classification) return null;
318
+ const canRestartSameRequest = classification !== "login_required";
319
+ return {
320
+ policy_version: 1,
321
+ classification,
322
+ safe_for_outer_ai_agent: true,
323
+ recommended_action: canRestartSameRequest
324
+ ? "restart_same_recommend_request_only"
325
+ : "ask_user_to_login_then_retry_same_recommend_request",
326
+ package_requirement: "@reconcrap/boss-recommend-mcp@>=2.0.30",
327
+ run_id: snapshot?.runId || snapshot?.run_id || null,
328
+ retryable: true,
329
+ same_request_sources: {
330
+ instruction: "run.context.instruction",
331
+ confirmation: "run.context.confirmation",
332
+ overrides: "run.context.overrides",
333
+ follow_up: "run.context.follow_up"
334
+ },
335
+ constraints: [
336
+ "Do not change instruction, criteria, filters, job, page_scope, target_count, post_action, or max_greet_count.",
337
+ "Do not switch to search/recruit/chat and do not add follow_up.chat.",
338
+ "Do not summarize, translate, or rewrite criteria.",
339
+ "Do not ask the user to reconfirm business choices unless Boss login is required or the stored context is missing.",
340
+ "Use the same Chrome debug port and recommend page route."
341
+ ],
342
+ artifacts: artifacts ? {
343
+ run_state_path: artifacts.run_state_path || null,
344
+ checkpoint_path: artifacts.checkpoint_path || null,
345
+ report_json: artifacts.report_json || null,
346
+ output_csv: artifacts.output_csv || null
347
+ } : null
348
+ };
349
+ }
350
+
291
351
  function ensureRecommendRunArtifacts(snapshot) {
292
352
  const artifacts = getRecommendRunArtifacts(snapshot?.runId || snapshot?.run_id);
293
353
  if (!artifacts) return null;
@@ -319,6 +379,7 @@ function ensureRecommendRunArtifacts(snapshot) {
319
379
  checkpoint,
320
380
  error: snapshot.error || null,
321
381
  last_message: snapshot.error?.message || snapshot.phase || snapshot.stage || null,
382
+ recovery: buildConstrainedAgentRecovery(snapshot, meta, artifacts),
322
383
  summary: artifactSummary,
323
384
  generated_at: new Date().toISOString()
324
385
  });
@@ -397,6 +458,7 @@ function buildLegacyRecommendResult(snapshot) {
397
458
  screen_params: clonePlain(meta.parsed?.screenParams || {}, {}),
398
459
  target_count_semantics: TARGET_COUNT_SEMANTICS,
399
460
  error: snapshot.error || null,
461
+ recovery: buildConstrainedAgentRecovery(snapshot, meta, artifacts),
400
462
  results: resultRows
401
463
  };
402
464
  }
@@ -411,6 +473,7 @@ function normalizeRunSnapshot(snapshot) {
411
473
  TERMINAL_STATUSES.has(snapshot.status)
412
474
  || snapshot.status === RUN_STATUS_PAUSED
413
475
  ) ? buildLegacyRecommendResult({ ...snapshot, progress }) : null;
476
+ const recovery = buildConstrainedAgentRecovery(snapshot, meta, artifacts);
414
477
  const oldContext = {
415
478
  workspace_root: meta.workspaceRoot || null,
416
479
  instruction: meta.args?.instruction || "",
@@ -451,6 +514,7 @@ function normalizeRunSnapshot(snapshot) {
451
514
  last_resumed_at: meta.lastResumedAt || null,
452
515
  last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
453
516
  },
517
+ recovery,
454
518
  result: legacyResult,
455
519
  artifacts
456
520
  };
@@ -483,6 +547,7 @@ function persistRecommendRunSnapshot(snapshot, {
483
547
  control: normalized.control,
484
548
  resume: normalized.resume,
485
549
  error: normalized.error,
550
+ recovery: normalized.recovery,
486
551
  result: normalized.result,
487
552
  summary: normalized.summary,
488
553
  artifacts: normalized.artifacts