@reconcrap/boss-recommend-mcp 2.0.53 → 2.0.55

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,26 +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";
6
+ getNodeBox,
7
+ getOuterHTML,
8
+ pressKey,
9
+ querySelectorAll,
10
+ sleep
11
+ } from "../../core/browser/index.js";
12
12
  import { candidateKeyFromProfile } from "../../core/infinite-list/index.js";
13
13
  import {
14
14
  buildScreeningCandidateFromDetail,
15
15
  htmlToText
16
16
  } from "../../core/screening/index.js";
17
17
  import {
18
- DETAIL_CLOSE_SELECTORS,
19
- DETAIL_NETWORK_PATTERNS,
20
- DETAIL_POPUP_SELECTORS,
21
- DETAIL_RESUME_IFRAME_SELECTORS
22
- } from "./constants.js";
23
- import {
24
- getRecommendRoots
25
- } from "./roots.js";
18
+ closeBossAccountRightsBlockingPanel,
19
+ findBossAccountRightsBlockingPanel
20
+ } from "../common/account-rights-panel.js";
21
+ import {
22
+ DETAIL_CLOSE_SELECTORS,
23
+ DETAIL_NETWORK_PATTERNS,
24
+ DETAIL_POPUP_SELECTORS,
25
+ DETAIL_RESUME_IFRAME_SELECTORS
26
+ } from "./constants.js";
27
+ import {
28
+ getRecommendRoots
29
+ } from "./roots.js";
26
30
  import {
27
31
  findRecommendCardNodeIds,
28
32
  readRecommendCardCandidate
@@ -38,149 +42,160 @@ const DETAIL_OUTSIDE_CLOSE_BOUNDARY_SELECTORS = Object.freeze([
38
42
  ]);
39
43
 
40
44
  export function matchesRecommendDetailNetwork(url) {
41
- return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
42
- }
43
-
45
+ return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
46
+ }
47
+
44
48
  export function createRecommendDetailNetworkRecorder(client) {
45
- const events = [];
46
- client.Network.responseReceived((event) => {
47
- const url = event?.response?.url || "";
48
- if (!matchesRecommendDetailNetwork(url)) return;
49
- events.push({
50
- requestId: event.requestId,
51
- url,
52
- status: event.response?.status,
53
- mimeType: event.response?.mimeType,
54
- type: event.type
55
- });
56
- });
57
- if (typeof client.Network.loadingFinished === "function") {
58
- client.Network.loadingFinished((event) => {
59
- const found = events.find((item) => item.requestId === event.requestId);
60
- if (!found) return;
61
- found.loading_finished = true;
62
- found.encodedDataLength = event.encodedDataLength;
63
- });
64
- }
65
- if (typeof client.Network.loadingFailed === "function") {
66
- client.Network.loadingFailed((event) => {
67
- const found = events.find((item) => item.requestId === event.requestId);
68
- if (!found) return;
69
- found.loading_failed = true;
70
- found.loading_error = event.errorText || event.blockedReason || "Network loading failed";
71
- });
72
- }
73
- return {
74
- events,
75
- clear() {
76
- events.length = 0;
77
- }
78
- };
79
- }
80
-
81
- export async function waitForRecommendDetailNetworkEvents(recorder, {
82
- minCount = 1,
83
- requireLoaded = true,
84
- timeoutMs = 3500,
85
- intervalMs = 100
86
- } = {}) {
87
- const started = Date.now();
88
- const events = Array.isArray(recorder) ? recorder : recorder?.events || [];
89
- let matching = [];
90
- while (Date.now() - started <= timeoutMs) {
91
- matching = events.filter((event) => (
92
- !requireLoaded
93
- || event.loading_finished === true
94
- || event.loading_failed === true
95
- ));
96
- if (matching.length >= minCount) {
97
- return {
98
- ok: true,
99
- elapsed_ms: Date.now() - started,
100
- count: matching.length,
101
- events: matching
102
- };
103
- }
104
- await sleep(intervalMs);
105
- }
106
- return {
107
- ok: false,
108
- elapsed_ms: Date.now() - started,
109
- count: matching.length,
110
- events: matching,
111
- total_event_count: events.length
112
- };
113
- }
114
-
115
- export async function readRecommendDetailNetworkBodies(client, events = [], {
116
- limit = 10
117
- } = {}) {
118
- const bodies = [];
119
- for (const event of events.slice(0, limit)) {
120
- try {
121
- const body = await client.Network.getResponseBody({ requestId: event.requestId });
122
- bodies.push({
123
- ...event,
124
- body,
125
- body_length: String(body?.body || "").length
126
- });
127
- } catch (error) {
128
- bodies.push({
129
- ...event,
130
- body_error: error?.message || String(error)
131
- });
132
- }
133
- }
134
- return bodies;
49
+ const events = [];
50
+ client.Network.responseReceived((event) => {
51
+ const url = event?.response?.url || "";
52
+ if (!matchesRecommendDetailNetwork(url)) return;
53
+ events.push({
54
+ requestId: event.requestId,
55
+ url,
56
+ status: event.response?.status,
57
+ mimeType: event.response?.mimeType,
58
+ type: event.type
59
+ });
60
+ });
61
+ if (typeof client.Network.loadingFinished === "function") {
62
+ client.Network.loadingFinished((event) => {
63
+ const found = events.find((item) => item.requestId === event.requestId);
64
+ if (!found) return;
65
+ found.loading_finished = true;
66
+ found.encodedDataLength = event.encodedDataLength;
67
+ });
68
+ }
69
+ if (typeof client.Network.loadingFailed === "function") {
70
+ client.Network.loadingFailed((event) => {
71
+ const found = events.find((item) => item.requestId === event.requestId);
72
+ if (!found) return;
73
+ found.loading_failed = true;
74
+ found.loading_error = event.errorText || event.blockedReason || "Network loading failed";
75
+ });
76
+ }
77
+ return {
78
+ events,
79
+ clear() {
80
+ events.length = 0;
81
+ }
82
+ };
135
83
  }
