@reconcrap/boss-recommend-mcp 2.0.5 → 2.0.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -8,6 +8,7 @@ import {
8
8
  querySelectorAll,
9
9
  sleep
10
10
  } from "../../core/browser/index.js";
11
+ import { candidateKeyFromProfile } from "../../core/infinite-list/index.js";
11
12
  import {
12
13
  buildScreeningCandidateFromDetail,
13
14
  htmlToText
@@ -22,6 +23,10 @@ import {
22
23
  getRecommendRoots,
23
24
  queryFirstAcrossRoots
24
25
  } from "./roots.js";
26
+ import {
27
+ findRecommendCardNodeIds,
28
+ readRecommendCardCandidate
29
+ } from "./cards.js";
25
30
 
26
31
  export function matchesRecommendDetailNetwork(url) {
27
32
  return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
@@ -146,14 +151,36 @@ export async function readRecommendDetailHtml(client, detailState) {
146
151
  let popupHTML = "";
147
152
  let resumeHTML = "";
148
153
  let resumeIframeDocumentNodeId = null;
154
+ const errors = [];
149
155
 
150
156
  if (detailState?.popup?.node_id) {
151
- popupHTML = await getOuterHTML(client, detailState.popup.node_id);
157
+ try {
158
+ popupHTML = await getOuterHTML(client, detailState.popup.node_id);
159
+ } catch (error) {
160
+ errors.push({
161
+ source: "popup",
162
+ node_id: detailState.popup.node_id,
163
+ stale_node: isStaleRecommendNodeError(error),
164
+ error: error?.message || String(error)
165
+ });
166
+ }
152
167
  }
153
168
 
154
169
  if (detailState?.resumeIframe?.node_id) {
155
- resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
156
- resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
170
+ try {
171
+ resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
172
+ resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
173
+ } catch (error) {
174
+ errors.push({
175
+ source: "resume_iframe",
176
+ node_id: detailState.resumeIframe.node_id,
177
+ document_node_id: resumeIframeDocumentNodeId,
178
+ stale_node: isStaleRecommendNodeError(error),
179
+ error: error?.message || String(error)
180
+ });
181
+ resumeIframeDocumentNodeId = null;
182
+ resumeHTML = "";
183
+ }
157
184
  }
158
185
 
159
186
  return {
@@ -161,7 +188,90 @@ export async function readRecommendDetailHtml(client, detailState) {
161
188
  resumeHTML,
162
189
  resumeIframeDocumentNodeId,
163
190
  popupText: htmlToText(popupHTML),
164
- resumeText: htmlToText(resumeHTML)
191
+ resumeText: htmlToText(resumeHTML),
192
+ errors
193
+ };
194
+ }
195
+
196
+ export function isStaleRecommendNodeError(error) {
197
+ const message = String(error?.message || error || "");
198
+ return /Could not find node with given id|No node with given id|Node is detached|Cannot find node/i.test(message);
199
+ }
200
+
201
+ export async function findRecommendCardNodeForCandidateKey(client, {
202
+ candidateKey = "",
203
+ rootState = null,
204
+ targetUrl = "",
205
+ source = "recommend-run-card-retry",
206
+ timeoutMs = 5000,
207
+ intervalMs = 250
208
+ } = {}) {
209
+ if (!candidateKey) {
210
+ return {
211
+ ok: false,
212
+ reason: "candidate_key_required"
213
+ };
214
+ }
215
+
216
+ const started = Date.now();
217
+ let lastError = null;
218
+ let lastCardCount = 0;
219
+ while (Date.now() - started <= timeoutMs) {
220
+ const currentRootState = rootState?.iframe?.documentNodeId
221
+ ? rootState
222
+ : await getRecommendRoots(client);
223
+ const frameNodeId = currentRootState?.iframe?.documentNodeId;
224
+ if (!frameNodeId) {
225
+ return {
226
+ ok: false,
227
+ reason: "recommend_frame_not_found"
228
+ };
229
+ }
230
+
231
+ const nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
232
+ lastCardCount = nodeIds.length;
233
+ for (let visibleIndex = 0; visibleIndex < nodeIds.length; visibleIndex += 1) {
234
+ const nodeId = nodeIds[visibleIndex];
235
+ try {
236
+ const candidate = await readRecommendCardCandidate(client, nodeId, {
237
+ targetUrl,
238
+ source,
239
+ metadata: {
240
+ visible_index: visibleIndex,
241
+ retry_reason: "stale_detail_node"
242
+ }
243
+ });
244
+ const key = candidateKeyFromProfile(candidate, {
245
+ nodeId,
246
+ visibleIndex,
247
+ attributes: candidate?.attributes || candidate?.metadata?.attributes || {}
248
+ });
249
+ if (key === candidateKey) {
250
+ return {
251
+ ok: true,
252
+ node_id: nodeId,
253
+ visible_index: visibleIndex,
254
+ candidate,
255
+ key,
256
+ root_state: currentRootState,
257
+ card_count: nodeIds.length
258
+ };
259
+ }
260
+ } catch (error) {
261
+ lastError = error;
262
+ }
263
+ }
264
+
265
+ if (intervalMs > 0) await sleep(intervalMs);
266
+ rootState = null;
267
+ }
268
+
269
+ return {
270
+ ok: false,
271
+ reason: "candidate_key_not_mounted",
272
+ candidate_key: candidateKey,
273
+ last_card_count: lastCardCount,
274
+ error: lastError?.message || null
165
275
  };
166
276
  }
167
277
 
@@ -181,6 +291,77 @@ export async function openRecommendCardDetail(client, cardNodeId, {
181
291
  };
182
292
  }
183
293
 
294
+ export async function openRecommendCardDetailWithFreshRetry(client, {
295
+ cardNodeId,
296
+ candidateKey = "",
297
+ cardCandidate = null,
298
+ rootState = null,
299
+ targetUrl = "",
300
+ timeoutMs = 12000,
301
+ scrollIntoView = true,
302
+ retryTimeoutMs = 5000,
303
+ retryIntervalMs = 250,
304
+ maxAttempts = 2
305
+ } = {}) {
306
+ let currentNodeId = cardNodeId;
307
+ let currentCandidate = cardCandidate;
308
+ let currentRootState = rootState;
309
+ const attempts = [];
310
+ const limit = Math.max(1, Number(maxAttempts) || 1);
311
+
312
+ for (let attemptIndex = 0; attemptIndex < limit; attemptIndex += 1) {
313
+ try {
314
+ const opened = await openRecommendCardDetail(client, currentNodeId, {
315
+ timeoutMs,
316
+ scrollIntoView
317
+ });
318
+ return {
319
+ ...opened,
320
+ card_node_id: currentNodeId,
321
+ card_candidate: currentCandidate,
322
+ retry_attempts: attempts
323
+ };
324
+ } catch (error) {
325
+ const stale = isStaleRecommendNodeError(error);
326
+ attempts.push({
327
+ attempt: attemptIndex + 1,
328
+ node_id: currentNodeId,
329
+ stale_node: stale,
330
+ error: error?.message || String(error)
331
+ });
332
+ if (!stale || attemptIndex >= limit - 1 || !candidateKey) {
333
+ error.recommend_detail_open_attempts = attempts;
334
+ throw error;
335
+ }
336
+
337
+ const resolved = await findRecommendCardNodeForCandidateKey(client, {
338
+ candidateKey,
339
+ rootState: currentRootState,
340
+ targetUrl,
341
+ timeoutMs: retryTimeoutMs,
342
+ intervalMs: retryIntervalMs
343
+ });
344
+ attempts[attempts.length - 1].refresh_lookup = {
345
+ ok: Boolean(resolved.ok),
346
+ node_id: resolved.node_id || null,
347
+ visible_index: resolved.visible_index ?? null,
348
+ card_count: resolved.card_count || resolved.last_card_count || 0,
349
+ reason: resolved.reason || null,
350
+ error: resolved.error || null
351
+ };
352
+ if (!resolved.ok || !resolved.node_id) {
353
+ error.recommend_detail_open_attempts = attempts;
354
+ throw error;
355
+ }
356
+ currentNodeId = resolved.node_id;
357
+ currentCandidate = resolved.candidate || currentCandidate;
358
+ currentRootState = resolved.root_state || null;
359
+ }
360
+ }
361
+
362
+ throw new Error("Recommend detail retry exhausted");
363
+ }
364
+
184
365
  export async function closeRecommendDetail(client, {
185
366
  attemptsLimit = 3
186
367
  } = {}) {
@@ -317,7 +498,8 @@ export async function extractRecommendDetailCandidate(client, {
317
498
  detail_popup_root: detailState?.popup?.root || null,
318
499
  resume_iframe_selector: detailState?.resumeIframe?.selector || null,
319
500
  resume_iframe_root: detailState?.resumeIframe?.root || null,
320
- resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId
501
+ resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId,
502
+ detail_html_errors: detailHtml.errors || []
321
503
  }
322
504
  });
323
505
 
@@ -334,7 +516,8 @@ export async function extractRecommendDetailCandidate(client, {
334
516
  popup_text: detailHtml.popupText,
335
517
  resume_text: detailHtml.resumeText,
336
518
  popup_html_length: detailHtml.popupHTML.length,
337
- resume_html_length: detailHtml.resumeHTML.length
519
+ resume_html_length: detailHtml.resumeHTML.length,
520
+ html_errors: detailHtml.errors || []
338
521
  },
339
522
  close_result: closeResult
340
523
  };
@@ -26,7 +26,7 @@ import {
26
26
  closeRecommendDetail,
27
27
  createRecommendDetailNetworkRecorder,
28
28
  extractRecommendDetailCandidate,
29
- openRecommendCardDetail,
29
+ openRecommendCardDetailWithFreshRetry,
30
30
  waitForRecommendDetailNetworkEvents
31
31
  } from "./detail.js";
32
32
  import {
@@ -596,9 +596,9 @@ export async function runRecommendWorkflow({
596
596
  }
597
597
 
598
598
  const index = results.length;
599
- const cardNodeId = nextCandidateResult.item.node_id;
599
+ let cardNodeId = nextCandidateResult.item.node_id;
600
600
  const candidateKey = nextCandidateResult.item.key;
601
- const cardCandidate = nextCandidateResult.item.candidate;
601
+ let cardCandidate = nextCandidateResult.item.candidate;
602
602
 
603
603
  let screeningCandidate = cardCandidate;
604
604
  let detailResult = null;
@@ -608,7 +608,17 @@ export async function runRecommendWorkflow({
608
608
  runControl.setPhase("recommend:detail");
609
609
  rootState = await ensureRecommendViewport(rootState, "detail");
610
610
  networkRecorder.clear();
611
- const openedDetail = await openRecommendCardDetail(client, cardNodeId);
611
+ const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
612
+ cardNodeId,
613
+ candidateKey,
614
+ cardCandidate,
615
+ rootState,
616
+ targetUrl,
617
+ maxAttempts: 2
618
+ });
619
+ cardNodeId = openedDetail.card_node_id || cardNodeId;
620
+ cardCandidate = openedDetail.card_candidate || cardCandidate;
621
+ screeningCandidate = cardCandidate;
612
622
  const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
613
623
  const networkWait = await waitForCvNetworkEvents(
614
624
  waitForRecommendDetailNetworkEvents,