@reconcrap/boss-recommend-mcp 2.0.27 → 2.0.29

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.27",
3
+ "version": "2.0.29",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -23,19 +23,28 @@ function normalizeJobText(value) {
23
23
  return normalizeText(value).replace(/\s+/g, "");
24
24
  }
25
25
 
26
- function trimSalarySuffix(label) {
26
+ function stripSalaryText(label) {
27
27
  return normalizeText(label)
28
- .replace(/\s+(?:\d+(?:-\d+)?K|面议|\d+-\d+元\/天).*$/i, "")
28
+ .replace(/\s*[((]\s*(?:\d+(?:-\d+)?K|面议|\d+-\d+元\/天)\s*[))]\s*$/i, "")
29
+ .replace(/\s+(?:\d+(?:-\d+)?K|面议|\d+-\d+元\/天)\s*$/i, "")
29
30
  .trim();
30
31
  }
31
32
 
33
+ function trimSalarySuffix(label) {
34
+ return stripSalaryText(label);
35
+ }
36
+
32
37
  export function jobLabelMatches(optionLabel, targetLabel) {
33
38
  const option = normalizeJobText(optionLabel);
34
39
  const target = normalizeJobText(targetLabel);
40
+ const optionWithoutSalary = normalizeJobText(stripSalaryText(optionLabel));
41
+ const targetWithoutSalary = normalizeJobText(stripSalaryText(targetLabel));
35
42
  if (!option || !target) return false;
36
43
  return option === target
37
44
  || option.startsWith(target)
38
- || normalizeJobText(trimSalarySuffix(optionLabel)) === target;
45
+ || optionWithoutSalary === target
46
+ || option === targetWithoutSalary
47
+ || optionWithoutSalary === targetWithoutSalary;
39
48
  }
40
49
 
41
50
  function isVisibleBox(box) {
@@ -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,
@@ -376,6 +378,66 @@ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
376
378
  };
377
379
  }
378
380
 
