@reconcrap/boss-recommend-mcp 1.2.2 → 1.2.3

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.
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -690,6 +690,121 @@ function parseGeekIdFromUrl(url) {
690
690
  return null;
691
691
  }
692
692
 
693
+ function collectGeekIdsFromPayload(payload, fallbackGeekId = null) {
694
+ if (!payload || typeof payload !== "object") return [];
695
+ const geekDetail = payload?.geekDetail || payload;
696
+ const baseInfo = geekDetail?.geekBaseInfo || {};
697
+ const ids = [
698
+ fallbackGeekId,
699
+ baseInfo.geekId,
700
+ baseInfo.encryptGeekId,
701
+ baseInfo.securityId,
702
+ geekDetail?.geekId,
703
+ geekDetail?.encryptGeekId,
704
+ geekDetail?.securityId,
705
+ payload?.geekId,
706
+ payload?.encryptGeekId,
707
+ payload?.securityId
708
+ ].map((value) => normalizeText(value)).filter(Boolean);
709
+ return Array.from(new Set(ids));
710
+ }
711
+
712
+ function hasResumePayloadShape(payload) {
713
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
714
+ const geekDetail = payload?.geekDetail && typeof payload.geekDetail === "object"
715
+ ? payload.geekDetail
716
+ : payload;
717
+ const baseInfo = geekDetail?.geekBaseInfo || {};
718
+ const hasIdentity = Boolean(
719
+ normalizeText(
720
+ baseInfo?.name
721
+ || geekDetail?.geekName
722
+ || payload?.geekName
723
+ || baseInfo?.geekId
724
+ || baseInfo?.encryptGeekId
725
+ || baseInfo?.securityId
726
+ || geekDetail?.geekId
727
+ || geekDetail?.encryptGeekId
728
+ || geekDetail?.securityId
729
+ || payload?.geekId
730
+ || payload?.encryptGeekId
731
+ || payload?.securityId
732
+ || ""
733
+ )
734
+ );
735
+ const hasResumeSections = [
736
+ geekDetail?.geekExpectList,
737
+ geekDetail?.geekWorkExpList,
738
+ geekDetail?.geekProjExpList,
739
+ geekDetail?.geekEduExpList,
740
+ geekDetail?.geekEducationList,
741
+ geekDetail?.geekSkillList
742
+ ].some((section) => Array.isArray(section) && section.length > 0);
743
+ const hasResumeTextFields = Boolean(
744
+ normalizeText(
745
+ geekDetail?.geekAdvantage
746
+ || baseInfo?.userDesc
747
+ || baseInfo?.userDescription
748
+ || ""
749
+ )
750
+ );
751
+ return hasIdentity && (hasResumeSections || hasResumeTextFields);
752
+ }
753
+
754
+ function findResumePayloadInObject(root, maxDepth = 4, visited = new Set()) {
755
+ if (root === null || root === undefined || maxDepth < 0) return null;
756
+ if (typeof root !== "object") return null;
757
+ if (visited.has(root)) return null;
758
+ visited.add(root);
759
+
760
+ if (hasResumePayloadShape(root)) {
761
+ return root;
762
+ }
763
+
764
+ if (maxDepth === 0) return null;
765
+
766
+ if (Array.isArray(root)) {
767
+ for (const item of root) {
768
+ const found = findResumePayloadInObject(item, maxDepth - 1, visited);
769
+ if (found) return found;
770
+ }
771
+ return null;
772
+ }
773
+
774
+ const priorityKeys = [
775
+ "zpData",
776
+ "data",
777
+ "result",
778
+ "geekDetail",
779
+ "detail",
780
+ "info"
781
+ ];
782
+ for (const key of priorityKeys) {
783
+ if (!(key in root)) continue;
784
+ const found = findResumePayloadInObject(root[key], maxDepth - 1, visited);
785
+ if (found) return found;
786
+ }
787
+
788
+ for (const value of Object.values(root)) {
789
+ const found = findResumePayloadInObject(value, maxDepth - 1, visited);
790
+ if (found) return found;
791
+ }
792
+ return null;
793
+ }
794
+
795
+ function extractResumePayloadFromResponseBody(parsedBody) {
796
+ return findResumePayloadInObject(parsedBody, 4) || null;
797
+ }
798
+
799
+ function isResumeInfoRequestUrl(url) {
800
+ const normalizedUrl = normalizeText(url).toLowerCase();
801
+ if (!normalizedUrl || !normalizedUrl.includes("/wapi/")) return false;
802
+ if (!normalizedUrl.includes("geek") || !normalizedUrl.includes("info")) return false;
803
+ if (/\/boss\/[^?#]*\/geek\/info\b/.test(normalizedUrl)) return true;
804
+ if (/\/geek\/info\b/.test(normalizedUrl)) return true;
805
+ return /[?&](?:geekid|geek_id|encryptgeekid|securityid)=/.test(normalizedUrl);
806
+ }
807
+
693
808
  function formatResumeApiData(data) {
694
809
  const parts = [];
695
810
  const geekDetail = data?.geekDetail || data || {};
@@ -2074,14 +2189,8 @@ class RecommendScreenCli {
2074
2189
  if (!payload || typeof payload !== "object") return;
2075
2190
  const geekDetail = payload.geekDetail || payload;
2076
2191
  const baseInfo = geekDetail.geekBaseInfo || {};
2077
- const geekId = normalizeText(
2078
- fallbackGeekId
2079
- || baseInfo.geekId
2080
- || baseInfo.encryptGeekId
2081
- || geekDetail.geekId
2082
- || payload.geekId
2083
- || ""
2084
- ) || null;
2192
+ const geekIds = collectGeekIdsFromPayload(payload, fallbackGeekId);
2193
+ const geekId = geekIds[0] || null;
2085
2194
  const candidateInfo = {
2086
2195
  name: baseInfo.name || geekDetail.geekName || payload.geekName || "",
2087
2196
  school: (geekDetail.geekEduExpList && geekDetail.geekEduExpList[0]?.school)
@@ -2093,17 +2202,18 @@ class RecommendScreenCli {
2093
2202
  company: (geekDetail.geekWorkExpList && geekDetail.geekWorkExpList[0]?.company) || "",
2094
2203
  position: (geekDetail.geekWorkExpList && geekDetail.geekWorkExpList[0]?.positionName) || "",
2095
2204
  resumeText: formatResumeApiData(payload),
2096
- alreadyInterested: payload.alreadyInterested === true
2205
+ alreadyInterested: payload.alreadyInterested === true || geekDetail.alreadyInterested === true
2097
2206
  };
2098
2207
  const wrapped = {
2099
2208
  ts: Date.now(),
2100
2209
  geekId: geekId || null,
2210
+ geekIds,
2101
2211
  data: payload,
2102
2212
  candidateInfo
2103
2213
  };
2104
2214
  this.latestResumeNetworkPayload = wrapped;
2105
- if (geekId) {
2106
- this.resumeNetworkByGeekId.set(geekId, wrapped);
2215
+ for (const id of geekIds) {
2216
+ this.resumeNetworkByGeekId.set(id, wrapped);
2107
2217
  }
2108
2218
  }
2109
2219
 
@@ -2136,7 +2246,7 @@ class RecommendScreenCli {
2136
2246
  handleNetworkRequestWillBeSent(params) {
2137
2247
  const url = normalizeText(params?.request?.url || "");
2138
2248
  if (!url) return;
2139
- if (url.includes("/wapi/zpitem/web/boss/search/geek/info")) {
2249
+ if (isResumeInfoRequestUrl(url)) {
2140
2250
  const geekId = parseGeekIdFromUrl(url);
2141
2251
  this.resumeNetworkRequests.set(params.requestId, {
2142
2252
  ts: Date.now(),
@@ -2206,9 +2316,13 @@ class RecommendScreenCli {
2206
2316
  try {
2207
2317
  const responseBody = await this.Network.getResponseBody({ requestId: params.requestId });
2208
2318
  if (!responseBody?.body) return;
2209
- const parsed = JSON.parse(responseBody.body);
2210
- if (parsed?.zpData) {
2211
- this.cacheResumeNetworkPayload(parsed.zpData, requestMeta.geekId);
2319
+ const rawBody = responseBody.base64Encoded
2320
+ ? Buffer.from(responseBody.body, "base64").toString("utf8")
2321
+ : responseBody.body;
2322
+ const parsed = JSON.parse(rawBody);
2323
+ const resumePayload = extractResumePayloadFromResponseBody(parsed);
2324
+ if (resumePayload) {
2325
+ this.cacheResumeNetworkPayload(resumePayload, requestMeta.geekId);
2212
2326
  }
2213
2327
  } catch {}
2214
2328
  }
@@ -3457,10 +3571,11 @@ class RecommendScreenCli {
3457
3571
  }
3458
3572
 
3459
3573
  const isFeaturedScope = this.args.pageScope === "featured";
3574
+ const allowImageFallback = !isFeaturedScope;
3460
3575
  let capture = null;
3461
3576
  let screening = null;
3462
3577
  let resumeSource = isFeaturedScope ? "network" : "image_fallback";
3463
- const networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(nextCandidate, 2400);
3578
+ const networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(nextCandidate, allowImageFallback ? 4200 : 2400);
3464
3579
  const candidateProfile = {
3465
3580
  name: networkCandidateInfo?.name || nextCandidate.name || "",
3466
3581
  school: networkCandidateInfo?.school || nextCandidate.school || "",
@@ -3479,6 +3594,10 @@ class RecommendScreenCli {
3479
3594
  screening = await this.callTextModel(networkCandidateInfo.resumeText);
3480
3595
  resumeSource = "network";
3481
3596
  this.resumeSourceStats.network += 1;
3597
+ } else if (networkCandidateInfo?.resumeText) {
3598
+ screening = await this.callTextModel(networkCandidateInfo.resumeText);
3599
+ resumeSource = "network";
3600
+ this.resumeSourceStats.network += 1;
3482
3601
  } else {
3483
3602
  capture = await this.captureResumeImage(nextCandidate);
3484
3603
  screening = await this.callVisionModel(capture.stitchedImage);
@@ -338,29 +338,28 @@ async function testFeaturedShouldUseNetworkResumeOnly() {
338
338
  assert.equal(result.result.resume_source, "network");
339
339
  }
340
340
 
341
- async function testRecommendShouldKeepImageCaptureEvenWhenNetworkResumeExists() {
342
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-recommend-image-main-"));
343
- const candidate = { key: "img-main-1", geek_id: "img-main-1", name: "recommend image main candidate" };
341
+ async function testRecommendShouldPreferNetworkResumeWhenAvailable() {
342
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-recommend-network-main-"));
343
+ const candidate = { key: "net-main-1", geek_id: "net-main-1", name: "recommend network main candidate" };
344
344
  const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
345
- candidates: [candidate],
346
- captureOutcomes: new Map([
347
- ["img-main-1", { stitchedImage: path.join(tempDir, "img-main-1.png") }]
348
- ]),
349
- screeningByKey: new Map([
350
- ["img-main-1", { passed: true, reason: "image path used", summary: "image path used" }]
351
- ])
345
+ candidates: [candidate]
352
346
  });
353
347
  cli.waitForNetworkResumeCandidateInfo = async () => ({
354
- resumeText: "这段 network 文本在 recommend 页面不应被用于筛选"
348
+ resumeText: "这段 network 文本在 recommend 页面应优先用于筛选"
355
349
  });
356
- cli.callTextModel = async () => {
357
- throw new Error("text model should not be called for recommend scope");
350
+ cli.callTextModel = async () => ({
351
+ passed: true,
352
+ reason: "network used",
353
+ summary: "network used"
354
+ });
355
+ cli.captureResumeImage = async () => {
356
+ throw new Error("capture should not be called when recommend network resume exists");
358
357
  };
359
358
 
360
359
  const result = await cli.run();
361
360
  assert.equal(result.status, "COMPLETED");
362
361
  assert.equal(result.result.passed_count, 1);
363
- assert.equal(result.result.resume_source, "image_fallback");
362
+ assert.equal(result.result.resume_source, "network");
364
363
  }
365
364
 
366
365
  async function testNetworkMissShouldFallbackToImageCapture() {
@@ -382,6 +381,53 @@ async function testNetworkMissShouldFallbackToImageCapture() {
382
381
  assert.equal(result.result.resume_source, "image_fallback");
383
382
  }
384
383
 
384
+ async function testLatestShouldPreferNetworkResumeWhenAvailable() {
385
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-latest-network-main-"));
386
+ const args = createArgs(tempDir);
387
+ args.pageScope = "latest";
388
+ const candidate = { key: "latest-net-1", geek_id: "latest-net-1", name: "latest network candidate" };
389
+ const cli = new FakeRecommendScreenCli(args, {
390
+ candidates: [candidate]
391
+ });
392
+ cli.waitForNetworkResumeCandidateInfo = async () => ({
393
+ resumeText: "最新页 network 简历可用"
394
+ });
395
+ cli.callTextModel = async () => ({
396
+ passed: true,
397
+ reason: "network used",
398
+ summary: "network used"
399
+ });
400
+ cli.captureResumeImage = async () => {
401
+ throw new Error("capture should not be called when latest network resume exists");
402
+ };
403
+
404
+ const result = await cli.run();
405
+ assert.equal(result.status, "COMPLETED");
406
+ assert.equal(result.result.passed_count, 1);
407
+ assert.equal(result.result.resume_source, "network");
408
+ }
409
+
410
+ async function testLatestNetworkMissShouldFallbackToImageCapture() {
411
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-latest-network-fallback-"));
412
+ const args = createArgs(tempDir);
413
+ args.pageScope = "latest";
414
+ const candidate = { key: "latest-img-1", geek_id: "latest-img-1", name: "latest image candidate" };
415
+ const cli = new FakeRecommendScreenCli(args, {
416
+ candidates: [candidate],
417
+ captureOutcomes: new Map([
418
+ ["latest-img-1", { stitchedImage: path.join(tempDir, "latest-img-1.png") }]
419
+ ]),
420
+ screeningByKey: new Map([
421
+ ["latest-img-1", { passed: false, reason: "image fallback used", summary: "image fallback used" }]
422
+ ])
423
+ });
424
+ cli.waitForNetworkResumeCandidateInfo = async () => null;
425
+
426
+ const result = await cli.run();
427
+ assert.equal(result.status, "COMPLETED");
428
+ assert.equal(result.result.resume_source, "image_fallback");
429
+ }
430
+
385
431
  async function testVisionModelFailureShouldSkipCandidateAndContinue() {
386
432
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-vision-failure-skip-"));
387
433
  const first = { key: "vision-fail-1", geek_id: "vision-fail-1", name: "vision-fail-1" };
@@ -803,8 +849,10 @@ async function main() {
803
849
  await testPageExhaustedBeforeTargetShouldRaiseRecoverableError();
804
850
  await testPageExhaustedWithoutTargetShouldStillComplete();
805
851
  await testFeaturedShouldUseNetworkResumeOnly();
806
- await testRecommendShouldKeepImageCaptureEvenWhenNetworkResumeExists();
852
+ await testRecommendShouldPreferNetworkResumeWhenAvailable();
807
853
  await testNetworkMissShouldFallbackToImageCapture();
854
+ await testLatestShouldPreferNetworkResumeWhenAvailable();
855
+ await testLatestNetworkMissShouldFallbackToImageCapture();
808
856
  await testVisionModelFailureShouldSkipCandidateAndContinue();
809
857
  await testFeaturedNetworkMissShouldSkipWithoutImageCapture();
810
858
  await testFeaturedFavoriteShouldNotUseDomFallback();