@reconcrap/boss-recommend-mcp 2.0.6 → 2.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,9 @@
1
1
  import { createRunLifecycleManager } from "../../core/run/index.js";
2
+ import {
3
+ addTiming,
4
+ imageEvidenceFilePath,
5
+ measureTiming
6
+ } from "../../core/run/timing.js";
2
7
  import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
3
8
  import { sleep } from "../../core/browser/index.js";
4
9
  import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
@@ -21,7 +26,13 @@ import {
21
26
  resetInfiniteListForRefreshRound
22
27
  } from "../../core/infinite-list/index.js";
23
28
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
24
- import { screenCandidate } from "../../core/screening/index.js";
29
+ import {
30
+ callScreeningLlm,
31
+ compactScreeningLlmResult,
32
+ createFailedLlmScreeningResult,
33
+ llmResultToScreening,
34
+ screenCandidate
35
+ } from "../../core/screening/index.js";
25
36
  import {
26
37
  closeRecommendDetail,
27
38
  createRecommendDetailNetworkRecorder,
@@ -165,10 +176,22 @@ function compactDetail(detailResult) {
165
176
  parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
166
177
  cv_acquisition: detailResult.cv_acquisition || null,
167
178
  image_evidence: summarizeImageEvidence(detailResult.image_evidence),
179
+ llm_screening: compactScreeningLlmResult(detailResult.llm_result),
168
180
  close_result: detailResult.close_result
169
181
  };
170
182
  }
171
183
 
