@reconcrap/boss-recommend-mcp 2.1.11 → 2.1.13

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/src/cli.js CHANGED
@@ -57,12 +57,15 @@ const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
57
57
  const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
58
58
  const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
59
59
  const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
60
- const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw", "qclaw"];
60
+ const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw", "qclaw"];
61
61
  const defaultMcpServerName = "boss-recommend";
62
+ const bossChatMcpServerName = "boss-chat";
63
+ const bossRecruitMcpServerName = "boss-recruit";
62
64
  const defaultMcpCommand = "npx";
63
65
  const recommendMcpPackageName = "@reconcrap/boss-recommend-mcp";
64
66
  const recommendMcpBinaryName = "boss-recommend-mcp";
65
67
  const globalMcpWrapperFileName = "boss-recommend-mcp-mcp-server";
68
+ const mcpToolsetEnv = "BOSS_RECOMMEND_MCP_TOOLSET";
66
69
  const supportedMcpLaunchModes = ["npx", "global-wrapper"];
67
70
  const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h", "list-jobs", "jobs", "recommend-jobs"]);
68
71
  const externalMcpTargetsEnv = "BOSS_RECOMMEND_MCP_CONFIG_TARGETS";
@@ -807,12 +810,35 @@ function shouldDefaultRecommendDetachedMcpEnv(options = {}) {
807
810
  return client === "openclaw"
808
811
  || client === "qclaw"
809
812
  || agent === "openclaw"
810
- || agent === "qclaw";
811
- }
812
-
813
- function getDefaultMcpEnv(options = {}) {
814
- return shouldDefaultRecommendDetachedMcpEnv(options)
815
- ? { ...detachedRecommendMcpEnv }
813
+ || agent === "qclaw";
814
+ }
815
+
816
+ function normalizeMcpToolsetOption(value) {
817
+ const raw = String(value || "").trim().toLowerCase().replace(/[\s_]+/g, "-");
818
+ if (!raw || raw === "all") return "";
819
+ if (raw === "boss-recommend" || raw === "recommend-page") return "recommend";
820
+ if (raw === "boss-chat" || raw === "chat-only" || raw === "chat-page") return "chat";
821
+ if (raw === "boss-recruit" || raw === "search" || raw === "search-page" || raw === "recruit-page") return "recruit";
822
+ if (["recommend", "chat", "recruit"].includes(raw)) return raw;
823
+ throw new Error(`Unsupported --toolset value: ${raw}. Supported: recommend, chat, recruit, all`);
824
+ }
825
+
826
+ function getMcpToolsetEnv(options = {}) {
827
+ const toolset = normalizeMcpToolsetOption(options.toolset || options["toolset"]);
828
+ return toolset ? { [mcpToolsetEnv]: toolset } : {};
829
+ }
830
+
831
+ function shouldUseSplitBossMcpServers(options = {}) {
832
+ if (options["server-name"] || options.serverName || options.toolset || options["toolset"]) return false;
833
+ if (options.split === true || options["split-tools"] === true || options.splitTools === true) return true;
834
+ const client = normalizeMcpClientName(options.client);
835
+ const agent = normalizeAgentName(options.agent);
836
+ return client === "trae" || agent === "trae" || agent === "trae-cn";
837
+ }
838
+
839
+ function getDefaultMcpEnv(options = {}) {
840
+ return shouldDefaultRecommendDetachedMcpEnv(options)
841
+ ? { ...detachedRecommendMcpEnv }
816
842
  : {};
817
843
  }
818
844
 
@@ -834,6 +860,7 @@ function buildMcpLaunchConfig(options = {}) {
834
860
  };
835
861
  const mergedEnv = {
836
862
  ...getDefaultMcpEnv(options),
863
+ ...getMcpToolsetEnv(options),
837
864
  ...(isPlainObject(env) ? env : {})
838
865
  };
