@reconcrap/boss-recommend-mcp 2.0.23 → 2.0.25

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.23",
3
+ "version": "2.0.25",
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
  }
@@ -369,6 +396,20 @@ function countResumeRequestSentMessageMarkers(lines = []) {
369
396
  ), 0);
370
397
  }
371
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
+
372
413
  function isRequestedResumeControlTarget(target = {}) {
373
414
  const label = normalizeDetailText(target.label);
374
415
  const className = String(target.attributes?.class || "");
@@ -402,7 +443,6 @@ function isConfirmText(text = "") {
402
443
  normalized === "确定"
403
444
  || normalized === "确认"
404
445
  || normalized === "提交"
405
- || normalized === "发送"
406
446
  || normalized === "继续"
407
447
  || normalized.includes("确定")
408
448
  || normalized.includes("确认")
@@ -468,6 +508,35 @@ async function findVisibleMatchingTarget(client, roots, selectors, predicate) {
468
508
  return null;
469
509
  }
470
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
+
471
540
  export async function selectChatPrimaryLabel(client, {
472
541
  label = "全部",
473
542
  timeoutMs = 8000,
@@ -885,39 +954,53 @@ export async function openChatOnlineResume(client, {
885
954
 
886
955
  export async function readChatConversationReadyState(client) {
887
956
  const rootState = await getChatRoots(client);
888
- 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(
889
964
  client,
890
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,
891
974
  CHAT_ONLINE_RESUME_BUTTON_SELECTORS,
892
975
  (target) => target.label.includes("在线简历") && !target.disabled
893
976
  );
894
977
  const attachmentResume = await findVisibleMatchingTarget(
895
978
  client,
896
- rootState.roots,
979
+ controlRoots,
897
980
  CHAT_ATTACHMENT_RESUME_BUTTON_SELECTORS,
898
981
  (target) => isAttachmentResumeText(target.label)
899
982
  );
900
983
  const askResume = await findVisibleMatchingTarget(
901
984
  client,
902
- rootState.roots,
985
+ controlRoots,
903
986
  CHAT_ASK_RESUME_BUTTON_SELECTORS,
904
987
  (target) => isAskResumeText(target.label) && !isAttachmentResumeTarget(target)
905
988
  );
906
989
  const requestedResume = await findVisibleMatchingTarget(
907
990
  client,
908
- rootState.roots,
991
+ requestedRoots,
909
992
  CHAT_ASK_RESUME_BUTTON_SELECTORS,
910
993
  (target) => isRequestedResumeControlTarget(target)
911
994
  );
912
995
  const editor = await findVisibleMatchingTarget(
913
996
  client,
914
- rootState.roots,
997
+ controlRoots,
915
998
  CHAT_EDITOR_SELECTORS,
916
999
  () => true
917
1000
  );
918
1001
  const sendButton = await findVisibleMatchingTarget(
919
1002
  client,
920
- rootState.roots,
1003
+ controlRoots,
921
1004
  CHAT_SEND_BUTTON_SELECTORS,
922
1005
  (target) => isSendText(target.label) || /submit/i.test(String(target.attributes?.class || ""))
923
1006
  );
@@ -1056,6 +1139,7 @@ export async function clickChatAskResume(client, {
1056
1139
  } = {}) {
1057
1140
  const started = Date.now();
1058
1141
  let lastState = null;
1142
+ let lastDisabledAskResume = null;
1059
1143
  while (Date.now() - started <= timeoutMs) {
1060
1144
  const state = await readChatConversationReadyState(client);
1061
1145
  lastState = state;
@@ -1089,6 +1173,9 @@ export async function clickChatAskResume(client, {
1089
1173
  };
1090
1174
  }
1091
1175
  }
1176
+ if (state.ask_resume?.node_id && state.ask_resume.disabled) {
1177
+ lastDisabledAskResume = state.ask_resume;
1178
+ }
1092
1179
  if (state.already_requested_resume) {
1093
1180
  return {
1094
1181
  ok: true,
@@ -1098,6 +1185,16 @@ export async function clickChatAskResume(client, {
1098
1185
  }
1099
1186
  await sleep(250);
1100
1187
  }
1188
+ if (lastDisabledAskResume) {
1189
+ return {
1190
+ ok: false,
1191
+ already_requested: true,
1192
+ request_pending: true,
1193
+ error: "ASK_RESUME_BUTTON_DISABLED",
1194
+ control: lastDisabledAskResume,
1195
+ state: lastState
1196
+ };
1197
+ }
1101
1198
  return {
1102
1199
  ok: false,
1103
1200
  error: "ASK_RESUME_BUTTON_NOT_FOUND",
@@ -1111,19 +1208,18 @@ export async function clickChatConfirmRequestResume(client, {
1111
1208
  } = {}) {
1112
1209
  const started = Date.now();
1113
1210
  let lastTarget = null;
1211
+ let lastState = null;
1114
1212
  while (Date.now() - started <= timeoutMs) {
1115
- const state = await readChatConversationReadyState(client);
1116
- if (state.already_requested_resume) {
1117
- return {
1118
- confirmed: true,
1119
- assumed_requested: true,
1120
- state
1121
- };
1122
- }
1213
+ lastState = await readChatConversationReadyState(client);
1123
1214
  const rootState = await getChatRoots(client);
1124
- const target = await findVisibleMatchingTarget(
1215
+ const confirmRoots = await resolveScopedRoots(
1125
1216
  client,
1126
1217
  rootState.roots,
1218
+ CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS
1219
+ );
1220
+ const target = await findVisibleMatchingTarget(
1221
+ client,
1222
+ confirmRoots,
1127
1223
  CHAT_CONFIRM_REQUEST_RESUME_SELECTORS,
1128
1224
  (item) => isConfirmText(item.label) && !item.disabled
1129
1225
  );
@@ -1157,7 +1253,8 @@ export async function clickChatConfirmRequestResume(client, {
1157
1253
  return {
1158
1254
  confirmed: false,
1159
1255
  error: "CONFIRM_BUTTON_NOT_FOUND",
1160
- control: lastTarget
1256
+ control: lastTarget,
1257
+ state: lastState
1161
1258
  };
1162
1259
  }
1163
1260
 
@@ -1185,18 +1282,25 @@ export async function getChatResumeRequestMessageState(client) {
1185
1282
  } catch {}
1186
1283
  const lines = text.split(/\r?\n/).map(normalizeDetailText).filter(Boolean);
1187
1284
  const matching = lines.filter((line) => isResumeRequestSentMessageText(line));
1285
+ const attachmentMatching = lines.filter((line) => isResumeAttachmentMessageText(line));
1188
1286
  const count = countResumeRequestSentMessageMarkers(lines);
1287
+ const resumeAttachmentCount = countResumeAttachmentMessageMarkers(lines);
1189
1288
  return {
1190
1289
  ok: Boolean(text),
1191
1290
  selector: messageRoot?.selector || "top",
1192
1291
  count,
1292
+ resume_attachment_count: resumeAttachmentCount,
1293
+ success_count: count + resumeAttachmentCount,
1193
1294
  last_text: matching[matching.length - 1] || lines[lines.length - 1] || "",
1295
+ last_resume_attachment_text: attachmentMatching[attachmentMatching.length - 1] || "",
1296
+ last_success_text: matching[matching.length - 1] || attachmentMatching[attachmentMatching.length - 1] || "",
1194
1297
  recent: lines.slice(-12)
1195
1298
  };
1196
1299
  }
1197
1300
 
1198
1301
  export async function waitForChatResumeRequestMessage(client, {
1199
1302
  baselineCount = 0,
1303
+ baselineResumeAttachmentCount = 0,
1200
1304
  timeoutMs = 6500,
1201
1305
  intervalMs = 260
1202
1306
  } = {}) {
@@ -1204,7 +1308,10 @@ export async function waitForChatResumeRequestMessage(client, {
1204
1308
  let state = null;
1205
1309
  while (Date.now() - started <= timeoutMs) {
1206
1310
  state = await getChatResumeRequestMessageState(client);
1207
- const observed = state.count > baselineCount;
1311
+ const observed = (
1312
+ state.count > baselineCount
1313
+ || state.resume_attachment_count > baselineResumeAttachmentCount
1314
+ );
1208
1315
  if (observed) {
1209
1316
  return {
1210
1317
  observed: true,
@@ -1224,6 +1331,7 @@ export async function waitForChatResumeRequestMessage(client, {
1224
1331
  export async function requestChatResumeForPassedCandidate(client, {
1225
1332
  greetingText = "Hi同学,能麻烦发下简历吗?",
1226
1333
  maxAttempts = 3,
1334
+ askResumeTimeoutMs = 8000,
1227
1335
  dryRun = false
1228
1336
  } = {}) {
1229
1337
  const effectiveGreetingText = normalizeDetailText(greetingText) || "Hi同学,能麻烦发下简历吗?";
@@ -1269,13 +1377,17 @@ export async function requestChatResumeForPassedCandidate(client, {
1269
1377
  const attempts = [];
1270
1378
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
1271
1379
  const before = await getChatResumeRequestMessageState(client);
1272
- const askResult = await clickChatAskResume(client);
1380
+ const askResult = await clickChatAskResume(client, {
1381
+ timeoutMs: askResumeTimeoutMs
1382
+ });
1273
1383
  let confirmResult = {
1274
1384
  confirmed: false,
1275
1385
  assumed_requested: Boolean(askResult.already_requested),
1276
1386
  skipped: true,
1277
1387
  reason: askResult.attachment_resume_available
1278
1388
  ? "attachment_resume_already_available"
1389
+ : askResult.request_pending
1390
+ ? "resume_request_already_pending"
1279
1391
  : askResult.ok
1280
1392
  ? "already_requested"
1281
1393
  : (askResult.error || "ask_resume_not_clicked")
@@ -1301,11 +1413,35 @@ export async function requestChatResumeForPassedCandidate(client, {
1301
1413
  attempts
1302
1414
  };
1303
1415
  }
1416
+ if (askResult.request_pending || askResult.already_requested) {
1417
+ attempts.push({
1418
+ attempt: attempt + 1,
1419
+ ask_result: askResult,
1420
+ confirm_result: confirmResult,
1421
+ message_before_count: before.count,
1422
+ message_after_count: before.count,
1423
+ resume_attachment_before_count: before.resume_attachment_count || 0,
1424
+ resume_attachment_after_count: before.resume_attachment_count || 0,
1425
+ message_observed: false,
1426
+ message_last_text: before.last_success_text || before.last_text || ""
1427
+ });
1428
+ return {
1429
+ requested: false,
1430
+ skipped: true,
1431
+ reason: "resume_request_already_pending",
1432
+ initial_state: initialState,
1433
+ close_before_greeting: closeBeforeGreeting,
1434
+ greeting_sent: true,
1435
+ greeting_send_result: sendResult,
1436
+ attempts
1437
+ };
1438
+ }
1304
1439
  if (askResult.ok && !askResult.already_requested) {
1305
1440
  confirmResult = await clickChatConfirmRequestResume(client);
1306
1441
  }
1307
1442
  const messageCheck = await waitForChatResumeRequestMessage(client, {
1308
- baselineCount: before.count
1443
+ baselineCount: before.count,
1444
+ baselineResumeAttachmentCount: before.resume_attachment_count
1309
1445
  });
1310
1446
  const messageObserved = Boolean(messageCheck.observed);
1311
1447
  attempts.push({
@@ -1314,8 +1450,10 @@ export async function requestChatResumeForPassedCandidate(client, {
1314
1450
  confirm_result: confirmResult,
1315
1451
  message_before_count: before.count,
1316
1452
  message_after_count: messageCheck.state?.count || 0,
1453
+ resume_attachment_before_count: before.resume_attachment_count || 0,
1454
+ resume_attachment_after_count: messageCheck.state?.resume_attachment_count || 0,
1317
1455
  message_observed: messageObserved,
1318
- message_last_text: messageCheck.state?.last_text || ""
1456
+ message_last_text: messageCheck.state?.last_success_text || messageCheck.state?.last_text || ""
1319
1457
  });
1320
1458
  if (messageObserved) {
1321
1459
  return {