@reconcrap/boss-recommend-mcp 2.1.15 → 2.1.16

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.
@@ -68,10 +68,11 @@ import {
68
68
  refreshRecommendListAtEnd
69
69
  } from "./refresh.js";
70
70
  import { selectRecommendJob } from "./jobs.js";
71
- import {
72
- normalizeRecommendPageScope,
73
- selectRecommendPageScope
74
- } from "./scopes.js";
71
+ import {
72
+ normalizeRecommendPageScope,
73
+ selectRecommendPageScope
74
+ } from "./scopes.js";
75
+ import { inspectRecentColleagueContact } from "./colleague-contact.js";
75
76
  import {
76
77
  RECOMMEND_BOTTOM_MARKER_SELECTORS,
77
78
  RECOMMEND_CARD_SELECTOR,
@@ -224,19 +225,20 @@ function compactCandidate(candidate) {
224
225
  };
225
226
  }
226
227
 
227
- function compactDetail(detailResult) {
228
- if (!detailResult) return null;
229
- return {
230
- popup_text_length: detailResult.detail?.popup_text?.length || 0,
231
- resume_text_length: detailResult.detail?.resume_text?.length || 0,
228
+ function compactDetail(detailResult) {
229
+ if (!detailResult) return null;
230
+ return {
231
+ popup_text_length: detailResult.detail?.popup_text?.length || 0,
232
+ resume_text_length: detailResult.detail?.resume_text?.length || 0,
232
233
  network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
233
234
  parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
234
- cv_acquisition: detailResult.cv_acquisition || null,
235
- image_evidence: summarizeImageEvidence(detailResult.image_evidence),
236
- llm_screening: compactScreeningLlmResult(detailResult.llm_result),
237
- close_result: detailResult.close_result
238
- };
239
- }
235
+ cv_acquisition: detailResult.cv_acquisition || null,
236
+ colleague_contact: detailResult.colleague_contact || null,
237
+ image_evidence: summarizeImageEvidence(detailResult.image_evidence),
238
+ llm_screening: compactScreeningLlmResult(detailResult.llm_result),
239
+ close_result: detailResult.close_result
240
+ };
241
+ }
240
242
 
241
243
  function normalizeScreeningMode(value) {
242
244
  const normalized = String(value || "llm").trim().toLowerCase();
@@ -427,18 +429,19 @@ function compactRefreshAttempt(refreshAttempt) {
427
429
  };
428
430
  }
429
431
 
430
- export function countRecommendResultStatuses(results = [], {
431
- greetCount = 0
432
- } = {}) {
433
- return {
434
- processed: results.length,
435
- screened: results.length,
436
- detail_opened: results.filter((item) => item.detail).length,
437
- passed: results.filter((item) => item.screening?.passed).length,
438
- llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
439
- greet_count: greetCount,
440
- post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
441
- image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
432
+ export function countRecommendResultStatuses(results = [], {
433
+ greetCount = 0
434
+ } = {}) {
435
+ return {
436
+ processed: results.length,
437
+ screened: results.length,
438
+ detail_opened: results.filter((item) => item.detail).length,
439
+ passed: results.filter((item) => item.screening?.passed).length,
440
+ skipped: results.filter((item) => item.screening?.passed === false).length,
441
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
442
+ greet_count: greetCount,
443
+ post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
444
+ image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
442
445
  detail_open_failed: results.filter((item) => (
443
446
  item.error?.code === "DETAIL_STALE_NODE"
444
447
  || item.error?.code === "DETAIL_OPEN_FAILED"
@@ -449,9 +452,17 @@ export function countRecommendResultStatuses(results = [], {
449
452
  || item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
450
453
  || item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
451
454
  || item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
452
- )).length
453
- };
454
- }
455
+ )).length,
456
+ colleague_contact_checked: results.filter((item) => item.detail?.colleague_contact?.checked).length,
457
+ recent_colleague_contact_skipped: results.filter((item) => (
458
+ item.screening?.status === "skip"
459
+ && item.screening?.reasons?.includes("skipped_recent_colleague_contact")
460
+ )).length,
461
+ colleague_contact_panel_missing: results.filter((item) => (
462
+ item.detail?.colleague_contact?.reason === "panel_missing"
463
+ )).length
464
+ };
465
+ }
455
466
 
