@reconcrap/boss-recommend-mcp 2.0.54 → 2.0.56

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.
@@ -3,23 +3,30 @@ import {
3
3
  clickPoint,
4
4
  DETERMINISTIC_CLICK_OPTIONS,
5
5
  getFrameDocumentNodeId,
6
- getNodeBox,
7
- getOuterHTML,
8
- pressKey,
9
- querySelectorAll,
10
- sleep
11
- } from "../../core/browser/index.js";
12
- import { candidateKeyFromProfile } from "../../core/infinite-list/index.js";
13
- import {
14
- buildScreeningCandidateFromDetail,
15
- htmlToText
16
- } from "../../core/screening/index.js";
17
- import {
18
- DETAIL_CLOSE_SELECTORS,
19
- DETAIL_NETWORK_PATTERNS,
20
- DETAIL_POPUP_SELECTORS,
21
- DETAIL_RESUME_IFRAME_SELECTORS
22
- } from "./constants.js";
6
+ getNodeBox,
7
+ getOuterHTML,
8
+ pressKey,
9
+ querySelectorAll,
10
+ scrollNodeIntoView,
11
+ sleep
12
+ } from "../../core/browser/index.js";
13
+ import { candidateKeyFromProfile } from "../../core/infinite-list/index.js";
14
+ import {
15
+ buildScreeningCandidateFromDetail,
16
+ htmlToText
17
+ } from "../../core/screening/index.js";
18
+ import {
19
+ closeBossAccountRightsBlockingPanel,
20
+ findBossAccountRightsBlockingPanel
21
+ } from "../common/account-rights-panel.js";
22
+ import {
23
+ DETAIL_CLOSE_SELECTORS,
24
+ DETAIL_NETWORK_PATTERNS,
25
+ DETAIL_POPUP_SELECTORS,
26
+ DETAIL_RESUME_IFRAME_SELECTORS,
27
+ RECOMMEND_AVATAR_PREVIEW_CLOSE_SELECTORS,
28
+ RECOMMEND_AVATAR_PREVIEW_SELECTORS
29
+ } from "./constants.js";
23
30
  import {
24
31
  getRecommendRoots
25
32
  } from "./roots.js";
@@ -41,7 +48,7 @@ export function matchesRecommendDetailNetwork(url) {
41
48
  return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
42
49
  }
43
50
 
