@reconcrap/boss-recommend-mcp 1.3.20 → 1.3.22

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/README.md CHANGED
@@ -245,6 +245,7 @@ Trae-CN / 长对话防循环建议:
245
245
 
246
246
  - 固定流程:`boss_chat_health_check` -> `prepare_boss_chat_run(空参可)` -> 一次性补齐 `job/start_from/target_count/criteria` -> `start_boss_chat_run`。
247
247
  - `start_boss_chat_run` 的工具 schema 已把 `job/start_from/target_count/criteria` 标记为必填;不要用它获取岗位列表。
248
+ - 若 `pending_questions` / UI 选项里出现“扫到底(必须传 `target_count="all"`)”,下一次工具调用请直接照抄 `"target_count": "all"`,不要只保留“扫到底”这层自然语言语义。
248
249
  - `start_boss_chat_run` 返回 `ACCEPTED` 后直接结束当前回合,不要自动轮询。
249
250
  - 缺参或校验失败时,一次性列出全部缺失/错误项,避免重复同一句提示触发宿主“陷入循环”保护。
250
251
  - 仅当用户明确要求“查进度”时再调用 `get_boss_chat_run`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.3.20",
3
+ "version": "1.3.22",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -34,5 +34,6 @@ target_count mapping:
34
34
  - `全部候选人` / `所有候选人` must also be treated as unlimited.
35
35
  - Always write the argument key as `target_count`.
36
36
  - For unlimited mode, prefer `"target_count": "all"` in the tool call; `-1` is accepted for compatibility and used internally by the CLI.
37
+ - If the tool/UI shows `扫到底(必须传 target_count="all")`, copy that literal into the next tool call instead of paraphrasing it.
37
38
  - If start_boss_chat_run returns NEED_INPUT for `target_count`, the previous tool call omitted the argument. Retry once using `next_call_example` and include `"target_count": "all"` or a positive integer.
38
39
  ```
@@ -55,6 +55,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
55
55
  - 当用户说“全部候选人/所有候选人”时,必须按“扫到底(unlimited)”处理,不要再追问正整数。
56
56
  - 参数名必须写 `target_count`(不要写“目标数量”等中文键名)。
57
57
  - 当用户选择“扫到底/全部候选人/所有候选人”时,调用参数优先写:`"target_count": "all"`;`-1` 只作为兼容输入和内部 CLI 表示。
58
+ - 若工具或提问选项里出现“扫到底(必须传 `target_count=\"all\"`)”之类字样,下一次工具调用时必须直接照抄这个字面量,不要只保留“扫到底”语义。
58
59
  - 禁止 agent 自行补全 `job/start_from/criteria` 并直接执行,必须由用户明确给出或确认。
59
60
  - chat-only 启动流程必须先进入聊天页并拉取岗位列表,再让用户从列表中选择 `job`。
60
61
  - 必须先用空参调用 `prepare_boss_chat_run` 获取 `job_options`;不要用 `start_boss_chat_run` 做预备调用。
package/src/boss-chat.js CHANGED
@@ -14,7 +14,8 @@ const PREPARE_BOSS_CHAT_MAX_ATTEMPTS = 3;
14
14
  const PREPARE_BOSS_CHAT_RETRY_DELAY_MS = 1200;
15
15
  const BOSS_CHAT_TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
16
16
  const CHAT_REQUIRED_FIELDS = ["job", "start_from", "target_count", "criteria"];
17
- export const TARGET_COUNT_ACCEPTED_EXAMPLES = ["all", -1, 20, "全部候选人"];
17
+ export const TARGET_COUNT_CANONICAL_ALL = "all";
18
+ export const TARGET_COUNT_ACCEPTED_EXAMPLES = [TARGET_COUNT_CANONICAL_ALL, -1, 20, "全部候选人"];
18
19
  const TARGET_COUNT_WRAPPER_KEYS = ["target_count", "targetCount", "value", "count", "limit"];