456
467
  function countPassedResults(results = []) {
457
468
  return countRecommendResultStatuses(results).passed;
@@ -612,10 +623,10 @@ function compactRecoverableDetailError(error) {
612
623
  return compactError(error, isStaleRecommendNodeError(error) ? "DETAIL_STALE_NODE" : "DETAIL_OPEN_FAILED");
613
624
  }
614
625
 
615
- function createRecoverableDetailFailureScreening(candidate, error) {
616
- return {
617
- status: "fail",
618
- passed: false,
626
+ function createRecoverableDetailFailureScreening(candidate, error) {
627
+ return {
628
+ status: "fail",
629
+ passed: false,
619
630
  score: 0,
620
631
  reasons: isStaleRecommendNodeError(error)
621
632
  ? ["detail_open_failed", "stale_node"]
@@ -623,9 +634,22 @@ function createRecoverableDetailFailureScreening(candidate, error) {
623
634
  ? ["detail_open_failed", "detail_open_miss"]
624
635
  : ["detail_open_failed"],
625
636
  error: compactRecoverableDetailError(error),
626
- candidate
627
- };
628
- }
637
+ candidate
638
+ };
639
+ }
640
+
641
+ function createRecentColleagueContactSkipScreening(candidate, colleagueContact) {
642
+ const matched = colleagueContact?.matched_row || null;
643
+ return {
644
+ status: "skip",
645
+ passed: false,
646
+ score: 0,
647
+ reasons: ["skipped_recent_colleague_contact"],
648
+ reason: matched?.text || "Candidate has recent colleague contact history",
649
+ matched_colleague_contact: matched,
650
+ candidate
651
+ };
652
+ }
629
653
 
630
654
  export async function runRecommendWorkflow({
631
655
  client,
@@ -657,15 +681,17 @@ export async function runRecommendWorkflow({
657
681
  executePostAction = true,
658
682
  actionTimeoutMs = 8000,
659
683
  actionIntervalMs = 500,
660
- actionAfterClickDelayMs = 900,
661
- screeningMode = "llm",
662
- llmConfig = null,
684
+ actionAfterClickDelayMs = 900,
685
+ screeningMode = "llm",
686
+ llmConfig = null,
663
687
  llmTimeoutMs = 120000,
664
688
  llmImageLimit = 8,
665
689
  llmImageDetail = "high",
666
690
  imageOutputDir = "",
667
691
  humanRestEnabled = false,
668
- humanBehavior = null
692
+ humanBehavior = null,
693
+ skipRecentColleagueContacted = true,
694
+ colleagueContactWindowDays = 14
669
695
  } = {}, runControl) {
670
696
  if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
671
697
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
@@ -689,10 +715,13 @@ export async function runRecommendWorkflow({
689
715
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
690
716
  const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
691
717
  const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
692
- const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
693
- const useLlmScreening = normalizedScreeningMode !== "deterministic";
694
- const postActionEnabled = normalizedPostAction !== "none";
695
- const targetPassCount = Math.max(1, Number(maxCandidates) || 1);
718
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
719
+ const useLlmScreening = normalizedScreeningMode !== "deterministic";
720
+ const postActionEnabled = normalizedPostAction !== "none";
721
+ const shouldSkipRecentColleagueContacted = skipRecentColleagueContacted !== false;
722
+ const normalizedColleagueContactWindowDays = Math.max(1, Number(colleagueContactWindowDays) || 14);
723
+ const colleagueContactReferenceDate = new Date();
724
+ const targetPassCount = Math.max(1, Number(maxCandidates) || 1);
696
725
  const detailCountLimit = detailLimit == null ? Number.POSITIVE_INFINITY : Math.max(0, Number(detailLimit) || 0);
697
726
  const effectiveDetailLimit = postActionEnabled ? Number.POSITIVE_INFINITY : detailCountLimit;
698
727
  const networkRecorder = effectiveDetailLimit > 0
@@ -785,6 +814,8 @@ export async function runRecommendWorkflow({
785
814
  human_behavior_profile: effectiveHumanBehavior.profile,
786
815
  human_rest_level: effectiveHumanBehavior.restLevel,
787
816
  human_rest_enabled: effectiveHumanRestEnabled,
817
+ skip_recent_colleague_contacted: shouldSkipRecentColleagueContacted,
818
+ colleague_contact_window_days: normalizedColleagueContactWindowDays,
788
819
  human_rest_count: humanRestState.rest_count,
789
820
  human_rest_ms: humanRestState.total_rest_ms,
790
821
  last_human_event: lastHumanEvent,
@@ -1109,9 +1140,11 @@ export async function runRecommendWorkflow({
1109
1140
  let cardCandidate = nextCandidateResult.item.candidate;
1110
1141
 
1111
1142
  let screeningCandidate = cardCandidate;
1112
- let detailResult = null;
1113
- let recoverableDetailError = null;
1114
- let detailStep = "not_started";
1143
+ let detailResult = null;
1144
+ let recoverableDetailError = null;
1145
+ let colleagueContact = null;
1146
+ let skipRecentColleagueContact = false;
1147
+ let detailStep = "not_started";
1115
1148
  if (index < effectiveDetailLimit) {
1116
1149
  try {
1117
1150
  await runControl.waitIfPaused();
@@ -1153,12 +1186,52 @@ export async function runRecommendWorkflow({
1153
1186
  });
1154
1187
  addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
1155
1188
  addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
1156
- cardNodeId = openedDetail.card_node_id || cardNodeId;
1157
- cardCandidate = openedDetail.card_candidate || cardCandidate;
1158
- screeningCandidate = cardCandidate;
1159
- const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1160
- detailStep = "wait_network";
1161
- const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
1189
+ cardNodeId = openedDetail.card_node_id || cardNodeId;
1190
+ cardCandidate = openedDetail.card_candidate || cardCandidate;
1191
+ screeningCandidate = cardCandidate;
1192
+ if (shouldSkipRecentColleagueContacted) {
1193
+ detailStep = "check_colleague_contact";
1194
+ try {
1195
+ colleagueContact = await measureTiming(timings, "colleague_contact_check_ms", () => inspectRecentColleagueContact(
1196
+ client,
1197
+ openedDetail.detail_state,
1198
+ {
1199
+ referenceDate: colleagueContactReferenceDate,
1200
+ windowDays: normalizedColleagueContactWindowDays
1201
+ }
1202
+ ));
1203
+ if (colleagueContact?.recent) {
1204
+ skipRecentColleagueContact = true;
1205
+ detailResult = {
1206
+ candidate: screeningCandidate,
1207
+ detail: {
1208
+ popup_text: "",
1209
+ resume_text: ""
1210
+ },
1211
+ colleague_contact: colleagueContact,
1212
+ cv_acquisition: {
1213
+ source: "skipped_recent_colleague_contact",
1214
+ skipped: true,
1215
+ reason: "skipped_recent_colleague_contact"
1216
+ }
1217
+ };
1218
+ detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1219
+ await maybeHumanActionCooldown("after_detail_close", timings);
1220
+ }
1221
+ } catch (error) {
1222
+ colleagueContact = {
1223
+ checked: false,
1224
+ recent: false,
1225
+ reason: "inspection_failed",
1226
+ error: error?.message || String(error),
1227
+ window_days: normalizedColleagueContactWindowDays
1228
+ };
1229
+ }
1230
+ }
1231
+ if (!skipRecentColleagueContact) {
1232
+ const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1233
+ detailStep = "wait_network";
1234
+ const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
1162
1235
  waitForRecommendDetailNetworkEvents,
1163
1236
  networkRecorder,
1164
1237
  {
@@ -1182,9 +1255,10 @@ export async function runRecommendWorkflow({
1182
1255
  networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
1183
1256
  networkParseIntervalMs: 250
1184
1257
  });
1185
- addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1186
-
1187
- const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1258
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1259
+ if (colleagueContact) detailResult.colleague_contact = colleagueContact;
1260
+
1261
+ const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1188
1262
  let source = "network";
1189
1263
  let imageEvidence = null;
1190
1264
  let captureTarget = null;
@@ -1296,9 +1370,10 @@ export async function runRecommendWorkflow({
1296
1370
  image_evidence: summarizeImageEvidence(imageEvidence),
1297
1371
  capture_target: captureTarget || null,
1298
1372
  capture_target_wait: captureTargetWait
1299
- };
1300
- screeningCandidate = detailResult.candidate;
1301
- } catch (error) {
1373
+ };
1374
+ screeningCandidate = detailResult.candidate;
1375
+ }
1376
+ } catch (error) {
1302
1377
  if (!isRecoverableRecommendDetailError(error)) throw error;
1303
1378
  const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1304
1379
  if (recoveryCount < 1) {
@@ -1325,11 +1400,11 @@ export async function runRecommendWorkflow({
1325
1400
  await runControl.waitIfPaused();
1326
1401
  runControl.throwIfCanceled();
1327
1402
  runControl.setPhase("recommend:screening");
1328
- let llmResult = null;
1329
- if (useLlmScreening) {
1330
- if (recoverableDetailError || detailResult?.image_evidence?.ok === false) {
1331
- llmResult = null;
1332
- } else if (!llmConfig) {
1403
+ let llmResult = null;
1404
+ if (useLlmScreening) {
1405
+ if (skipRecentColleagueContact || recoverableDetailError || detailResult?.image_evidence?.ok === false) {
1406
+ llmResult = null;
1407
+ } else if (!llmConfig) {
1333
1408
  llmResult = createMissingLlmConfigResult();
1334
1409
  } else {
1335
1410
  try {
@@ -1354,12 +1429,14 @@ export async function runRecommendWorkflow({
1354
1429
  }
1355
1430
  llmResult = createFailedLlmScreeningResult(error);
1356
1431
  }
1357
- }
1358
- if (detailResult) detailResult.llm_result = llmResult;
1359
- }
1360
- const screening = recoverableDetailError
1361
- ? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
1362
- : detailResult?.image_evidence?.ok === false
1432
+ }
1433
+ if (detailResult) detailResult.llm_result = llmResult;
1434
+ }
1435
+ const screening = skipRecentColleagueContact
1436
+ ? createRecentColleagueContactSkipScreening(screeningCandidate, colleagueContact)
1437
+ : recoverableDetailError
1438
+ ? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
1439
+ : detailResult?.image_evidence?.ok === false
1363
1440
  ? createImageCaptureFailureScreening(screeningCandidate, {
1364
1441
  code: detailResult.image_evidence.error_code,
1365
1442
  message: detailResult.image_evidence.error
@@ -1371,7 +1448,7 @@ export async function runRecommendWorkflow({
1371
1448
  let postActionResult = null;
1372
1449
  let closeFailureError = null;
1373
1450
  let closeRecoveryFailure = null;
1374
- if (postActionEnabled && detailResult) {
1451
+ if (postActionEnabled && detailResult && !skipRecentColleagueContact) {
1375
1452
  const postActionStarted = Date.now();
1376
1453
  await runControl.waitIfPaused();
1377
1454
  runControl.throwIfCanceled();
@@ -1397,7 +1474,7 @@ export async function runRecommendWorkflow({
1397
1474
  }
1398
1475
  addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
1399
1476
  }
1400
- if (detailResult && closeDetail) {
1477
+ if (detailResult && closeDetail && !detailResult.close_result?.closed) {
1401
1478
  detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
1402
1479
  await maybeHumanActionCooldown("after_detail_close", timings);
1403
1480
  if (!detailResult.close_result?.closed) {
@@ -1597,6 +1674,8 @@ export function createRecommendRunService({
1597
1674
  imageOutputDir = "",
1598
1675
  humanRestEnabled = false,
1599
1676
  humanBehavior = null,
1677
+ skipRecentColleagueContacted = true,
1678
+ colleagueContactWindowDays = 14,
1600
1679
  name = "recommend-domain-run"
1601
1680
  } = {}) {
1602
1681
  if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
@@ -1605,6 +1684,8 @@ export function createRecommendRunService({
1605
1684
  const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
1606
1685
  const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
1607
1686
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1687
+ const shouldSkipRecentColleagueContacted = skipRecentColleagueContacted !== false;
1688
+ const normalizedColleagueContactWindowDays = Math.max(1, Number(colleagueContactWindowDays) || 14);
1608
1689
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
1609
1690
  legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
1610
1691
  });
@@ -1649,6 +1730,8 @@ export function createRecommendRunService({
1649
1730
  llm_image_limit: llmImageLimit,
1650
1731
  llm_image_detail: llmImageDetail,
1651
1732
  image_output_dir: imageOutputDir || "",
1733
+ skip_recent_colleague_contacted: shouldSkipRecentColleagueContacted,
1734
+ colleague_contact_window_days: normalizedColleagueContactWindowDays,
1652
1735
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1653
1736
  human_behavior_profile: effectiveHumanBehavior.profile,
1654
1737
  human_behavior: effectiveHumanBehavior,
@@ -1662,13 +1745,17 @@ export function createRecommendRunService({
1662
1745
  processed: 0,
1663
1746
  screened: 0,
1664
1747
  detail_opened: 0,
1665
- llm_screened: 0,
1666
- passed: 0,
1667
- greet_count: 0,
1748
+ llm_screened: 0,
1749
+ passed: 0,
1750
+ skipped: 0,
1751
+ greet_count: 0,
1668
1752
  post_action_clicked: 0,
1669
1753
  image_capture_failed: 0,
1670
1754
  detail_open_failed: 0,
1671
1755
  transient_recovered: 0,
1756
+ colleague_contact_checked: 0,
1757
+ recent_colleague_contact_skipped: 0,
1758
+ colleague_contact_panel_missing: 0,
1672
1759
  context_recoveries: 0,
1673
1760
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1674
1761
  human_behavior_profile: effectiveHumanBehavior.profile,
@@ -1717,7 +1804,9 @@ export function createRecommendRunService({
1717
1804
  llmImageDetail,
1718
1805
  imageOutputDir,
1719
1806
  humanRestEnabled: effectiveHumanRestEnabled,
1720
- humanBehavior: effectiveHumanBehavior
1807
+ humanBehavior: effectiveHumanBehavior,
1808
+ skipRecentColleagueContacted: shouldSkipRecentColleagueContacted,
1809
+ colleagueContactWindowDays: normalizedColleagueContactWindowDays
1721
1810
  }, runControl)
1722
1811
  });
1723
1812
  }
@@ -110,6 +110,10 @@ export const RECRUIT_SEARCH_SELECTORS = Object.freeze({
110
110
  "label.checkbox",
111
111
  '[ka="search_change_view_resume"]'
112
112
  ],
113
+ exchangeResumeLabel: [
114
+ 'label.checkbox.high_search_checkbox[ka="search_change_exchange_resume"]',
115
+ '[ka="search_change_exchange_resume"]'
116
+ ],
113
117
  experienceOption: [
114
118
  ".experience-select .exp-item",
115
119
  ".experience-select li",
@@ -172,6 +172,16 @@ function normalizeRecentViewedOverride(value) {
172
172
  return null;
173
173
  }
174
174
 
175
+ function normalizeBooleanOverride(value) {
176
+ if (typeof value === "boolean") return value;
177
+ if (typeof value === "number") return value !== 0;
178
+ const normalized = normalizeText(value).toLowerCase();
179
+ if (!normalized) return null;
180
+ if (["true", "yes", "y", "1", "on", "enable", "enabled", "需要", "是", "开启"].includes(normalized)) return true;
181
+ if (["false", "no", "n", "0", "off", "disable", "disabled", "不需要", "否", "关闭"].includes(normalized)) return false;
182
+ return null;
183
+ }
184
+
175
185
  function normalizeStringOverride(value) {
176
186
  if (typeof value !== "string") return null;
177
187
  const normalized = value.trim();
@@ -483,6 +493,13 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
483
493
  const rawInstruction = String(instruction || "");
484
494
  const text = normalizeText(rawInstruction);
485
495
  const finalConfirmed = confirmation?.final_confirmed === true;
496
+ const hasSkipRecentColleagueOverride = Object.prototype.hasOwnProperty.call(
497
+ overrides || {},
498
+ "skip_recent_colleague_contacted"
499
+ );
500
+ const confirmationSkipRecentColleagueContacted = normalizeBooleanOverride(
501
+ confirmation?.skip_recent_colleague_contacted_value
502
+ );
486
503
  const explicitSchools = extractSchoolFilterExplicit(rawInstruction);
487
504
  const explicitRecentViewed = extractRecentViewedExplicit(rawInstruction);
488
505
  const explicitKeyword = extractFieldLineValue(rawInstruction, ["搜索关键词", "关键词", "keyword"]);
@@ -509,6 +526,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
509
526
  schools: explicitSchools.explicit ? explicitSchools.schools : extractSchools(text),
510
527
  schools_explicit: explicitSchools.explicit,
511
528
  filter_recent_viewed: explicitRecentViewed !== null ? explicitRecentViewed : extractRecentViewedFilter(text),
529
+ skip_recent_colleague_contacted: confirmationSkipRecentColleagueContacted ?? true,
512
530
  keyword_explicit: explicitKeyword || extractKeywordExplicit(text),
513
531
  keyword_auto: extractKeywordAuto(text),
514
532
  target_count: explicitTargetCount || extractTargetCount(text),
@@ -576,6 +594,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
576
594
  ? overrides.filter_recent_viewed
577
595
  : overrides.recent_not_view
578
596
  );
597
+ const overrideSkipRecentColleagueContacted = normalizeBooleanOverride(overrides.skip_recent_colleague_contacted);
579
598
  const overridePostAction = normalizePostAction(overrides.post_action);
580
599
  if (overrideCity) parsed.city = overrideCity;
581
600
  if (overrideDegree) parsed.degree = overrideDegree;
@@ -604,6 +623,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
604
623
  if (overrideJob) parsed.job_override = overrideJob;
605
624
  if (overrideCriteria) parsed.criteria_override = overrideCriteria;
606
625
  if (overrideRecentViewed !== null) parsed.filter_recent_viewed = overrideRecentViewed;
626
+ if (overrideSkipRecentColleagueContacted !== null) parsed.skip_recent_colleague_contacted = overrideSkipRecentColleagueContacted;
607
627
  if (overridePostAction) parsed.post_action_override = overridePostAction;
608
628
  if (Number.isFinite(overrides.max_greet_count) && overrides.max_greet_count > 0) {
609
629
  parsed.max_greet_count_override = Number.parseInt(String(overrides.max_greet_count), 10);
@@ -628,6 +648,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
628
648
  gender: parsed.gender,
629
649
  age: parsed.age,
630
650
  filter_recent_viewed: parsed.filter_recent_viewed,
651
+ skip_recent_colleague_contacted: parsed.skip_recent_colleague_contacted !== false,
631
652
  keyword: keywordResolution.keyword
632
653
  };
633
654
  const criteria = parsed.criteria_override || confirmationCriteria || parsed.criteria_explicit || null;
@@ -642,7 +663,9 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
642
663
  criteria,
643
664
  target_count: parsed.target_count,
644
665
  post_action: postAction,
645
- max_greet_count: maxGreetCount
666
+ max_greet_count: maxGreetCount,
667
+ skip_recent_colleague_contacted: parsed.skip_recent_colleague_contacted !== false,
668
+ search_exchange_resume_filter_days: 30
646
669
  };
647
670
  const missingBeforeDefaults = collectMissingFields(baseSearchParams, baseScreenParams, parsed);
648
671
 
@@ -659,6 +682,12 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
659
682
  const missingAfterDefaults = collectUnresolvedMissingFields(missingBeforeDefaults, appliedDefaults);
660
683
  const suspicious_fields = collectSuspiciousFields(searchParams, screenParams);
661
684
  const needs_recent_viewed_filter_confirmation = !finalConfirmed && searchParams.filter_recent_viewed === null;
685
+ const needs_skip_recent_colleague_contacted_confirmation = (
686
+ !finalConfirmed
687
+ && !hasSkipRecentColleagueOverride
688
+ && confirmationSkipRecentColleagueContacted === null
689
+ && confirmation?.skip_recent_colleague_contacted_confirmed !== true
690
+ );
662
691
  const needs_criteria_confirmation = Boolean(screenParams.criteria) && !finalConfirmed && confirmation?.criteria_confirmed !== true;
663
692
  const pending_questions = [
664
693
  ...buildMissingFieldQuestions(missingAfterDefaults, defaultPreview),
@@ -672,6 +701,17 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
672
701
  ]
673
702
  }]
674
703
  : []),
704
+ ...(needs_skip_recent_colleague_contacted_confirmation
705
+ ? [{
706
+ field: "skip_recent_colleague_contacted",
707
+ question: "是否跳过近期已被同事触达的人选?搜索页会开启 Boss 的“近30天未和同事交换简历”过滤。",
708
+ value: true,
709
+ options: [
710
+ { label: "跳过(推荐)", value: true },
711
+ { label: "不跳过", value: false }
712
+ ]
713
+ }]
714
+ : []),
675
715
  ...(needs_criteria_confirmation
676
716
  ? [{
677
717
  field: "criteria",
@@ -705,6 +745,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
705
745
  suspicious_fields,
706
746
  needs_keyword_confirmation: keywordResolution.needsConfirmation,
707
747
  needs_recent_viewed_filter_confirmation,
748
+ needs_skip_recent_colleague_contacted_confirmation,
708
749
  needs_criteria_confirmation,
709
750
  needs_search_params_confirmation: !finalConfirmed && confirmation?.search_params_confirmed !== true,
710
751
  proposed_keyword: keywordResolution.proposedKeyword,
@@ -12,7 +12,8 @@ import {
12
12
  configureHumanInteraction,
13
13
  createHumanRestController,
14
14
  humanDelay,
15
- normalizeHumanBehaviorOptions
15
+ normalizeHumanBehaviorOptions,
16
+ sleep
16
17
  } from "../../core/browser/index.js";
17
18
  import {
18
19
  compactCvAcquisitionState,
@@ -74,7 +75,10 @@ import {
74
75
  RECRUIT_CARD_SELECTOR,
75
76
  RECRUIT_LIST_CONTAINER_SELECTORS
76
77
  } from "./constants.js";
77
- import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
78
+ import {
79
+ describeGreetQuotaAfterSpend,
80
+ GREET_CREDITS_EXHAUSTED_CODE
81
+ } from "../../core/greet-quota/index.js";
78
82
 
79
83
  function compactScreening(screening) {
80
84
  return {
@@ -176,7 +180,8 @@ async function runRecruitPostAction({
176
180
  greetCount = 0,
177
181
  maxGreetCount = null,
178
182
  executePostAction = true,
179
- afterClickDelayMs = 900
183
+ afterClickDelayMs = 900,
184
+ lastGreetQuotaAfterSpend = null
180
185
  } = {}) {
181
186
  const plan = resolveRecruitPostAction({
182
187
  postAction,
@@ -206,6 +211,13 @@ async function runRecruitPostAction({
206
211
  const summary = actionDiscovery?.summary || {};
207
212
  const control = summary.greet?.control || summary.greet;
208
213
  if (!control?.found && !control?.node_id) {
214
+ if (plan.effective === "greet" && lastGreetQuotaAfterSpend?.exhausted_after_spend) {
215
+ result.reason = "greet_credits_exhausted";
216
+ result.out_of_greet_credits = true;
217
+ result.stop_run = true;
218
+ result.greet_quota_after_last_click = lastGreetQuotaAfterSpend;
219
+ return result;
220
+ }
209
221
  result.reason = `${plan.effective}_control_not_found`;
210
222
  return result;
211
223
  }
@@ -256,6 +268,9 @@ async function runRecruitPostAction({
256
268
  }
257
269
  result.click_result = clickResult;
258
270
  result.action_clicked = true;
271
+ result.greet_quota_after_click = describeGreetQuotaAfterSpend(
272
+ clickResult.greet_quota?.found ? clickResult.greet_quota : control.greet_quota || control.label || ""
273
+ );
259
274
  result.counted_as_greet = plan.effective === "greet";
260
275
  result.reason = "clicked";
261
276
  if (afterClickDelayMs > 0) await sleep(afterClickDelayMs);
@@ -578,6 +593,8 @@ export async function runRecruitWorkflow({
578
593
  const normalizedPostAction = normalizeRecruitPostAction(postAction);
579
594
  const postActionEnabled = normalizedPostAction !== "none";
580
595
  const useLlmScreening = normalizedScreeningMode !== "deterministic";
596
+ const searchExchangeResumeFilterRequested = normalizedSearchParams.skip_recent_colleague_contacted !== false;
597
+ let searchExchangeResumeFilterApplied = false;
581
598
  const limit = Math.max(1, Number(maxCandidates) || 1);
582
599
  const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
583
600
  const networkRecorder = detailCountLimit > 0
@@ -603,6 +620,7 @@ export async function runRecruitWorkflow({
603
620
  const results = [];
604
621
  const refreshAttempts = [];
605
622
  let greetCount = 0;
623
+ let lastGreetQuotaAfterSpend = null;
606
624
  let refreshRounds = 0;
607
625
  let contextRecoveryAttempts = 0;
608
626
  const candidateRecoveryCounts = new Map();
@@ -668,6 +686,8 @@ export async function runRecruitWorkflow({
668
686
  human_rest_enabled: effectiveHumanRestEnabled,
669
687
  human_rest_count: humanRestState.rest_count,
670
688
  human_rest_ms: humanRestState.total_rest_ms,
689
+ search_exchange_resume_filter_requested: searchExchangeResumeFilterRequested ? 1 : 0,
690
+ search_exchange_resume_filter_applied: searchExchangeResumeFilterApplied ? 1 : 0,
671
691
  last_human_event: lastHumanEvent,
672
692
  ...extra
673
693
  });
@@ -812,6 +832,12 @@ export async function runRecruitWorkflow({
812
832
  resetTimeoutMs,
813
833
  cityOptionTimeoutMs
814
834
  });
835
+ const exchangeResumeStep = searchResult.steps.find((step) => step.step === "exchange_resume");
836
+ searchExchangeResumeFilterApplied = Boolean(
837
+ searchExchangeResumeFilterRequested
838
+ && exchangeResumeStep?.result?.applied
839
+ && exchangeResumeStep?.result?.requested === true
840
+ );
815
841
  runControl.checkpoint({
816
842
  search: {
817
843
  search_params: searchResult.search_params,
@@ -1248,11 +1274,15 @@ export async function runRecruitWorkflow({
1248
1274
  greetCount,
1249
1275
  maxGreetCount: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
1250
1276
  executePostAction,
1251
- afterClickDelayMs: actionAfterClickDelayMs
1277
+ afterClickDelayMs: actionAfterClickDelayMs,
1278
+ lastGreetQuotaAfterSpend
1252
1279
  });
1253
1280
  if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
1254
1281
  greetCount += 1;
1255
1282
  }
1283
+ if (postActionResult.greet_quota_after_click?.found) {
1284
+ lastGreetQuotaAfterSpend = postActionResult.greet_quota_after_click;
1285
+ }
1256
1286
  addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
1257
1287
  }
1258
1288
  if (postActionEnabled && detailResult && closeDetail) {
@@ -1390,6 +1420,9 @@ export async function runRecruitWorkflow({
1390
1420
  human_rest: humanRestController.getState(),
1391
1421
  last_human_event: lastHumanEvent,
1392
1422
  list_end_reason: listEndReason || null,
1423
+ search_exchange_resume_filter_requested: searchExchangeResumeFilterRequested ? 1 : 0,
1424
+ search_exchange_resume_filter_applied: searchExchangeResumeFilterApplied ? 1 : 0,
1425
+ last_greet_quota_after_spend: lastGreetQuotaAfterSpend,
1393
1426
  refresh_rounds: refreshRounds,
1394
1427
  refresh_attempts: refreshAttempts,
1395
1428
  context_recoveries: contextRecoveryAttempts,
@@ -1451,6 +1484,7 @@ export function createRecruitRunService({
1451
1484
  const normalizedSearchParams = normalizeSearchParams(searchParams);
1452
1485
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
1453
1486
  const normalizedPostAction = normalizeRecruitPostAction(postAction);
1487
+ const searchExchangeResumeFilterRequested = normalizedSearchParams.skip_recent_colleague_contacted !== false;
1454
1488
  const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
1455
1489
  const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
1456
1490
  const effectiveHumanBehavior = normalizeHumanBehaviorOptions(humanBehavior, {
@@ -1493,6 +1527,8 @@ export function createRecruitRunService({
1493
1527
  human_behavior: effectiveHumanBehavior,
1494
1528
  human_rest_level: effectiveHumanBehavior.restLevel,
1495
1529
  human_rest_enabled: effectiveHumanRestEnabled,
1530
+ search_exchange_resume_filter_requested: searchExchangeResumeFilterRequested ? 1 : 0,
1531
+ search_exchange_resume_filter_applied: 0,
1496
1532
  post_action: normalizedPostAction,
1497
1533
  max_greet_count: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
1498
1534
  execute_post_action: Boolean(executePostAction),
@@ -1520,6 +1556,8 @@ export function createRecruitRunService({
1520
1556
  human_rest_ms: 0,
1521
1557
  greet_count: 0,
1522
1558
  post_action_clicked: 0,
1559
+ search_exchange_resume_filter_requested: searchExchangeResumeFilterRequested ? 1 : 0,
1560
+ search_exchange_resume_filter_applied: 0,
1523
1561
  last_human_event: null
1524
1562
  },
1525
1563
  checkpoint: {},