@reconcrap/boss-recommend-mcp 2.0.5 → 2.0.7

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.
@@ -8,6 +8,7 @@ import {
8
8
  querySelectorAll,
9
9
  sleep
10
10
  } from "../../core/browser/index.js";
11
+ import { candidateKeyFromProfile } from "../../core/infinite-list/index.js";
11
12
  import {
12
13
  buildScreeningCandidateFromDetail,
13
14
  htmlToText
@@ -22,6 +23,10 @@ import {
22
23
  getRecommendRoots,
23
24
  queryFirstAcrossRoots
24
25
  } from "./roots.js";
26
+ import {
27
+ findRecommendCardNodeIds,
28
+ readRecommendCardCandidate
29
+ } from "./cards.js";
25
30
 
26
31
  export function matchesRecommendDetailNetwork(url) {
27
32
  return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
@@ -146,14 +151,36 @@ export async function readRecommendDetailHtml(client, detailState) {
146
151
  let popupHTML = "";
147
152
  let resumeHTML = "";
148
153
  let resumeIframeDocumentNodeId = null;
154
+ const errors = [];
149
155
 
150
156
  if (detailState?.popup?.node_id) {
151
- popupHTML = await getOuterHTML(client, detailState.popup.node_id);
157
+ try {
158
+ popupHTML = await getOuterHTML(client, detailState.popup.node_id);
159
+ } catch (error) {
160
+ errors.push({
161
+ source: "popup",
162
+ node_id: detailState.popup.node_id,
163
+ stale_node: isStaleRecommendNodeError(error),
164
+ error: error?.message || String(error)
165
+ });
166
+ }
152
167
  }
153
168
 
154
169
  if (detailState?.resumeIframe?.node_id) {
155
- resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
156
- resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
170
+ try {
171
+ resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
172
+ resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
173
+ } catch (error) {
174
+ errors.push({
175
+ source: "resume_iframe",
176
+ node_id: detailState.resumeIframe.node_id,
177
+ document_node_id: resumeIframeDocumentNodeId,
178
+ stale_node: isStaleRecommendNodeError(error),
179
+ error: error?.message || String(error)
180
+ });
181
+ resumeIframeDocumentNodeId = null;
182
+ resumeHTML = "";
183
+ }
157
184
  }
158
185
 
159
186
  return {
@@ -161,7 +188,90 @@ export async function readRecommendDetailHtml(client, detailState) {
161
188
  resumeHTML,
162
189
  resumeIframeDocumentNodeId,
163
190
  popupText: htmlToText(popupHTML),
164
- resumeText: htmlToText(resumeHTML)
191
+ resumeText: htmlToText(resumeHTML),
192
+ errors
193
+ };
194
+ }
195
+
196
+ export function isStaleRecommendNodeError(error) {
197
+ const message = String(error?.message || error || "");
198
+ return /Could not find node with given id|No node with given id|Node is detached|Cannot find node/i.test(message);
199
+ }
200
+
201
+ export async function findRecommendCardNodeForCandidateKey(client, {
202
+ candidateKey = "",
203
+ rootState = null,
204
+ targetUrl = "",
205
+ source = "recommend-run-card-retry",
206
+ timeoutMs = 5000,
207
+ intervalMs = 250
208
+ } = {}) {
209
+ if (!candidateKey) {
210
+ return {
211
+ ok: false,
212
+ reason: "candidate_key_required"
213
+ };
214
+ }
215
+
216
+ const started = Date.now();
217
+ let lastError = null;
218
+ let lastCardCount = 0;
219
+ while (Date.now() - started <= timeoutMs) {
220
+ const currentRootState = rootState?.iframe?.documentNodeId
221
+ ? rootState
222
+ : await getRecommendRoots(client);
223
+ const frameNodeId = currentRootState?.iframe?.documentNodeId;
224
+ if (!frameNodeId) {
225
+ return {
226
+ ok: false,
227
+ reason: "recommend_frame_not_found"
228
+ };
229
+ }
230
+
231
+ const nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
232
+ lastCardCount = nodeIds.length;
233
+ for (let visibleIndex = 0; visibleIndex < nodeIds.length; visibleIndex += 1) {
234
+ const nodeId = nodeIds[visibleIndex];
235
+ try {
236
+ const candidate = await readRecommendCardCandidate(client, nodeId, {
237
+ targetUrl,
238
+ source,
239
+ metadata: {
240
+ visible_index: visibleIndex,
241
+ retry_reason: "stale_detail_node"
242
+ }
243
+ });
244
+ const key = candidateKeyFromProfile(candidate, {
245
+ nodeId,
246
+ visibleIndex,
247
+ attributes: candidate?.attributes || candidate?.metadata?.attributes || {}
248
+ });
249
+ if (key === candidateKey) {
250
+ return {
251
+ ok: true,
252
+ node_id: nodeId,
253
+ visible_index: visibleIndex,
254
+ candidate,
255
+ key,
256
+ root_state: currentRootState,
257
+ card_count: nodeIds.length
258
+ };
259
+ }
260
+ } catch (error) {
261
+ lastError = error;
262
+ }
263
+ }
264
+
265
+ if (intervalMs > 0) await sleep(intervalMs);
266
+ rootState = null;
267
+ }
268
+
269
+ return {
270
+ ok: false,
271
+ reason: "candidate_key_not_mounted",
272
+ candidate_key: candidateKey,
273
+ last_card_count: lastCardCount,
274
+ error: lastError?.message || null
165
275
  };
166
276
  }
167
277
 
@@ -181,6 +291,77 @@ export async function openRecommendCardDetail(client, cardNodeId, {
181
291
  };
182
292
  }
183
293
 
294
+ export async function openRecommendCardDetailWithFreshRetry(client, {
295
+ cardNodeId,
296
+ candidateKey = "",
297
+ cardCandidate = null,
298
+ rootState = null,
299
+ targetUrl = "",
300
+ timeoutMs = 12000,
301
+ scrollIntoView = true,
302
+ retryTimeoutMs = 5000,
303
+ retryIntervalMs = 250,
304
+ maxAttempts = 2
305
+ } = {}) {
306
+ let currentNodeId = cardNodeId;
307
+ let currentCandidate = cardCandidate;
308
+ let currentRootState = rootState;
309
+ const attempts = [];
310
+ const limit = Math.max(1, Number(maxAttempts) || 1);
311
+
312
+ for (let attemptIndex = 0; attemptIndex < limit; attemptIndex += 1) {
313
+ try {
314
+ const opened = await openRecommendCardDetail(client, currentNodeId, {
315
+ timeoutMs,
316
+ scrollIntoView
317
+ });
318
+ return {
319
+ ...opened,
320
+ card_node_id: currentNodeId,
321
+ card_candidate: currentCandidate,
322
+ retry_attempts: attempts
323
+ };
324
+ } catch (error) {
325
+ const stale = isStaleRecommendNodeError(error);
326
+ attempts.push({
327
+ attempt: attemptIndex + 1,
328
+ node_id: currentNodeId,
329
+ stale_node: stale,
330
+ error: error?.message || String(error)
331
+ });
332
+ if (!stale || attemptIndex >= limit - 1 || !candidateKey) {
333
+ error.recommend_detail_open_attempts = attempts;
334
+ throw error;
335
+ }
336
+
337
+ const resolved = await findRecommendCardNodeForCandidateKey(client, {
338
+ candidateKey,
339
+ rootState: currentRootState,
340
+ targetUrl,
341
+ timeoutMs: retryTimeoutMs,
342
+ intervalMs: retryIntervalMs
343
+ });
344
+ attempts[attempts.length - 1].refresh_lookup = {
345
+ ok: Boolean(resolved.ok),
346
+ node_id: resolved.node_id || null,
347
+ visible_index: resolved.visible_index ?? null,
348
+ card_count: resolved.card_count || resolved.last_card_count || 0,
349
+ reason: resolved.reason || null,
350
+ error: resolved.error || null
351
+ };
352
+ if (!resolved.ok || !resolved.node_id) {
353
+ error.recommend_detail_open_attempts = attempts;
354
+ throw error;
355
+ }
356
+ currentNodeId = resolved.node_id;
357
+ currentCandidate = resolved.candidate || currentCandidate;
358
+ currentRootState = resolved.root_state || null;
359
+ }
360
+ }
361
+
362
+ throw new Error("Recommend detail retry exhausted");
363
+ }
364
+
184
365
  export async function closeRecommendDetail(client, {
185
366
  attemptsLimit = 3
186
367
  } = {}) {
@@ -317,7 +498,8 @@ export async function extractRecommendDetailCandidate(client, {
317
498
  detail_popup_root: detailState?.popup?.root || null,
318
499
  resume_iframe_selector: detailState?.resumeIframe?.selector || null,
319
500
  resume_iframe_root: detailState?.resumeIframe?.root || null,
320
- resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId
501
+ resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId,
502
+ detail_html_errors: detailHtml.errors || []
321
503
  }
322
504
  });
323
505
 
@@ -334,7 +516,8 @@ export async function extractRecommendDetailCandidate(client, {
334
516
  popup_text: detailHtml.popupText,
335
517
  resume_text: detailHtml.resumeText,
336
518
  popup_html_length: detailHtml.popupHTML.length,
337
- resume_html_length: detailHtml.resumeHTML.length
519
+ resume_html_length: detailHtml.resumeHTML.length,
520
+ html_errors: detailHtml.errors || []
338
521
  },
339
522
  close_result: closeResult
340
523
  };
@@ -21,12 +21,18 @@ import {
21
21
  resetInfiniteListForRefreshRound
22
22
  } from "../../core/infinite-list/index.js";
23
23
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
24
- import { screenCandidate } from "../../core/screening/index.js";
24
+ import {
25
+ callScreeningLlm,
26
+ compactScreeningLlmResult,
27
+ createFailedLlmScreeningResult,
28
+ llmResultToScreening,
29
+ screenCandidate
30
+ } from "../../core/screening/index.js";
25
31
  import {
26
32
  closeRecommendDetail,
27
33
  createRecommendDetailNetworkRecorder,
28
34
  extractRecommendDetailCandidate,
29
- openRecommendCardDetail,
35
+ openRecommendCardDetailWithFreshRetry,
30
36
  waitForRecommendDetailNetworkEvents
31
37
  } from "./detail.js";
32
38
  import {
@@ -165,10 +171,22 @@ function compactDetail(detailResult) {
165
171
  parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
166
172
  cv_acquisition: detailResult.cv_acquisition || null,
167
173
  image_evidence: summarizeImageEvidence(detailResult.image_evidence),
174
+ llm_screening: compactScreeningLlmResult(detailResult.llm_result),
168
175
  close_result: detailResult.close_result
169
176
  };
170
177
  }
171
178
 
179
+ function normalizeScreeningMode(value) {
180
+ const normalized = String(value || "llm").trim().toLowerCase();
181
+ return ["deterministic", "local", "local_scorer"].includes(normalized)
182
+ ? "deterministic"
183
+ : "llm";
184
+ }
185
+
186
+ function createMissingLlmConfigResult() {
187
+ return createFailedLlmScreeningResult(new Error("LLM screening config is required for production recommend runs"));
188
+ }
189
+
172
190
  function compactActionDiscovery(discovery) {
173
191
  if (!discovery) return null;
174
192
  return {
@@ -354,13 +372,20 @@ export async function runRecommendWorkflow({
354
372
  executePostAction = true,
355
373
  actionTimeoutMs = 8000,
356
374
  actionIntervalMs = 500,
357
- actionAfterClickDelayMs = 900
375
+ actionAfterClickDelayMs = 900,
376
+ screeningMode = "llm",
377
+ llmConfig = null,
378
+ llmTimeoutMs = 120000,
379
+ llmImageLimit = 8,
380
+ llmImageDetail = "high"
358
381
  } = {}, runControl) {
359
382
  if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
360
383
  const normalizedFilter = normalizeFilter(filter);
361
384
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
362
385
  const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
363
386
  const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
387
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
388
+ const useLlmScreening = normalizedScreeningMode !== "deterministic";
364
389
  const postActionEnabled = normalizedPostAction !== "none";
365
390
  const limit = Math.max(1, Number(maxCandidates) || 1);
366
391
  const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
@@ -484,6 +509,8 @@ export async function runRecommendWorkflow({
484
509
  passed: 0,
485
510
  greet_count: 0,
486
511
  post_action_clicked: 0,
512
+ screening_mode: normalizedScreeningMode,
513
+ llm_screened: 0,
487
514
  unique_seen: compactInfiniteListState(listState).seen_count,
488
515
  scroll_count: 0,
489
516
  refresh_rounds: 0,
@@ -562,6 +589,7 @@ export async function runRecommendWorkflow({
562
589
  screened: results.length,
563
590
  detail_opened: results.filter((item) => item.detail).length,
564
591
  passed: results.filter((item) => item.screening.passed).length,
592
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
565
593
  unique_seen: compactInfiniteListState(listState).seen_count,
566
594
  scroll_count: compactInfiniteListState(listState).scroll_count,
567
595
  refresh_rounds: refreshRounds,
@@ -596,9 +624,9 @@ export async function runRecommendWorkflow({
596
624
  }
597
625
 
598
626
  const index = results.length;
599
- const cardNodeId = nextCandidateResult.item.node_id;
627
+ let cardNodeId = nextCandidateResult.item.node_id;
600
628
  const candidateKey = nextCandidateResult.item.key;
601
- const cardCandidate = nextCandidateResult.item.candidate;
629
+ let cardCandidate = nextCandidateResult.item.candidate;
602
630
 
603
631
  let screeningCandidate = cardCandidate;
604
632
  let detailResult = null;
@@ -608,7 +636,17 @@ export async function runRecommendWorkflow({
608
636
  runControl.setPhase("recommend:detail");
609
637
  rootState = await ensureRecommendViewport(rootState, "detail");
610
638
  networkRecorder.clear();
611
- const openedDetail = await openRecommendCardDetail(client, cardNodeId);
639
+ const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
640
+ cardNodeId,
641
+ candidateKey,
642
+ cardCandidate,
643
+ rootState,
644
+ targetUrl,
645
+ maxAttempts: 2
646
+ });
647
+ cardNodeId = openedDetail.card_node_id || cardNodeId;
648
+ cardCandidate = openedDetail.card_candidate || cardCandidate;
649
+ screeningCandidate = cardCandidate;
612
650
  const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
613
651
  const networkWait = await waitForCvNetworkEvents(
614
652
  waitForRecommendDetailNetworkEvents,
@@ -686,7 +724,30 @@ export async function runRecommendWorkflow({
686
724
  await runControl.waitIfPaused();
687
725
  runControl.throwIfCanceled();
688
726
  runControl.setPhase("recommend:screening");
689
- const screening = screenCandidate(screeningCandidate, { criteria });
727
+ let llmResult = null;
728
+ if (useLlmScreening) {
729
+ if (!llmConfig) {
730
+ llmResult = createMissingLlmConfigResult();
731
+ } else {
732
+ try {
733
+ llmResult = await callScreeningLlm({
734
+ candidate: screeningCandidate,
735
+ criteria,
736
+ config: llmConfig,
737
+ timeoutMs: llmTimeoutMs,
738
+ imageEvidence: detailResult?.image_evidence || null,
739
+ maxImages: llmImageLimit,
740
+ imageDetail: llmImageDetail
741
+ });
742
+ } catch (error) {
743
+ llmResult = createFailedLlmScreeningResult(error);
744
+ }
745
+ }
746
+ if (detailResult) detailResult.llm_result = llmResult;
747
+ }
748
+ const screening = useLlmScreening
749
+ ? llmResultToScreening(llmResult, screeningCandidate)
750
+ : screenCandidate(screeningCandidate, { criteria });
690
751
  let actionDiscovery = null;
691
752
  let postActionResult = null;
692
753
  if (postActionEnabled && detailResult) {
@@ -721,6 +782,7 @@ export async function runRecommendWorkflow({
721
782
  card_node_id: cardNodeId,
722
783
  candidate: compactCandidate(screeningCandidate),
723
784
  detail: compactDetail(detailResult),
785
+ llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
724
786
  screening: compactScreening(screening),
725
787
  action_discovery: compactActionDiscovery(actionDiscovery),
726
788
  post_action: postActionResult
@@ -740,6 +802,7 @@ export async function runRecommendWorkflow({
740
802
  screened: results.length,
741
803
  detail_opened: results.filter((item) => item.detail).length,
742
804
  passed: results.filter((item) => item.screening.passed).length,
805
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
743
806
  greet_count: greetCount,
744
807
  post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
745
808
  unique_seen: compactInfiniteListState(listState).seen_count,
@@ -754,6 +817,7 @@ export async function runRecommendWorkflow({
754
817
  last_score: screening.score
755
818
  });
756
819
  runControl.checkpoint({
820
+ results,
757
821
  last_candidate: {
758
822
  id: screeningCandidate.id || null,
759
823
  key: candidateKey,
@@ -763,6 +827,7 @@ export async function runRecommendWorkflow({
763
827
  passed: screening.passed,
764
828
  score: screening.score
765
829
  },
830
+ llm_screening: compactScreeningLlmResult(llmResult),
766
831
  post_action: postActionResult
767
832
  }
768
833
  });
@@ -796,6 +861,7 @@ export async function runRecommendWorkflow({
796
861
  processed: results.length,
797
862
  screened: results.length,
798
863
  detail_opened: results.filter((item) => item.detail).length,
864
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
799
865
  passed: results.filter((item) => item.screening.passed).length,
800
866
  greet_count: greetCount,
801
867
  post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
@@ -841,6 +907,11 @@ export function createRecommendRunService({
841
907
  actionTimeoutMs = 8000,
842
908
  actionIntervalMs = 500,
843
909
  actionAfterClickDelayMs = 900,
910
+ screeningMode = "llm",
911
+ llmConfig = null,
912
+ llmTimeoutMs = 120000,
913
+ llmImageLimit = 8,
914
+ llmImageDetail = "high",
844
915
  name = "recommend-domain-run"
845
916
  } = {}) {
846
917
  if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
@@ -848,6 +919,7 @@ export function createRecommendRunService({
848
919
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
849
920
  const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
850
921
  const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
922
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
851
923
  const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
852
924
  const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
853
925
  return manager.startRun({
@@ -878,7 +950,12 @@ export function createRecommendRunService({
878
950
  post_action: normalizedPostAction,
879
951
  max_greet_count: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
880
952
  execute_post_action: Boolean(executePostAction),
881
- action_timeout_ms: actionTimeoutMs
953
+ action_timeout_ms: actionTimeoutMs,
954
+ screening_mode: normalizedScreeningMode,
955
+ llm_configured: Boolean(llmConfig),
956
+ llm_timeout_ms: llmTimeoutMs,
957
+ llm_image_limit: llmImageLimit,
958
+ llm_image_detail: llmImageDetail
882
959
  },
883
960
  progress: {
884
961
  card_count: 0,
@@ -886,6 +963,7 @@ export function createRecommendRunService({
886
963
  processed: 0,
887
964
  screened: 0,
888
965
  detail_opened: 0,
966
+ llm_screened: 0,
889
967
  passed: 0,
890
968
  greet_count: 0,
891
969
  post_action_clicked: 0
@@ -921,7 +999,12 @@ export function createRecommendRunService({
921
999
  executePostAction,
922
1000
  actionTimeoutMs,
923
1001
  actionIntervalMs,
924
- actionAfterClickDelayMs
1002
+ actionAfterClickDelayMs,
1003
+ screeningMode: normalizedScreeningMode,
1004
+ llmConfig,
1005
+ llmTimeoutMs,
1006
+ llmImageLimit,
1007
+ llmImageDetail
925
1008
  }, runControl)
926
1009
  });
927
1010
  }