136
84
 
137
- export async function waitForRecommendDetail(client, {
138
- timeoutMs = 10000,
139
- intervalMs = 250
140
- } = {}) {
141
- const started = Date.now();
142
- let lastState = null;
143
- while (Date.now() - started <= timeoutMs) {
144
- lastState = await readRecommendDetailState(client);
145
- if (lastState?.popup || lastState?.resumeIframe) return lastState;
146
- await sleep(intervalMs);
147
- }
148
- return lastState;
85
+ export async function findRecommendBlockingPanel(client, options = {}) {
86
+ return findBossAccountRightsBlockingPanel(client, options);
149
87
  }
150
88
 
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);
155
- return {
156
- iframe: rootState.iframe,
157
- roots: rootState.roots,
158
- popup,
159
- resumeIframe
160
- };
89
+ export async function closeRecommendBlockingPanels(client, options = {}) {
90
+ return closeBossAccountRightsBlockingPanel(client, {
91
+ resolveRoots: getRecommendRoots,
92
+ ...options
93
+ });
161
94
  }
162
95
 
96
+ export async function waitForRecommendDetailNetworkEvents(recorder, {
97
+ minCount = 1,
98
+ requireLoaded = true,
99
+ timeoutMs = 3500,
100
+ intervalMs = 100
101
+ } = {}) {
102
+ const started = Date.now();
103
+ const events = Array.isArray(recorder) ? recorder : recorder?.events || [];
104
+ let matching = [];
105
+ while (Date.now() - started <= timeoutMs) {
106
+ matching = events.filter((event) => (
107
+ !requireLoaded
108
+ || event.loading_finished === true
109
+ || event.loading_failed === true
110
+ ));
111
+ if (matching.length >= minCount) {
112
+ return {
113
+ ok: true,
114
+ elapsed_ms: Date.now() - started,
115
+ count: matching.length,
116
+ events: matching
117
+ };
118
+ }
119
+ await sleep(intervalMs);
120
+ }
121
+ return {
122
+ ok: false,
123
+ elapsed_ms: Date.now() - started,
124
+ count: matching.length,
125
+ events: matching,
126
+ total_event_count: events.length
127
+ };
128
+ }
129
+
130
+ export async function readRecommendDetailNetworkBodies(client, events = [], {
131
+ limit = 10
132
+ } = {}) {
133
+ const bodies = [];
134
+ for (const event of events.slice(0, limit)) {
135
+ try {
136
+ const body = await client.Network.getResponseBody({ requestId: event.requestId });
137
+ bodies.push({
138
+ ...event,
139
+ body,
140
+ body_length: String(body?.body || "").length
141
+ });
142
+ } catch (error) {
143
+ bodies.push({
144
+ ...event,
145
+ body_error: error?.message || String(error)
146
+ });
147
+ }
148
+ }
149
+ return bodies;
150
+ }
151
+
152
+ export async function waitForRecommendDetail(client, {
153
+ timeoutMs = 10000,
154
+ intervalMs = 250
155
+ } = {}) {
156
+ const started = Date.now();
157
+ let lastState = null;
158
+ while (Date.now() - started <= timeoutMs) {
159
+ lastState = await readRecommendDetailState(client);
160
+ if (lastState?.popup || lastState?.resumeIframe) return lastState;
161
+ await sleep(intervalMs);
162
+ }
163
+ return lastState;
164
+ }
165
+
166
+ async function readRecommendDetailState(client) {
167
+ const rootState = await getRecommendRoots(client);
168
+ const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
169
+ const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
170
+ return {
171
+ iframe: rootState.iframe,
172
+ roots: rootState.roots,
173
+ popup,
174
+ resumeIframe
175
+ };
176
+ }
177
+
163
178
  export async function waitForRecommendDetailClosed(client, {
164
179
  timeoutMs = 4000,
165
180
  intervalMs = 250
166
181
  } = {}) {
167
- const started = Date.now();
168
- let lastState = null;
169
- while (Date.now() - started <= timeoutMs) {
170
- lastState = await readRecommendDetailState(client);
171
- if (!lastState?.popup && !lastState?.resumeIframe) {
172
- return {
173
- closed: true,
174
- elapsed_ms: Date.now() - started,
175
- state: lastState
176
- };
177
- }
178
- await sleep(intervalMs);
179
- }
180
- return {
181
- closed: false,
182
- elapsed_ms: Date.now() - started,
183
- state: lastState
182
+ const started = Date.now();
183
+ let lastState = null;
184
+ while (Date.now() - started <= timeoutMs) {
185
+ lastState = await readRecommendDetailState(client);
186
+ if (!lastState?.popup && !lastState?.resumeIframe) {
187
+ return {
188
+ closed: true,
189
+ elapsed_ms: Date.now() - started,
190
+ state: lastState
191
+ };
192
+ }
193
+ await sleep(intervalMs);
194
+ }
195
+ return {
196
+ closed: false,
197
+ elapsed_ms: Date.now() - started,
198
+ state: lastState
184
199
  };
185
200
  }
186
201
 
@@ -242,74 +257,74 @@ async function verifyRecommendDetailStillOpen(client, {
242
257
  async function findVisibleDetailTarget(client, roots, selectors) {
243
258
  for (const root of roots) {
244
259
  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,
254
- selector,
255
- node_id: nodeId,
256
- center: box.center,
257
- rect: box.rect
258
- };
259
- }
260
- } catch {}
261
- }
262
- }
263
- }
264
- return null;
265
- }
266
-
267
- export async function readRecommendDetailHtml(client, detailState) {
268
- let popupHTML = "";
269
- let resumeHTML = "";
270
- let resumeIframeDocumentNodeId = null;
271
- const errors = [];
272
-
273
- if (detailState?.popup?.node_id) {
274
- try {
275
- popupHTML = await getOuterHTML(client, detailState.popup.node_id);
276
- } catch (error) {
277
- errors.push({
278
- source: "popup",
279
- node_id: detailState.popup.node_id,
280
- stale_node: isStaleRecommendNodeError(error),
281
- error: error?.message || String(error)
282
- });
283
- }
284
- }
285
-
286
- if (detailState?.resumeIframe?.node_id) {
287
- try {
288
- resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
289
- resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
290
- } catch (error) {
291
- errors.push({
292
- source: "resume_iframe",
293
- node_id: detailState.resumeIframe.node_id,
294
- document_node_id: resumeIframeDocumentNodeId,
295
- stale_node: isStaleRecommendNodeError(error),
296
- error: error?.message || String(error)
297
- });
298
- resumeIframeDocumentNodeId = null;
299
- resumeHTML = "";
300
- }
301
- }
302
-
303
- return {
304
- popupHTML,
305
- resumeHTML,
306
- resumeIframeDocumentNodeId,
307
- popupText: htmlToText(popupHTML),
308
- resumeText: htmlToText(resumeHTML),
309
- errors
310
- };
311
- }
312
-
260
+ for (const selector of selectors) {
261
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
262
+ for (const nodeId of nodeIds) {
263
+ try {
264
+ const box = await getNodeBox(client, nodeId);
265
+ if (box.rect.width > 2 && box.rect.height > 2) {
266
+ return {
267
+ root: root.name,
268
+ root_node_id: root.nodeId,
269
+ selector,
270
+ node_id: nodeId,
271
+ center: box.center,
272
+ rect: box.rect
273
+ };
274
+ }
275
+ } catch {}
276
+ }
277
+ }
278
+ }
279
+ return null;
280
+ }
281
+
282
+ export async function readRecommendDetailHtml(client, detailState) {
283
+ let popupHTML = "";
284
+ let resumeHTML = "";
285
+ let resumeIframeDocumentNodeId = null;
286
+ const errors = [];
287
+
288
+ if (detailState?.popup?.node_id) {
289
+ try {
290
+ popupHTML = await getOuterHTML(client, detailState.popup.node_id);
291
+ } catch (error) {
292
+ errors.push({
293
+ source: "popup",
294
+ node_id: detailState.popup.node_id,
295
+ stale_node: isStaleRecommendNodeError(error),
296
+ error: error?.message || String(error)
297
+ });
298
+ }
299
+ }
300
+
301
+ if (detailState?.resumeIframe?.node_id) {
302
+ try {
303
+ resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
304
+ resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
305
+ } catch (error) {
306
+ errors.push({
307
+ source: "resume_iframe",
308
+ node_id: detailState.resumeIframe.node_id,
309
+ document_node_id: resumeIframeDocumentNodeId,
310
+ stale_node: isStaleRecommendNodeError(error),
311
+ error: error?.message || String(error)
312
+ });
313
+ resumeIframeDocumentNodeId = null;
314
+ resumeHTML = "";
315
+ }
316
+ }
317
+
318
+ return {
319
+ popupHTML,
320
+ resumeHTML,
321
+ resumeIframeDocumentNodeId,
322
+ popupText: htmlToText(popupHTML),
323
+ resumeText: htmlToText(resumeHTML),
324
+ errors
325
+ };
326
+ }
327
+
313
328
  export function isStaleRecommendNodeError(error) {
314
329
  const message = String(error?.message || error || "");
315
330
  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);