839
866
  if (Object.keys(mergedEnv).length > 0) {
@@ -853,19 +880,36 @@ function buildMcpLaunchConfig(options = {}) {
853
880
  ? ["start"]
854
881
  : buildDefaultMcpArgs(options);
855
882
  const launchConfig = { command, args: launchArgs };
856
- const mergedEnv = {
857
- ...getDefaultMcpEnv(options),
858
- ...(isPlainObject(env) ? env : {})
859
- };
883
+ const mergedEnv = {
884
+ ...getDefaultMcpEnv(options),
885
+ ...getMcpToolsetEnv(options),
886
+ ...(isPlainObject(env) ? env : {})
887
+ };
860
888
  if (Object.keys(mergedEnv).length > 0) {
861
889
  launchConfig.env = mergedEnv;
862
890
  }
863
891
  return launchConfig;
864
- }
865
-
866
- function mergeExistingMcpEntryEnv(existingEntry, launchConfig) {
867
- if (!isPlainObject(existingEntry?.env) || !isPlainObject(launchConfig)) {
868
- return launchConfig;
892
+ }
893
+
894
+ function buildBossMcpServerEntries(options = {}) {
895
+ if (shouldUseSplitBossMcpServers(options)) {
896
+ return {
897
+ [defaultMcpServerName]: buildMcpLaunchConfig({ ...options, toolset: "recommend" }),
898
+ [bossChatMcpServerName]: buildMcpLaunchConfig({ ...options, toolset: "chat" }),
899
+ [bossRecruitMcpServerName]: buildMcpLaunchConfig({ ...options, toolset: "recruit" })
900
+ };
901
+ }
902
+ const serverName = typeof options["server-name"] === "string" && options["server-name"].trim()
903
+ ? options["server-name"].trim()
904
+ : defaultMcpServerName;
905
+ return {
906
+ [serverName]: buildMcpLaunchConfig(options)
907
+ };
908
+ }
909
+
910
+ function mergeExistingMcpEntryEnv(existingEntry, launchConfig) {
911
+ if (!isPlainObject(existingEntry?.env) || !isPlainObject(launchConfig)) {
912
+ return launchConfig;
869
913
  }
870
914
  return {
871
915
  ...launchConfig,
@@ -874,28 +918,21 @@ function mergeExistingMcpEntryEnv(existingEntry, launchConfig) {
874
918
  ...(isPlainObject(launchConfig.env) ? launchConfig.env : {})
875
919
  }
876
920
  };
877
- }
878
-
879
- function buildMcpConfigFileContent(options = {}) {
880
- const serverName = typeof options["server-name"] === "string" && options["server-name"].trim()
881
- ? options["server-name"].trim()
882
- : defaultMcpServerName;
883
- const launchConfig = buildMcpLaunchConfig(options);
884
- if (normalizeMcpClientName(options.client) === "qclaw") {
885
- return {
886
- mcp: {
887
- servers: {
888
- [serverName]: launchConfig
889
- }
890
- }
891
- };
892
- }
893
- return {
894
- mcpServers: {
895
- [serverName]: launchConfig
896
- }
897
- };
898
- }
921
+ }
922
+
923
+ function buildMcpConfigFileContent(options = {}) {
924
+ const servers = buildBossMcpServerEntries(options);
925
+ if (normalizeMcpClientName(options.client) === "qclaw") {
926
+ return {
927
+ mcp: {
928
+ servers
929
+ }
930
+ };
931
+ }
932
+ return {
933
+ mcpServers: servers
934
+ };
935
+ }
899
936
 
900
937
  function writeMcpConfigFiles(options = {}) {
901
938
  const clients = parseMcpClientTargets(options.client);
@@ -1039,25 +1076,28 @@ function getMcpServersFromConfig(config = {}, useQClawShape = false) {
1039
1076
  return {};
1040
1077
  }
1041
1078
 
1042
- function mergeMcpServerConfigFile(filePath, options = {}) {
1043
- const current = readJsonObjectFileSafe(filePath);
1044
- const useQClawShape = isQClawMcpConfigTarget(filePath, options, current);
1045
- const nextConfig = buildMcpConfigFileContent({ ...options, client: useQClawShape ? "qclaw" : options.client });
1046
- const nextServers = useQClawShape ? nextConfig.mcp?.servers : nextConfig.mcpServers;
1047
- const serverName = Object.keys(nextServers || {})[0] || defaultMcpServerName;
1048
- const existingServers = getMcpServersFromConfig(current, useQClawShape);
1049
- const existingEntry = existingServers[serverName];
1050
- const launchConfig = mergeExistingMcpEntryEnv(
1051
- existingEntry,
1052
- nextServers?.[serverName] || buildMcpLaunchConfig(options)
1053
- );
1054
- const retainedServers = {};
1055
- const migratedLegacyServers = [];
1056
- for (const [name, config] of Object.entries(existingServers)) {
1057
- if (name === serverName) continue;
1058
- if (isBossMcpServerEntry(name, config)) {
1059
- migratedLegacyServers.push(name);
1060
- continue;
1079
+ function mergeMcpServerConfigFile(filePath, options = {}) {
1080
+ const current = readJsonObjectFileSafe(filePath);
1081
+ const useQClawShape = isQClawMcpConfigTarget(filePath, options, current);
1082
+ const nextConfig = buildMcpConfigFileContent({ ...options, client: useQClawShape ? "qclaw" : options.client });
1083
+ const nextServers = useQClawShape ? nextConfig.mcp?.servers : nextConfig.mcpServers;
1084
+ const serverNames = Object.keys(nextServers || {});
1085
+ const nextBossServerNames = new Set(serverNames);
1086
+ const existingServers = getMcpServersFromConfig(current, useQClawShape);
1087
+ const mergedBossServers = {};
1088
+ for (const serverName of serverNames) {
1089
+ mergedBossServers[serverName] = mergeExistingMcpEntryEnv(
1090
+ existingServers[serverName],
1091
+ nextServers?.[serverName] || buildMcpLaunchConfig(options)
1092
+ );
1093
+ }
1094
+ const retainedServers = {};
1095
+ const migratedLegacyServers = [];
1096
+ for (const [name, config] of Object.entries(existingServers)) {
1097
+ if (nextBossServerNames.has(name)) continue;
1098
+ if (isBossMcpServerEntry(name, config)) {
1099
+ migratedLegacyServers.push(name);
1100
+ continue;
1061
1101
  }
1062
1102
  retainedServers[name] = config;
1063
1103
  }
@@ -1065,20 +1105,20 @@ function mergeMcpServerConfigFile(filePath, options = {}) {
1065
1105
  ? {
1066
1106
  ...current,
1067
1107
  mcp: {
1068
- ...(current?.mcp && typeof current.mcp === "object" && !Array.isArray(current.mcp) ? current.mcp : {}),
1069
- servers: {
1070
- ...retainedServers,
1071
- [serverName]: launchConfig
1072
- }
1073
- }
1074
- }
1075
- : {
1076
- ...current,
1077
- mcpServers: {
1078
- ...retainedServers,
1079
- [serverName]: launchConfig
1080
- }
1081
- };
1108
+ ...(current?.mcp && typeof current.mcp === "object" && !Array.isArray(current.mcp) ? current.mcp : {}),
1109
+ servers: {
1110
+ ...retainedServers,
1111
+ ...mergedBossServers
1112
+ }
1113
+ }
1114
+ }
1115
+ : {
1116
+ ...current,
1117
+ mcpServers: {
1118
+ ...retainedServers,
1119
+ ...mergedBossServers
1120
+ }
1121
+ };
1082
1122
 
1083
1123
  ensureDir(path.dirname(filePath));
1084
1124
  const before = pathExists(filePath) ? fs.readFileSync(filePath, "utf8") : "";
@@ -1088,14 +1128,15 @@ function mergeMcpServerConfigFile(filePath, options = {}) {
1088
1128
  backupFile = `${filePath}.boss-mcp-migration-${new Date().toISOString().replace(/[:.]/g, "-")}.bak`;
1089
1129
  fs.writeFileSync(backupFile, before, "utf8");
1090
1130
  }
1091
- fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf8");
1092
- const updated = before.trim() !== next.trim() || JSON.stringify(existingEntry || null) !== JSON.stringify(launchConfig);
1093
- return {
1094
- file: filePath,
1095
- server: serverName,
1096
- config_shape: useQClawShape ? "qclaw" : "mcpServers",
1097
- updated,
1098
- migrated_legacy_servers: migratedLegacyServers,
1131
+ fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf8");
1132
+ const updated = before.trim() !== next.trim();
1133
+ return {
1134
+ file: filePath,
1135
+ server: serverNames[0] || defaultMcpServerName,
1136
+ servers: serverNames,
1137
+ config_shape: useQClawShape ? "qclaw" : "mcpServers",
1138
+ updated,
1139
+ migrated_legacy_servers: migratedLegacyServers,
1099
1140
  backup_file: backupFile
1100
1141
  };
1101
1142
  }
@@ -1108,12 +1149,13 @@ function installExternalMcpConfigs(options = {}) {
1108
1149
  try {
1109
1150
  const existed = pathExists(target);
1110
1151
  const merged = mergeMcpServerConfigFile(target, options);
1111
- applied.push({
1112
- file: target,
1113
- server: merged.server,
1114
- created: !existed,
1115
- updated: merged.updated,
1116
- migrated_legacy_servers: merged.migrated_legacy_servers,
1152
+ applied.push({
1153
+ file: target,
1154
+ server: merged.server,
1155
+ servers: merged.servers,
1156
+ created: !existed,
1157
+ updated: merged.updated,
1158
+ migrated_legacy_servers: merged.migrated_legacy_servers,
1117
1159
  backup_file: merged.backup_file
1118
1160
  });
1119
1161
  } catch (error) {
@@ -2718,6 +2760,7 @@ function printHelp() {
2718
2760
  console.log(" boss-recommend-mcp launch-chrome Launch or reuse Chrome debug instance and open Boss recommend page");
2719
2761
  console.log(" boss-recommend-mcp where Print installed package, skill, and config paths");
2720
2762
  console.log(" boss-recommend-mcp install --mcp-launch global-wrapper Use ~/.boss-recommend-mcp/bin wrapper so npm global upgrades affect MCP hosts");
2763
+ console.log(" boss-recommend-mcp start --toolset recommend Start a narrowed MCP server (all|recommend|chat|recruit)");
2721
2764
  console.log("");
2722
2765
  console.log("Run command:");
2723
2766
  console.log(" boss-recommend-mcp prepare-run --instruction \"...\" --overrides-file overrides.json --confirmation-file confirmation.json --rest-level medium");
@@ -3397,6 +3440,7 @@ export const __testables = {
3397
3440
  buildBossChatCliInput,
3398
3441
  buildDefaultMcpArgs,
3399
3442
  buildMcpLaunchConfig,
3443
+ buildMcpConfigFileContent,
3400
3444
  ensureGlobalMcpWrapper,
3401
3445
  getGlobalMcpWrapperPath,
3402
3446
  collectRuntimeDirectories,
@@ -16,9 +16,7 @@ export const LEGACY_RESULT_HEADER = [
16
16
  "简历来源",
17
17
  "原始判定通过",
18
18
  "最终判定通过",
19
- "证据总数",
20
- "证据命中数",
21
- "证据门控降级",
19
+ "LLM thinking_level",
22
20
  "错误码",
23
21
  "错误信息",
24
22
  "候选人ID",
@@ -182,12 +180,6 @@ function firstBoolean(...values) {
182
180
  return "";
183
181
  }
184
182
 
185
- function evidenceCount(llm = {}) {
186
- if (Number.isFinite(llm.evidence_count)) return llm.evidence_count;
187
- if (Array.isArray(llm.evidence)) return llm.evidence.length;
188
- return "";
189
- }
190
-
191
183
  function actionResultText(row = {}) {
192
184
  const action = row.post_action || row.action || {};
193
185
  if (action.requested === true && !action.skipped) {
@@ -258,10 +250,10 @@ export function legacyScreenResultRow(row = {}) {
258
250
  ? "passed"
259
251
  : "skipped";
260
252
  const cot = firstText(
261
- llm.reasoning_content,
262
- llm.raw_reasoning_content,
263
253
  llm.decision_cot,
264
254
  llm.cot,
255
+ llm.reasoning_content,
256
+ llm.raw_reasoning_content,
265
257
  llm.raw_model_output,
266
258
  llm.raw_content,
267
259
  row.decision_cot,
@@ -276,7 +268,6 @@ export function legacyScreenResultRow(row = {}) {
276
268
  candidate.source,
277
269
  screening.candidate?.source
278
270
  );
279
- const totalEvidence = evidenceCount(llm);
280
271
  return [
281
272
  identity.name,
282
273
  identity.school,
@@ -290,9 +281,7 @@ export function legacyScreenResultRow(row = {}) {
290
281
  cvSource,
291
282
  rawPassed,
292
283
  finalPassed,
293
- totalEvidence,
294
- totalEvidence,
295
- "",
284
+ firstText(llm.provider?.thinking_level),
296
285
  row.error_code || error.code || error.name || (llm.error ? "LLM_SCREENING_ERROR" : ""),
297
286
  row.error_message || error.message || llm.error || "",
298
287
  candidate.id || row.candidate_id || "",
@@ -1477,7 +1477,7 @@ export function llmResultToScreening(llmResult, candidate) {
1477
1477
  }
1478
1478
 
1479
1479
  export function isRecoverableLlmScreeningError(error) {
1480
- return /(?:LLM response missing boolean passed decision|LLM response was not valid JSON)/i
1480
+ return /(?:LLM response missing boolean passed decision|LLM response missing brief summary|LLM response was not valid JSON)/i
1481
1481
  .test(String(error?.message || error || ""));
1482
1482
  }
1483
1483
 
@@ -1504,6 +1504,7 @@ export function createFailedLlmScreeningResult(error) {
1504
1504
  export function buildScreeningLlmMessages({
1505
1505
  candidate,
1506
1506
  criteria,
1507
+ thinkingLevel = "low",
1507
1508
  imageEvidence = null,
1508
1509
  imagePaths = [],
1509
1510
  imageInputs = null,
@@ -1512,6 +1513,8 @@ export function buildScreeningLlmMessages({
1512
1513
  }) {
1513
1514
  const safeCriteria = normalizeText(criteria || "判断候选人是否符合本次招聘筛选标准");
1514
1515
  const safeText = String(candidate?.text?.raw || candidate?.text || "");
1516
+ const normalizedThinkingLevel = normalizeLlmThinkingLevel(thinkingLevel) || "low";
1517
+ const requestSummary = normalizedThinkingLevel === "current";
1515
1518
  const images = Array.isArray(imageInputs)
1516
1519
  ? imageInputs
1517
1520
  : buildScreeningLlmImageInputs({
@@ -1520,6 +1523,11 @@ export function buildScreeningLlmMessages({
1520
1523
  maxImages,
1521
1524
  detail: imageDetail
1522
1525
  });
1526
+ const outputShape = requestSummary
1527
+ ? "4) 只返回 JSON,格式为:"
1528
+ + "{\"passed\": true/false, \"summary\": \"少于100个中文词的筛选总结\"}"
1529
+ : "4) 只返回 JSON,格式为:"
1530
+ + "{\"passed\": true/false}";
1523
1531
  const prompt =
1524
1532
  `请根据以下标准判断候选人是否通过筛选。\n\n筛选标准:\n${safeCriteria}\n\n`
1525
1533
  + `候选人信息:\n${safeText || "候选人的完整简历信息在后续截图中,请按截图顺序阅读。"}\n\n`
@@ -1529,9 +1537,10 @@ export function buildScreeningLlmMessages({
1529
1537
  + "要求:\n"
1530
1538
  + "1) 只能依据候选人信息或截图中真实出现的内容判断。\n"
1531
1539
  + "2) 若证据不足或截图无法确认,必须返回 passed=false。\n"
1532
- + "3) 不要输出评估原因、证据列表、解释或额外文字。\n"
1533
- + "4) 只返回 JSON,格式为:"
1534
- + "{\"passed\": true/false}";
1540
+ + (requestSummary
1541
+ ? "3) summary 必须为少于100个中文词的简短筛选总结,可包含核心依据和主要风险;不要输出推理过程。\n"
1542
+ : "3) 不要输出评估原因、证据列表、解释或额外文字。\n")
1543
+ + outputShape;
1535
1544
  const userContent = images.length
1536
1545
  ? [
1537
1546
  { type: "text", text: prompt },
@@ -1546,7 +1555,9 @@ export function buildScreeningLlmMessages({
1546
1555
  role: "system",
1547
1556
  content:
1548
1557
  "你是一位严谨的招聘筛选助手。必须完整阅读输入内容,严禁编造不存在的候选人经历。"
1549
- + "只能返回严格 JSON,不要输出原因、证据或额外文字。"
1558
+ + (requestSummary
1559
+ ? "只能返回严格 JSON。必须包含 passed 和 summary;summary 用中文,少于100个词,只概括筛选结论、核心依据和主要风险,不要输出推理过程。"
1560
+ : "只能返回严格 JSON,不要输出原因、证据或额外文字。")
1550
1561
  },
1551
1562
  {
1552
1563
  role: "user",
@@ -1608,6 +1619,7 @@ async function callScreeningLlmWithProvider({
1608
1619
  messages: buildScreeningLlmMessages({
1609
1620
  candidate,
1610
1621
  criteria,
1622
+ thinkingLevel,
1611
1623
  imageInputs
1612
1624
  })
1613
1625
  };
@@ -1665,20 +1677,27 @@ async function callScreeningLlmWithProvider({
1665
1677
  if (passed === null) {
1666
1678
  throw new Error(`LLM response missing boolean passed decision: ${content.slice(0, 240)}`);
1667
1679
  }
1680
+ const normalizedThinkingLevel = normalizeLlmThinkingLevel(thinkingLevel) || "low";
1681
+ const summary = normalizeBlockText(parsed?.summary || parsed?.screen_summary || parsed?.brief_summary);
1682
+ if (normalizedThinkingLevel === "current" && !summary) {
1683
+ throw new Error(`LLM response missing brief summary for current thinking level: ${content.slice(0, 240)}`);
1684
+ }
1668
1685
  const evidence = Array.isArray(parsed?.evidence)
1669
1686
  ? parsed.evidence.map(normalizeText).filter(Boolean)
1670
1687
  : [];
1671
- const decisionCot = firstReasoningText([
1672
- parsed?.cot,
1673
- parsed?.decision_cot,
1674
- parsed?.reasoning,
1675
- parsed?.chain_of_thought,
1676
- reasoningContent
1677
- ].map(normalizeBlockText).filter(Boolean)) || reasoningContent;
1688
+ const decisionCot = normalizedThinkingLevel === "current"
1689
+ ? summary
1690
+ : (firstReasoningText([
1691
+ parsed?.cot,
1692
+ parsed?.decision_cot,
1693
+ parsed?.reasoning,
1694
+ parsed?.chain_of_thought,
1695
+ reasoningContent
1696
+ ].map(normalizeBlockText).filter(Boolean)) || reasoningContent);
1678
1697
  const providerName = normalizeText(config.llmProviderName || config.name || config.label || config.id);
1679
1698
  const providerIndex = Number.isFinite(Number(config.llmProviderIndex)) ? Number(config.llmProviderIndex) : 0;
1680
1699
  const providerCount = Number.isFinite(Number(config.llmProviderCount)) ? Number(config.llmProviderCount) : 1;
1681
- return {
1700
+ const result = {
1682
1701
  ok: true,
1683
1702
  provider: {
1684
1703
  baseUrl: redactBaseUrl(baseUrl),
@@ -1708,6 +1727,7 @@ async function callScreeningLlmWithProvider({
1708
1727
  provider_attempt_count: attempt,
1709
1728
  screened_at: nowIso()
1710
1729
  };
1730
+ return result;
1711
1731
  } catch (error) {
1712
1732
  lastError = error;
1713
1733
  if (attempt >= maxAttempts || !isRetryableLlmRequestError(error)) {