@reconcrap/boss-recommend-mcp 2.0.50 → 2.0.52

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,26 @@ 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";
23
- import {
24
- getRecommendRoots
25
- } from "./roots.js";
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";
23
+ import {
24
+ getRecommendRoots
25
+ } from "./roots.js";
26
26
  import {
27
27
  findRecommendCardNodeIds,
28
28
  readRecommendCardCandidate
@@ -38,149 +38,149 @@ const DETAIL_OUTSIDE_CLOSE_BOUNDARY_SELECTORS = Object.freeze([
38
38
  ]);
39
39
 
40
40
  export function matchesRecommendDetailNetwork(url) {
41
- return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
42
- }
43
-
44
- 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;
135
- }
136
-
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;
149
- }
150
-
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
- };
161
- }
162
-
41
+ return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
42
+ }
43
+
44
+ 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;
135
+ }
136
+
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;
149
+ }
150
+
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
+ };
161
+ }
162
+
163
163
  export async function waitForRecommendDetailClosed(client, {
164
164
  timeoutMs = 4000,
165
165
  intervalMs = 250
166
166
  } = {}) {
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
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
184
184
  };
185
185
  }
186
186
 
@@ -242,74 +242,74 @@ async function verifyRecommendDetailStillOpen(client, {
242
242
  async function findVisibleDetailTarget(client, roots, selectors) {
243
243
  for (const root of roots) {
244
244
  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
-
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
+
313
313
  export function isStaleRecommendNodeError(error) {
314
314
  const message = String(error?.message || error || "");
315
315
  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 +321,138 @@ export function isRecommendDetailOpenMissError(error) {
321
321
  }
322
322
 
323
323
  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
- };
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
+ };
456
456
  } catch (error) {
457
457
  const stale = isStaleRecommendNodeError(error);
458
458
  const detailOpenMiss = isRecommendDetailOpenMissError(error);
@@ -467,88 +467,88 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
467
467
  error.recommend_detail_open_attempts = attempts;
468
468
  throw error;
469
469
  }
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 {
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 {
518
518
  if (closeTarget.center) {
519
519
  await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
520
520
  } else {
521
521
  await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
522
522
  }
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
- });
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
+ });
552
552
  if (closedAfterClick.closed) {
553
553
  return {
554
554
  closed: true,
@@ -578,22 +578,22 @@ export async function closeRecommendDetail(client, {
578
578
 
579
579
  await pressEscape(client);
580
580
  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
- }
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
+ }
597
597
  }
598
598
 
599
599
  const verification = await verifyRecommendDetailStillOpen(client);
@@ -621,37 +621,37 @@ export async function closeRecommendDetail(client, {
621
621
  verification
622
622
  };
623
623
  }
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
-
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
+
655
655
  async function pressEscape(client) {
656
656
  await pressKey(client, "Escape", {
657
657
  code: "Escape",
@@ -753,64 +753,64 @@ async function clickOutsideRecommendDetail(client, detailState) {
753
753
  }
754
754
 
755
755
  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
- }
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
+ }