@reconcrap/boss-recommend-mcp 1.3.25 → 1.3.26

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": "1.3.25",
3
+ "version": "1.3.26",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
package/src/cli.js CHANGED
@@ -39,7 +39,6 @@ const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|his
39
39
  const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw"];
40
40
  const defaultMcpServerName = "boss-recommend";
41
41
  const defaultMcpCommand = "npx";
42
- const defaultMcpArgs = ["-y", "@reconcrap/boss-recommend-mcp@latest", "start"];
43
42
  const recommendMcpPackageName = "@reconcrap/boss-recommend-mcp";
44
43
  const recommendMcpBinaryName = "boss-recommend-mcp";
45
44
  const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h"]);
@@ -65,6 +64,29 @@ function getPackageVersion() {
65
64
 
66
65
  const packageVersion = getPackageVersion();
67
66
 
67
+ function isInstalledPackageRoot(rootPath = packageRoot) {
68
+ const normalized = path.resolve(String(rootPath || ""))
69
+ .replace(/\\/g, "/")
70
+ .toLowerCase();
71
+ return (
72
+ normalized.includes("/appdata/local/npm-cache/_npx/")
73
+ || normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
74
+ );
75
+ }
76
+
77
+ function getDefaultMcpPackageSpecifier(options = {}) {
78
+ const version = String(options.packageVersion || packageVersion).trim();
79
+ const rootPath = options.packageRootPath || packageRoot;
80
+ if (version && version !== "0.0.0" && isInstalledPackageRoot(rootPath)) {
81
+ return `${recommendMcpPackageName}@${version}`;
82
+ }
83
+ return `${recommendMcpPackageName}@latest`;
84
+ }
85
+
86
+ function buildDefaultMcpArgs(options = {}) {
87
+ return ["-y", getDefaultMcpPackageSpecifier(options), "start"];
88
+ }
89
+
68
90
  function getCodexHome() {
69
91
  return process.env.CODEX_HOME
70
92
  ? path.resolve(process.env.CODEX_HOME)
@@ -338,7 +360,7 @@ function buildMcpLaunchConfig(options = {}) {
338
360
  ? args
339
361
  : command === "boss-recommend-mcp"
340
362
  ? ["start"]
341
- : defaultMcpArgs.slice();
363
+ : buildDefaultMcpArgs();
342
364
  const launchConfig = { command, args: launchArgs };
343
365
  if (env && typeof env === "object" && !Array.isArray(env) && Object.keys(env).length > 0) {
344
366
  launchConfig.env = env;
@@ -1606,9 +1628,13 @@ export async function runCli(argv = process.argv) {
1606
1628
 
1607
1629
  export const __testables = {
1608
1630
  buildBossChatCliInput,
1631
+ buildDefaultMcpArgs,
1632
+ buildMcpLaunchConfig,
1609
1633
  getBossChatCliRunTarget,
1634
+ getDefaultMcpPackageSpecifier,
1610
1635
  getRunFollowUp,
1611
1636
  installSkill,
1637
+ isInstalledPackageRoot,
1612
1638
  runBossChatCliCommand,
1613
1639
  runPipelineOnce
1614
1640
  };
@@ -16,6 +16,7 @@ import {
16
16
  import { __testables as cliTestables } from "./cli.js";
17
17
  import { __testables as indexTestables } from "./index.js";
18
18
  import { BossChatApp } from "../vendor/boss-chat-cli/src/app.js";
19
+ import { __testables as vendorCliTestables } from "../vendor/boss-chat-cli/src/cli.js";
19
20
  import { BossChatPage } from "../vendor/boss-chat-cli/src/browser/chat-page.js";
20
21
  import { LlmClient, parseLlmJson } from "../vendor/boss-chat-cli/src/services/llm.js";
21
22
 
@@ -684,6 +685,118 @@ async function testBossChatCliShouldSupportRunAndFollowUpParsing() {
684
685
  });
685
686
  }
686
687
 
688
+ async function testVendorBossChatCliShouldWaitForHydratedChatShell() {
689
+ const pageStates = [
690
+ { href: "https://www.zhipin.com/web/chat/index", hasListContainer: false, listItemCount: 0 },
691
+ { href: "https://www.zhipin.com/web/chat/index", hasListContainer: false, listItemCount: 0 },
692
+ { href: "https://www.zhipin.com/web/chat/index", hasListContainer: true, listItemCount: 40 },
693
+ ];
694
+ const jobsPerAttempt = [
695
+ [],
696
+ [],
697
+ [{ value: "job-1", label: "AI应用开发工程师(2026) _ 杭州", active: false }],
698
+ ];
699
+ let ensureCallCount = 0;
700
+ let listJobsCallCount = 0;
701
+ const page = {
702
+ async ensureOnChatPage() {
703
+ const next = pageStates[Math.min(ensureCallCount, pageStates.length - 1)];
704
+ ensureCallCount += 1;
705
+ return next;
706
+ },
707
+ async listJobs() {
708
+ const next = jobsPerAttempt[Math.min(listJobsCallCount, jobsPerAttempt.length - 1)];
709
+ listJobsCallCount += 1;
710
+ return next;
711
+ },
712
+ };
713
+
714
+ const hydrated = await vendorCliTestables.waitForChatShellHydration({
715
+ page,
716
+ maxAttempts: 4,
717
+ delayMs: 0,
718
+ });
719
+ assert.equal(Array.isArray(hydrated.jobs), true);
720
+ assert.equal(hydrated.jobs.length, 1);
721
+ assert.equal(hydrated.pageState.listItemCount, 40);
722
+ assert.equal(ensureCallCount >= 3, true);
723
+ }
724
+
725
+ async function testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile() {
726
+ const page = {
727
+ _attempt: 0,
728
+ async ensureOnChatPage() {
729
+ return {
730
+ href: "https://www.zhipin.com/web/chat/index",
731
+ hasListContainer: this._attempt >= 1,
732
+ listItemCount: this._attempt >= 1 ? 10 : 0,
733
+ };
734
+ },
735
+ async listJobs() {
736
+ this._attempt += 1;
737
+ if (this._attempt < 2) {
738
+ return [];
739
+ }
740
+ return [
741
+ {
742
+ value: "job-1",
743
+ label: "AI应用开发工程师(2026) _ 杭州",
744
+ active: false,
745
+ },
746
+ ];
747
+ },
748
+ };
749
+
750
+ const profile = await vendorCliTestables.promptRunProfile({
751
+ page,
752
+ persistentProfile: {
753
+ llm: {
754
+ baseUrl: "https://api.example.com/v1",
755
+ apiKey: "sk-test-key",
756
+ model: "gpt-4.1-mini",
757
+ },
758
+ chrome: {
759
+ port: 9222,
760
+ },
761
+ runtime: {},
762
+ },
763
+ overrides: {
764
+ jobSelection: "AI应用开发工程师(2026) _ 杭州",
765
+ startFrom: "unread",
766
+ screeningCriteria: "小样本联通性验证",
767
+ targetCount: 1,
768
+ },
769
+ });
770
+ assert.equal(profile.jobSelection.label, "AI应用开发工程师(2026) _ 杭州");
771
+ assert.equal(profile.startFrom, "unread");
772
+ assert.equal(profile.targetCount, 1);
773
+ }
774
+
775
+ function testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig() {
776
+ const installedSpecifier = cliTestables.getDefaultMcpPackageSpecifier({
777
+ packageVersion: "1.3.25",
778
+ packageRootPath: "C:\\Users\\yaolin\\AppData\\Roaming\\npm\\node_modules\\@reconcrap\\boss-recommend-mcp",
779
+ });
780
+ assert.equal(installedSpecifier, "@reconcrap/boss-recommend-mcp@1.3.25");
781
+
782
+ const cachedSpecifier = cliTestables.getDefaultMcpPackageSpecifier({
783
+ packageVersion: "1.3.25",
784
+ packageRootPath: "C:\\Users\\yaolin\\AppData\\Local\\npm-cache\\_npx\\abcd1234\\node_modules\\@reconcrap\\boss-recommend-mcp",
785
+ });
786
+ assert.equal(cachedSpecifier, "@reconcrap/boss-recommend-mcp@1.3.25");
787
+
788
+ const sourceSpecifier = cliTestables.getDefaultMcpPackageSpecifier({
789
+ packageVersion: "1.3.25-dev",
790
+ packageRootPath: "C:\\Users\\yaolin\\Documents\\codex_projects\\boss recommend pipeline\\boss-recommend-mcp",
791
+ });
792
+ assert.equal(sourceSpecifier, "@reconcrap/boss-recommend-mcp@latest");
793
+
794
+ const launchConfig = cliTestables.buildMcpLaunchConfig({});
795
+ assert.equal(launchConfig.command, "npx");
796
+ assert.equal(Array.isArray(launchConfig.args), true);
797
+ assert.equal(launchConfig.args[0], "-y");
798
+ }
799
+
687
800
  function testBossChatLlmEvidenceGateShouldDemoteMissingEvidence() {
688
801
  const parsed = parseLlmJson(
689
802
  JSON.stringify({
@@ -1030,6 +1143,9 @@ async function main() {
1030
1143
  await testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad();
1031
1144
  await testBossChatMcpToolsShouldValidateAndRoute();
1032
1145
  await testBossChatCliShouldSupportRunAndFollowUpParsing();
1146
+ await testVendorBossChatCliShouldWaitForHydratedChatShell();
1147
+ await testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile();
1148
+ testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig();
1033
1149
  testBossChatLlmEvidenceGateShouldDemoteMissingEvidence();
1034
1150
  testBossChatLlmEvidenceGateShouldDemoteUnmatchedEvidence();
1035
1151
  await testBossChatLlmTextChunkFallbackShouldWork();
@@ -45,6 +45,10 @@ const MINIMAL_TERMINAL_PATTERNS = [/^进度: /, /^候选人结果: /];
45
45
  const CHAT_INDEX_URL = 'https://www.zhipin.com/web/chat/index';
46
46
  const CHAT_START_REQUIRED_FIELDS = ['job', 'start_from', 'target_count', 'criteria'];
47
47
  const CHAT_PAGE_RENAVIGATE_MAX_ATTEMPTS = 3;
48
+ const CHAT_PAGE_HYDRATION_MAX_ATTEMPTS = 12;
49
+ const CHAT_PAGE_HYDRATION_DELAY_MS = 250;
50
+ const CHAT_JOB_LIST_MAX_ATTEMPTS = 16;
51
+ const CHAT_JOB_LIST_DELAY_MS = 250;
48
52
 
49
53
  function sanitizePathToken(value, fallback = 'run') {
50
54
  const token = String(value || '')
@@ -71,6 +75,10 @@ function shouldPrintToMinimalTerminal(message) {
71
75
  return MINIMAL_TERMINAL_PATTERNS.some((pattern) => pattern.test(message));
72
76
  }
73
77
 
78
+ function sleep(ms) {
79
+ return new Promise((resolve) => setTimeout(resolve, ms));
80
+ }
81
+
74
82
  async function createRunLogger(dataDir, { runId = '', detachedWorker = false } = {}) {
75
83
  const logsDir = path.join(dataDir, 'logs');
76
84
  await mkdir(logsDir, { recursive: true });
@@ -498,7 +506,7 @@ function resolveJobSelection(jobs, input) {
498
506
  }
499
507
 
500
508
  async function promptRunProfile({ page, persistentProfile, overrides }) {
501
- const jobs = await page.listJobs();
509
+ const jobs = await resolveJobsWithRetry({ page });
502
510
  if (!Array.isArray(jobs) || jobs.length === 0) {
503
511
  throw new Error('未解析到岗位列表,请确认岗位下拉可见。');
504
512
  }
@@ -647,6 +655,102 @@ function buildPreparePendingQuestions(args, jobs = []) {
647
655
  return pendingQuestions;
648
656
  }
649
657
 
658
+ function hasHydratedChatShell(pageState, jobs = []) {
659
+ const hasChatList =
660
+ Boolean(pageState?.hasListContainer)
661
+ || Number(pageState?.listItemCount || 0) > 0;
662
+ return hasChatList || (Array.isArray(jobs) && jobs.length > 0);
663
+ }
664
+
665
+ async function waitForChatShellHydration({
666
+ page,
667
+ maxAttempts = CHAT_PAGE_HYDRATION_MAX_ATTEMPTS,
668
+ delayMs = CHAT_PAGE_HYDRATION_DELAY_MS,
669
+ } = {}) {
670
+ let lastPageState = null;
671
+ let lastJobs = [];
672
+ let lastError = null;
673
+
674
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
675
+ try {
676
+ lastPageState = await page.ensureOnChatPage();
677
+ } catch (error) {
678
+ lastError = error;
679
+ break;
680
+ }
681
+
682
+ try {
683
+ lastJobs = await page.listJobs();
684
+ } catch (error) {
685
+ lastError = error;
686
+ lastJobs = [];
687
+ }
688
+
689
+ if (hasHydratedChatShell(lastPageState, lastJobs)) {
690
+ return {
691
+ pageState: lastPageState,
692
+ jobs: lastJobs,
693
+ };
694
+ }
695
+
696
+ if (attempt < maxAttempts) {
697
+ await sleep(delayMs);
698
+ }
699
+ }
700
+
701
+ const lastErrorMessage = String(lastError?.message || lastError || '');
702
+ if (/ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE/.test(lastErrorMessage)) {
703
+ throw lastError;
704
+ }
705
+
706
+ return {
707
+ pageState: lastPageState,
708
+ jobs: lastJobs,
709
+ };
710
+ }
711
+
712
+ async function resolveJobsWithRetry({
713
+ page,
714
+ maxAttempts = CHAT_JOB_LIST_MAX_ATTEMPTS,
715
+ delayMs = CHAT_JOB_LIST_DELAY_MS,
716
+ } = {}) {
717
+ let lastError = null;
718
+
719
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
720
+ try {
721
+ const jobs = await page.listJobs();
722
+ if (Array.isArray(jobs) && jobs.length > 0) {
723
+ return jobs;
724
+ }
725
+ } catch (error) {
726
+ lastError = error;
727
+ const message = String(error?.message || error || '');
728
+ if (/ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE/.test(message)) {
729
+ throw error;
730
+ }
731
+ }
732
+
733
+ const hydrated = await waitForChatShellHydration({
734
+ page,
735
+ maxAttempts: 1,
736
+ delayMs,
737
+ });
738
+ if (Array.isArray(hydrated?.jobs) && hydrated.jobs.length > 0) {
739
+ return hydrated.jobs;
740
+ }
741
+
742
+ if (attempt < maxAttempts) {
743
+ await sleep(delayMs);
744
+ }
745
+ }
746
+
747
+ if (lastError) {
748
+ throw lastError;
749
+ }
750
+
751
+ return [];
752
+ }
753
+
650
754
  async function connectBossChatPage(chromeClient) {
651
755
  const isBossDomainTarget = (target) =>
652
756
  target?.type === 'page' && /zhipin\.com/i.test(String(target?.url || ''));
@@ -674,13 +778,25 @@ async function connectBossChatPage(chromeClient) {
674
778
  };
675
779
  } catch (error) {
676
780
  const message = String(error?.message || error || '');
781
+ if (/CHAT_LIST_CONTAINER_NOT_FOUND/.test(message)) {
782
+ blankChatPage = true;
783
+ const hydrated = await waitForChatShellHydration({ page });
784
+ if (hasHydratedChatShell(hydrated?.pageState, hydrated?.jobs)) {
785
+ return {
786
+ target,
787
+ page,
788
+ recoveredToChatIndex,
789
+ blankChatPage,
790
+ renavigateAttempts,
791
+ };
792
+ }
793
+ }
677
794
  const canRetry =
678
795
  /ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE|CHAT_LIST_CONTAINER_NOT_FOUND/.test(message)
679
796
  && attempt <= CHAT_PAGE_RENAVIGATE_MAX_ATTEMPTS;
680
797
 
681
798
  if (!canRetry) {
682
799
  if (/CHAT_LIST_CONTAINER_NOT_FOUND/.test(message)) {
683
- blankChatPage = true;
684
800
  await page.ensureOnChatPage();
685
801
  break;
686
802
  }
@@ -734,7 +850,7 @@ async function handlePrepareRunCommand(args, dataDir) {
734
850
  try {
735
851
  chromeClient = new ChromeClient(mergedProfile.chrome.port);
736
852
  const { target, page, recoveredToChatIndex, blankChatPage, renavigateAttempts } = await connectBossChatPage(chromeClient);
737
- const jobs = await page.listJobs();
853
+ const jobs = await resolveJobsWithRetry({ page });
738
854
  if (!Array.isArray(jobs) || jobs.length === 0) {
739
855
  return {
740
856
  status: 'FAILED',
@@ -1423,12 +1539,22 @@ async function main() {
1423
1539
  }
1424
1540
  }
1425
1541
 
1426
- main().catch((error) => {
1427
- const runLogPath = String(error?.runLogPath || '').trim();
1428
- if (runLogPath) {
1429
- console.error(`执行失败,详细日志见: ${runLogPath}`);
1430
- } else {
1431
- console.error(`执行失败: ${error.message}`);
1432
- }
1433
- process.exitCode = 1;
1434
- });
1542
+ export const __testables = {
1543
+ connectBossChatPage,
1544
+ hasHydratedChatShell,
1545
+ promptRunProfile,
1546
+ resolveJobsWithRetry,
1547
+ waitForChatShellHydration,
1548
+ };
1549
+
1550
+ if (process.argv[1] && path.resolve(process.argv[1]) === CLI_FILE_PATH) {
1551
+ main().catch((error) => {
1552
+ const runLogPath = String(error?.runLogPath || '').trim();
1553
+ if (runLogPath) {
1554
+ console.error(`执行失败,详细日志见: ${runLogPath}`);
1555
+ } else {
1556
+ console.error(`执行失败: ${error.message}`);
1557
+ }
1558
+ process.exitCode = 1;
1559
+ });
1560
+ }