19
20
  const LLM_THINKING_LEVEL_FIELDS = [
20
21
  "llmThinkingLevel",
@@ -128,6 +129,55 @@ function cloneForDiagnostics(value) {
128
129
  }
129
130
  }
130
131
 
132
+ export function buildTargetCountCompatibilityHints({
133
+ argumentName = "target_count",
134
+ recommendedArgumentPatch = { target_count: TARGET_COUNT_CANONICAL_ALL },
135
+ includeOptions = true
136
+ } = {}) {
137
+ const normalizedArgumentName = normalizeText(argumentName) || "target_count";
138
+ const clonedRecommendedPatch = cloneForDiagnostics(recommendedArgumentPatch)
139
+ || { target_count: TARGET_COUNT_CANONICAL_ALL };
140
+ const literal = `${normalizedArgumentName}="${TARGET_COUNT_CANONICAL_ALL}"`;
141
+ const base = {
142
+ argument_name: normalizedArgumentName,
143
+ answer_format: `${normalizedArgumentName} = 正整数 | "${TARGET_COUNT_CANONICAL_ALL}"`,
144
+ canonical_unlimited_value: TARGET_COUNT_CANONICAL_ALL,
145
+ recommended_value: TARGET_COUNT_CANONICAL_ALL,
146
+ recommended_argument_patch: clonedRecommendedPatch,
147
+ accepted_examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice()
148
+ };
149
+ if (!includeOptions) return base;
150
+ return {
151
+ ...base,
152
+ options: [
153
+ {
154
+ label: `扫到底(必须传 ${literal},推荐)`,
155
+ value: TARGET_COUNT_CANONICAL_ALL,
156
+ canonical_value: TARGET_COUNT_CANONICAL_ALL,
157
+ argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
158
+ },
159
+ {
160
+ label: `不限(等价于 ${literal})`,
161
+ value: "unlimited",
162
+ canonical_value: TARGET_COUNT_CANONICAL_ALL,
163
+ argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
164
+ },
165
+ {
166
+ label: `全部候选人(等价于 ${literal})`,
167
+ value: "全部候选人",
168
+ canonical_value: TARGET_COUNT_CANONICAL_ALL,
169
+ argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
170
+ },
171
+ {
172
+ label: `所有候选人(等价于 ${literal})`,
173
+ value: "所有候选人",
174
+ canonical_value: TARGET_COUNT_CANONICAL_ALL,
175
+ argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
176
+ }
177
+ ]
178
+ };
179
+ }
180
+
131
181
  export function normalizeTargetCountInput(value) {
132
182
  if (value === undefined || value === null) {
133
183
  return {
@@ -369,16 +419,16 @@ function getMissingBossChatStartFields(input = {}) {
369
419
 
370
420
  function buildTargetCountQuestionHint(item = {}) {
371
421
  const next = { ...item };
372
- next.question = "请输入 target_count:正整数,或 all(扫到底)。";
373
- next.options = [
374
- { label: "扫到底(推荐)", value: "all" },
375
- { label: "不限", value: "unlimited" },
376
- { label: "全部候选人", value: "全部候选人" },
377
- { label: "所有候选人", value: "所有候选人" }
378
- ];
379
- next.examples = TARGET_COUNT_ACCEPTED_EXAMPLES.slice();
380
- next.argument_name = "target_count";
381
- return next;
422
+ const hints = buildTargetCountCompatibilityHints({
423
+ argumentName: "target_count",
424
+ recommendedArgumentPatch: { target_count: TARGET_COUNT_CANONICAL_ALL }
425
+ });
426
+ return {
427
+ ...next,
428
+ ...hints,
429
+ question: `请输入 target_count:正整数,或直接填写 "${TARGET_COUNT_CANONICAL_ALL}"(扫到底)。`,
430
+ examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice()
431
+ };
382
432
  }
383
433
 
384
434
  function normalizePendingQuestions(pendingQuestions = []) {
@@ -406,8 +456,13 @@ function buildNextCallExample(input = {}, missingFields = []) {
406
456
  function buildTargetCountNeedInputDiagnostics(input = {}, missingFields = []) {
407
457
  if (!Array.isArray(missingFields) || !missingFields.includes("target_count")) return {};
408
458
  const normalized = normalizeBossChatStartInput(input);
459
+ const hints = buildTargetCountCompatibilityHints({
460
+ argumentName: "target_count",
461
+ recommendedArgumentPatch: { target_count: TARGET_COUNT_CANONICAL_ALL },
462
+ includeOptions: false
463
+ });
409
464
  return {
410
- accepted_examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice(),
465
+ ...hints,
411
466
  ...(normalized.targetCountRawValue !== undefined ? { received_target_count: normalized.targetCountRawValue } : {}),
412
467
  ...(normalized.targetCountParseError ? { target_count_parse_error: normalized.targetCountParseError } : {})
413
468
  };
package/src/cli.js CHANGED
@@ -45,6 +45,10 @@ const recommendMcpBinaryName = "boss-recommend-mcp";
45
45
  const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h"]);
46
46
  const externalMcpTargetsEnv = "BOSS_RECOMMEND_MCP_CONFIG_TARGETS";
47
47
  const externalSkillDirsEnv = "BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS";
48
+ const installConfigDefaults = Object.freeze({
49
+ llmThinkingLevel: "low",
50
+ humanRestEnabled: false
51
+ });
48
52
 
49
53
  function getSkillSourceDir(name = skillName) {
50
54
  return path.join(packageRoot, "skills", name);
@@ -655,6 +659,21 @@ function resolveCliConfigTarget(options = {}) {
655
659
  };
656
660
  }
657
661
 
662
+ function applyMissingInstallConfigDefaults(config = {}) {
663
+ const nextConfig = { ...config };
664
+ const patchedKeys = [];
665
+ for (const [key, defaultValue] of Object.entries(installConfigDefaults)) {
666
+ if (!Object.prototype.hasOwnProperty.call(nextConfig, key)) {
667
+ nextConfig[key] = defaultValue;
668
+ patchedKeys.push(key);
669
+ }
670
+ }
671
+ return {
672
+ nextConfig,
673
+ patchedKeys
674
+ };
675
+ }
676
+
658
677
  function ensureUserConfig(options = {}) {
659
678
  const { configPath, workspacePreferred } = resolveCliConfigTarget(options);
660
679
  const writeTargets = dedupePaths([configPath, workspacePreferred]).filter(Boolean);
@@ -671,7 +690,27 @@ function ensureUserConfig(options = {}) {
671
690
  }
672
691
  const stat = fs.statSync(targetPath);
673
692
  if (stat.isFile()) {
674
- return { path: targetPath, created: false };
693
+ try {
694
+ const existingConfig = readJsonObjectFile(targetPath);
695
+ const patched = applyMissingInstallConfigDefaults(existingConfig);
696
+ if (patched.patchedKeys.length > 0) {
697
+ fs.writeFileSync(targetPath, JSON.stringify(patched.nextConfig, null, 2), "utf8");
698
+ }
699
+ return {
700
+ path: targetPath,
701
+ created: false,
702
+ patched: patched.patchedKeys.length > 0,
703
+ patched_keys: patched.patchedKeys
704
+ };
705
+ } catch (error) {
706
+ return {
707
+ path: targetPath,
708
+ created: false,
709
+ patched: false,
710
+ patched_keys: [],
711
+ patch_error: error?.message || "screening-config.json 解析失败,跳过自动补字段。"
712
+ };
713
+ }
675
714
  }
676
715
  lastError = new Error(`Config target is a directory and cannot be used as file: ${targetPath}`);
677
716
  } catch (error) {
@@ -1286,6 +1325,11 @@ function installAll(options = {}) {
1286
1325
  ? `screening-config.json created: ${configResult.path}`
1287
1326
  : `screening-config.json already exists: ${configResult.path}`
1288
1327
  );
1328
+ if (Array.isArray(configResult.patched_keys) && configResult.patched_keys.length > 0) {
1329
+ console.log(`screening-config.json patched missing defaults: ${configResult.patched_keys.join(", ")}`);
1330
+ } else if (configResult.patch_error) {
1331
+ console.warn(`screening-config.json skip default patch: ${configResult.patch_error}`);
1332
+ }
1289
1333
  console.log(`请在该目录修改 baseUrl/apiKey/model 并替换占位词后再运行:${path.dirname(configResult.path)}`);
1290
1334
  console.log(`MCP config templates exported to: ${mcpTemplateResult.outputDir}`);
1291
1335
  for (const item of mcpTemplateResult.files) {
@@ -1482,6 +1526,11 @@ export async function runCli(argv = process.argv) {
1482
1526
  case "init-config": {
1483
1527
  const result = ensureUserConfig(options);
1484
1528
  console.log(result.created ? `Config template created at: ${result.path}` : `Config already exists at: ${result.path}`);
1529
+ if (Array.isArray(result.patched_keys) && result.patched_keys.length > 0) {
1530
+ console.log(`Config patched missing defaults: ${result.patched_keys.join(", ")}`);
1531
+ } else if (result.patch_error) {
1532
+ console.warn(`Config skip default patch: ${result.patch_error}`);
1533
+ }
1485
1534
  break;
1486
1535
  }
1487
1536
  case "set-port": {
package/src/index.js CHANGED
@@ -164,7 +164,8 @@ function createTargetCountInputSchema(description) {
164
164
  additionalProperties: true
165
165
  }
166
166
  ],
167
- description
167
+ description: `${description} 若用户选择扫到底/不限/全部候选人,优先字面传 "all"。`,
168
+ examples: ["all", 20, { value: "all" }]
168
169
  };
169
170
  }
170
171
 
@@ -486,7 +487,21 @@ function createBossChatStartInputSchema({ requireFullInput = false } = {}) {
486
487
  safe_pacing: { type: "boolean" },
487
488
  batch_rest_enabled: { type: "boolean" }
488
489
  },
489
- additionalProperties: false
490
+ additionalProperties: false,
491
+ examples: [
492
+ {
493
+ job: "530272634",
494
+ start_from: "unread",
495
+ target_count: "all",
496
+ criteria: "请扫到底筛选符合条件的人选"
497
+ },
498
+ {
499
+ job: "530272634",
500
+ start_from: "unread",
501
+ target_count: 20,
502
+ criteria: "请筛选 20 位符合条件的人选"
503
+ }
504
+ ]
490
505
  };
491
506
  if (requireFullInput) {
492
507
  schema.required = ["job", "start_from", "criteria"];
@@ -653,7 +668,7 @@ function createToolsSchema() {
653
668
  },
654
669
  {
655
670
  name: TOOL_BOSS_CHAT_START_RUN,
656
- description: "异步启动一次 boss-chat 任务。必须一次性提供 job、start_from、target_count、criteria;扫到底请传 target_count=\"all\"。",
671
+ description: "异步启动一次 boss-chat 任务。必须一次性提供 job、start_from、target_count、criteria;若用户选择扫到底/不限/全部候选人,必须字面传 target_count=\"all\"。",
657
672
  inputSchema: createBossChatStartInputSchema({ requireFullInput: true })
658
673
  },
659
674
  {
package/src/pipeline.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  switchRecommendTab
15
15
  } from "./adapters.js";
16
16
  import {
17
+ buildTargetCountCompatibilityHints,
17
18
  cancelBossChatRun,
18
19
  getBossChatRun,
19
20
  normalizeTargetCountInput,
@@ -445,12 +446,22 @@ function normalizeFollowUpChatInput(followUp = null, defaults = null) {
445
446
  });
446
447
  }
447
448
  if (!hasExplicitTargetCount) {
449
+ const targetCountHints = buildTargetCountCompatibilityHints({
450
+ argumentName: "follow_up.chat.target_count",
451
+ recommendedArgumentPatch: {
452
+ follow_up: {
453
+ chat: {
454
+ target_count: "all"
455
+ }
456
+ }
457
+ }
458
+ });
448
459
  missing_fields.push("follow_up.chat.target_count");
449
460
  pending_questions.push({
461
+ ...targetCountHints,
450
462
  field: "follow_up.chat.target_count",
451
- question: "请填写 boss-chat follow-up 本次处理人数上限(正整数,或 all/-1 表示扫到底,必填)。",
463
+ question: "请填写 boss-chat follow-up 本次处理人数上限。若扫到底,请在 follow_up.chat.target_count 里字面填写 \"all\"。",
452
464
  value: summary.target_count,
453
- accepted_examples: ["all", -1, 20, "全部候选人"],
454
465
  ...(explicitTarget.rawValue !== undefined ? { received_target_count: explicitTarget.rawValue } : {}),
455
466
  ...(explicitTarget.parseError ? { target_count_parse_error: explicitTarget.parseError } : {})
456
467
  });
@@ -226,6 +226,9 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
226
226
  assert.deepEqual(prepared.missing_fields, ["job", "start_from", "target_count", "criteria"]);
227
227
  const preparedTargetQuestion = prepared.pending_questions.find((item) => item.field === "target_count");
228
228
  assert.equal(preparedTargetQuestion.argument_name, "target_count");
229
+ assert.equal(preparedTargetQuestion.recommended_value, "all");
230
+ assert.equal(preparedTargetQuestion.recommended_argument_patch.target_count, "all");
231
+ assert.equal(preparedTargetQuestion.options.some((item) => item.label.includes('target_count="all"')), true);
229
232
  assert.equal(prepared.next_call_example.target_count, "all");
230
233
 
231
234
  const preflight = await startBossChatRun({
@@ -240,6 +243,7 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
240
243
  const preflightTargetQuestion = preflight.pending_questions.find((item) => item.field === "target_count");
241
244
  assert.equal(Boolean(preflightTargetQuestion), true);
242
245
  assert.equal(preflightTargetQuestion.argument_name, "target_count");
246
+ assert.equal(preflightTargetQuestion.recommended_argument_patch.target_count, "all");
243
247
  assert.equal(Array.isArray(preflightTargetQuestion.options), true);
244
248
 
245
249
  const stateAfterPrepare = readStubState(workspaceRoot);
@@ -331,6 +335,7 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
331
335
  assert.equal(Boolean(invalidTarget.target_count_parse_error), true);
332
336
  assert.equal(invalidTarget.next_call_example.target_count, "all");
333
337
  assert.equal(invalidTarget.accepted_examples.includes("all"), true);
338
+ assert.equal(invalidTarget.recommended_argument_patch.target_count, "all");
334
339
 
335
340
  const running = await getBossChatRun({
336
341
  workspaceRoot,
@@ -408,11 +413,15 @@ async function testBossChatMcpToolsShouldValidateAndRoute() {
408
413
  assert.deepEqual(startToolSchema.required, ["job", "start_from", "criteria"]);
409
414
  assert.equal(startToolSchema.anyOf.some((item) => item.required?.includes("target_count")), true);
410
415
  assert.equal(startToolSchema.anyOf.some((item) => item.required?.includes("targetCount")), true);
416
+ assert.equal(startToolSchema.properties.target_count.examples.includes("all"), true);
417
+ assert.equal(startToolSchema.examples.some((item) => item.target_count === "all"), true);
411
418
 
412
419
  const prepared = await callTool(workspaceRoot, TOOL_BOSS_CHAT_PREPARE_RUN, {}, 101);
413
420
  assert.equal(prepared.status, "NEED_INPUT");
414
421
  assert.deepEqual(prepared.missing_fields, ["job", "start_from", "target_count", "criteria"]);
415
- assert.equal(prepared.pending_questions.find((item) => item.field === "target_count").argument_name, "target_count");
422
+ const preparedTargetCountQuestion = prepared.pending_questions.find((item) => item.field === "target_count");
423
+ assert.equal(preparedTargetCountQuestion.argument_name, "target_count");
424
+ assert.equal(preparedTargetCountQuestion.recommended_argument_patch.target_count, "all");
416
425
 
417
426
  const needInput = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {}, 11);
418
427
  assert.equal(needInput.status, "NEED_INPUT");
@@ -422,7 +431,9 @@ async function testBossChatMcpToolsShouldValidateAndRoute() {
422
431
  const targetQuestion = needInput.pending_questions.find((item) => item.field === "target_count");
423
432
  assert.equal(Boolean(targetQuestion), true);
424
433
  assert.equal(targetQuestion.argument_name, "target_count");
434
+ assert.equal(targetQuestion.recommended_argument_patch.target_count, "all");
425
435
  assert.equal(targetQuestion.options.some((item) => item.value === "all"), true);
436
+ assert.equal(targetQuestion.options.some((item) => item.label.includes('target_count="all"')), true);
426
437
 
427
438
  const missingTargetOnly = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
428
439
  job: "算法工程师",
@@ -445,6 +456,7 @@ async function testBossChatMcpToolsShouldValidateAndRoute() {
445
456
  assert.equal(invalidTargetOnly.received_target_count, "not a target");
446
457
  assert.equal(Boolean(invalidTargetOnly.target_count_parse_error), true);
447
458
  assert.equal(invalidTargetOnly.next_call_example.target_count, "all");
459
+ assert.equal(invalidTargetOnly.recommended_argument_patch.target_count, "all");
448
460
 
449
461
  const invalidStartResponse = await handleRequest(
450
462
  makeToolCall(11, TOOL_BOSS_CHAT_START_RUN, {
@@ -2184,6 +2184,8 @@ async function testFollowUpChatMissingFieldsShouldExposeRecommendDefaults() {
2184
2184
  assert.equal(criteriaQuestion?.value, "默认沿用 recommend 的筛选条件");
2185
2185
  assert.equal(startFromQuestion?.value, "unread");
2186
2186
  assert.equal(targetCountQuestion?.value, 18);
2187
+ assert.equal(targetCountQuestion?.recommended_argument_patch?.follow_up?.chat?.target_count, "all");
2188
+ assert.equal(targetCountQuestion?.options?.some((item) => item.label.includes('follow_up.chat.target_count="all"')), true);
2187
2189
  }
2188
2190
 
2189
2191
  async function testFollowUpChatMissingStartFromShouldNeedInput() {
@@ -2221,7 +2223,9 @@ async function testFollowUpChatMissingTargetCountShouldNeedInput() {
2221
2223
 
2222
2224
  assert.equal(result.status, "NEED_INPUT");
2223
2225
  assert.equal(result.missing_fields.includes("follow_up.chat.target_count"), true);
2224
- assert.equal(result.pending_questions.some((item) => item.field === "follow_up.chat.target_count"), true);
2226
+ const targetQuestion = result.pending_questions.find((item) => item.field === "follow_up.chat.target_count");
2227
+ assert.equal(Boolean(targetQuestion), true);
2228
+ assert.equal(targetQuestion.recommended_argument_patch?.follow_up?.chat?.target_count, "all");
2225
2229
  }
2226
2230
 
2227
2231
  async function testFollowUpChatInvalidTargetCountShouldNeedInputWithDiagnostics() {
@@ -2244,6 +2248,7 @@ async function testFollowUpChatInvalidTargetCountShouldNeedInputWithDiagnostics(
2244
2248
  assert.equal(targetQuestion?.received_target_count, "not a target");
2245
2249
  assert.equal(Boolean(targetQuestion?.target_count_parse_error), true);
2246
2250
  assert.equal(targetQuestion?.accepted_examples.includes("all"), true);
2251
+ assert.equal(targetQuestion?.recommended_argument_patch?.follow_up?.chat?.target_count, "all");
2247
2252
  }
2248
2253
 
2249
2254
  async function testFollowUpChatAllTargetCountShouldLaunchUnlimited() {