@reconcrap/boss-recommend-mcp 2.0.13 → 2.0.15

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.13",
3
+ "version": "2.0.15",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -223,6 +223,15 @@ function matchJobOption(option, jobLabel = "") {
223
223
  ));
224
224
  }
225
225
 
226
+ function activeMatchingJobOption(options = [], jobLabel = "") {
227
+ return (options || []).find((option) => option.active && matchJobOption(option, jobLabel)) || null;
228
+ }
229
+
230
+ function selectedLabelMatches(label = "", jobLabel = "") {
231
+ const normalized = normalizeJobText(label);
232
+ return Boolean(normalized && matchJobOption({ label: normalized, value: normalized, title: normalized }, jobLabel));
233
+ }
234
+
226
235
  async function clickFirstVisible(client, rootNodeId, selectors = []) {
227
236
  for (const selector of selectors) {
228
237
  const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
@@ -247,6 +256,62 @@ async function clickFirstVisible(client, rootNodeId, selectors = []) {
247
256
  };
248
257
  }
249
258
 
259
+ async function waitForChatJobOptions(client, rootNodeId, {
260
+ timeoutMs = 12000,
261
+ intervalMs = 300,
262
+ requireVisible = false
263
+ } = {}) {
264
+ const started = Date.now();
265
+ let latest = null;
266
+ while (Date.now() - started <= timeoutMs) {
267
+ const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
268
+ latest = await readChatJobOptions(client, currentRootNodeId, {
269
+ timeoutMs: Math.min(intervalMs, 300),
270
+ intervalMs
271
+ });
272
+ const options = latest.job_options || [];
273
+ if (options.length && (!requireVisible || options.some((option) => option.visible))) {
274
+ return latest;
275
+ }
276
+ await sleep(intervalMs);
277
+ }
278
+ return latest || {
279
+ selector: "",
280
+ source: "chat-job-list",
281
+ selected_label: "",
282
+ job_options: []
283
+ };
284
+ }
285
+
286
+ async function waitForSelectedChatJob(client, rootNodeId, jobLabel = "", {
287
+ timeoutMs = 5000,
288
+ intervalMs = 300
289
+ } = {}) {
290
+ const started = Date.now();
291
+ let latest = null;
292
+ while (Date.now() - started <= timeoutMs) {
293
+ const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
294
+ latest = await readChatJobOptions(client, currentRootNodeId, {
295
+ timeoutMs: Math.min(intervalMs, 300),
296
+ intervalMs
297
+ });
298
+ if (
299
+ selectedLabelMatches(latest.selected_label, jobLabel)
300
+ || activeMatchingJobOption(latest.job_options || [], jobLabel)
301
+ ) {
302
+ return {
303
+ verified: true,
304
+ result: latest
305
+ };
306
+ }
307
+ await sleep(intervalMs);
308
+ }
309
+ return {
310
+ verified: false,
311
+ result: latest
312
+ };
313
+ }
314
+
250
315
  export async function selectChatJob(client, rootNodeId, {
251
316
  jobLabel = "",
252
317
  timeoutMs = 12000,
@@ -267,14 +332,34 @@ export async function selectChatJob(client, rootNodeId, {
267
332
  intervalMs
268
333
  });
269
334
  let matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
335
+ if (
336
+ matched
337
+ && (
338
+ matched.active
339
+ || selectedLabelMatches(optionsResult.selected_label, matched.label)
340
+ || selectedLabelMatches(optionsResult.selected_label, requested)
341
+ )
342
+ ) {
343
+ return {
344
+ selected: true,
345
+ verified: true,
346
+ already_current: true,
347
+ requested,
348
+ selected_option: matched,
349
+ options: optionsResult.job_options || [],
350
+ selected_label: optionsResult.selected_label || matched.label
351
+ };
352
+ }
353
+
270
354
  if (!matched || !matched.visible) {
271
355
  const triggerRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
272
356
  const trigger = await clickFirstVisible(client, triggerRootNodeId, CHAT_JOB_TRIGGER_SELECTORS);
273
357
  if (settleMs > 0) await sleep(settleMs);
274
358
  currentRootNodeId = await freshTopRootNodeId(client, triggerRootNodeId);
275
- optionsResult = await readChatJobOptions(client, currentRootNodeId, {
359
+ optionsResult = await waitForChatJobOptions(client, currentRootNodeId, {
276
360
  timeoutMs,
277
- intervalMs
361
+ intervalMs,
362
+ requireVisible: true
278
363
  });
279
364
  matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
280
365
  if (!matched || !matched.visible) {
@@ -292,6 +377,7 @@ export async function selectChatJob(client, rootNodeId, {
292
377
  if (matched.active || normalizeJobText(optionsResult.selected_label).toLowerCase() === normalizeJobText(matched.label).toLowerCase()) {
293
378
  return {
294
379
  selected: true,
380
+ verified: true,
295
381
  already_current: true,
296
382
  requested,
297
383
  selected_option: matched,
@@ -310,22 +396,27 @@ export async function selectChatJob(client, rootNodeId, {
310
396
  if (settleMs > 0) await sleep(settleMs);
311
397
 
312
398
  const afterRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
313
- const after = await readChatJobOptions(client, afterRootNodeId, {
314
- timeoutMs: Math.min(timeoutMs, 3000),
399
+ const verification = await waitForSelectedChatJob(client, afterRootNodeId, matched.label, {
400
+ timeoutMs: Math.min(timeoutMs, 5000),
315
401
  intervalMs
316
402
  });
403
+ const after = verification.result || {
404
+ selected_label: "",
405
+ job_options: []
406
+ };
317
407
  const afterMatch = (after.job_options || []).find((option) => matchJobOption(option, matched.label)) || matched;
318
- const selectedLabel = normalizeJobText(after.selected_label || afterMatch.label || "");
319
- const verified = selectedLabel
320
- ? matchJobOption({ label: selectedLabel, value: selectedLabel, title: selectedLabel }, matched.label)
321
- : true;
408
+ const selectedLabel = normalizeJobText(after.selected_label || "");
409
+ const activeMatch = activeMatchingJobOption(after.job_options || [], matched.label);
410
+ const verified = Boolean(verification.verified || selectedLabelMatches(selectedLabel, matched.label) || activeMatch);
322
411
 
323
412
  return {
324
- selected: true,
413
+ selected: verified,
325
414
  verified,
326
415
  already_current: false,
416
+ reason: verified ? "verified" : "job_selection_not_verified",
327
417
  requested,
328
418
  selected_option: afterMatch,
419
+ active_option: activeMatch,
329
420
  options: after.job_options || optionsResult.job_options || [],
330
421
  selected_label: selectedLabel,
331
422
  before: optionsResult,
@@ -203,6 +203,115 @@ function createSkippedDetailResult(cardCandidate, reason, error = null) {
203
203
  };
204
204
  }
205
205
 
206
+ const CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH = 500;
207
+ const CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH = 180;
208
+ const CHAT_FULL_CV_SECTION_PATTERNS = Object.freeze([
209
+ /教育(?:经历|背景|经验)?/i,
210
+ /工作(?:经历|经验)?/i,
211
+ /项目(?:经历|经验)?/i,
212
+ /实习(?:经历|经验)?/i,
213
+ /科研(?:经历|经验)?/i,
214
+ /论文|会议|专利/i,
215
+ /个人(?:优势|总结|介绍|评价)/i,
216
+ /专业技能|技能(?:特长|标签)?/i,
217
+ /求职(?:期望|意向)/i,
218
+ /校园经历|在校经历|竞赛|证书/i
219
+ ]);
220
+
221
+ function detailTextForFullCvCheck(detailResult = {}) {
222
+ return [
223
+ detailResult?.detail?.popup_text,
224
+ detailResult?.detail?.content_text,
225
+ detailResult?.detail?.resume_iframe_text
226
+ ].filter(Boolean).join("\n\n");
227
+ }
228
+
229
+ function resumeSectionMatchCount(text = "") {
230
+ const normalized = normalizeText(text);
231
+ if (!normalized) return 0;
232
+ return CHAT_FULL_CV_SECTION_PATTERNS
233
+ .filter((pattern) => pattern.test(normalized))
234
+ .length;
235
+ }
236
+
237
+ function hasResumeLikeDomText(text = "") {
238
+ const normalized = normalizeText(text);
239
+ if (normalized.length >= CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH) return true;
240
+ return normalized.length >= CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH
241
+ && resumeSectionMatchCount(normalized) >= 2;
242
+ }
243
+
244
+ function networkProfileTextLength(profileResult = {}) {
245
+ return normalizeText(profileResult?.profile?.text || "").length;
246
+ }
247
+
248
+ function isFullCvNetworkProfile(profileResult = {}) {
249
+ if (!profileResult?.ok) return false;
250
+ const sourceKeys = profileResult.profile?.source_keys || {};
251
+ const textLength = networkProfileTextLength(profileResult);
252
+ const sectionCount = resumeSectionMatchCount(profileResult.profile?.text || "");
253
+
254
+ if (sourceKeys.geek_detail_info || sourceKeys.geek_detail) return true;
255
+ if (sourceKeys.network_html_text) {
256
+ return textLength >= CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH
257
+ || sectionCount >= 2;
258
+ }
259
+ if (sourceKeys.chat_history_resume) {
260
+ const educationCount = Number(sourceKeys.education_count) || 0;
261
+ const workCount = Number(sourceKeys.work_count) || 0;
262
+ return (educationCount + workCount) > 0
263
+ && (textLength >= CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH || sectionCount >= 2);
264
+ }
265
+ return false;
266
+ }
267
+
268
+ function hasUsableImageEvidence(imageEvidence = null) {
269
+ if (!imageEvidence || imageEvidence.ok === false) return false;
270
+ return Boolean(
271
+ (Array.isArray(imageEvidence.llm_file_paths) && imageEvidence.llm_file_paths.length)
272
+ || (Array.isArray(imageEvidence.file_paths) && imageEvidence.file_paths.length)
273
+ || Number(imageEvidence.llm_screenshot_count) > 0
274
+ || Number(imageEvidence.unique_screenshot_count) > 0
275
+ || Number(imageEvidence.screenshot_count) > 0
276
+ || Number(imageEvidence.capture_count) > 0
277
+ );
278
+ }
279
+
280
+ export function summarizeChatFullCvEvidence({
281
+ detailResult = null,
282
+ contentWait = null,
283
+ imageEvidence = null
284
+ } = {}) {
285
+ const parsedProfiles = (detailResult?.parsed_network_profiles || []).filter((item) => item?.ok);
286
+ const fullNetworkProfiles = parsedProfiles.filter(isFullCvNetworkProfile);
287
+ const profileOnlyCount = Math.max(0, parsedProfiles.length - fullNetworkProfiles.length);
288
+ const detailText = detailTextForFullCvCheck(detailResult);
289
+ const domTextLength = detailText.length;
290
+ const domSectionCount = resumeSectionMatchCount(detailText);
291
+ const domFullCv = Boolean(contentWait?.ok) && hasResumeLikeDomText(detailText);
292
+ const imageFullCv = hasUsableImageEvidence(imageEvidence);
293
+ const source = fullNetworkProfiles.length
294
+ ? "network"
295
+ : domFullCv
296
+ ? "dom"
297
+ : imageFullCv
298
+ ? "image"
299
+ : null;
300
+ return {
301
+ full_cv_acquired: Boolean(source),
302
+ source,
303
+ network_full_cv_count: fullNetworkProfiles.length,
304
+ network_profile_only_count: profileOnlyCount,
305
+ parsed_network_profile_count: parsedProfiles.length,
306
+ dom_full_cv: domFullCv,
307
+ dom_text_length: domTextLength,
308
+ dom_section_count: domSectionCount,
309
+ content_wait_ok: Boolean(contentWait?.ok),
310
+ image_full_cv: imageFullCv,
311
+ image_summary: summarizeImageEvidence(imageEvidence)
312
+ };
313
+ }
314
+
206
315
  async function resolveFreshChatCardNodeId(client, {
207
316
  fallbackNodeId,
208
317
  candidate,
@@ -306,6 +415,9 @@ async function setupChatRunContext(client, {
306
415
  if (normalizeText(job) && !jobSelection.selected) {
307
416
  throw new Error(`Chat job selection failed: ${jobSelection.reason || "unknown"}`);
308
417
  }
418
+ if (normalizeText(job) && jobSelection.verified !== true) {
419
+ throw new Error(`Chat job selection was not verified: requested=${jobSelection.requested || job}; selected=${jobSelection.selected_label || "unknown"}`);
420
+ }
309
421
  rootState = await getChatRoots(client);
310
422
  if (ensureViewport) {
311
423
  rootState = await ensureViewport(rootState, "context_job");
@@ -770,11 +882,12 @@ export async function runChatWorkflow({
770
882
  });
771
883
  addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
772
884
  parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
773
- if (parsedNetworkProfileCount > 0) {
885
+ const networkEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
886
+ if (networkEvidence.network_full_cv_count > 0) {
774
887
  contentWait = {
775
888
  ok: true,
776
889
  skipped: true,
777
- reason: "network_profile_parsed_before_dom_wait",
890
+ reason: "network_full_cv_parsed_before_dom_wait",
778
891
  elapsed_ms: 0,
779
892
  text_length: 0
780
893
  };
@@ -816,8 +929,9 @@ export async function runChatWorkflow({
816
929
  let imageEvidence = null;
817
930
  let llmResult = null;
818
931
  const captureNodeId = captureNodeIdFromResumeState(resumeState);
932
+ let fullCvEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
819
933
  const shouldCaptureImage = normalizedDetailSource === "image"
820
- || (normalizedDetailSource === "cascade" && parsedNetworkProfileCount < 1);
934
+ || (normalizedDetailSource === "cascade" && !fullCvEvidence.full_cv_acquired);
821
935
  if (shouldCaptureImage) {
822
936
  if (captureNodeId) {
823
937
  detailStep = "capture_image_fallback";
@@ -858,12 +972,20 @@ export async function runChatWorkflow({
858
972
  }
859
973
  }));
860
974
  source = "image";
975
+ fullCvEvidence = summarizeChatFullCvEvidence({
976
+ detailResult,
977
+ contentWait,
978
+ imageEvidence
979
+ });
861
980
  recordCvImageFallback(cvAcquisitionState, {
981
+ reason: fullCvEvidence.network_profile_only_count > 0
982
+ ? "profile_only_network_image_fallback"
983
+ : "network_miss_image_fallback",
862
984
  parsedNetworkProfileCount,
863
985
  waitResult: networkWait,
864
986
  imageEvidence
865
987
  });
866
- if (callLlmOnImage) {
988
+ if (callLlmOnImage && fullCvEvidence.full_cv_acquired) {
867
989
  detailStep = "llm_image_screening";
868
990
  if (!llmConfig) {
869
991
  llmResult = createMissingLlmConfigResult();
@@ -885,14 +1007,39 @@ export async function runChatWorkflow({
885
1007
  }
886
1008
  } else {
887
1009
  source = "missing_capture_node";
1010
+ fullCvEvidence = summarizeChatFullCvEvidence({
1011
+ detailResult,
1012
+ contentWait,
1013
+ imageEvidence
1014
+ });
888
1015
  recordCvNetworkMiss(cvAcquisitionState, {
889
1016
  reason: "network_miss_no_capture_node",
890
1017
  parsedNetworkProfileCount,
891
1018
  waitResult: networkWait
892
1019
  });
893
1020
  }
894
- } else if (parsedNetworkProfileCount > 0) {
1021
+ } else if (fullCvEvidence.network_full_cv_count > 0) {
1022
+ source = "network";
895
1023
  recordCvNetworkHit(cvAcquisitionState, {
1024
+ reason: "full_cv_network_profile",
1025
+ parsedNetworkProfileCount,
1026
+ waitResult: networkWait
1027
+ });
1028
+ } else if (fullCvEvidence.dom_full_cv) {
1029
+ source = "dom";
1030
+ if (normalizedDetailSource !== "dom") {
1031
+ recordCvNetworkMiss(cvAcquisitionState, {
1032
+ reason: parsedNetworkProfileCount > 0
1033
+ ? "profile_only_network_dom_fallback"
1034
+ : "network_miss_dom_fallback",
1035
+ parsedNetworkProfileCount,
1036
+ waitResult: networkWait
1037
+ });
1038
+ }
1039
+ } else if (parsedNetworkProfileCount > 0) {
1040
+ source = "profile_only_network";
1041
+ recordCvNetworkMiss(cvAcquisitionState, {
1042
+ reason: "profile_only_network_not_full_cv",
896
1043
  parsedNetworkProfileCount,
897
1044
  waitResult: networkWait
898
1045
  });
@@ -906,25 +1053,29 @@ export async function runChatWorkflow({
906
1053
  }
907
1054
 
908
1055
  if (useLlmScreening && !llmResult) {
909
- detailStep = "llm_screening";
910
- if (!llmConfig) {
911
- llmResult = createMissingLlmConfigResult();
1056
+ if (!fullCvEvidence.full_cv_acquired) {
1057
+ detailUnavailableReason = "full_cv_not_acquired";
912
1058
  } else {
913
- try {
914
- const llmTimingKey = imageEvidence?.file_paths?.length
915
- ? "vision_model_ms"
916
- : "text_model_ms";
917
- llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
918
- candidate: detailResult.candidate,
919
- criteria,
920
- config: llmConfig,
921
- timeoutMs: llmTimeoutMs,
922
- imageEvidence,
923
- maxImages: llmImageLimit,
924
- imageDetail: llmImageDetail
925
- }));
926
- } catch (error) {
927
- llmResult = createFailedLlmResult(error);
1059
+ detailStep = "llm_screening";
1060
+ if (!llmConfig) {
1061
+ llmResult = createMissingLlmConfigResult();
1062
+ } else {
1063
+ try {
1064
+ const llmTimingKey = imageEvidence?.file_paths?.length
1065
+ ? "vision_model_ms"
1066
+ : "text_model_ms";
1067
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1068
+ candidate: detailResult.candidate,
1069
+ criteria,
1070
+ config: llmConfig,
1071
+ timeoutMs: llmTimeoutMs,
1072
+ imageEvidence,
1073
+ maxImages: llmImageLimit,
1074
+ imageDetail: llmImageDetail
1075
+ }));
1076
+ } catch (error) {
1077
+ llmResult = createFailedLlmResult(error);
1078
+ }
928
1079
  }
929
1080
  }
930
1081
  }
@@ -952,7 +1103,8 @@ export async function runChatWorkflow({
952
1103
  text_length: contentWait.text_length
953
1104
  },
954
1105
  parsed_network_profile_count: parsedNetworkProfileCount,
955
- image_evidence: summarizeImageEvidence(imageEvidence)
1106
+ image_evidence: summarizeImageEvidence(imageEvidence),
1107
+ full_cv_evidence: fullCvEvidence
956
1108
  };
957
1109
  }
958
1110
  } catch (error) {
@@ -976,22 +1128,9 @@ export async function runChatWorkflow({
976
1128
  runControl.setPhase("chat:screening");
977
1129
  let cardOnlyLlmResult = null;
978
1130
  if (useLlmScreening && !detailUnavailableReason && !detailResult?.llm_result) {
979
- if (!llmConfig) {
980
- cardOnlyLlmResult = createMissingLlmConfigResult();
981
- } else {
982
- try {
983
- cardOnlyLlmResult = await measureTiming(timings, "text_model_ms", () => callScreeningLlm({
984
- candidate: screeningCandidate,
985
- criteria,
986
- config: llmConfig,
987
- timeoutMs: llmTimeoutMs,
988
- maxImages: llmImageLimit,
989
- imageDetail: llmImageDetail
990
- }));
991
- } catch (error) {
992
- cardOnlyLlmResult = createFailedLlmResult(error);
993
- }
994
- }
1131
+ detailUnavailableReason = detailResult
1132
+ ? "full_cv_not_acquired"
1133
+ : "detail_not_opened_full_cv_required";
995
1134
  }
996
1135
  const effectiveLlmResult = detailResult?.llm_result || cardOnlyLlmResult;
997
1136
  const screening = detailUnavailableReason