@reconcrap/boss-recommend-mcp 2.0.33 → 2.0.35

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.33",
3
+ "version": "2.0.35",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  getMainFrameUrl,
3
+ sleep,
3
4
  waitForMainFrameUrl
4
5
  } from "../../core/browser/index.js";
5
6
  import { CHAT_TARGET_URL } from "./constants.js";
@@ -60,19 +61,25 @@ export async function assertChatShellNotResumeTopLevel(client, {
60
61
  export async function recoverChatShell(client, {
61
62
  targetUrl = CHAT_TARGET_URL,
62
63
  timeoutMs = 60000,
63
- intervalMs = 500
64
+ intervalMs = 500,
65
+ forceNavigate = false,
66
+ settleMs = 1200
64
67
  } = {}) {
65
68
  const before = await getChatTopLevelState(client);
66
- if (before.is_chat_shell) {
69
+ if (before.is_chat_shell && !forceNavigate) {
67
70
  return {
68
71
  recovered: false,
69
72
  before,
70
73
  after: before,
71
- navigate_url: null
74
+ navigate_url: null,
75
+ force_navigate: false
72
76
  };
73
77
  }
74
78
 
75
- await client.Page.navigate({ url: targetUrl });
79
+ const navigateResult = await client.Page.navigate({ url: targetUrl });
80
+ if (forceNavigate && settleMs > 0) {
81
+ await sleep(settleMs);
82
+ }
76
83
  const waited = await waitForMainFrameUrl(client, isChatShellUrl, {
77
84
  timeoutMs,
78
85
  intervalMs
@@ -80,9 +87,12 @@ export async function recoverChatShell(client, {
80
87
  const after = await getChatTopLevelState(client);
81
88
  return {
82
89
  recovered: waited.ok && after.is_chat_shell,
90
+ refreshed: Boolean(forceNavigate && before.is_chat_shell && after.is_chat_shell),
83
91
  before,
84
92
  after,
85
93
  wait: waited,
86
- navigate_url: targetUrl
94
+ navigate_result: navigateResult || null,
95
+ navigate_url: targetUrl,
96
+ force_navigate: Boolean(forceNavigate)
87
97
  };
88
98
  }
@@ -161,6 +161,17 @@ function resultOpenedDetail(result) {
161
161
  return Boolean(result?.detail && !result.detail?.cv_acquisition?.skipped);
162
162
  }
163
163
 
164
+ export function countChatResultStatuses(results = []) {
165
+ return {
166
+ processed: results.length,
167
+ screened: results.length,
168
+ detail_opened: results.filter(resultOpenedDetail).length,
169
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
170
+ passed: results.filter((item) => item.screening?.passed).length,
171
+ skipped: results.filter((item) => item.screening?.status === "skip").length
172
+ };
173
+ }
174
+
164
175
  export function chatDetailSkipReasonFromReadyState(state = {}) {
165
176
  if (state?.attachment_resume_enabled) return "attachment_resume_already_available";
166
177
  return "";
@@ -173,6 +184,11 @@ export function makeChatResumeModalOpenBeforeCandidateClickError(closeResult = n
173
184
  return error;
174
185
  }
175
186
 
187
+ export function isChatResumeModalCloseFailureError(error) {
188
+ return error?.code === "CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK"
189
+ || /CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK/i.test(String(error?.message || error || ""));
190
+ }
191
+
176
192
  export async function ensureNoOpenChatResumeModalBeforeCandidateClick(client, {
177
193
  closeAttempts = 3
178
194
  } = {}) {
@@ -319,12 +335,24 @@ function createSkippedDetailResult(cardCandidate, reason, error = null) {
319
335
  cv_acquisition: {
320
336
  source: reason,
321
337
  skipped: true,
322
- error: error?.message || null
338
+ error: error?.message || null,
339
+ error_code: error?.code || null
323
340
  },
324
341
  close_result: null
325
342
  };
326
343
  }
327
344
 
345
+ function compactChatRuntimeError(error) {
346
+ if (!error) return null;
347
+ return {
348
+ name: error.name || "Error",
349
+ code: error.code || null,
350
+ message: error.message || String(error),
351
+ close_result: error.close_result || null,
352
+ page_state: error.page_state || null
353
+ };
354
+ }
355
+
328
356
  const CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH = 500;
329
357
  const CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH = 180;
330
358
  const CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH = 650;
@@ -745,21 +773,25 @@ export async function runChatWorkflow({
745
773
  chat_context: contextSetup
746
774
  });
747
775
 
748
- async function recoverAndReapplyChatContext(reason, error = null) {
776
+ async function recoverAndReapplyChatContext(reason, error = null, {
777
+ forceRefresh = false
778
+ } = {}) {
749
779
  runControl.setPhase("chat:recover_shell");
750
- const recovery = await recoverChatShell(client, {
780
+ const shellRecovery = await recoverChatShell(client, {
751
781
  targetUrl,
752
- timeoutMs: readyTimeoutMs
782
+ timeoutMs: readyTimeoutMs,
783
+ forceNavigate: forceRefresh
753
784
  });
754
785
  runControl.checkpoint({
755
786
  chat_shell_recovery: {
756
787
  reason,
757
788
  error: error?.message || null,
758
- ...recovery
789
+ total_refresh: Boolean(forceRefresh),
790
+ ...shellRecovery
759
791
  }
760
792
  });
761
- if (!recovery.recovered && !recovery.after?.is_chat_shell) {
762
- throw new Error(`Chat shell recovery failed after ${reason}: ${recovery.after?.url || recovery.before?.url || "unknown"}`);
793
+ if (!shellRecovery.recovered && !shellRecovery.after?.is_chat_shell) {
794
+ throw new Error(`Chat shell recovery failed after ${reason}: ${shellRecovery.after?.url || shellRecovery.before?.url || "unknown"}`);
763
795
  }
764
796
  await closeChatResumeModal(client, { attemptsLimit: 2 });
765
797
  const recoveredSetup = await setupChatRunContext(client, {
@@ -771,6 +803,24 @@ export async function runChatWorkflow({
771
803
  ensureViewport: ensureChatViewport
772
804
  });
773
805
  rootState = recoveredSetup.rootState;
806
+ const counters = countChatResultStatuses(results);
807
+ const candidateList = resetInfiniteListForRefreshRound(listState, {
808
+ reason,
809
+ round: listState.ledger?.length || 0,
810
+ method: forceRefresh ? "total_refresh_reapply_chat_context" : "reapply_chat_context",
811
+ metadata: {
812
+ processed: counters.processed,
813
+ passed: counters.passed,
814
+ skipped: counters.skipped
815
+ }
816
+ });
817
+ const recovery = {
818
+ reason,
819
+ total_refresh: Boolean(forceRefresh),
820
+ shell: shellRecovery,
821
+ candidate_list: candidateList,
822
+ counters
823
+ };
774
824
  contextSetup = {
775
825
  ...recoveredSetup.contextSetup,
776
826
  recovered_from: reason,
@@ -778,7 +828,8 @@ export async function runChatWorkflow({
778
828
  previous_context: contextSetup
779
829
  };
780
830
  runControl.checkpoint({
781
- chat_context: contextSetup
831
+ chat_context: contextSetup,
832
+ candidate_list: candidateList
782
833
  });
783
834
  return recovery;
784
835
  }
@@ -811,6 +862,7 @@ export async function runChatWorkflow({
811
862
  detail_opened: 0,
812
863
  llm_screened: 0,
813
864
  passed: 0,
865
+ skipped: 0,
814
866
  requested: 0,
815
867
  request_satisfied: 0,
816
868
  request_skipped: 0,
@@ -846,6 +898,7 @@ export async function runChatWorkflow({
846
898
  detail_opened: 0,
847
899
  llm_screened: 0,
848
900
  passed: 0,
901
+ skipped: 0,
849
902
  requested: requestedCount,
850
903
  request_satisfied: requestSatisfiedCount,
851
904
  request_skipped: requestSkippedCount,
@@ -863,6 +916,7 @@ export async function runChatWorkflow({
863
916
  detail_opened: 0,
864
917
  llm_screened: 0,
865
918
  passed: 0,
919
+ skipped: 0,
866
920
  requested: 0,
867
921
  request_satisfied: 0,
868
922
  request_skipped: 0,
@@ -1000,11 +1054,23 @@ export async function runChatWorkflow({
1000
1054
  let detailUnavailableReason = "";
1001
1055
  if (index < detailCountLimit) {
1002
1056
  let detailStep = "start";
1057
+ const checkpointInProgressCandidate = (patch = {}) => runControl.checkpoint({
1058
+ in_progress_candidate: {
1059
+ index,
1060
+ key: candidateKey,
1061
+ card_node_id: effectiveCardNodeId || cardNodeId,
1062
+ candidate: compactCandidate(cardCandidate),
1063
+ detail_step: detailStep,
1064
+ counters: countChatResultStatuses(results),
1065
+ ...patch
1066
+ }
1067
+ });
1003
1068
  try {
1004
1069
  await runControl.waitIfPaused();
1005
1070
  runControl.throwIfCanceled();
1006
1071
  runControl.setPhase("chat:detail");
1007
1072
  rootState = await ensureChatViewport(rootState, "detail");
1073
+ checkpointInProgressCandidate({ event: "detail_start" });
1008
1074
 
1009
1075
  detailStep = "select_candidate";
1010
1076
  networkRecorder.clear();
@@ -1402,11 +1468,23 @@ export async function runChatWorkflow({
1402
1468
  }
1403
1469
 
1404
1470
  let closeResult = null;
1471
+ let closeRecovery = null;
1405
1472
  if (closeResume) {
1406
1473
  detailStep = "close_resume_modal";
1474
+ checkpointInProgressCandidate({
1475
+ event: "before_close_resume_modal",
1476
+ source,
1477
+ image_evidence: summarizeImageEvidence(imageEvidence),
1478
+ llm_screening: compactLlmResult(llmResult),
1479
+ full_cv_evidence: fullCvEvidence
1480
+ });
1407
1481
  closeResult = await measureTiming(timings, "close_detail_ms", () => closeChatResumeModal(client));
1408
1482
  if (!closeResult?.closed) {
1409
- throw makeChatResumeModalOpenBeforeCandidateClickError(closeResult);
1483
+ closeRecovery = await recoverAndReapplyChatContext(
1484
+ "resume_modal_close_failed:close_resume_modal",
1485
+ makeChatResumeModalOpenBeforeCandidateClickError(closeResult),
1486
+ { forceRefresh: true }
1487
+ );
1410
1488
  }
1411
1489
  }
1412
1490
  detailResult.close_result = closeResult;
@@ -1435,15 +1513,28 @@ export async function runChatWorkflow({
1435
1513
  image_evidence: summarizeImageEvidence(imageEvidence),
1436
1514
  capture_target: captureTarget || null,
1437
1515
  capture_target_wait: captureTargetWait,
1438
- full_cv_evidence: fullCvEvidence
1516
+ full_cv_evidence: fullCvEvidence,
1517
+ close_recovery: closeRecovery
1439
1518
  };
1440
1519
  }
1441
1520
  } catch (error) {
1521
+ checkpointInProgressCandidate({
1522
+ event: "detail_error",
1523
+ error: compactChatRuntimeError(error)
1524
+ });
1442
1525
  if (isForbiddenChatResumeNavigationError(error)) {
1443
1526
  detailUnavailableReason = "forbidden_top_level_resume_navigation";
1444
1527
  const recovery = await recoverAndReapplyChatContext(detailUnavailableReason, error);
1445
1528
  detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
1446
1529
  detailResult.cv_acquisition.recovery = recovery;
1530
+ } else if (isChatResumeModalCloseFailureError(error)) {
1531
+ const recoveryReason = `resume_modal_close_failed:${detailStep}`;
1532
+ const recovery = await recoverAndReapplyChatContext(recoveryReason, error, { forceRefresh: true });
1533
+ checkpointInProgressCandidate({
1534
+ event: "retry_after_modal_recovery",
1535
+ recovery
1536
+ });
1537
+ continue;
1447
1538
  } else if (isUnsafeChatOnlineResumeLinkError(error)) {
1448
1539
  detailUnavailableReason = "unsafe_online_resume_navigation_link";
1449
1540
  detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
@@ -1516,16 +1607,18 @@ export async function runChatWorkflow({
1516
1607
  }
1517
1608
  });
1518
1609
 
1610
+ const counters = countChatResultStatuses(results);
1519
1611
  runControl.updateProgress({
1520
1612
  card_count: cardNodeIds.length,
1521
1613
  target_count: passTarget || (processUntilListEnd ? "all" : processedLimit),
1522
1614
  target_pass_count: passTarget,
1523
1615
  processed_limit: processedLimit,
1524
- processed: results.length,
1525
- screened: results.length,
1526
- detail_opened: results.filter(resultOpenedDetail).length,
1527
- llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
1528
- passed: results.filter((item) => item.screening.passed).length,
1616
+ processed: counters.processed,
1617
+ screened: counters.screened,
1618
+ detail_opened: counters.detail_opened,
1619
+ llm_screened: counters.llm_screened,
1620
+ passed: counters.passed,
1621
+ skipped: counters.skipped,
1529
1622
  requested: requestedCount,
1530
1623
  request_satisfied: requestSatisfiedCount,
1531
1624
  request_skipped: requestSkippedCount,
@@ -1541,6 +1634,7 @@ export async function runChatWorkflow({
1541
1634
  const checkpointStarted = Date.now();
1542
1635
  runControl.checkpoint({
1543
1636
  results,
1637
+ in_progress_candidate: null,
1544
1638
  last_candidate: {
1545
1639
  id: screeningCandidate.id || null,
1546
1640
  key: candidateKey,
@@ -1564,6 +1658,7 @@ export async function runChatWorkflow({
1564
1658
  }
1565
1659
 
1566
1660
  runControl.setPhase("chat:done");
1661
+ const finalCounters = countChatResultStatuses(results);
1567
1662
  return {
1568
1663
  domain: "chat",
1569
1664
  target_url: targetUrl,
@@ -1579,11 +1674,12 @@ export async function runChatWorkflow({
1579
1674
  process_until_list_end: Boolean(processUntilListEnd),
1580
1675
  processed_limit: processedLimit,
1581
1676
  detail_source: normalizedDetailSource,
1582
- processed: results.length,
1583
- screened: results.length,
1584
- detail_opened: results.filter(resultOpenedDetail).length,
1585
- llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
1586
- passed: results.filter((item) => item.screening.passed).length,
1677
+ processed: finalCounters.processed,
1678
+ screened: finalCounters.screened,
1679
+ detail_opened: finalCounters.detail_opened,
1680
+ llm_screened: finalCounters.llm_screened,
1681
+ passed: finalCounters.passed,
1682
+ skipped: finalCounters.skipped,
1587
1683
  requested: requestedCount,
1588
1684
  request_satisfied: requestSatisfiedCount,
1589
1685
  request_skipped: requestSkippedCount,
@@ -1685,6 +1781,7 @@ export function createChatRunService({
1685
1781
  detail_opened: 0,
1686
1782
  llm_screened: 0,
1687
1783
  passed: 0,
1784
+ skipped: 0,
1688
1785
  requested: 0,
1689
1786
  request_satisfied: 0,
1690
1787
  request_skipped: 0
@@ -4,7 +4,8 @@ import {
4
4
  waitForRecommendCardNodeIds
5
5
  } from "./cards.js";
6
6
  import {
7
- RECOMMEND_RECENT_NOT_VIEW_LABEL
7
+ RECOMMEND_RECENT_NOT_VIEW_LABEL,
8
+ RECOMMEND_TARGET_URL
8
9
  } from "./constants.js";
9
10
  import { selectAndConfirmFirstSafeFilter } from "./filters.js";
10
11
  import { selectRecommendJob } from "./jobs.js";
@@ -94,6 +95,8 @@ export async function refreshRecommendListAtEnd(client, {
94
95
  fallbackPageScope = "recommend",
95
96
  filter = {},
96
97
  preferEndRefreshButton = true,
98
+ forceNavigate = false,
99
+ targetUrl = RECOMMEND_TARGET_URL,
97
100
  forceRecentNotView = true,
98
101
  cardTimeoutMs = 30000,
99
102
  buttonSettleMs = 8000,
@@ -156,8 +159,17 @@ export async function refreshRecommendListAtEnd(client, {
156
159
  }
157
160
  }
158
161
 
162
+ let fallbackMethod = "page_reload";
159
163
  try {
160
- await client.Page.reload({ ignoreCache: true });
164
+ let method = "page_reload";
165
+ if (forceNavigate && typeof client?.Page?.navigate === "function") {
166
+ await client.Page.navigate({ url: targetUrl || RECOMMEND_TARGET_URL });
167
+ method = "page_navigate";
168
+ fallbackMethod = method;
169
+ } else {
170
+ await client.Page.reload({ ignoreCache: true });
171
+ fallbackMethod = method;
172
+ }
161
173
  if (reloadSettleMs > 0) await sleep(reloadSettleMs);
162
174
  currentRootState = await waitForRecommendRoots(client, {
163
175
  timeoutMs: Math.max(30000, reloadSettleMs * 4),
@@ -202,8 +214,9 @@ export async function refreshRecommendListAtEnd(client, {
202
214
  });
203
215
  return {
204
216
  ok: cardNodeIds.length > 0,
205
- method: "page_reload",
217
+ method,
206
218
  attempts,
219
+ target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
207
220
  job_selection: jobSelection,
208
221
  page_scope: pageScopeResult,
209
222
  filter: filterResult,
@@ -214,10 +227,11 @@ export async function refreshRecommendListAtEnd(client, {
214
227
  } catch (error) {
215
228
  return {
216
229
  ok: false,
217
- method: "page_reload",
218
- reason: "page_reload_failed",
230
+ method: fallbackMethod,
231
+ reason: fallbackMethod === "page_navigate" ? "page_navigate_failed" : "page_reload_failed",
219
232
  error: error?.message || String(error),
220
233
  attempts,
234
+ target_url: fallbackMethod === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
221
235
  card_count: 0,
222
236
  root_state: currentRootState,
223
237
  forced_recent_not_view: forceRecentNotView