@reconcrap/boss-recommend-mcp 1.3.5 → 1.3.7

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
@@ -215,6 +215,7 @@ chat-only 交互建议:
215
215
 
216
216
  - 先调用一次 `start_boss_chat_run`(可不带参数),服务会先导航到 `https://www.zhipin.com/web/chat/index` 并返回 `NEED_INPUT`,其中包含岗位 `job_options` 与待补字段。
217
217
  - 然后基于 `job_options` 让用户选择 `job`,并补齐 `start_from`、`target_count`、`criteria` 后再次调用 `start_boss_chat_run` 启动任务。
218
+ - `target_count` 支持正整数;若用户给出 `全部候选人` / `所有候选人`,会自动按不限(扫到底)处理。
218
219
 
219
220
  Trae-CN / 长对话防循环建议:
220
221
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -23,4 +23,11 @@ Anti-loop rules:
23
23
  - On validation errors, list all missing/invalid fields once.
24
24
  - Do not call start_boss_chat_run repeatedly in one turn.
25
25
  - Do not call get_boss_chat_run unless user explicitly asks for progress.
26
+
27
+ target_count mapping:
28
+ - Positive integer means explicit cap (for example 20).
29
+ - `all` / `unlimited` / `全部` / `不限` / `扫到底` / `全量` means unlimited.
30
+ - `全部候选人` / `所有候选人` must also be treated as unlimited.
31
+ - Always write the argument key as `target_count`.
32
+ - For unlimited mode, send `"target_count": "all"` in the tool call.
26
33
  ```
@@ -37,6 +37,12 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
37
37
  - `safe_pacing`
38
38
  - `batch_rest_enabled`
39
39
 
40
+ `target_count` 填写规则(关键):
41
+
42
+ - 正整数:如 `20`
43
+ - 扫到底:`all` / `unlimited` / `全部` / `不限` / `扫到底` / `全量`
44
+ - 同义短语也可直接用:`全部候选人` / `所有候选人`(等价于扫到底)
45
+
40
46
  ## Hard Rules
41
47
 
42
48
  - LLM 配置必须复用 `boss-recommend-mcp` 的 `screening-config.json`;不要再向用户单独要 `baseUrl/apiKey/model`。
@@ -46,6 +52,9 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
46
52
  - 不得在 recommend 任务尚未完成时并行启动独立 chat run。
47
53
  - `job` / `start_from` / `criteria` 缺一不可;缺参时只补缺口。
48
54
  - `target_count` 在 chat-only 启动前也是必填项,不能默认省略。
55
+ - 当用户说“全部候选人/所有候选人”时,必须按“扫到底(unlimited)”处理,不要再追问正整数。
56
+ - 参数名必须写 `target_count`(不要写“目标数量”等中文键名)。
57
+ - 当用户选择“扫到底/全部候选人/所有候选人”时,调用参数统一写:`"target_count": "all"`。
49
58
  - 禁止 agent 自行补全 `job/start_from/criteria` 并直接执行,必须由用户明确给出或确认。
50
59
  - chat-only 启动流程必须先进入聊天页并拉取岗位列表,再让用户从列表中选择 `job`。
51
60
  - 允许先用空参调用 `start_boss_chat_run` 触发 `NEED_INPUT`;若返回了 `job_options`,必须完整展示所有岗位选项给用户确认。
package/src/boss-chat.js CHANGED
@@ -33,19 +33,29 @@ function parsePositiveInteger(value, fallback = null) {
33
33
  function isUnlimitedTargetCountToken(value) {
34
34
  const token = normalizeText(value).toLowerCase();
35
35
  if (!token) return false;
36
- return [
36
+ const compact = token.replace(/\s+/g, "");
37
+ const knownTokens = new Set([
37
38
  "all",
38
39
  "unlimited",
39
40
  "infinity",
40
41
  "inf",
41
42
  "max",
42
43
  "full",
44
+ "allcandidates",
43
45
  "全部",
44
46
  "全量",
45
47
  "不限",
46
48
  "扫到底",
49
+ "全部候选人",
50
+ "所有候选人",
51
+ "全部人选",
52
+ "所有人选",
47
53
  "直到完成所有人选"
48
- ].includes(token);
54
+ ]);
55
+ if (knownTokens.has(token) || knownTokens.has(compact)) return true;
56
+ if (/^(?:all|unlimited|infinity|inf|max|full)(?:candidate|candidates)?$/i.test(compact)) return true;
57
+ if (/^(?:全部|所有|全量|不限)(?:候选人|人选|牛人|人才|人员)?$/u.test(compact)) return true;
58
+ return false;
49
59
  }
50
60
 
51
61
  function parseBossChatTargetCount(value) {
@@ -251,6 +261,42 @@ function getMissingBossChatStartFields(input = {}) {
251
261
  return missing;
252
262
  }
253
263
 
264
+ function buildTargetCountQuestionHint(item = {}) {
265
+ const next = { ...item };
266
+ next.question = "请输入 target_count:正整数,或 all(扫到底)。";
267
+ next.options = [
268
+ { label: "扫到底(推荐)", value: "all" },
269
+ { label: "不限", value: "unlimited" },
270
+ { label: "全部候选人", value: "全部候选人" },
271
+ { label: "所有候选人", value: "所有候选人" }
272
+ ];
273
+ next.examples = ["all", 20];
274
+ next.argument_name = "target_count";
275
+ return next;
276
+ }
277
+
278
+ function normalizePendingQuestions(pendingQuestions = []) {
279
+ return pendingQuestions.map((item) => {
280
+ if (String(item?.field || "") !== "target_count") return item;
281
+ return buildTargetCountQuestionHint(item);
282
+ });
283
+ }
284
+
285
+ function buildNextCallExample(input = {}, missingFields = []) {
286
+ if (!Array.isArray(missingFields) || missingFields.length === 0) return null;
287
+ const normalized = normalizeBossChatStartInput(input);
288
+ const sample = {};
289
+ if (normalized.job) sample.job = normalized.job;
290
+ if (normalized.startFrom) sample.start_from = normalized.startFrom;
291
+ if (normalized.criteria) sample.criteria = normalized.criteria;
292
+ if (normalized.targetCountProvided) {
293
+ sample.target_count = normalized.targetCountArg === "-1" ? "all" : normalized.targetCount;
294
+ } else if (missingFields.includes("target_count")) {
295
+ sample.target_count = "all";
296
+ }
297
+ return Object.keys(sample).length > 0 ? sample : null;
298
+ }
299
+
254
300
  function buildBossChatCliArgs(command, input, resolvedConfig) {
255
301
  const args = [command, "--json"];
256
302
  if (command === "prepare-run") {
@@ -445,12 +491,15 @@ export async function startBossChatRun({ workspaceRoot, input = {} }) {
445
491
  const pendingQuestions = Array.isArray(prepared?.pending_questions)
446
492
  ? prepared.pending_questions.filter((item) => missingFields.includes(String(item?.field || "")))
447
493
  : [];
494
+ const normalizedPendingQuestions = normalizePendingQuestions(pendingQuestions);
495
+ const nextCallExample = buildNextCallExample(input, missingFields);
448
496
  return {
449
497
  ...prepared,
450
498
  status: "NEED_INPUT",
451
499
  required_fields: CHAT_REQUIRED_FIELDS.slice(),
452
500
  missing_fields: missingFields,
453
- pending_questions: pendingQuestions,
501
+ pending_questions: normalizedPendingQuestions,
502
+ ...(nextCallExample ? { next_call_example: nextCallExample } : {}),
454
503
  message: prepared?.message
455
504
  || "已获取 Boss 聊天页岗位列表,请先补齐 job / start_from / target_count / criteria。"
456
505
  };
package/src/cli.js CHANGED
@@ -195,6 +195,14 @@ function parsePositivePort(raw) {
195
195
  return Number.isFinite(port) && port > 0 ? port : null;
196
196
  }
197
197
 
198
+ function parseBossChatTargetCountOption(raw) {
199
+ if (raw === undefined || raw === null) return undefined;
200
+ const text = String(raw).trim();
201
+ if (!text) return undefined;
202
+ const parsed = parsePositivePort(text);
203
+ return parsed ?? text;
204
+ }
205
+
198
206
  function parseBooleanOption(raw, fallback = undefined) {
199
207
  if (raw === undefined || raw === null || raw === "") return fallback;
200
208
  if (raw === true) return true;
@@ -1327,7 +1335,7 @@ function buildBossChatCliInput(options = {}) {
1327
1335
  job: typeof options.job === "string" ? options.job.trim() : undefined,
1328
1336
  start_from: String(options["start-from"] || options.start_from || "").trim().toLowerCase() || undefined,
1329
1337
  criteria: typeof options.criteria === "string" ? options.criteria.trim() : undefined,
1330
- target_count: parsePositivePort(options.targetCount || options["target-count"] || options.target_count),
1338
+ target_count: parseBossChatTargetCountOption(options.targetCount || options["target-count"] || options.target_count),
1331
1339
  port: parsePositivePort(options.port),
1332
1340
  dry_run: options["dry-run"] === true || options.dryRun === true,
1333
1341
  no_state: options["no-state"] === true || options.noState === true,
package/src/index.js CHANGED
@@ -77,19 +77,29 @@ function normalizeText(value) {
77
77
  function isUnlimitedTargetCountToken(value) {
78
78
  const token = normalizeText(value).toLowerCase();
79
79
  if (!token) return false;
80
- return [
80
+ const compact = token.replace(/\s+/g, "");
81
+ const knownTokens = new Set([
81
82
  "all",
82
83
  "unlimited",
83
84
  "infinity",
84
85
  "inf",
85
86
  "max",
86
87
  "full",
88
+ "allcandidates",
87
89
  "全部",
88
90
  "全量",
89
91
  "不限",
90
92
  "扫到底",
93
+ "全部候选人",
94
+ "所有候选人",
95
+ "全部人选",
96
+ "所有人选",
91
97
  "直到完成所有人选"
92
- ].includes(token);
98
+ ]);
99
+ if (knownTokens.has(token) || knownTokens.has(compact)) return true;
100
+ if (/^(?:all|unlimited|infinity|inf|max|full)(?:candidate|candidates)?$/i.test(compact)) return true;
101
+ if (/^(?:全部|所有|全量|不限)(?:候选人|人选|牛人|人才|人员)?$/u.test(compact)) return true;
102
+ return false;
93
103
  }
94
104
 
95
105
  function parsePositiveInteger(raw, fallback) {
@@ -383,7 +393,7 @@ function createRunInputSchema() {
383
393
  },
384
394
  {
385
395
  type: "string",
386
- enum: ["all", "unlimited", "全部", "不限", "扫到底", "全量"]
396
+ enum: ["all", "unlimited", "全部", "不限", "扫到底", "全量", "全部候选人", "所有候选人"]
387
397
  }
388
398
  ]
389
399
  },
@@ -432,10 +442,10 @@ function createBossChatStartInputSchema() {
432
442
  },
433
443
  {
434
444
  type: "string",
435
- enum: ["all", "unlimited", "全部", "不限", "扫到底", "全量"]
445
+ enum: ["all", "unlimited", "全部", "不限", "扫到底", "全量", "全部候选人", "所有候选人"]
436
446
  }
437
447
  ],
438
- description: "本次处理人数上限;支持正整数或 all/不限(扫到底)"
448
+ description: "本次处理人数上限;支持正整数或 all/不限/全部候选人(扫到底)"
439
449
  },
440
450
  port: {
441
451
  type: "integer",
@@ -713,7 +723,7 @@ function validateBossChatStartArgs(args) {
713
723
  typeof rawTargetCount === "string" && isUnlimitedTargetCountToken(rawTargetCount);
714
724
  const numericUnlimited = Number.isFinite(targetCount) && targetCount === -1;
715
725
  if ((!Number.isFinite(targetCount) || targetCount <= 0) && !tokenAllowed && !numericUnlimited) {
716
- return "target_count must be a positive integer or one of: all, unlimited, 全部, 不限, 扫到底, 全量";
726
+ return "target_count must be a positive integer or one of: all, unlimited, 全部, 不限, 扫到底, 全量, 全部候选人, 所有候选人";
717
727
  }
718
728
  }
719
729
  if (Object.prototype.hasOwnProperty.call(args, "port")) {
@@ -210,6 +210,11 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
210
210
  assert.deepEqual(preflight.required_fields, ["job", "start_from", "target_count", "criteria"]);
211
211
  assert.equal(Array.isArray(preflight.job_options), true);
212
212
  assert.equal(preflight.job_options.length, 2);
213
+ assert.equal(Array.isArray(preflight.pending_questions), true);
214
+ const preflightTargetQuestion = preflight.pending_questions.find((item) => item.field === "target_count");
215
+ assert.equal(Boolean(preflightTargetQuestion), true);
216
+ assert.equal(preflightTargetQuestion.argument_name, "target_count");
217
+ assert.equal(Array.isArray(preflightTargetQuestion.options), true);
213
218
 
214
219
  const stateAfterPrepare = readStubState(workspaceRoot);
215
220
  assert.equal(stateAfterPrepare.last_prepare_args.profile, "default");
@@ -242,6 +247,20 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
242
247
  assert.equal(stateAfterStart.last_start_args.model, "gpt-4.1-mini");
243
248
  assert.equal(stateAfterStart.last_start_args.port, "9666");
244
249
 
250
+ const startedAll = await startBossChatRun({
251
+ workspaceRoot,
252
+ input: {
253
+ profile: "default",
254
+ job: "算法工程师",
255
+ start_from: "all",
256
+ criteria: "全部候选人都过一遍",
257
+ target_count: "全部候选人"
258
+ }
259
+ });
260
+ assert.equal(startedAll.status, "ACCEPTED");
261
+ const stateAfterStartAll = readStubState(workspaceRoot);
262
+ assert.equal(stateAfterStartAll.last_start_args.targetCount, "-1");
263
+
245
264
  const running = await getBossChatRun({
246
265
  workspaceRoot,
247
266
  input: {
@@ -287,6 +306,19 @@ async function testBossChatMcpToolsShouldValidateAndRoute() {
287
306
  assert.deepEqual(needInput.required_fields, ["job", "start_from", "target_count", "criteria"]);
288
307
  assert.equal(Array.isArray(needInput.job_options), true);
289
308
  assert.equal(needInput.job_options.length, 2);
309
+ const targetQuestion = needInput.pending_questions.find((item) => item.field === "target_count");
310
+ assert.equal(Boolean(targetQuestion), true);
311
+ assert.equal(targetQuestion.argument_name, "target_count");
312
+ assert.equal(targetQuestion.options.some((item) => item.value === "all"), true);
313
+
314
+ const missingTargetOnly = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
315
+ job: "算法工程师",
316
+ start_from: "all",
317
+ criteria: "全部候选人都过一遍"
318
+ }, 111);
319
+ assert.equal(missingTargetOnly.status, "NEED_INPUT");
320
+ assert.deepEqual(missingTargetOnly.missing_fields, ["target_count"]);
321
+ assert.equal(missingTargetOnly.next_call_example.target_count, "all");
290
322
 
291
323
  const invalidStartResponse = await handleRequest(
292
324
  makeToolCall(11, TOOL_BOSS_CHAT_START_RUN, {
@@ -313,6 +345,16 @@ async function testBossChatMcpToolsShouldValidateAndRoute() {
313
345
  }, 14);
314
346
  assert.equal(started.status, "ACCEPTED");
315
347
 
348
+ const startedAll = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
349
+ job: "算法工程师",
350
+ start_from: "all",
351
+ criteria: "全部候选人都过一遍",
352
+ target_count: "全部候选人"
353
+ }, 140);
354
+ assert.equal(startedAll.status, "ACCEPTED");
355
+ const stateAfterStartAll = readStubState(workspaceRoot);
356
+ assert.equal(stateAfterStartAll.last_start_args.targetCount, "-1");
357
+
316
358
  const running = await callTool(workspaceRoot, TOOL_BOSS_CHAT_GET_RUN, {
317
359
  run_id: started.run_id,
318
360
  profile: "default"
@@ -386,6 +428,18 @@ async function testBossChatCliShouldSupportRunAndFollowUpParsing() {
386
428
  assert.equal(typeof payload.run_id, "string");
387
429
  const state = readStubState(workspaceRoot);
388
430
  assert.equal(state.get_calls[payload.run_id] || 0, 0);
431
+
432
+ await captureConsoleLogs(async () => {
433
+ await cliTestables.runBossChatCliCommand("run", {
434
+ "workspace-root": workspaceRoot,
435
+ job: "算法工程师",
436
+ "start-from": "all",
437
+ criteria: "全部候选人都过一遍",
438
+ targetCount: "全部候选人"
439
+ });
440
+ });
441
+ const allState = readStubState(workspaceRoot);
442
+ assert.equal(allState.last_start_args.targetCount, "-1");
389
443
  });
390
444
  }
391
445