@@ -321,138 +336,138 @@ export function isRecommendDetailOpenMissError(error) {
321
336
  }
322
337
 
323
338
  export async function findRecommendCardNodeForCandidateKey(client, {
324
- candidateKey = "",
325
- rootState = null,
326
- targetUrl = "",
327
- source = "recommend-run-card-retry",
328
- timeoutMs = 5000,
329
- intervalMs = 250
330
- } = {}) {
331
- if (!candidateKey) {
332
- return {
333
- ok: false,
334
- reason: "candidate_key_required"
335
- };
336
- }
337
-
338
- const started = Date.now();
339
- let lastError = null;
340
- let lastCardCount = 0;
341
- while (Date.now() - started <= timeoutMs) {
342
- const currentRootState = rootState?.iframe?.documentNodeId
343
- ? rootState
344
- : await getRecommendRoots(client);
345
- const frameNodeId = currentRootState?.iframe?.documentNodeId;
346
- if (!frameNodeId) {
347
- return {
348
- ok: false,
349
- reason: "recommend_frame_not_found"
350
- };
351
- }
352
-
353
- const nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
354
- lastCardCount = nodeIds.length;
355
- for (let visibleIndex = 0; visibleIndex < nodeIds.length; visibleIndex += 1) {
356
- const nodeId = nodeIds[visibleIndex];
357
- try {
358
- const candidate = await readRecommendCardCandidate(client, nodeId, {
359
- targetUrl,
360
- source,
361
- metadata: {
362
- visible_index: visibleIndex,
363
- retry_reason: "stale_detail_node"
364
- }
365
- });
366
- const key = candidateKeyFromProfile(candidate, {
367
- nodeId,
368
- visibleIndex,
369
- attributes: candidate?.attributes || candidate?.metadata?.attributes || {}
370
- });
371
- if (key === candidateKey) {
372
- return {
373
- ok: true,
374
- node_id: nodeId,
375
- visible_index: visibleIndex,
376
- candidate,
377
- key,
378
- root_state: currentRootState,
379
- card_count: nodeIds.length
380
- };
381
- }
382
- } catch (error) {
383
- lastError = error;
384
- }
385
- }
386
-
387
- if (intervalMs > 0) await sleep(intervalMs);
388
- rootState = null;
389
- }
390
-
391
- return {
392
- ok: false,
393
- reason: "candidate_key_not_mounted",
394
- candidate_key: candidateKey,
395
- last_card_count: lastCardCount,
396
- error: lastError?.message || null
397
- };
398
- }
399
-
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
- }
425
-
426
- export async function openRecommendCardDetailWithFreshRetry(client, {
427
- cardNodeId,
428
- candidateKey = "",
429
- cardCandidate = null,
430
- rootState = null,
431
- targetUrl = "",
432
- timeoutMs = 12000,
433
- scrollIntoView = true,
434
- retryTimeoutMs = 5000,
435
- retryIntervalMs = 250,
436
- maxAttempts = 2
437
- } = {}) {
438
- let currentNodeId = cardNodeId;
439
- let currentCandidate = cardCandidate;
440
- let currentRootState = rootState;
441
- const attempts = [];
442
- const limit = Math.max(1, Number(maxAttempts) || 1);
443
-
444
- for (let attemptIndex = 0; attemptIndex < limit; attemptIndex += 1) {
445
- try {
446
- const opened = await openRecommendCardDetail(client, currentNodeId, {
447
- timeoutMs,
448
- scrollIntoView
449
- });
450
- return {
451
- ...opened,
452
- card_node_id: currentNodeId,
453
- card_candidate: currentCandidate,
454
- retry_attempts: attempts
455
- };
339
+ candidateKey = "",
340
+ rootState = null,
341
+ targetUrl = "",
342
+ source = "recommend-run-card-retry",
343
+ timeoutMs = 5000,
344
+ intervalMs = 250
345
+ } = {}) {
346
+ if (!candidateKey) {
347
+ return {
348
+ ok: false,
349
+ reason: "candidate_key_required"
350
+ };
351
+ }
352
+
353
+ const started = Date.now();
354
+ let lastError = null;
355
+ let lastCardCount = 0;
356
+ while (Date.now() - started <= timeoutMs) {
357
+ const currentRootState = rootState?.iframe?.documentNodeId
358
+ ? rootState
359
+ : await getRecommendRoots(client);
360
+ const frameNodeId = currentRootState?.iframe?.documentNodeId;
361
+ if (!frameNodeId) {
362
+ return {
363
+ ok: false,
364
+ reason: "recommend_frame_not_found"
365
+ };
366
+ }
367
+
368
+ const nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
369
+ lastCardCount = nodeIds.length;
370
+ for (let visibleIndex = 0; visibleIndex < nodeIds.length; visibleIndex += 1) {
371
+ const nodeId = nodeIds[visibleIndex];
372
+ try {
373
+ const candidate = await readRecommendCardCandidate(client, nodeId, {
374
+ targetUrl,
375
+ source,
376
+ metadata: {
377
+ visible_index: visibleIndex,
378
+ retry_reason: "stale_detail_node"
379
+ }
380
+ });
381
+ const key = candidateKeyFromProfile(candidate, {
382
+ nodeId,
383
+ visibleIndex,
384
+ attributes: candidate?.attributes || candidate?.metadata?.attributes || {}
385
+ });
386
+ if (key === candidateKey) {
387
+ return {
388
+ ok: true,
389
+ node_id: nodeId,
390
+ visible_index: visibleIndex,
391
+ candidate,
392
+ key,
393
+ root_state: currentRootState,
394
+ card_count: nodeIds.length
395
+ };
396
+ }
397
+ } catch (error) {
398
+ lastError = error;
399
+ }
400
+ }
401
+
402
+ if (intervalMs > 0) await sleep(intervalMs);
403
+ rootState = null;
404
+ }
405
+
406
+ return {
407
+ ok: false,
408
+ reason: "candidate_key_not_mounted",
409
+ candidate_key: candidateKey,
410
+ last_card_count: lastCardCount,
411
+ error: lastError?.message || null
412
+ };
413
+ }
414
+
415
+ export async function openRecommendCardDetail(client, cardNodeId, {
416
+ timeoutMs = 12000,
417
+ scrollIntoView = true
418
+ } = {}) {
419
+ const started = Date.now();
420
+ const clickStarted = Date.now();
421
+ const cardBox = await clickNodeCenter(client, cardNodeId, { scrollIntoView });
422
+ const candidateClickMs = Date.now() - clickStarted;
423
+ const detailStarted = Date.now();
424
+ const detailState = await waitForRecommendDetail(client, { timeoutMs });
425
+ const detailOpenMs = Date.now() - detailStarted;
426
+ if (!detailState?.popup && !detailState?.resumeIframe) {
427
+ throw new Error("Candidate detail did not open or no known detail selectors mounted");
428
+ }
429
+
430
+ return {
431
+ card_box: cardBox,
432
+ detail_state: detailState,
433
+ timings: {
434
+ candidate_click_ms: candidateClickMs,
435
+ detail_open_ms: detailOpenMs,
436
+ open_total_ms: Date.now() - started
437
+ }
438
+ };
439
+ }
440
+
441
+ export async function openRecommendCardDetailWithFreshRetry(client, {
442
+ cardNodeId,
443
+ candidateKey = "",
444
+ cardCandidate = null,
445
+ rootState = null,
446
+ targetUrl = "",
447
+ timeoutMs = 12000,
448
+ scrollIntoView = true,
449
+ retryTimeoutMs = 5000,
450
+ retryIntervalMs = 250,
451
+ maxAttempts = 2
452
+ } = {}) {
453
+ let currentNodeId = cardNodeId;
454
+ let currentCandidate = cardCandidate;
455
+ let currentRootState = rootState;
456
+ const attempts = [];
457
+ const limit = Math.max(1, Number(maxAttempts) || 1);
458
+
459
+ for (let attemptIndex = 0; attemptIndex < limit; attemptIndex += 1) {
460
+ try {
461
+ const opened = await openRecommendCardDetail(client, currentNodeId, {
462
+ timeoutMs,
463
+ scrollIntoView
464
+ });
465
+ return {
466
+ ...opened,
467
+ card_node_id: currentNodeId,
468
+ card_candidate: currentCandidate,
469
+ retry_attempts: attempts
470
+ };
456
471
  } catch (error) {
457
472
  const stale = isStaleRecommendNodeError(error);
458
473
  const detailOpenMiss = isRecommendDetailOpenMissError(error);
@@ -467,88 +482,88 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
467
482
  error.recommend_detail_open_attempts = attempts;
468
483
  throw error;
469
484
  }
470
-
471
- const resolved = await findRecommendCardNodeForCandidateKey(client, {
472
- candidateKey,
473
- rootState: currentRootState,
474
- targetUrl,
475
- timeoutMs: retryTimeoutMs,
476
- intervalMs: retryIntervalMs
477
- });
478
- attempts[attempts.length - 1].refresh_lookup = {
479
- ok: Boolean(resolved.ok),
480
- node_id: resolved.node_id || null,
481
- visible_index: resolved.visible_index ?? null,
482
- card_count: resolved.card_count || resolved.last_card_count || 0,
483
- reason: resolved.reason || null,
484
- error: resolved.error || null
485
- };
486
- if (!resolved.ok || !resolved.node_id) {
487
- error.recommend_detail_open_attempts = attempts;
488
- throw error;
489
- }
490
- currentNodeId = resolved.node_id;
491
- currentCandidate = resolved.candidate || currentCandidate;
492
- currentRootState = resolved.root_state || null;
493
- }
494
- }
495
-
496
- throw new Error("Recommend detail retry exhausted");
497
- }
498
-
499
- export async function closeRecommendDetail(client, {
500
- attemptsLimit = 4,
501
- closeWaitMs = 5000,
502
- escapeWaitMs = 3500
503
- } = {}) {
504
- const attempts = [];
505
- for (let index = 0; index < attemptsLimit; index += 1) {
506
- const existingState = await waitForRecommendDetail(client, { timeoutMs: 500 });
507
- if (!existingState?.popup && !existingState?.resumeIframe) {
508
- return {
509
- closed: true,
510
- attempts
511
- };
512
- }
513
-
514
- const rootState = await getRecommendRoots(client);
515
- const closeTarget = await findVisibleCloseTarget(client, rootState.roots, DETAIL_CLOSE_SELECTORS);
516
- if (closeTarget) {
517
- try {
485
+
486
+ const resolved = await findRecommendCardNodeForCandidateKey(client, {
487
+ candidateKey,
488
+ rootState: currentRootState,
489
+ targetUrl,
490
+ timeoutMs: retryTimeoutMs,
491
+ intervalMs: retryIntervalMs
492
+ });
493
+ attempts[attempts.length - 1].refresh_lookup = {
494
+ ok: Boolean(resolved.ok),
495
+ node_id: resolved.node_id || null,
496
+ visible_index: resolved.visible_index ?? null,
497
+ card_count: resolved.card_count || resolved.last_card_count || 0,
498
+ reason: resolved.reason || null,
499
+ error: resolved.error || null
500
+ };
501
+ if (!resolved.ok || !resolved.node_id) {
502
+ error.recommend_detail_open_attempts = attempts;
503
+ throw error;
504
+ }
505
+ currentNodeId = resolved.node_id;
506
+ currentCandidate = resolved.candidate || currentCandidate;
507
+ currentRootState = resolved.root_state || null;
508
+ }
509
+ }
510
+
511
+ throw new Error("Recommend detail retry exhausted");
512
+ }
513
+
514
+ export async function closeRecommendDetail(client, {
515
+ attemptsLimit = 4,
516
+ closeWaitMs = 5000,
517
+ escapeWaitMs = 3500
518
+ } = {}) {
519
+ const attempts = [];
520
+ for (let index = 0; index < attemptsLimit; index += 1) {
521
+ const existingState = await waitForRecommendDetail(client, { timeoutMs: 500 });
522
+ if (!existingState?.popup && !existingState?.resumeIframe) {
523
+ return {
524
+ closed: true,
525
+ attempts
526
+ };
527
+ }
528
+
529
+ const rootState = await getRecommendRoots(client);
530
+ const closeTarget = await findVisibleCloseTarget(client, rootState.roots, DETAIL_CLOSE_SELECTORS);
531
+ if (closeTarget) {
532
+ try {
518
533
  if (closeTarget.center) {
519
534
  await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
520
535
  } else {
521
536
  await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
522
537
  }
523
- attempts.push({
524
- mode: "close-selector",
525
- selector: closeTarget.selector,
526
- root: closeTarget.root
527
- });
528
- } catch (error) {
529
- attempts.push({
530
- mode: "close-selector-error",
531
- selector: closeTarget.selector,
532
- root: closeTarget.root,
533
- error: error?.message || String(error)
534
- });
535
- await pressEscape(client);
536
- attempts.push({ mode: "Escape-after-close-selector-error" });
537
- }
538
- } else {
539
- await pressEscape(client);
540
- attempts.push({ mode: "Escape" });
541
- }
542
-
543
- const closedAfterClick = await waitForRecommendDetailClosed(client, {
544
- timeoutMs: closeWaitMs,
545
- intervalMs: 250
546
- });
547
- attempts.push({
548
- mode: "wait-closed-after-primary",
549
- closed: closedAfterClick.closed,
550
- elapsed_ms: closedAfterClick.elapsed_ms
551
- });
538
+ attempts.push({
539
+ mode: "close-selector",
540
+ selector: closeTarget.selector,
541
+ root: closeTarget.root
542
+ });
543
+ } catch (error) {
544
+ attempts.push({
545
+ mode: "close-selector-error",
546
+ selector: closeTarget.selector,
547
+ root: closeTarget.root,
548
+ error: error?.message || String(error)
549
+ });
550
+ await pressEscape(client);
551
+ attempts.push({ mode: "Escape-after-close-selector-error" });
552
+ }
553
+ } else {
554
+ await pressEscape(client);
555
+ attempts.push({ mode: "Escape" });
556
+ }
557
+
558
+ const closedAfterClick = await waitForRecommendDetailClosed(client, {
559
+ timeoutMs: closeWaitMs,
560
+ intervalMs: 250
561
+ });
562
+ attempts.push({
563
+ mode: "wait-closed-after-primary",
564
+ closed: closedAfterClick.closed,
565
+ elapsed_ms: closedAfterClick.elapsed_ms
566
+ });
552
567
  if (closedAfterClick.closed) {
553
568
  return {
554
569
  closed: true,
@@ -578,22 +593,22 @@ export async function closeRecommendDetail(client, {
578
593
 
579
594
  await pressEscape(client);
580
595
  attempts.push({ mode: "Escape-fallback" });
581
-
582
- const closedAfterEscape = await waitForRecommendDetailClosed(client, {
583
- timeoutMs: escapeWaitMs,
584
- intervalMs: 250
585
- });
586
- attempts.push({
587
- mode: "wait-closed-after-escape",
588
- closed: closedAfterEscape.closed,
589
- elapsed_ms: closedAfterEscape.elapsed_ms
590
- });
591
- if (closedAfterEscape.closed) {
592
- return {
593
- closed: true,
594
- attempts
595
- };
596
- }
596
+
597
+ const closedAfterEscape = await waitForRecommendDetailClosed(client, {
598
+ timeoutMs: escapeWaitMs,
599
+ intervalMs: 250
600
+ });
601
+ attempts.push({
602
+ mode: "wait-closed-after-escape",
603
+ closed: closedAfterEscape.closed,
604
+ elapsed_ms: closedAfterEscape.elapsed_ms
605
+ });
606
+ if (closedAfterEscape.closed) {
607
+ return {
608
+ closed: true,
609
+ attempts
610
+ };
611
+ }
597
612
  }
598
613
 
599
614
  const verification = await verifyRecommendDetailStillOpen(client);
@@ -621,37 +636,37 @@ export async function closeRecommendDetail(client, {
621
636
  verification
622
637
  };
623
638
  }
624
-
625
- async function findVisibleCloseTarget(client, roots, selectors) {
626
- let fallback = null;
627
- for (const root of roots) {
628
- if (!root?.nodeId) continue;
629
- for (const selector of selectors) {
630
- const nodeIds = await querySelectorAll(client, root.nodeId, selector);
631
- for (const nodeId of nodeIds) {
632
- const target = {
633
- root: root.name,
634
- root_node_id: root.nodeId,
635
- selector,
636
- node_id: nodeId
637
- };
638
- if (!fallback) fallback = target;
639
- try {
640
- const box = await getNodeBox(client, nodeId);
641
- if (box.rect.width > 2 && box.rect.height > 2) {
642
- return {
643
- ...target,
644
- center: box.center,
645
- rect: box.rect
646
- };
647
- }
648
- } catch {}
649
- }
650
- }
651
- }
652
- return fallback;
653
- }
654
-
639
+
640
+ async function findVisibleCloseTarget(client, roots, selectors) {
641
+ let fallback = null;
642
+ for (const root of roots) {
643
+ if (!root?.nodeId) continue;
644
+ for (const selector of selectors) {
645
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
646
+ for (const nodeId of nodeIds) {
647
+ const target = {
648
+ root: root.name,
649
+ root_node_id: root.nodeId,
650
+ selector,
651
+ node_id: nodeId
652
+ };
653
+ if (!fallback) fallback = target;
654
+ try {
655
+ const box = await getNodeBox(client, nodeId);
656
+ if (box.rect.width > 2 && box.rect.height > 2) {
657
+ return {
658
+ ...target,
659
+ center: box.center,
660
+ rect: box.rect
661
+ };
662
+ }
663
+ } catch {}
664
+ }
665
+ }
666
+ }
667
+ return fallback;
668
+ }
669
+
655
670
  async function pressEscape(client) {
656
671
  await pressKey(client, "Escape", {
657
672
  code: "Escape",
@@ -753,64 +768,64 @@ async function clickOutsideRecommendDetail(client, detailState) {
753
768
  }
754
769
 
755
770
  export async function extractRecommendDetailCandidate(client, {
756
- cardCandidate,
757
- cardNodeId,
758
- detailState,
759
- networkEvents = [],
760
- targetUrl = "",
761
- closeDetail = true,
762
- networkParseRetryMs = 1800,
763
- networkParseIntervalMs = 250
764
- } = {}) {
765
- const detailHtml = await readRecommendDetailHtml(client, detailState);
766
- const detailText = [
767
- detailHtml.popupText,
768
- detailHtml.resumeText
769
- ].filter(Boolean).join("\n\n");
770
-
771
- const parseStarted = Date.now();
772
- let networkBodies = [];
773
- let detailCandidateResult = null;
774
- do {
775
- networkBodies = await readRecommendDetailNetworkBodies(client, networkEvents);
776
- detailCandidateResult = buildScreeningCandidateFromDetail({
777
- cardCandidate,
778
- detailText,
779
- networkBodies,
780
- metadata: {
781
- target_url: targetUrl,
782
- card_node_id: cardNodeId,
783
- detail_popup_selector: detailState?.popup?.selector || null,
784
- detail_popup_root: detailState?.popup?.root || null,
785
- resume_iframe_selector: detailState?.resumeIframe?.selector || null,
786
- resume_iframe_root: detailState?.resumeIframe?.root || null,
787
- resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId,
788
- detail_html_errors: detailHtml.errors || []
789
- }
790
- });
791
- if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
792
- if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
793
- await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
794
- } while (true);
795
-
796
- let closeResult = null;
797
- if (closeDetail) {
798
- closeResult = await closeRecommendDetail(client);
799
- }
800
-
801
- return {
802
- candidate: detailCandidateResult.candidate,
803
- parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
804
- network_bodies: networkBodies,
805
- network_parse_retry_elapsed_ms: Date.now() - parseStarted,
806
- network_event_count: networkEvents.length,
807
- detail: {
808
- popup_text: detailHtml.popupText,
809
- resume_text: detailHtml.resumeText,
810
- popup_html_length: detailHtml.popupHTML.length,
811
- resume_html_length: detailHtml.resumeHTML.length,
812
- html_errors: detailHtml.errors || []
813
- },
814
- close_result: closeResult
815
- };
816
- }
771
+ cardCandidate,
772
+ cardNodeId,
773
+ detailState,
774
+ networkEvents = [],
775
+ targetUrl = "",
776
+ closeDetail = true,
777
+ networkParseRetryMs = 1800,
778
+ networkParseIntervalMs = 250
779
+ } = {}) {
780
+ const detailHtml = await readRecommendDetailHtml(client, detailState);
781
+ const detailText = [
782
+ detailHtml.popupText,
783
+ detailHtml.resumeText
784
+ ].filter(Boolean).join("\n\n");
785
+
786
+ const parseStarted = Date.now();
787
+ let networkBodies = [];
788
+ let detailCandidateResult = null;
789
+ do {
790
+ networkBodies = await readRecommendDetailNetworkBodies(client, networkEvents);
791
+ detailCandidateResult = buildScreeningCandidateFromDetail({
792
+ cardCandidate,
793
+ detailText,
794
+ networkBodies,
795
+ metadata: {
796
+ target_url: targetUrl,
797
+ card_node_id: cardNodeId,
798
+ detail_popup_selector: detailState?.popup?.selector || null,
799
+ detail_popup_root: detailState?.popup?.root || null,
800
+ resume_iframe_selector: detailState?.resumeIframe?.selector || null,
801
+ resume_iframe_root: detailState?.resumeIframe?.root || null,
802
+ resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId,
803
+ detail_html_errors: detailHtml.errors || []
804
+ }
805
+ });
806
+ if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
807
+ if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
808
+ await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
809
+ } while (true);
810
+
811
+ let closeResult = null;
812
+ if (closeDetail) {
813
+ closeResult = await closeRecommendDetail(client);
814
+ }
815
+
816
+ return {
817
+ candidate: detailCandidateResult.candidate,
818
+ parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
819
+ network_bodies: networkBodies,
820
+ network_parse_retry_elapsed_ms: Date.now() - parseStarted,
821
+ network_event_count: networkEvents.length,
822
+ detail: {
823
+ popup_text: detailHtml.popupText,
824
+ resume_text: detailHtml.resumeText,
825
+ popup_html_length: detailHtml.popupHTML.length,
826
+ resume_html_length: detailHtml.resumeHTML.length,
827
+ html_errors: detailHtml.errors || []
828
+ },
829
+ close_result: closeResult
830
+ };
831
+ }