@reconcrap/boss-recommend-mcp 2.0.22 → 2.0.24

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.22",
3
+ "version": "2.0.24",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -46,6 +46,33 @@ import {
46
46
 
47
47
  export const CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE = "CHAT_UNSAFE_ONLINE_RESUME_LINK";
48
48
 
49
+ const CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS = Object.freeze([
50
+ ".conversation-main",
51
+ ".conversation-editor",
52
+ ".chat-message-list",
53
+ ".toolbar-box-right",
54
+ ".operate-exchange-left",
55
+ ".operate-icon-item",
56
+ ".exchange-tooltip",
57
+ ".boss-popup__wrapper",
58
+ ".boss-dialog",
59
+ ".dialog-wrap.active",
60
+ ".geek-detail-modal"
61
+ ]);
62
+
63
+ const CHAT_REQUESTED_RESUME_SCOPE_SELECTORS = Object.freeze([
64
+ ".chat-message-list",
65
+ ".conversation-editor",
66
+ ".conversation-main",
67
+ ".toolbar-box-right",
68
+ ".operate-exchange-left",
69
+ ".operate-icon-item",
70
+ ".exchange-tooltip",
71
+ ".boss-popup__wrapper",
72
+ ".boss-dialog",
73
+ ".dialog-wrap.active"
74
+ ]);
75
+
49
76
  export function matchesChatProfileNetwork(url) {
50
77
  return CHAT_PROFILE_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
51
78
  }
@@ -336,6 +363,53 @@ function isRequestedResumeText(text = "") {
336
363
  );
337
364
  }
338
365
 