44
- export function createRecommendDetailNetworkRecorder(client) {
51
+ export function createRecommendDetailNetworkRecorder(client) {
45
52
  const events = [];
46
53
  client.Network.responseReceived((event) => {
47
54
  const url = event?.response?.url || "";
@@ -76,9 +83,29 @@ export function createRecommendDetailNetworkRecorder(client) {
76
83
  events.length = 0;
77
84
  }
78
85
  };
79
- }
80
-
81
- export async function waitForRecommendDetailNetworkEvents(recorder, {
86
+ }
87
+
88
+ export async function findRecommendBlockingPanel(client, options = {}) {
89
+ return findBossAccountRightsBlockingPanel(client, options);
90
+ }
91
+
92
+ export async function closeRecommendBlockingPanels(client, options = {}) {
93
+ return closeBossAccountRightsBlockingPanel(client, {
94
+ resolveRoots: getRecommendRoots,
95
+ ...options
96
+ });
97
+ }
98
+
99
+ function looksLikeRecommendAvatarPreviewHtml(html = "") {
100
+ return /\bavatar-preview\b|\bfigure-preview\b/i.test(String(html || ""));
101
+ }
102
+
103
+ function isRecommendAvatarPreviewOpenError(error) {
104
+ return error?.code === "RECOMMEND_AVATAR_PREVIEW_OPENED"
105
+ || /RECOMMEND_AVATAR_PREVIEW_OPENED/i.test(String(error?.message || error || ""));
106
+ }
107
+
108
+ export async function waitForRecommendDetailNetworkEvents(recorder, {
82
109
  minCount = 1,
83
110
  requireLoaded = true,
84
111
  timeoutMs = 3500,
@@ -148,10 +175,10 @@ export async function waitForRecommendDetail(client, {
148
175
  return lastState;
149
176
  }
150
177
 
151
- async function readRecommendDetailState(client) {
152
- const rootState = await getRecommendRoots(client);
153
- const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
154
- const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
178
+ async function readRecommendDetailState(client) {
179
+ const rootState = await getRecommendRoots(client);
180
+ const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
181
+ const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
155
182
  return {
156
183
  iframe: rootState.iframe,
157
184
  roots: rootState.roots,
@@ -184,6 +211,46 @@ export async function waitForRecommendDetailClosed(client, {
184
211
  };
185
212
  }
186
213
 
214
+ export async function readRecommendAvatarPreviewState(client) {
215
+ const rootState = await getRecommendRoots(client);
216
+ const topRoot = rootState.rootNodes?.top
217
+ ? [{ name: "top", nodeId: rootState.rootNodes.top }]
218
+ : rootState.roots.filter((root) => root?.name === "top");
219
+ const preview = await findVisibleDetailTarget(client, topRoot, RECOMMEND_AVATAR_PREVIEW_SELECTORS, {
220
+ includeAvatarPreview: true
221
+ });
222
+ return {
223
+ open: Boolean(preview),
224
+ root: rootState.topRoot,
225
+ roots: topRoot,
226
+ preview
227
+ };
228
+ }
229
+
230
+ export async function waitForRecommendAvatarPreviewClosed(client, {
231
+ timeoutMs = 1200,
232
+ intervalMs = 120
233
+ } = {}) {
234
+ const started = Date.now();
235
+ let state = null;
236
+ while (Date.now() - started <= timeoutMs) {
237
+ state = await readRecommendAvatarPreviewState(client);
238
+ if (!state.open) {
239
+ return {
240
+ closed: true,
241
+ elapsed_ms: Date.now() - started,
242
+ state
243
+ };
244
+ }
245
+ await sleep(intervalMs);
246
+ }
247
+ return {
248
+ closed: false,
249
+ elapsed_ms: Date.now() - started,
250
+ state
251
+ };
252
+ }
253
+
187
254
  function compactRect(rect) {
188
255
  if (!rect) return null;
189
256
  return {
@@ -239,18 +306,28 @@ async function verifyRecommendDetailStillOpen(client, {
239
306
  };
240
307
  }
241
308
 
242
- async function findVisibleDetailTarget(client, roots, selectors) {
309
+ async function findVisibleDetailTarget(client, roots, selectors, {
310
+ includeAvatarPreview = false
311
+ } = {}) {
243
312
  for (const root of roots) {
244
313
  if (!root?.nodeId) continue;
245
- for (const selector of selectors) {
246
- const nodeIds = await querySelectorAll(client, root.nodeId, selector);
247
- for (const nodeId of nodeIds) {
248
- try {
249
- const box = await getNodeBox(client, nodeId);
250
- if (box.rect.width > 2 && box.rect.height > 2) {
251
- return {
252
- root: root.name,
253
- root_node_id: root.nodeId,
314
+ for (const selector of selectors) {
315
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
316
+ for (const nodeId of nodeIds) {
317
+ try {
318
+ const box = await getNodeBox(client, nodeId);
319
+ if (box.rect.width > 2 && box.rect.height > 2) {
320
+ if (!includeAvatarPreview) {
321
+ try {
322
+ const html = await getOuterHTML(client, nodeId);
323
+ if (looksLikeRecommendAvatarPreviewHtml(html)) continue;
324
+ } catch {
325
+ // If the node went stale, let the next candidate decide.
326
+ }
327
+ }
328
+ return {
329
+ root: root.name,
330
+ root_node_id: root.nodeId,
254
331
  selector,
255
332
  node_id: nodeId,
256
333
  center: box.center,
@@ -317,7 +394,104 @@ export function isStaleRecommendNodeError(error) {
317
394
 
318
395
  export function isRecommendDetailOpenMissError(error) {
319
396
  const message = String(error?.message || error || "");
320
- return /Candidate detail did not open|no known detail selectors mounted/i.test(message);
397
+ return isRecommendAvatarPreviewOpenError(error)
398
+ || /Candidate detail did not open|no known detail selectors mounted/i.test(message);
399
+ }
400
+
401
+ export function resolveRecommendCardDetailClickPoint(cardBox, {
402
+ attemptIndex = 0
403
+ } = {}) {
404
+ const rect = cardBox?.rect || {};
405
+ const width = Number(rect.width) || 0;
406
+ const height = Number(rect.height) || 0;
407
+ if (width <= 2 || height <= 2) {
408
+ return {
409
+ ...(cardBox?.center || { x: 0, y: 0 }),
410
+ mode: "card-center-fallback",
411
+ reason: "invalid_card_rect"
412
+ };
413
+ }
414
+
415
+ const xFractions = [0.22, 0.50, 0.72];
416
+ const xFraction = xFractions[Math.min(Math.max(0, attemptIndex), xFractions.length - 1)];
417
+ const minOffsetX = Math.min(width - 12, Math.max(110, Math.min(180, width * 0.18)));
418
+ const maxOffsetX = Math.max(minOffsetX, width - Math.min(220, Math.max(90, width * 0.22)));
419
+ const rawOffsetX = width * xFraction;
420
+ const offsetX = clampPointCoordinate(rawOffsetX, minOffsetX, maxOffsetX);
421
+ const offsetY = clampPointCoordinate(height * 0.28, Math.min(34, height / 2), Math.max(36, height - 28));
422
+ return {
423
+ x: rect.x + offsetX,
424
+ y: rect.y + offsetY,
425
+ mode: "card-body-safe-point",
426
+ attempt_index: attemptIndex,
427
+ offset_x: Math.round(offsetX),
428
+ offset_y: Math.round(offsetY)
429
+ };
430
+ }
431
+
432
+ async function clickRecommendCardDetailPoint(client, nodeId, {
433
+ scrollIntoView = true,
434
+ attemptIndex = 0
435
+ } = {}) {
436
+ if (scrollIntoView) {
437
+ try {
438
+ await scrollNodeIntoView(client, nodeId);
439
+ await sleep(150);
440
+ } catch {
441
+ // Recommend list cards are selected from visible nodes; if this CDP
442
+ // helper races the virtual list, let the box lookup/retry decide.
443
+ }
444
+ }
445
+ const box = await getNodeBox(client, nodeId);
446
+ const clickTarget = resolveRecommendCardDetailClickPoint(box, { attemptIndex });
447
+ const clickResult = await clickPoint(client, clickTarget.x, clickTarget.y, DETERMINISTIC_CLICK_OPTIONS);
448
+ return {
449
+ ...box,
450
+ click_target: clickTarget,
451
+ click_result: clickResult
452
+ };
453
+ }
454
+
455
+ async function waitForRecommendDetailOpenOutcome(client, {
456
+ timeoutMs = 10000,
457
+ intervalMs = 250
458
+ } = {}) {
459
+ const started = Date.now();
460
+ let detailState = null;
461
+ let avatarPreview = null;
462
+ while (Date.now() - started <= timeoutMs) {
463
+ detailState = await readRecommendDetailState(client);
464
+ if (detailState?.popup || detailState?.resumeIframe) {
465
+ return {
466
+ kind: "detail",
467
+ elapsed_ms: Date.now() - started,
468
+ detail_state: detailState
469
+ };
470
+ }
471
+ avatarPreview = await readRecommendAvatarPreviewState(client);
472
+ if (avatarPreview.open) {
473
+ return {
474
+ kind: "avatar_preview",
475
+ elapsed_ms: Date.now() - started,
476
+ avatar_preview: avatarPreview
477
+ };
478
+ }
479
+ await sleep(intervalMs);
480
+ }
481
+ return {
482
+ kind: "none",
483
+ elapsed_ms: Date.now() - started,
484
+ detail_state: detailState,
485
+ avatar_preview: avatarPreview
486
+ };
487
+ }
488
+
489
+ function makeRecommendAvatarPreviewOpenedError(outcome, clickAttempts = []) {
490
+ const error = new Error("RECOMMEND_AVATAR_PREVIEW_OPENED: candidate avatar preview opened instead of resume detail");
491
+ error.code = "RECOMMEND_AVATAR_PREVIEW_OPENED";
492
+ error.avatar_preview = outcome?.avatar_preview || null;
493
+ error.click_attempts = clickAttempts;
494
+ return error;
321
495
  }
322
496
 
323
497
  export async function findRecommendCardNodeForCandidateKey(client, {
@@ -350,8 +524,17 @@ export async function findRecommendCardNodeForCandidateKey(client, {
350
524
  };
351
525
  }
352
526
 
353
- const nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
354
- lastCardCount = nodeIds.length;
527
+ let nodeIds = [];
528
+ try {
529
+ nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
530
+ } catch (error) {
531
+ lastError = error;
532
+ if (!isStaleRecommendNodeError(error)) throw error;
533
+ rootState = null;
534
+ if (intervalMs > 0) await sleep(intervalMs);
535
+ continue;
536
+ }
537
+ lastCardCount = nodeIds.length;
355
538
  for (let visibleIndex = 0; visibleIndex < nodeIds.length; visibleIndex += 1) {
356
539
  const nodeId = nodeIds[visibleIndex];
357
540
  try {
@@ -397,35 +580,71 @@ export async function findRecommendCardNodeForCandidateKey(client, {
397
580
  };
398
581
  }
399
582
 
400
- export async function openRecommendCardDetail(client, cardNodeId, {
401
- timeoutMs = 12000,
402
- scrollIntoView = true
403
- } = {}) {
404
- const started = Date.now();
405
- const clickStarted = Date.now();
406
- const cardBox = await clickNodeCenter(client, cardNodeId, { scrollIntoView });
407
- const candidateClickMs = Date.now() - clickStarted;
408
- const detailStarted = Date.now();
409
- const detailState = await waitForRecommendDetail(client, { timeoutMs });
410
- const detailOpenMs = Date.now() - detailStarted;
411
- if (!detailState?.popup && !detailState?.resumeIframe) {
412
- throw new Error("Candidate detail did not open or no known detail selectors mounted");
413
- }
414
-
415
- return {
416
- card_box: cardBox,
417
- detail_state: detailState,
418
- timings: {
419
- candidate_click_ms: candidateClickMs,
420
- detail_open_ms: detailOpenMs,
421
- open_total_ms: Date.now() - started
422
- }
423
- };
424
- }
583
+ export async function openRecommendCardDetail(client, cardNodeId, {
584
+ timeoutMs = 12000,
585
+ scrollIntoView = true
586
+ } = {}) {
587
+ const started = Date.now();
588
+ const clickAttempts = [];
589
+ const maxClickAttempts = 3;
590
+ let lastOutcome = null;
591
+ let lastCardBox = null;
592
+ let candidateClickMs = 0;
593
+ let detailOpenMs = 0;
594
+
595
+ for (let attemptIndex = 0; attemptIndex < maxClickAttempts; attemptIndex += 1) {
596
+ const clickStarted = Date.now();
597
+ lastCardBox = await clickRecommendCardDetailPoint(client, cardNodeId, {
598
+ scrollIntoView: attemptIndex === 0 ? scrollIntoView : false,
599
+ attemptIndex
600
+ });
601
+ candidateClickMs += Date.now() - clickStarted;
602
+ const detailStarted = Date.now();
603
+ lastOutcome = await waitForRecommendDetailOpenOutcome(client, {
604
+ timeoutMs: attemptIndex === 0 ? timeoutMs : Math.max(2500, Math.floor(timeoutMs / 3)),
605
+ intervalMs: 250
606
+ });
607
+ detailOpenMs += Date.now() - detailStarted;
608
+ clickAttempts.push({
609
+ attempt: attemptIndex + 1,
610
+ click_target: lastCardBox.click_target,
611
+ click_result: lastCardBox.click_result,
612
+ outcome: lastOutcome.kind,
613
+ elapsed_ms: lastOutcome.elapsed_ms
614
+ });
615
+
616
+ if (lastOutcome.kind === "detail") {
617
+ return {
618
+ card_box: lastCardBox,
619
+ click_attempts: clickAttempts,
620
+ detail_state: lastOutcome.detail_state,
621
+ timings: {
622
+ candidate_click_ms: candidateClickMs,
623
+ detail_open_ms: detailOpenMs,
624
+ open_total_ms: Date.now() - started
625
+ }
626
+ };
627
+ }
628
+
629
+ if (lastOutcome.kind === "avatar_preview") {
630
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2, waitMs: 350 });
631
+ throw makeRecommendAvatarPreviewOpenedError(lastOutcome, clickAttempts);
632
+ }
633
+ break;
634
+ }
635
+
636
+ if (lastOutcome?.kind === "avatar_preview") {
637
+ throw makeRecommendAvatarPreviewOpenedError(lastOutcome, clickAttempts);
638
+ }
639
+ const error = new Error("Candidate detail did not open or no known detail selectors mounted");
640
+ error.click_attempts = clickAttempts;
641
+ error.last_open_outcome = lastOutcome;
642
+ throw error;
643
+ }
425
644
 
426
- export async function openRecommendCardDetailWithFreshRetry(client, {
427
- cardNodeId,
428
- candidateKey = "",
645
+ export async function openRecommendCardDetailWithFreshRetry(client, {
646
+ cardNodeId,
647
+ candidateKey = "",
429
648
  cardCandidate = null,
430
649
  rootState = null,
431
650
  targetUrl = "",
@@ -492,12 +711,100 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
492
711
  currentRootState = resolved.root_state || null;
493
712
  }
494
713
  }
495
-
496
- throw new Error("Recommend detail retry exhausted");
497
- }
498
-
499
- export async function closeRecommendDetail(client, {
500
- attemptsLimit = 4,
714
+
715
+ throw new Error("Recommend detail retry exhausted");
716
+ }
717
+
718
+ export async function closeRecommendAvatarPreview(client, {
719
+ attemptsLimit = 2,
720
+ waitMs = 500
721
+ } = {}) {
722
+ const attempts = [];
723
+ for (let index = 0; index < attemptsLimit; index += 1) {
724
+ const state = await readRecommendAvatarPreviewState(client);
725
+ if (!state.open) {
726
+ return {
727
+ closed: true,
728
+ already_closed: true,
729
+ attempts
730
+ };
731
+ }
732
+
733
+ const closeTarget = await findVisibleCloseTarget(client, state.roots, RECOMMEND_AVATAR_PREVIEW_CLOSE_SELECTORS);
734
+ if (closeTarget) {
735
+ try {
736
+ if (closeTarget.center) {
737
+ await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
738
+ } else {
739
+ await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
740
+ }
741
+ attempts.push({
742
+ mode: "avatar-preview-close-selector",
743
+ selector: closeTarget.selector,
744
+ root: closeTarget.root
745
+ });
746
+ } catch (error) {
747
+ attempts.push({
748
+ mode: "avatar-preview-close-selector-error",
749
+ selector: closeTarget.selector,
750
+ root: closeTarget.root,
751
+ error: error?.message || String(error)
752
+ });
753
+ }
754
+ } else {
755
+ await pressEscape(client);
756
+ attempts.push({ mode: "avatar-preview-Escape" });
757
+ }
758
+
759
+ const closed = await waitForRecommendAvatarPreviewClosed(client, {
760
+ timeoutMs: waitMs,
761
+ intervalMs: 100
762
+ });
763
+ attempts.push({
764
+ mode: "wait-avatar-preview-closed",
765
+ closed: closed.closed,
766
+ elapsed_ms: closed.elapsed_ms
767
+ });
768
+ if (closed.closed) {
769
+ return {
770
+ closed: true,
771
+ already_closed: false,
772
+ attempts
773
+ };
774
+ }
775
+
776
+ await pressEscape(client);
777
+ attempts.push({ mode: "avatar-preview-Escape-fallback" });
778
+ const closedAfterEscape = await waitForRecommendAvatarPreviewClosed(client, {
779
+ timeoutMs: waitMs,
780
+ intervalMs: 100
781
+ });
782
+ attempts.push({
783
+ mode: "wait-avatar-preview-closed-after-escape",
784
+ closed: closedAfterEscape.closed,
785
+ elapsed_ms: closedAfterEscape.elapsed_ms
786
+ });
787
+ if (closedAfterEscape.closed) {
788
+ return {
789
+ closed: true,
790
+ already_closed: false,
791
+ attempts
792
+ };
793
+ }
794
+ }
795
+
796
+ const state = await readRecommendAvatarPreviewState(client);
797
+ return {
798
+ closed: !state.open,
799
+ already_closed: false,
800
+ reason: state.open ? "avatar_preview_still_visible_after_close_attempts" : null,
801
+ attempts,
802
+ state
803
+ };
804
+ }
805
+
806
+ export async function closeRecommendDetail(client, {
807
+ attemptsLimit = 4,
501
808
  closeWaitMs = 5000,
502
809
  escapeWaitMs = 3500
503
810
  } = {}) {