@reconcrap/boss-recommend-mcp 2.0.18 → 2.0.20

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.18",
3
+ "version": "2.0.20",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -150,6 +150,14 @@ export const CHAT_RESUME_MODAL_SELECTORS = Object.freeze([
150
150
  ".resume-recommend"
151
151
  ]);
152
152
 
153
+ export const CHAT_RESUME_FAST_MODAL_SELECTORS = Object.freeze([
154
+ ".boss-popup__wrapper.new-chat-resume-dialog-main-ui",
155
+ ".new-chat-resume-dialog-main-ui",
156
+ ".resume-common-dialog.search-resume",
157
+ ".resume-recommend",
158
+ 'iframe[src*="/web/frame/c-resume/"]'
159
+ ]);
160
+
153
161
  export const CHAT_RESUME_CONTENT_SELECTORS = Object.freeze([
154
162
  ".resume-center-side .resume-detail-wrap",
155
163
  ".resume-detail-wrap",
@@ -171,14 +179,18 @@ export const CHAT_RESUME_IFRAME_SELECTORS = Object.freeze([
171
179
  export const CHAT_RESUME_CLOSE_SELECTORS = Object.freeze([
172
180
  ".boss-popup__close",
173
181
  ".boss-dialog__close",
182
+ ".new-chat-resume-dialog-main-ui .boss-popup__close",
183
+ ".new-chat-resume-dialog-main-ui .icon-close",
184
+ ".new-chat-resume-dialog-main-ui [class*=\"close\"]",
185
+ ".boss-popup__wrapper [class*=\"close\"]",
186
+ ".boss-dialog [class*=\"close\"]",
174
187
  ".popup-close",
175
188
  ".modal-close",
176
189
  ".dialog-close",
177
190
  ".close-btn",
178
191
  ".icon-close",
179
192
  '[aria-label*="关闭"]',
180
- '[title*="关闭"]',
181
- '[class*="close"]'
193
+ '[title*="关闭"]'
182
194
  ]);
183
195
 
184
196
  export const CHAT_PROFILE_NETWORK_PATTERNS = Object.freeze([
@@ -28,6 +28,7 @@ import {
28
28
  CHAT_PROFILE_NETWORK_PATTERNS,
29
29
  CHAT_RESUME_CLOSE_SELECTORS,
30
30
  CHAT_RESUME_CONTENT_SELECTORS,
31
+ CHAT_RESUME_FAST_MODAL_SELECTORS,
31
32
  CHAT_RESUME_IFRAME_SELECTORS,
32
33
  CHAT_RESUME_MODAL_SELECTORS,
33
34
  CHAT_SEND_BUTTON_SELECTORS
@@ -573,6 +574,43 @@ export async function waitForChatResumeModal(client, {
573
574
  return lastState;
574
575
  }
575
576
 
577
+ export async function quickChatResumeModalOpenProbe(client, {
578
+ selectors = CHAT_RESUME_FAST_MODAL_SELECTORS
579
+ } = {}) {
580
+ const rootState = await getChatRoots(client);
581
+ for (const root of rootState.roots) {
582
+ if (!root?.nodeId) continue;
583
+ for (const selector of selectors) {
584
+ let nodeIds = [];
585
+ try {
586
+ nodeIds = await querySelectorAll(client, root.nodeId, selector);
587
+ } catch {
588
+ nodeIds = [];
589
+ }
590
+ for (const nodeId of nodeIds.slice(0, 4)) {
591
+ try {
592
+ const box = await getNodeBox(client, nodeId);
593
+ if (box?.rect?.width > 8 && box?.rect?.height > 8) {
594
+ return {
595
+ open: true,
596
+ root: root.name,
597
+ selector,
598
+ node_id: nodeId,
599
+ rect: box.rect,
600
+ center: box.center
601
+ };
602
+ }
603
+ } catch {
604
+ // Hidden or stale modal probes are ignored.
605
+ }
606
+ }
607
+ }
608
+ }
609
+ return {
610
+ open: false
611
+ };
612
+ }
613
+
576
614
  export async function readChatResumeHtml(client, resumeState) {
577
615
  let popupHTML = "";
578
616
  let contentHTML = "";
@@ -52,6 +52,7 @@ import {
52
52
  extractChatProfileCandidate,
53
53
  isUnsafeChatOnlineResumeLinkError,
54
54
  openChatOnlineResume,
55
+ quickChatResumeModalOpenProbe,
55
56
  readChatConversationReadyState,
56
57
  requestChatResumeForPassedCandidate,
57
58
  selectChatMessageFilter,
@@ -153,6 +154,41 @@ function resultOpenedDetail(result) {
153
154
  return Boolean(result?.detail && !result.detail?.cv_acquisition?.skipped);
154
155
  }
155
156
 
157
+ export function chatDetailSkipReasonFromReadyState(state = {}) {
158
+ if (state?.attachment_resume_enabled) return "attachment_resume_already_available";
159
+ return "";
160
+ }
161
+
162
+ export function makeChatResumeModalOpenBeforeCandidateClickError(closeResult = null) {
163
+ const error = new Error("CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK");
164
+ error.code = "CHAT_RESUME_MODAL_OPEN_BEFORE_CANDIDATE_CLICK";
165
+ error.close_result = closeResult || null;
166
+ return error;
167
+ }
168
+
169
+ export async function ensureNoOpenChatResumeModalBeforeCandidateClick(client, {
170
+ closeAttempts = 3
171
+ } = {}) {
172
+ const probe = await quickChatResumeModalOpenProbe(client);
173
+ if (!probe.open) {
174
+ return {
175
+ closed: true,
176
+ already_closed: true,
177
+ probe
178
+ };
179
+ }
180
+ const closeResult = await closeChatResumeModal(client, { attemptsLimit: closeAttempts });
181
+ if (closeResult?.closed) {
182
+ return {
183
+ closed: true,
184
+ already_closed: false,
185
+ probe,
186
+ close_result: closeResult
187
+ };
188
+ }
189
+ throw makeChatResumeModalOpenBeforeCandidateClickError(closeResult);
190
+ }
191
+
156
192
  function llmToScreening(llmResult, candidate) {
157
193
  return {
158
194
  status: llmResult?.passed ? "pass" : "fail",
@@ -453,6 +489,7 @@ async function selectFreshChatCandidate(client, {
453
489
  } = {}) {
454
490
  let lastError = null;
455
491
  for (let attempt = 0; attempt < 3; attempt += 1) {
492
+ const modalGuard = await ensureNoOpenChatResumeModalBeforeCandidateClick(client);
456
493
  const rootState = await getChatRoots(client);
457
494
  const freshNodeId = await resolveFreshChatCardNodeId(client, {
458
495
  fallbackNodeId: cardNodeId,
@@ -474,6 +511,7 @@ async function selectFreshChatCandidate(client, {
474
511
  ready,
475
512
  card_node_id: freshNodeId,
476
513
  refreshed_node: freshNodeId !== cardNodeId,
514
+ modal_guard: modalGuard,
477
515
  attempt: attempt + 1
478
516
  };
479
517
  } catch (error) {
@@ -962,20 +1000,37 @@ export async function runChatWorkflow({
962
1000
  }
963
1001
  effectiveCardNodeId = selected.card_node_id || cardNodeId;
964
1002
  const selectionNetworkEvents = networkRecorder.events.slice();
1003
+ try {
1004
+ preActionState = await readChatConversationReadyState(client);
1005
+ } catch (error) {
1006
+ preActionState = {
1007
+ error: error?.message || String(error)
1008
+ };
1009
+ }
1010
+ const preDetailSkipReason = chatDetailSkipReasonFromReadyState(preActionState);
1011
+ if (preDetailSkipReason) {
1012
+ detailUnavailableReason = preDetailSkipReason;
1013
+ detailResult = createSkippedDetailResult(cardCandidate, preDetailSkipReason);
1014
+ detailResult.cv_acquisition.pre_detail_state = preActionState;
1015
+ detailResult.cv_acquisition.selection_ready_state = selected.ready;
1016
+ }
965
1017
  if (!selected.ready?.ok) {
966
- if (selected.ready?.reason === "active_candidate_mismatch") {
1018
+ if (detailResult) {
1019
+ // Already classified by the pre-detail conversation state.
1020
+ } else if (selected.ready?.reason === "active_candidate_mismatch") {
967
1021
  detailUnavailableReason = "active_candidate_mismatch";
968
1022
  detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
969
1023
  detailResult.cv_acquisition.selection_ready_state = selected.ready;
970
1024
  } else {
971
1025
  detailStep = "read_conversation_ready_state";
972
- preActionState = await readChatConversationReadyState(client);
973
1026
  if (preActionState.attachment_resume_enabled) {
974
1027
  detailUnavailableReason = "attachment_resume_already_available";
975
1028
  detailResult = createSkippedDetailResult(cardCandidate, "attachment_resume_already_available");
1029
+ detailResult.cv_acquisition.pre_detail_state = preActionState;
976
1030
  } else {
977
1031
  detailUnavailableReason = "online_resume_button_unavailable";
978
1032
  detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
1033
+ detailResult.cv_acquisition.pre_detail_state = preActionState;
979
1034
  }
980
1035
  }
981
1036
  }
@@ -1322,6 +1377,9 @@ export async function runChatWorkflow({
1322
1377
  if (closeResume) {
1323
1378
  detailStep = "close_resume_modal";
1324
1379
  closeResult = await measureTiming(timings, "close_detail_ms", () => closeChatResumeModal(client));
1380
+ if (!closeResult?.closed) {
1381
+ throw makeChatResumeModalOpenBeforeCandidateClickError(closeResult);
1382
+ }
1325
1383
  }
1326
1384
  detailResult.close_result = closeResult;
1327
1385
  detailResult.image_evidence = imageEvidence;