366
+ function isResumeRequestSentMessageText(text = "") {
367
+ const normalized = normalizeDetailText(text);
368
+ return Boolean(
369
+ normalized.includes("简历请求已发送")
370
+ || normalized.includes("已发送简历")
371
+ || normalized.includes("已求简历")
372
+ || normalized.includes("已索要简历")
373
+ || normalized.includes("已发送")
374
+ );
375
+ }
376
+
377
+ function countTextOccurrences(text = "", needle = "") {
378
+ if (!needle) return 0;
379
+ let count = 0;
380
+ let index = 0;
381
+ while (index < text.length) {
382
+ const found = text.indexOf(needle, index);
383
+ if (found < 0) break;
384
+ count += 1;
385
+ index = found + needle.length;
386
+ }
387
+ return count;
388
+ }
389
+
390
+ function countResumeRequestSentMessageMarkers(lines = []) {
391
+ const markers = ["简历请求已发送", "已发送简历", "已求简历", "已索要简历", "已发送"];
392
+ return lines.reduce((total, line) => (
393
+ total + markers.reduce((lineTotal, marker) => (
394
+ lineTotal + countTextOccurrences(line, marker)
395
+ ), 0)
396
+ ), 0);
397
+ }
398
+
399
+ function isResumeAttachmentMessageText(text = "") {
400
+ const normalized = normalizeDetailText(text);
401
+ return Boolean(
402
+ /点击.*附件简历/.test(normalized)
403
+ || /预览附件简历/.test(normalized)
404
+ || /查看附件简历/.test(normalized)
405
+ || /(?:简历|resume)[^\s]*\.(?:pdf|docx?|jpg|jpeg|png)\b/i.test(normalized)
406
+ );
407
+ }
408
+
409
+ function countResumeAttachmentMessageMarkers(lines = []) {
410
+ return lines.reduce((total, line) => total + (isResumeAttachmentMessageText(line) ? 1 : 0), 0);
411
+ }
412
+
339
413
  function isRequestedResumeControlTarget(target = {}) {
340
414
  const label = normalizeDetailText(target.label);
341
415
  const className = String(target.attributes?.class || "");
@@ -369,7 +443,6 @@ function isConfirmText(text = "") {
369
443
  normalized === "确定"
370
444
  || normalized === "确认"
371
445
  || normalized === "提交"
372
- || normalized === "发送"
373
446
  || normalized === "继续"
374
447
  || normalized.includes("确定")
375
448
  || normalized.includes("确认")
@@ -435,6 +508,35 @@ async function findVisibleMatchingTarget(client, roots, selectors, predicate) {
435
508
  return null;
436
509
  }
437
510
 
511
+ async function resolveScopedRoots(client, roots = [], selectors = [], {
512
+ fallbackToRoots = true
513
+ } = {}) {
514
+ const scoped = [];
515
+ const seen = new Set();
516
+ for (const root of roots) {
517
+ if (!root?.nodeId) continue;
518
+ for (const selector of selectors) {
519
+ let nodeIds = [];
520
+ try {
521
+ nodeIds = await querySelectorAll(client, root.nodeId, selector);
522
+ } catch {
523
+ nodeIds = [];
524
+ }
525
+ for (const nodeId of nodeIds) {
526
+ const key = `${root.name}:${nodeId}`;
527
+ if (seen.has(key)) continue;
528
+ seen.add(key);
529
+ scoped.push({
530
+ name: `${root.name}:${selector}`,
531
+ nodeId
532
+ });
533
+ }
534
+ }
535
+ }
536
+ if (scoped.length || !fallbackToRoots) return scoped;
537
+ return roots;
538
+ }
539
+
438
540
  export async function selectChatPrimaryLabel(client, {
439
541
  label = "全部",
440
542
  timeoutMs = 8000,
@@ -852,39 +954,53 @@ export async function openChatOnlineResume(client, {
852
954
 
853
955
  export async function readChatConversationReadyState(client) {
854
956
  const rootState = await getChatRoots(client);
855
- const onlineResume = await findVisibleMatchingTarget(
957
+ const scopedControlRoots = await resolveScopedRoots(
958
+ client,
959
+ rootState.roots,
960
+ CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS,
961
+ { fallbackToRoots: false }
962
+ );
963
+ const scopedRequestedRoots = await resolveScopedRoots(
856
964
  client,
857
965
  rootState.roots,
966
+ CHAT_REQUESTED_RESUME_SCOPE_SELECTORS,
967
+ { fallbackToRoots: false }
968
+ );
969
+ const controlRoots = scopedControlRoots.length ? scopedControlRoots : rootState.roots;
970
+ const requestedRoots = scopedRequestedRoots.length ? scopedRequestedRoots : rootState.roots;
971
+ const onlineResume = await findVisibleMatchingTarget(
972
+ client,
973
+ controlRoots,
858
974
  CHAT_ONLINE_RESUME_BUTTON_SELECTORS,
859
975
  (target) => target.label.includes("在线简历") && !target.disabled
860
976
  );
861
977
  const attachmentResume = await findVisibleMatchingTarget(
862
978
  client,
863
- rootState.roots,
979
+ controlRoots,
864
980
  CHAT_ATTACHMENT_RESUME_BUTTON_SELECTORS,
865
981
  (target) => isAttachmentResumeText(target.label)
866
982
  );
867
983
  const askResume = await findVisibleMatchingTarget(
868
984
  client,
869
- rootState.roots,
985
+ controlRoots,
870
986
  CHAT_ASK_RESUME_BUTTON_SELECTORS,
871
987
  (target) => isAskResumeText(target.label) && !isAttachmentResumeTarget(target)
872
988
  );
873
989
  const requestedResume = await findVisibleMatchingTarget(
874
990
  client,
875
- rootState.roots,
991
+ requestedRoots,
876
992
  CHAT_ASK_RESUME_BUTTON_SELECTORS,
877
993
  (target) => isRequestedResumeControlTarget(target)
878
994
  );
879
995
  const editor = await findVisibleMatchingTarget(
880
996
  client,
881
- rootState.roots,
997
+ controlRoots,
882
998
  CHAT_EDITOR_SELECTORS,
883
999
  () => true
884
1000
  );
885
1001
  const sendButton = await findVisibleMatchingTarget(
886
1002
  client,
887
- rootState.roots,
1003
+ controlRoots,
888
1004
  CHAT_SEND_BUTTON_SELECTORS,
889
1005
  (target) => isSendText(target.label) || /submit/i.test(String(target.attributes?.class || ""))
890
1006
  );
@@ -1034,13 +1150,6 @@ export async function clickChatAskResume(client, {
1034
1150
  control: state.attachment_resume
1035
1151
  };
1036
1152
  }
1037
- if (state.already_requested_resume) {
1038
- return {
1039
- ok: true,
1040
- already_requested: true,
1041
- control: state.requested_resume
1042
- };
1043
- }
1044
1153
  if (state.ask_resume?.node_id && !state.ask_resume.disabled) {
1045
1154
  try {
1046
1155
  if (state.ask_resume.center) {
@@ -1063,6 +1172,13 @@ export async function clickChatAskResume(client, {
1063
1172
  };
1064
1173
  }
1065
1174
  }
1175
+ if (state.already_requested_resume) {
1176
+ return {
1177
+ ok: true,
1178
+ already_requested: true,
1179
+ control: state.requested_resume
1180
+ };
1181
+ }
1066
1182
  await sleep(250);
1067
1183
  }
1068
1184
  return {
@@ -1078,19 +1194,18 @@ export async function clickChatConfirmRequestResume(client, {
1078
1194
  } = {}) {
1079
1195
  const started = Date.now();
1080
1196
  let lastTarget = null;
1197
+ let lastState = null;
1081
1198
  while (Date.now() - started <= timeoutMs) {
1082
- const state = await readChatConversationReadyState(client);
1083
- if (state.already_requested_resume) {
1084
- return {
1085
- confirmed: true,
1086
- assumed_requested: true,
1087
- state
1088
- };
1089
- }
1199
+ lastState = await readChatConversationReadyState(client);
1090
1200
  const rootState = await getChatRoots(client);
1091
- const target = await findVisibleMatchingTarget(
1201
+ const confirmRoots = await resolveScopedRoots(
1092
1202
  client,
1093
1203
  rootState.roots,
1204
+ CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS
1205
+ );
1206
+ const target = await findVisibleMatchingTarget(
1207
+ client,
1208
+ confirmRoots,
1094
1209
  CHAT_CONFIRM_REQUEST_RESUME_SELECTORS,
1095
1210
  (item) => isConfirmText(item.label) && !item.disabled
1096
1211
  );
@@ -1124,7 +1239,8 @@ export async function clickChatConfirmRequestResume(client, {
1124
1239
  return {
1125
1240
  confirmed: false,
1126
1241
  error: "CONFIRM_BUTTON_NOT_FOUND",
1127
- control: lastTarget
1242
+ control: lastTarget,
1243
+ state: lastState
1128
1244
  };
1129
1245
  }
1130
1246
 
@@ -1151,18 +1267,26 @@ export async function getChatResumeRequestMessageState(client) {
1151
1267
  text = htmlToText(await getOuterHTML(client, nodeId));
1152
1268
  } catch {}
1153
1269
  const lines = text.split(/\r?\n/).map(normalizeDetailText).filter(Boolean);
1154
- const matching = lines.filter((line) => line.includes("简历请求已发送"));
1270
+ const matching = lines.filter((line) => isResumeRequestSentMessageText(line));
1271
+ const attachmentMatching = lines.filter((line) => isResumeAttachmentMessageText(line));
1272
+ const count = countResumeRequestSentMessageMarkers(lines);
1273
+ const resumeAttachmentCount = countResumeAttachmentMessageMarkers(lines);
1155
1274
  return {
1156
1275
  ok: Boolean(text),
1157
1276
  selector: messageRoot?.selector || "top",
1158
- count: matching.length,
1277
+ count,
1278
+ resume_attachment_count: resumeAttachmentCount,
1279
+ success_count: count + resumeAttachmentCount,
1159
1280
  last_text: matching[matching.length - 1] || lines[lines.length - 1] || "",
1281
+ last_resume_attachment_text: attachmentMatching[attachmentMatching.length - 1] || "",
1282
+ last_success_text: matching[matching.length - 1] || attachmentMatching[attachmentMatching.length - 1] || "",
1160
1283
  recent: lines.slice(-12)
1161
1284
  };
1162
1285
  }
1163
1286
 
1164
1287
  export async function waitForChatResumeRequestMessage(client, {
1165
1288
  baselineCount = 0,
1289
+ baselineResumeAttachmentCount = 0,
1166
1290
  timeoutMs = 6500,
1167
1291
  intervalMs = 260
1168
1292
  } = {}) {
@@ -1170,9 +1294,10 @@ export async function waitForChatResumeRequestMessage(client, {
1170
1294
  let state = null;
1171
1295
  while (Date.now() - started <= timeoutMs) {
1172
1296
  state = await getChatResumeRequestMessageState(client);
1173
- const observed = state.count > baselineCount
1174
- || state.last_text.includes("简历请求已发送")
1175
- || state.recent.some((item) => item.includes("简历请求已发送"));
1297
+ const observed = (
1298
+ state.count > baselineCount
1299
+ || state.resume_attachment_count > baselineResumeAttachmentCount
1300
+ );
1176
1301
  if (observed) {
1177
1302
  return {
1178
1303
  observed: true,
@@ -1204,14 +1329,6 @@ export async function requestChatResumeForPassedCandidate(client, {
1204
1329
  initial_state: initialState
1205
1330
  };
1206
1331
  }
1207
- if (initialState.already_requested_resume) {
1208
- return {
1209
- requested: true,
1210
- skipped: true,
1211
- reason: "resume_already_requested",
1212
- initial_state: initialState
1213
- };
1214
- }
1215
1332
  if (dryRun) {
1216
1333
  return {
1217
1334
  requested: false,
@@ -1281,7 +1398,8 @@ export async function requestChatResumeForPassedCandidate(client, {
1281
1398
  confirmResult = await clickChatConfirmRequestResume(client);
1282
1399
  }
1283
1400
  const messageCheck = await waitForChatResumeRequestMessage(client, {
1284
- baselineCount: before.count
1401
+ baselineCount: before.count,
1402
+ baselineResumeAttachmentCount: before.resume_attachment_count
1285
1403
  });
1286
1404
  const messageObserved = Boolean(messageCheck.observed);
1287
1405
  attempts.push({
@@ -1290,8 +1408,10 @@ export async function requestChatResumeForPassedCandidate(client, {
1290
1408
  confirm_result: confirmResult,
1291
1409
  message_before_count: before.count,
1292
1410
  message_after_count: messageCheck.state?.count || 0,
1411
+ resume_attachment_before_count: before.resume_attachment_count || 0,
1412
+ resume_attachment_after_count: messageCheck.state?.resume_attachment_count || 0,
1293
1413
  message_observed: messageObserved,
1294
- message_last_text: messageCheck.state?.last_text || ""
1414
+ message_last_text: messageCheck.state?.last_success_text || messageCheck.state?.last_text || ""
1295
1415
  });
1296
1416
  if (messageObserved) {
1297
1417
  return {