@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.22

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.
Files changed (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +8 -8
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2419 -2415
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1326 -1326
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +32 -32
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +650 -650
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +504 -361
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +987 -854
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +13 -0
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +929 -915
  66. package/src/recommend-scheduler.js +496 -496
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,54 +1,54 @@
1
- import {
2
- clickNodeCenter,
3
- clickPoint,
4
- DETERMINISTIC_CLICK_OPTIONS,
5
- getFrameDocumentNodeId,
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";
1
+ import {
2
+ clickNodeCenter,
3
+ clickPoint,
4
+ DETERMINISTIC_CLICK_OPTIONS,
5
+ getFrameDocumentNodeId,
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";
30
30
  import {
31
31
  getRecommendRoots
32
32
  } from "./roots.js";
33
- import {
34
- findRecommendCardNodeIds,
35
- readRecommendCardCandidate
36
- } from "./cards.js";
37
-
38
- const DETAIL_OUTSIDE_CLOSE_BOUNDARY_SELECTORS = Object.freeze([
39
- ".resume-center-side .resume-detail-wrap",
40
- ".resume-detail-wrap",
41
- ".boss-popup__wrapper .boss-popup__body",
42
- ".boss-popup__wrapper .dialog-body",
43
- ".dialog-wrap.active .resume-detail-wrap",
44
- ".geek-detail-modal .resume-detail-wrap"
45
- ]);
46
-
47
- export function matchesRecommendDetailNetwork(url) {
33
+ import {
34
+ findRecommendCardNodeIds,
35
+ readRecommendCardCandidate
36
+ } from "./cards.js";
37
+
38
+ const DETAIL_OUTSIDE_CLOSE_BOUNDARY_SELECTORS = Object.freeze([
39
+ ".resume-center-side .resume-detail-wrap",
40
+ ".resume-detail-wrap",
41
+ ".boss-popup__wrapper .boss-popup__body",
42
+ ".boss-popup__wrapper .dialog-body",
43
+ ".dialog-wrap.active .resume-detail-wrap",
44
+ ".geek-detail-modal .resume-detail-wrap"
45
+ ]);
46
+
47
+ export function matchesRecommendDetailNetwork(url) {
48
48
  return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
49
49
  }
50
50
 
51
- export function createRecommendDetailNetworkRecorder(client) {
51
+ export function createRecommendDetailNetworkRecorder(client) {
52
52
  const events = [];
53
53
  client.Network.responseReceived((event) => {
54
54
  const url = event?.response?.url || "";
@@ -83,29 +83,29 @@ export function createRecommendDetailNetworkRecorder(client) {
83
83
  events.length = 0;
84
84
  }
85
85
  };
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, {
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, {
109
109
  minCount = 1,
110
110
  requireLoaded = true,
111
111
  timeoutMs = 3500,
@@ -175,10 +175,10 @@ export async function waitForRecommendDetail(client, {
175
175
  return lastState;
176
176
  }
177
177
 
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);
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);
182
182
  return {
183
183
  iframe: rootState.iframe,
184
184
  roots: rootState.roots,
@@ -187,10 +187,10 @@ async function readRecommendDetailState(client) {
187
187
  };
188
188
  }
189
189
 
190
- export async function waitForRecommendDetailClosed(client, {
191
- timeoutMs = 4000,
192
- intervalMs = 250
193
- } = {}) {
190
+ export async function waitForRecommendDetailClosed(client, {
191
+ timeoutMs = 4000,
192
+ intervalMs = 250
193
+ } = {}) {
194
194
  const started = Date.now();
195
195
  let lastState = null;
196
196
  while (Date.now() - started <= timeoutMs) {
@@ -208,126 +208,126 @@ export async function waitForRecommendDetailClosed(client, {
208
208
  closed: false,
209
209
  elapsed_ms: Date.now() - started,
210
210
  state: lastState
211
- };
212
- }
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
-
254
- function compactRect(rect) {
255
- if (!rect) return null;
256
- return {
257
- x: Math.round(Number(rect.x) || 0),
258
- y: Math.round(Number(rect.y) || 0),
259
- width: Math.round(Number(rect.width) || 0),
260
- height: Math.round(Number(rect.height) || 0)
261
- };
262
- }
263
-
264
- function compactDetailTarget(target) {
265
- if (!target) return null;
266
- return {
267
- root: target.root || "",
268
- root_node_id: target.root_node_id || null,
269
- selector: target.selector || "",
270
- node_id: target.node_id || null,
271
- rect: compactRect(target.rect)
272
- };
273
- }
274
-
275
- function compactDetailOpenState(state) {
276
- if (!state) {
277
- return {
278
- open: false,
279
- popup: null,
280
- resume_iframe: null,
281
- iframe_document_node_id: null
282
- };
283
- }
284
- return {
285
- open: Boolean(state.popup || state.resumeIframe),
286
- popup: compactDetailTarget(state.popup),
287
- resume_iframe: compactDetailTarget(state.resumeIframe),
288
- iframe_document_node_id: state.iframe?.documentNodeId || null
289
- };
290
- }
291
-
292
- async function verifyRecommendDetailStillOpen(client, {
293
- settleMs = 350
294
- } = {}) {
295
- const firstState = await readRecommendDetailState(client);
296
- if (settleMs > 0) await sleep(settleMs);
297
- const secondState = await readRecommendDetailState(client);
298
- const first = compactDetailOpenState(firstState);
299
- const second = compactDetailOpenState(secondState);
300
- const stableOpen = Boolean(first.open && second.open);
301
- return {
302
- open: Boolean(second.open),
303
- stable_open: stableOpen,
304
- first,
305
- second
306
- };
307
- }
308
-
309
- async function findVisibleDetailTarget(client, roots, selectors, {
310
- includeAvatarPreview = false
311
- } = {}) {
312
- for (const root of roots) {
313
- if (!root?.nodeId) continue;
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,
211
+ };
212
+ }
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
+
254
+ function compactRect(rect) {
255
+ if (!rect) return null;
256
+ return {
257
+ x: Math.round(Number(rect.x) || 0),
258
+ y: Math.round(Number(rect.y) || 0),
259
+ width: Math.round(Number(rect.width) || 0),
260
+ height: Math.round(Number(rect.height) || 0)
261
+ };
262
+ }
263
+
264
+ function compactDetailTarget(target) {
265
+ if (!target) return null;
266
+ return {
267
+ root: target.root || "",
268
+ root_node_id: target.root_node_id || null,
269
+ selector: target.selector || "",
270
+ node_id: target.node_id || null,
271
+ rect: compactRect(target.rect)
272
+ };
273
+ }
274
+
275
+ function compactDetailOpenState(state) {
276
+ if (!state) {
277
+ return {
278
+ open: false,
279
+ popup: null,
280
+ resume_iframe: null,
281
+ iframe_document_node_id: null
282
+ };
283
+ }
284
+ return {
285
+ open: Boolean(state.popup || state.resumeIframe),
286
+ popup: compactDetailTarget(state.popup),
287
+ resume_iframe: compactDetailTarget(state.resumeIframe),
288
+ iframe_document_node_id: state.iframe?.documentNodeId || null
289
+ };
290
+ }
291
+
292
+ async function verifyRecommendDetailStillOpen(client, {
293
+ settleMs = 350
294
+ } = {}) {
295
+ const firstState = await readRecommendDetailState(client);
296
+ if (settleMs > 0) await sleep(settleMs);
297
+ const secondState = await readRecommendDetailState(client);
298
+ const first = compactDetailOpenState(firstState);
299
+ const second = compactDetailOpenState(secondState);
300
+ const stableOpen = Boolean(first.open && second.open);
301
+ return {
302
+ open: Boolean(second.open),
303
+ stable_open: stableOpen,
304
+ first,
305
+ second
306
+ };
307
+ }
308
+
309
+ async function findVisibleDetailTarget(client, roots, selectors, {
310
+ includeAvatarPreview = false
311
+ } = {}) {
312
+ for (const root of roots) {
313
+ if (!root?.nodeId) continue;
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,
331
331
  selector,
332
332
  node_id: nodeId,
333
333
  center: box.center,
@@ -387,114 +387,114 @@ export async function readRecommendDetailHtml(client, detailState) {
387
387
  };
388
388
  }
389
389
 
390
- export function isStaleRecommendNodeError(error) {
391
- const message = String(error?.message || error || "");
392
- return /Could not find node with given id|No node with given id|Node is detached|Cannot find node|Could not compute box model/i.test(message);
393
- }
394
-
395
- export function isRecommendDetailOpenMissError(error) {
396
- const message = String(error?.message || error || "");
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;
495
- }
496
-
497
- export async function findRecommendCardNodeForCandidateKey(client, {
390
+ export function isStaleRecommendNodeError(error) {
391
+ const message = String(error?.message || error || "");
392
+ return /Could not find node with given id|No node with given id|Node is detached|Cannot find node|Could not compute box model/i.test(message);
393
+ }
394
+
395
+ export function isRecommendDetailOpenMissError(error) {
396
+ const message = String(error?.message || error || "");
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;
495
+ }
496
+
497
+ export async function findRecommendCardNodeForCandidateKey(client, {
498
498
  candidateKey = "",
499
499
  rootState = null,
500
500
  targetUrl = "",
@@ -524,17 +524,17 @@ export async function findRecommendCardNodeForCandidateKey(client, {
524
524
  };
525
525
  }
526
526
 
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;
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;
538
538
  for (let visibleIndex = 0; visibleIndex < nodeIds.length; visibleIndex += 1) {
539
539
  const nodeId = nodeIds[visibleIndex];
540
540
  try {
@@ -580,71 +580,71 @@ export async function findRecommendCardNodeForCandidateKey(client, {
580
580
  };
581
581
  }
582
582
 
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
- }
644
-
645
- export async function openRecommendCardDetailWithFreshRetry(client, {
646
- cardNodeId,
647
- candidateKey = "",
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
+ }
644
+
645
+ export async function openRecommendCardDetailWithFreshRetry(client, {
646
+ cardNodeId,
647
+ candidateKey = "",
648
648
  cardCandidate = null,
649
649
  rootState = null,
650
650
  targetUrl = "",
@@ -672,20 +672,20 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
672
672
  card_candidate: currentCandidate,
673
673
  retry_attempts: attempts
674
674
  };
675
- } catch (error) {
676
- const stale = isStaleRecommendNodeError(error);
677
- const detailOpenMiss = isRecommendDetailOpenMissError(error);
678
- attempts.push({
679
- attempt: attemptIndex + 1,
680
- node_id: currentNodeId,
681
- stale_node: stale,
682
- detail_open_miss: detailOpenMiss,
683
- error: error?.message || String(error)
684
- });
685
- if ((!stale && !detailOpenMiss) || attemptIndex >= limit - 1 || !candidateKey) {
686
- error.recommend_detail_open_attempts = attempts;
687
- throw error;
688
- }
675
+ } catch (error) {
676
+ const stale = isStaleRecommendNodeError(error);
677
+ const detailOpenMiss = isRecommendDetailOpenMissError(error);
678
+ attempts.push({
679
+ attempt: attemptIndex + 1,
680
+ node_id: currentNodeId,
681
+ stale_node: stale,
682
+ detail_open_miss: detailOpenMiss,
683
+ error: error?.message || String(error)
684
+ });
685
+ if ((!stale && !detailOpenMiss) || attemptIndex >= limit - 1 || !candidateKey) {
686
+ error.recommend_detail_open_attempts = attempts;
687
+ throw error;
688
+ }
689
689
 
690
690
  const resolved = await findRecommendCardNodeForCandidateKey(client, {
691
691
  candidateKey,
@@ -711,100 +711,100 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
711
711
  currentRootState = resolved.root_state || null;
712
712
  }
713
713
  }
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,
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,
808
808
  closeWaitMs = 5000,
809
809
  escapeWaitMs = 3500
810
810
  } = {}) {
@@ -822,11 +822,11 @@ export async function closeRecommendDetail(client, {
822
822
  const closeTarget = await findVisibleCloseTarget(client, rootState.roots, DETAIL_CLOSE_SELECTORS);
823
823
  if (closeTarget) {
824
824
  try {
825
- if (closeTarget.center) {
826
- await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
827
- } else {
828
- await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
829
- }
825
+ if (closeTarget.center) {
826
+ await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
827
+ } else {
828
+ await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
829
+ }
830
830
  attempts.push({
831
831
  mode: "close-selector",
832
832
  selector: closeTarget.selector,
@@ -856,35 +856,35 @@ export async function closeRecommendDetail(client, {
856
856
  closed: closedAfterClick.closed,
857
857
  elapsed_ms: closedAfterClick.elapsed_ms
858
858
  });
859
- if (closedAfterClick.closed) {
860
- return {
861
- closed: true,
862
- attempts
863
- };
864
- }
865
-
866
- const outsideClick = await clickOutsideRecommendDetail(client, closedAfterClick.state || existingState);
867
- attempts.push(outsideClick);
868
- if (outsideClick.clicked) {
869
- const closedAfterOutsideClick = await waitForRecommendDetailClosed(client, {
870
- timeoutMs: closeWaitMs,
871
- intervalMs: 250
872
- });
873
- attempts.push({
874
- mode: "wait-closed-after-outside-click",
875
- closed: closedAfterOutsideClick.closed,
876
- elapsed_ms: closedAfterOutsideClick.elapsed_ms
877
- });
878
- if (closedAfterOutsideClick.closed) {
879
- return {
880
- closed: true,
881
- attempts
882
- };
883
- }
884
- }
885
-
886
- await pressEscape(client);
887
- attempts.push({ mode: "Escape-fallback" });
859
+ if (closedAfterClick.closed) {
860
+ return {
861
+ closed: true,
862
+ attempts
863
+ };
864
+ }
865
+
866
+ const outsideClick = await clickOutsideRecommendDetail(client, closedAfterClick.state || existingState);
867
+ attempts.push(outsideClick);
868
+ if (outsideClick.clicked) {
869
+ const closedAfterOutsideClick = await waitForRecommendDetailClosed(client, {
870
+ timeoutMs: closeWaitMs,
871
+ intervalMs: 250
872
+ });
873
+ attempts.push({
874
+ mode: "wait-closed-after-outside-click",
875
+ closed: closedAfterOutsideClick.closed,
876
+ elapsed_ms: closedAfterOutsideClick.elapsed_ms
877
+ });
878
+ if (closedAfterOutsideClick.closed) {
879
+ return {
880
+ closed: true,
881
+ attempts
882
+ };
883
+ }
884
+ }
885
+
886
+ await pressEscape(client);
887
+ attempts.push({ mode: "Escape-fallback" });
888
888
 
889
889
  const closedAfterEscape = await waitForRecommendDetailClosed(client, {
890
890
  timeoutMs: escapeWaitMs,
@@ -901,33 +901,33 @@ export async function closeRecommendDetail(client, {
901
901
  attempts
902
902
  };
903
903
  }
904
- }
905
-
906
- const verification = await verifyRecommendDetailStillOpen(client);
907
- attempts.push({
908
- mode: "final-close-verification",
909
- open: verification.open,
910
- stable_open: verification.stable_open,
911
- popup: verification.second.popup,
912
- resume_iframe: verification.second.resume_iframe
913
- });
914
- if (!verification.open) {
915
- return {
916
- closed: true,
917
- attempts,
918
- verification
919
- };
920
- }
921
-
922
- return {
923
- closed: false,
924
- reason: verification.stable_open
925
- ? "detail_still_visible_after_close_attempts"
926
- : "detail_visibility_ambiguous_after_close_attempts",
927
- attempts,
928
- verification
929
- };
930
- }
904
+ }
905
+
906
+ const verification = await verifyRecommendDetailStillOpen(client);
907
+ attempts.push({
908
+ mode: "final-close-verification",
909
+ open: verification.open,
910
+ stable_open: verification.stable_open,
911
+ popup: verification.second.popup,
912
+ resume_iframe: verification.second.resume_iframe
913
+ });
914
+ if (!verification.open) {
915
+ return {
916
+ closed: true,
917
+ attempts,
918
+ verification
919
+ };
920
+ }
921
+
922
+ return {
923
+ closed: false,
924
+ reason: verification.stable_open
925
+ ? "detail_still_visible_after_close_attempts"
926
+ : "detail_visibility_ambiguous_after_close_attempts",
927
+ attempts,
928
+ verification
929
+ };
930
+ }
931
931
 
932
932
  async function findVisibleCloseTarget(client, roots, selectors) {
933
933
  let fallback = null;
@@ -959,107 +959,107 @@ async function findVisibleCloseTarget(client, roots, selectors) {
959
959
  return fallback;
960
960
  }
961
961
 
962
- async function pressEscape(client) {
963
- await pressKey(client, "Escape", {
964
- code: "Escape",
965
- windowsVirtualKeyCode: 27,
966
- nativeVirtualKeyCode: 27
967
- });
968
- }
969
-
970
- function clampPointCoordinate(value, min, max) {
971
- return Math.max(min, Math.min(max, value));
972
- }
973
-
974
- async function getClickViewport(client) {
975
- try {
976
- const metrics = typeof client?.Page?.getLayoutMetrics === "function"
977
- ? await client.Page.getLayoutMetrics()
978
- : null;
979
- const viewport = metrics?.cssLayoutViewport || metrics?.layoutViewport || metrics?.visualViewport || {};
980
- return {
981
- width: Number(viewport.clientWidth || viewport.width || 1440),
982
- height: Number(viewport.clientHeight || viewport.height || 900)
983
- };
984
- } catch {
985
- return {
986
- width: 1440,
987
- height: 900
988
- };
989
- }
990
- }
991
-
992
- function getOutsideClickPoint(rect, viewport) {
993
- if (!rect || rect.width <= 2 || rect.height <= 2) return null;
994
- const margin = 24;
995
- const minX = 8;
996
- const minY = 8;
997
- const maxX = Math.max(minX, (Number(viewport?.width) || 1440) - 8);
998
- const maxY = Math.max(minY, (Number(viewport?.height) || 900) - 8);
999
- const midX = rect.x + rect.width / 2;
1000
- const midY = rect.y + Math.min(Math.max(rect.height * 0.2, 48), Math.max(48, rect.height - 24));
1001
- const candidates = [
1002
- { side: "left", x: rect.x - margin, y: midY },
1003
- { side: "right", x: rect.x + rect.width + margin, y: midY },
1004
- { side: "above", x: midX, y: rect.y - margin },
1005
- { side: "below", x: midX, y: rect.y + rect.height + margin },
1006
- { side: "viewport-corner", x: 16, y: 16 }
1007
- ];
1008
-
1009
- for (const candidate of candidates) {
1010
- const x = clampPointCoordinate(candidate.x, minX, maxX);
1011
- const y = clampPointCoordinate(candidate.y, minY, maxY);
1012
- const insideRect = (
1013
- x >= rect.x
1014
- && x <= rect.x + rect.width
1015
- && y >= rect.y
1016
- && y <= rect.y + rect.height
1017
- );
1018
- if (!insideRect) {
1019
- return {
1020
- ...candidate,
1021
- x,
1022
- y
1023
- };
1024
- }
1025
- }
1026
- return null;
1027
- }
1028
-
1029
- async function clickOutsideRecommendDetail(client, detailState) {
1030
- const rootState = detailState?.roots?.length
1031
- ? detailState
1032
- : await readRecommendDetailState(client);
1033
- const boundaryTarget = await findVisibleDetailTarget(
1034
- client,
1035
- rootState.roots || [],
1036
- DETAIL_OUTSIDE_CLOSE_BOUNDARY_SELECTORS
1037
- );
1038
- const target = boundaryTarget || rootState.resumeIframe || rootState.popup || null;
1039
- const viewport = await getClickViewport(client);
1040
- const point = getOutsideClickPoint(target?.rect, viewport);
1041
- if (!point) {
1042
- return {
1043
- clicked: false,
1044
- mode: "outside-modal-click",
1045
- reason: "no_outside_click_point",
1046
- selector: target?.selector || null,
1047
- root: target?.root || null
1048
- };
1049
- }
1050
- await clickPoint(client, point.x, point.y, DETERMINISTIC_CLICK_OPTIONS);
1051
- return {
1052
- clicked: true,
1053
- mode: "outside-modal-click",
1054
- selector: target?.selector || null,
1055
- root: target?.root || null,
1056
- side: point.side,
1057
- x: Math.round(point.x),
1058
- y: Math.round(point.y)
1059
- };
1060
- }
1061
-
1062
- export async function extractRecommendDetailCandidate(client, {
962
+ async function pressEscape(client) {
963
+ await pressKey(client, "Escape", {
964
+ code: "Escape",
965
+ windowsVirtualKeyCode: 27,
966
+ nativeVirtualKeyCode: 27
967
+ });
968
+ }
969
+
970
+ function clampPointCoordinate(value, min, max) {
971
+ return Math.max(min, Math.min(max, value));
972
+ }
973
+
974
+ async function getClickViewport(client) {
975
+ try {
976
+ const metrics = typeof client?.Page?.getLayoutMetrics === "function"
977
+ ? await client.Page.getLayoutMetrics()
978
+ : null;
979
+ const viewport = metrics?.cssLayoutViewport || metrics?.layoutViewport || metrics?.visualViewport || {};
980
+ return {
981
+ width: Number(viewport.clientWidth || viewport.width || 1440),
982
+ height: Number(viewport.clientHeight || viewport.height || 900)
983
+ };
984
+ } catch {
985
+ return {
986
+ width: 1440,
987
+ height: 900
988
+ };
989
+ }
990
+ }
991
+
992
+ function getOutsideClickPoint(rect, viewport) {
993
+ if (!rect || rect.width <= 2 || rect.height <= 2) return null;
994
+ const margin = 24;
995
+ const minX = 8;
996
+ const minY = 8;
997
+ const maxX = Math.max(minX, (Number(viewport?.width) || 1440) - 8);
998
+ const maxY = Math.max(minY, (Number(viewport?.height) || 900) - 8);
999
+ const midX = rect.x + rect.width / 2;
1000
+ const midY = rect.y + Math.min(Math.max(rect.height * 0.2, 48), Math.max(48, rect.height - 24));
1001
+ const candidates = [
1002
+ { side: "left", x: rect.x - margin, y: midY },
1003
+ { side: "right", x: rect.x + rect.width + margin, y: midY },
1004
+ { side: "above", x: midX, y: rect.y - margin },
1005
+ { side: "below", x: midX, y: rect.y + rect.height + margin },
1006
+ { side: "viewport-corner", x: 16, y: 16 }
1007
+ ];
1008
+
1009
+ for (const candidate of candidates) {
1010
+ const x = clampPointCoordinate(candidate.x, minX, maxX);
1011
+ const y = clampPointCoordinate(candidate.y, minY, maxY);
1012
+ const insideRect = (
1013
+ x >= rect.x
1014
+ && x <= rect.x + rect.width
1015
+ && y >= rect.y
1016
+ && y <= rect.y + rect.height
1017
+ );
1018
+ if (!insideRect) {
1019
+ return {
1020
+ ...candidate,
1021
+ x,
1022
+ y
1023
+ };
1024
+ }
1025
+ }
1026
+ return null;
1027
+ }
1028
+
1029
+ async function clickOutsideRecommendDetail(client, detailState) {
1030
+ const rootState = detailState?.roots?.length
1031
+ ? detailState
1032
+ : await readRecommendDetailState(client);
1033
+ const boundaryTarget = await findVisibleDetailTarget(
1034
+ client,
1035
+ rootState.roots || [],
1036
+ DETAIL_OUTSIDE_CLOSE_BOUNDARY_SELECTORS
1037
+ );
1038
+ const target = boundaryTarget || rootState.resumeIframe || rootState.popup || null;
1039
+ const viewport = await getClickViewport(client);
1040
+ const point = getOutsideClickPoint(target?.rect, viewport);
1041
+ if (!point) {
1042
+ return {
1043
+ clicked: false,
1044
+ mode: "outside-modal-click",
1045
+ reason: "no_outside_click_point",
1046
+ selector: target?.selector || null,
1047
+ root: target?.root || null
1048
+ };
1049
+ }
1050
+ await clickPoint(client, point.x, point.y, DETERMINISTIC_CLICK_OPTIONS);
1051
+ return {
1052
+ clicked: true,
1053
+ mode: "outside-modal-click",
1054
+ selector: target?.selector || null,
1055
+ root: target?.root || null,
1056
+ side: point.side,
1057
+ x: Math.round(point.x),
1058
+ y: Math.round(point.y)
1059
+ };
1060
+ }
1061
+
1062
+ export async function extractRecommendDetailCandidate(client, {
1063
1063
  cardCandidate,
1064
1064
  cardNodeId,
1065
1065
  detailState,