381
+ export function isRecoverableImageCaptureError(error) {
382
+ const code = String(error?.code || "");
383
+ if (code === "IMAGE_CAPTURE_TIMEOUT" || code === "IMAGE_CAPTURE_TOTAL_TIMEOUT") return true;
384
+ return /Image fallback capture timed out/i.test(String(error?.message || error || ""));
385
+ }
386
+
387
+ function collectPartialImageEvidencePaths(basePath = "", extension = "jpg", maxCount = 12) {
388
+ const resolved = String(basePath || "").trim();
389
+ if (!resolved) return [];
390
+ const parsed = path.parse(resolved);
391
+ const ext = parsed.ext || `.${String(extension || "jpg").replace(/^\./, "") || "jpg"}`;
392
+ const files = [];
393
+ for (let index = 0; index < Math.max(1, Number(maxCount) || 1); index += 1) {
394
+ const page = String(index + 1).padStart(2, "0");
395
+ const candidatePath = path.join(parsed.dir, `${parsed.name}-page-${page}${ext}`);
396
+ if (fs.existsSync(candidatePath)) files.push(candidatePath);
397
+ }
398
+ return files;
399
+ }
400
+
401
+ export function createRecoverableImageCaptureEvidence(error, {
402
+ elapsedMs = 0,
403
+ filePath = "",
404
+ extension = "jpg",
405
+ maxScreenshots = 8
406
+ } = {}) {
407
+ const filePaths = collectPartialImageEvidencePaths(filePath, extension, maxScreenshots);
408
+ return {
409
+ schema_version: 1,
410
+ ok: false,
411
+ source: "image-scroll-sequence",
412
+ elapsed_ms: Math.max(0, Math.round(Number(error?.elapsed_ms ?? elapsedMs) || 0)),
413
+ capture_count: filePaths.length,
414
+ screenshot_count: filePaths.length,
415
+ unique_screenshot_count: filePaths.length,
416
+ dropped_duplicate_count: 0,
417
+ total_byte_length: 0,
418
+ original_total_byte_length: 0,
419
+ llm_screenshot_count: 0,
420
+ llm_total_byte_length: 0,
421
+ llm_original_total_byte_length: 0,
422
+ llm_composition_error: null,
423
+ error_code: error?.code || "IMAGE_CAPTURE_FAILED",
424
+ error: error?.message || String(error || "Image capture failed"),
425
+ file_paths: filePaths,
426
+ llm_file_paths: []
427
+ };
428
+ }
429
+
430
+ function createImageCaptureFailureScreening(candidate, error) {
431
+ return {
432
+ status: "fail",
433
+ passed: false,
434
+ score: 0,
435
+ reasons: ["image_capture_failed"],
436
+ error: compactError(error, "IMAGE_CAPTURE_FAILED"),
437
+ candidate
438
+ };
439
+ }
440
+
379
441
  export async function runRecommendWorkflow({
380
442
  client,
381
443
  targetUrl = "",
@@ -742,42 +804,57 @@ export async function runRecommendWorkflow({
742
804
  || openedDetail.detail_state?.resumeIframe?.node_id
743
805
  || null;
744
806
  if (captureNodeId) {
745
- imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
746
- filePath: imageEvidenceFilePath({
747
- imageOutputDir,
748
- domain: "recommend",
749
- runId: runControl?.runId,
750
- index,
751
- extension: "jpg"
752
- }),
753
- format: "jpeg",
754
- quality: 72,
755
- optimize: true,
756
- resizeMaxWidth: 1100,
757
- captureViewport: true,
758
- padding: 4,
759
- maxScreenshots: maxImagePages,
760
- wheelDeltaY: imageWheelDeltaY,
761
- settleMs: 350,
762
- scrollMethod: "dom-anchor-fallback-input",
763
- stepTimeoutMs: 45000,
764
- totalTimeoutMs: 90000,
765
- duplicateStopCount: 1,
766
- skipDuplicateScreenshots: true,
767
- composeForLlm: true,
768
- llmPagesPerImage: 3,
769
- llmResizeMaxWidth: 1100,
770
- llmQuality: 72,
771
- metadata: {
772
- domain: "recommend",
773
- capture_mode: "scroll_sequence",
774
- acquisition_reason: "network_miss_image_fallback",
775
- run_candidate_index: index,
776
- candidate_key: candidateKey
777
- }
778
- }));
779
- source = "image";
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";
853
+ }
780
854
  recordCvImageFallback(cvAcquisitionState, {
855
+ reason: source === "image_capture_failed"
856
+ ? "network_miss_image_capture_failed"
857
+ : "network_miss_image_fallback",
781
858
  parsedNetworkProfileCount,
782
859
  waitResult: networkWait,
783
860
  imageEvidence
@@ -809,7 +886,9 @@ export async function runRecommendWorkflow({
809
886
  runControl.setPhase("recommend:screening");
810
887
  let llmResult = null;
811
888
  if (useLlmScreening) {
812
- if (!llmConfig) {
889
+ if (detailResult?.image_evidence?.ok === false) {
890
+ llmResult = null;
891
+ } else if (!llmConfig) {
813
892
  llmResult = createMissingLlmConfigResult();
814
893
  } else {
815
894
  try {
@@ -831,9 +910,14 @@ export async function runRecommendWorkflow({
831
910
  }
832
911
  if (detailResult) detailResult.llm_result = llmResult;
833
912
  }
834
- const screening = useLlmScreening
835
- ? llmResultToScreening(llmResult, screeningCandidate)
836
- : screenCandidate(screeningCandidate, { criteria });
913
+ const screening = detailResult?.image_evidence?.ok === false
914
+ ? createImageCaptureFailureScreening(screeningCandidate, {
915
+ code: detailResult.image_evidence.error_code,
916
+ message: detailResult.image_evidence.error
917
+ })
918
+ : useLlmScreening
919
+ ? llmResultToScreening(llmResult, screeningCandidate)
920
+ : screenCandidate(screeningCandidate, { criteria });
837
921
  let actionDiscovery = null;
838
922
  let postActionResult = null;
839
923
  if (postActionEnabled && detailResult) {
@@ -875,6 +959,12 @@ export async function runRecommendWorkflow({
875
959
  screening: compactScreening(screening),
876
960
  action_discovery: compactActionDiscovery(actionDiscovery),
877
961
  post_action: postActionResult,
962
+ error: detailResult?.image_evidence?.ok === false
963
+ ? compactError({
964
+ code: detailResult.image_evidence.error_code,
965
+ message: detailResult.image_evidence.error
966
+ }, "IMAGE_CAPTURE_FAILED")
967
+ : null,
878
968
  timings
879
969
  };
880
970
  results.push(compactResult);
@@ -896,6 +986,7 @@ export async function runRecommendWorkflow({
896
986
  llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
897
987
  greet_count: greetCount,
898
988
  post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
989
+ image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
899
990
  unique_seen: compactInfiniteListState(listState).seen_count,
900
991
  scroll_count: compactInfiniteListState(listState).scroll_count,
901
992
  refresh_rounds: refreshRounds,
@@ -920,6 +1011,7 @@ export async function runRecommendWorkflow({
920
1011
  score: screening.score
921
1012
  },
922
1013
  llm_screening: compactScreeningLlmResult(llmResult),
1014
+ error: compactResult.error,
923
1015
  post_action: postActionResult
924
1016
  }
925
1017
  });
@@ -317,6 +317,8 @@ function ensureRecommendRunArtifacts(snapshot) {
317
317
  progress: snapshot.progress || {},
318
318
  context: snapshot.context || {},
319
319
  checkpoint,
320
+ error: snapshot.error || null,
321
+ last_message: snapshot.error?.message || snapshot.phase || snapshot.stage || null,
320
322
  summary: artifactSummary,
321
323
  generated_at: new Date().toISOString()
322
324
  });