184
+ function normalizeScreeningMode(value) {
185
+ const normalized = String(value || "llm").trim().toLowerCase();
186
+ return ["deterministic", "local", "local_scorer"].includes(normalized)
187
+ ? "deterministic"
188
+ : "llm";
189
+ }
190
+
191
+ function createMissingLlmConfigResult() {
192
+ return createFailedLlmScreeningResult(new Error("LLM screening config is required for production recommend runs"));
193
+ }
194
+
172
195
  function compactActionDiscovery(discovery) {
173
196
  if (!discovery) return null;
174
197
  return {
@@ -354,13 +377,21 @@ export async function runRecommendWorkflow({
354
377
  executePostAction = true,
355
378
  actionTimeoutMs = 8000,
356
379
  actionIntervalMs = 500,
357
- actionAfterClickDelayMs = 900
380
+ actionAfterClickDelayMs = 900,
381
+ screeningMode = "llm",
382
+ llmConfig = null,
383
+ llmTimeoutMs = 120000,
384
+ llmImageLimit = 8,
385
+ llmImageDetail = "high",
386
+ imageOutputDir = ""
358
387
  } = {}, runControl) {
359
388
  if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
360
389
  const normalizedFilter = normalizeFilter(filter);
361
390
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
362
391
  const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
363
392
  const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
393
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
394
+ const useLlmScreening = normalizedScreeningMode !== "deterministic";
364
395
  const postActionEnabled = normalizedPostAction !== "none";
365
396
  const limit = Math.max(1, Number(maxCandidates) || 1);
366
397
  const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
@@ -484,6 +515,8 @@ export async function runRecommendWorkflow({
484
515
  passed: 0,
485
516
  greet_count: 0,
486
517
  post_action_clicked: 0,
518
+ screening_mode: normalizedScreeningMode,
519
+ llm_screened: 0,
487
520
  unique_seen: compactInfiniteListState(listState).seen_count,
488
521
  scroll_count: 0,
489
522
  refresh_rounds: 0,
@@ -493,12 +526,14 @@ export async function runRecommendWorkflow({
493
526
  });
494
527
 
495
528
  while (results.length < limit) {
529
+ const candidateStarted = Date.now();
530
+ const timings = {};
496
531
  await runControl.waitIfPaused();
497
532
  runControl.throwIfCanceled();
498
533
  runControl.setPhase("recommend:candidate");
499
534
  rootState = await ensureRecommendViewport(rootState, "candidate_loop");
500
535
 
501
- const nextCandidateResult = await getNextInfiniteListCandidate({
536
+ const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
502
537
  client,
503
538
  state: listState,
504
539
  maxScrolls: listMaxScrolls,
@@ -525,7 +560,7 @@ export async function runRecommendWorkflow({
525
560
  visible_index: visibleIndex
526
561
  }
527
562
  })
528
- });
563
+ }));
529
564
  if (!nextCandidateResult.ok) {
530
565
  listEndReason = nextCandidateResult.reason || "list_exhausted";
531
566
  if (
@@ -562,6 +597,7 @@ export async function runRecommendWorkflow({
562
597
  screened: results.length,
563
598
  detail_opened: results.filter((item) => item.detail).length,
564
599
  passed: results.filter((item) => item.screening.passed).length,
600
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
565
601
  unique_seen: compactInfiniteListState(listState).seen_count,
566
602
  scroll_count: compactInfiniteListState(listState).scroll_count,
567
603
  refresh_rounds: refreshRounds,
@@ -616,11 +652,13 @@ export async function runRecommendWorkflow({
616
652
  targetUrl,
617
653
  maxAttempts: 2
618
654
  });
655
+ addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
656
+ addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
619
657
  cardNodeId = openedDetail.card_node_id || cardNodeId;
620
658
  cardCandidate = openedDetail.card_candidate || cardCandidate;
621
659
  screeningCandidate = cardCandidate;
622
660
  const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
623
- const networkWait = await waitForCvNetworkEvents(
661
+ const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
624
662
  waitForRecommendDetailNetworkEvents,
625
663
  networkRecorder,
626
664
  {
@@ -629,7 +667,10 @@ export async function runRecommendWorkflow({
629
667
  requireLoaded: true,
630
668
  intervalMs: 120
631
669
  }
632
- );
670
+ ));
671
+ if (networkWait?.elapsed_ms != null) {
672
+ timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
673
+ }
633
674
  detailResult = await extractRecommendDetailCandidate(client, {
634
675
  cardCandidate,
635
676
  cardNodeId,
@@ -652,7 +693,13 @@ export async function runRecommendWorkflow({
652
693
  || openedDetail.detail_state?.resumeIframe?.node_id
653
694
  || null;
654
695
  if (captureNodeId) {
655
- imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
696
+ imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
697
+ filePath: imageEvidenceFilePath({
698
+ imageOutputDir,
699
+ domain: "recommend",
700
+ runId: runControl?.runId,
701
+ index
702
+ }),
656
703
  padding: 4,
657
704
  maxScreenshots: maxImagePages,
658
705
  wheelDeltaY: imageWheelDeltaY,
@@ -664,7 +711,7 @@ export async function runRecommendWorkflow({
664
711
  run_candidate_index: index,
665
712
  candidate_key: candidateKey
666
713
  }
667
- });
714
+ }));
668
715
  source = "image";
669
716
  recordCvImageFallback(cvAcquisitionState, {
670
717
  parsedNetworkProfileCount,
@@ -696,10 +743,37 @@ export async function runRecommendWorkflow({
696
743
  await runControl.waitIfPaused();
697
744
  runControl.throwIfCanceled();
698
745
  runControl.setPhase("recommend:screening");
699
- const screening = screenCandidate(screeningCandidate, { criteria });
746
+ let llmResult = null;
747
+ if (useLlmScreening) {
748
+ if (!llmConfig) {
749
+ llmResult = createMissingLlmConfigResult();
750
+ } else {
751
+ try {
752
+ const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
753
+ ? "vision_model_ms"
754
+ : "text_model_ms";
755
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
756
+ candidate: screeningCandidate,
757
+ criteria,
758
+ config: llmConfig,
759
+ timeoutMs: llmTimeoutMs,
760
+ imageEvidence: detailResult?.image_evidence || null,
761
+ maxImages: llmImageLimit,
762
+ imageDetail: llmImageDetail
763
+ }));
764
+ } catch (error) {
765
+ llmResult = createFailedLlmScreeningResult(error);
766
+ }
767
+ }
768
+ if (detailResult) detailResult.llm_result = llmResult;
769
+ }
770
+ const screening = useLlmScreening
771
+ ? llmResultToScreening(llmResult, screeningCandidate)
772
+ : screenCandidate(screeningCandidate, { criteria });
700
773
  let actionDiscovery = null;
701
774
  let postActionResult = null;
702
775
  if (postActionEnabled && detailResult) {
776
+ const postActionStarted = Date.now();
703
777
  await runControl.waitIfPaused();
704
778
  runControl.throwIfCanceled();
705
779
  runControl.setPhase("recommend:post-action");
@@ -721,19 +795,23 @@ export async function runRecommendWorkflow({
721
795
  if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
722
796
  greetCount += 1;
723
797
  }
798
+ addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
724
799
  }
725
800
  if (detailResult && closeDetail) {
726
- detailResult.close_result = await closeRecommendDetail(client);
801
+ detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
727
802
  }
803
+ timings.total_ms = Date.now() - candidateStarted;
728
804
  const compactResult = {
729
805
  index,
730
806
  candidate_key: candidateKey,
731
807
  card_node_id: cardNodeId,
732
808
  candidate: compactCandidate(screeningCandidate),
733
809
  detail: compactDetail(detailResult),
810
+ llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
734
811
  screening: compactScreening(screening),
735
812
  action_discovery: compactActionDiscovery(actionDiscovery),
736
- post_action: postActionResult
813
+ post_action: postActionResult,
814
+ timings
737
815
  };
738
816
  results.push(compactResult);
739
817
  markInfiniteListCandidateProcessed(listState, candidateKey, {
@@ -750,6 +828,7 @@ export async function runRecommendWorkflow({
750
828
  screened: results.length,
751
829
  detail_opened: results.filter((item) => item.detail).length,
752
830
  passed: results.filter((item) => item.screening.passed).length,
831
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
753
832
  greet_count: greetCount,
754
833
  post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
755
834
  unique_seen: compactInfiniteListState(listState).seen_count,
@@ -763,7 +842,9 @@ export async function runRecommendWorkflow({
763
842
  last_candidate_key: candidateKey,
764
843
  last_score: screening.score
765
844
  });
845
+ const checkpointStarted = Date.now();
766
846
  runControl.checkpoint({
847
+ results,
767
848
  last_candidate: {
768
849
  id: screeningCandidate.id || null,
769
850
  key: candidateKey,
@@ -773,9 +854,11 @@ export async function runRecommendWorkflow({
773
854
  passed: screening.passed,
774
855
  score: screening.score
775
856
  },
857
+ llm_screening: compactScreeningLlmResult(llmResult),
776
858
  post_action: postActionResult
777
859
  }
778
860
  });
861
+ addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
779
862
 
780
863
  if (postActionResult?.stop_run) {
781
864
  listEndReason = postActionResult.reason || "post_action_stop";
@@ -783,7 +866,10 @@ export async function runRecommendWorkflow({
783
866
  }
784
867
 
785
868
  if (delayMs > 0) {
869
+ const sleepStarted = Date.now();
786
870
  await runControl.sleep(delayMs);
871
+ addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
872
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
787
873
  }
788
874
  }
789
875
 
@@ -806,6 +892,7 @@ export async function runRecommendWorkflow({
806
892
  processed: results.length,
807
893
  screened: results.length,
808
894
  detail_opened: results.filter((item) => item.detail).length,
895
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
809
896
  passed: results.filter((item) => item.screening.passed).length,
810
897
  greet_count: greetCount,
811
898
  post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
@@ -851,6 +938,12 @@ export function createRecommendRunService({
851
938
  actionTimeoutMs = 8000,
852
939
  actionIntervalMs = 500,
853
940
  actionAfterClickDelayMs = 900,
941
+ screeningMode = "llm",
942
+ llmConfig = null,
943
+ llmTimeoutMs = 120000,
944
+ llmImageLimit = 8,
945
+ llmImageDetail = "high",
946
+ imageOutputDir = "",
854
947
  name = "recommend-domain-run"
855
948
  } = {}) {
856
949
  if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
@@ -858,6 +951,7 @@ export function createRecommendRunService({
858
951
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
859
952
  const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
860
953
  const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
954
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
861
955
  const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
862
956
  const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
863
957
  return manager.startRun({
@@ -888,7 +982,13 @@ export function createRecommendRunService({
888
982
  post_action: normalizedPostAction,
889
983
  max_greet_count: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
890
984
  execute_post_action: Boolean(executePostAction),
891
- action_timeout_ms: actionTimeoutMs
985
+ action_timeout_ms: actionTimeoutMs,
986
+ screening_mode: normalizedScreeningMode,
987
+ llm_configured: Boolean(llmConfig),
988
+ llm_timeout_ms: llmTimeoutMs,
989
+ llm_image_limit: llmImageLimit,
990
+ llm_image_detail: llmImageDetail,
991
+ image_output_dir: imageOutputDir || ""
892
992
  },
893
993
  progress: {
894
994
  card_count: 0,
@@ -896,6 +996,7 @@ export function createRecommendRunService({
896
996
  processed: 0,
897
997
  screened: 0,
898
998
  detail_opened: 0,
999
+ llm_screened: 0,
899
1000
  passed: 0,
900
1001
  greet_count: 0,
901
1002
  post_action_clicked: 0
@@ -931,7 +1032,13 @@ export function createRecommendRunService({
931
1032
  executePostAction,
932
1033
  actionTimeoutMs,
933
1034
  actionIntervalMs,
934
- actionAfterClickDelayMs
1035
+ actionAfterClickDelayMs,
1036
+ screeningMode: normalizedScreeningMode,
1037
+ llmConfig,
1038
+ llmTimeoutMs,
1039
+ llmImageLimit,
1040
+ llmImageDetail,
1041
+ imageOutputDir
935
1042
  }, runControl)
936
1043
  });
937
1044
  }
@@ -4,9 +4,16 @@ import {
4
4
  querySelectorAll,
5
5
  sleep
6
6
  } from "../../core/browser/index.js";
7
+ import { mergeBossCandidateCardFields } from "../../core/boss-cards/index.js";
7
8
  import { normalizeCandidateFromHtml } from "../../core/screening/index.js";
8
9
  import { RECRUIT_CARD_SELECTOR } from "./constants.js";
9
10
 
11
+ function mergeRecruitCardFields(candidate, outerHTML = "") {
12
+ return mergeBossCandidateCardFields(candidate, outerHTML, {
13
+ metadataKey: "search_card_fields"
14
+ });
15
+ }
16
+
10
17
  export async function findRecruitCardNodeIds(client, frameNodeId, {
11
18
  selector = RECRUIT_CARD_SELECTOR
12
19
  } = {}) {
@@ -37,7 +44,7 @@ export async function readRecruitCardCandidate(client, cardNodeId, {
37
44
  getAttributesMap(client, cardNodeId),
38
45
  getOuterHTML(client, cardNodeId)
39
46
  ]);
40
- return normalizeCandidateFromHtml({
47
+ const candidate = normalizeCandidateFromHtml({
41
48
  domain: "recruit",
42
49
  source,
43
50
  html: outerHTML,
@@ -48,6 +55,7 @@ export async function readRecruitCardCandidate(client, cardNodeId, {
48
55
  ...metadata
49
56
  }
50
57
  });
58
+ return mergeRecruitCardFields(candidate, outerHTML);
51
59
  }
52
60
 
53
61
  export async function readFirstRecruitCardCandidate(client, frameNodeId, options = {}) {
@@ -213,17 +213,22 @@ export async function waitForRecruitDetailContent(client, {
213
213
  export async function openRecruitCardDetail(client, cardNodeId, {
214
214
  timeoutMs = 12000
215
215
  } = {}) {
216
+ const openedStarted = Date.now();
216
217
  const attempts = [];
218
+ const clickStarted = Date.now();
217
219
  const cardBox = await clickNodeCenter(client, cardNodeId, {
218
220
  scrollIntoView: true
219
221
  });
222
+ let candidateClickMs = Date.now() - clickStarted;
220
223
  attempts.push({
221
224
  mode: "card-center",
222
225
  center: cardBox.center
223
226
  });
227
+ const detailStarted = Date.now();
224
228
  let detailState = await waitForRecruitDetail(client, { timeoutMs });
225
229
 
226
230
  if (!detailState?.popup && !detailState?.resumeIframe) {
231
+ const fallbackClickStarted = Date.now();
227
232
  const leftTitlePoint = {
228
233
  x: cardBox.rect.x + Math.min(140, Math.max(40, cardBox.rect.width * 0.2)),
229
234
  y: cardBox.rect.y + Math.min(42, Math.max(24, cardBox.rect.height * 0.28))
@@ -232,6 +237,7 @@ export async function openRecruitCardDetail(client, cardNodeId, {
232
237
  clickCount: 2,
233
238
  delayMs: 120
234
239
  });
240
+ candidateClickMs += Date.now() - fallbackClickStarted;
235
241
  attempts.push({
236
242
  mode: "card-left-title-double-click",
237
243
  center: leftTitlePoint
@@ -248,7 +254,12 @@ export async function openRecruitCardDetail(client, cardNodeId, {
248
254
  return {
249
255
  card_box: cardBox,
250
256
  open_attempts: attempts,
251
- detail_state: detailState
257
+ detail_state: detailState,
258
+ timings: {
259
+ candidate_click_ms: candidateClickMs,
260
+ detail_open_ms: Date.now() - detailStarted,
261
+ open_total_ms: Date.now() - openedStarted
262
+ }
252
263
  };
253
264
  }
254
265