@reconcrap/boss-recommend-mcp 1.3.9 → 1.3.11

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
@@ -2,6 +2,11 @@
2
2
 
3
3
  Boss 推荐页自动化流水线 MCP(stdio)服务。
4
4
 
5
+ 发布入口:
6
+
7
+ - npm: `@reconcrap/boss-recommend-mcp`(https://www.npmjs.com/package/@reconcrap/boss-recommend-mcp)
8
+ - GitHub: `reconcrap-cpu/boss-recommend-mcp`(https://github.com/reconcrap-cpu/boss-recommend-mcp)
9
+
5
10
  它把 recommend 页面上的两段能力串起来:
6
11
 
7
12
  - `boss-recommend-search-cli`: 只负责推荐页筛选项
@@ -79,13 +84,25 @@ MCP 工具:
79
84
 
80
85
  ## 安装
81
86
 
87
+ 推荐(npm 全局安装):
88
+
82
89
  ```bash
83
- npm install
90
+ npm install -g @reconcrap/boss-recommend-mcp@latest
91
+ boss-recommend-mcp start
92
+ ```
93
+
94
+ 无需安装(npx 直接运行):
95
+
96
+ ```bash
97
+ npx -y @reconcrap/boss-recommend-mcp@latest start
84
98
  ```
85
99
 
86
- 安装后可以直接运行:
100
+ 从 GitHub 源码运行(开发/调试):
87
101
 
88
102
  ```bash
103
+ git clone https://github.com/reconcrap-cpu/boss-recommend-mcp.git
104
+ cd boss-recommend-mcp
105
+ npm install
89
106
  node src/cli.js start
90
107
  ```
91
108
 
@@ -101,8 +118,9 @@ BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS # JSON 数组或系统 path 分隔路径列
101
118
  或使用 CLI fallback:
102
119
 
103
120
  ```bash
104
- node src/cli.js run --instruction "推荐页筛选985男生,近14天没有,有大模型平台经验,符合标准的收藏"
105
121
  npx -y @reconcrap/boss-recommend-mcp@latest run --instruction "推荐页筛选985男生,近14天没有,有大模型平台经验,符合标准的收藏"
122
+ # 源码模式(GitHub clone 后)
123
+ node src/cli.js run --instruction "推荐页筛选985男生,近14天没有,有大模型平台经验,符合标准的收藏"
106
124
  ```
107
125
 
108
126
  ## 配置
@@ -152,6 +170,8 @@ config/screening-config.example.json
152
170
 
153
171
  ## 常用命令
154
172
 
173
+ npm 包安装后可直接使用可执行命令 `boss-recommend-mcp`。以下示例展示源码模式(`node src/cli.js`):
174
+
155
175
  ```bash
156
176
  node src/cli.js install --agent trae-cn
157
177
  node src/cli.js init-config
@@ -217,7 +237,7 @@ chat-only 交互建议:
217
237
 
218
238
  - 先调用一次 `prepare_boss_chat_run`(可不带参数),服务会先导航到 `https://www.zhipin.com/web/chat/index` 并返回 `NEED_INPUT`,其中包含岗位 `job_options` 与待补字段。
219
239
  - 然后基于 `job_options` 让用户选择 `job`,并补齐 `start_from`、`target_count`、`criteria` 后调用 `start_boss_chat_run` 启动任务。
220
- - `target_count` 支持正整数;若用户给出 `全部候选人` / `所有候选人`,会自动按不限(扫到底)处理。
240
+ - `target_count` 支持正整数、`all`、`-1`;若用户给出 `全部候选人` / `所有候选人`,会自动按不限(扫到底)处理。
221
241
 
222
242
  Trae-CN / 长对话防循环建议:
223
243
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.3.9",
3
+ "version": "1.3.11",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -2,6 +2,9 @@
2
2
 
3
3
  Bundled chat-page automation skill shipped with `boss-recommend-mcp`.
4
4
 
5
+ Package: `@reconcrap/boss-recommend-mcp` (npm)
6
+ Source: `https://github.com/reconcrap-cpu/boss-recommend-mcp`
7
+
5
8
  Use this skill when the user wants a chat-only Boss workflow without installing `boss-chat` separately.
6
9
 
7
10
  ## Stable Prompt Template (Trae-CN)
@@ -27,9 +30,9 @@ Anti-loop rules:
27
30
 
28
31
  target_count mapping:
29
32
  - Positive integer means explicit cap (for example 20).
30
- - `all` / `unlimited` / `全部` / `不限` / `扫到底` / `全量` means unlimited.
33
+ - `all` / `-1` / `unlimited` / `全部` / `不限` / `扫到底` / `全量` means unlimited.
31
34
  - `全部候选人` / `所有候选人` must also be treated as unlimited.
32
35
  - Always write the argument key as `target_count`.
33
- - For unlimited mode, send `"target_count": "all"` in the tool call.
36
+ - For unlimited mode, prefer `"target_count": "all"` in the tool call; `-1` is accepted for compatibility and used internally by the CLI.
34
37
  - 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.
35
38
  ```
@@ -40,7 +40,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
40
40
  `target_count` 填写规则(关键):
41
41
 
42
42
  - 正整数:如 `20`
43
- - 扫到底:`all` / `unlimited` / `全部` / `不限` / `扫到底` / `全量`
43
+ - 扫到底:`all` / `-1` / `unlimited` / `全部` / `不限` / `扫到底` / `全量`
44
44
  - 同义短语也可直接用:`全部候选人` / `所有候选人`(等价于扫到底)
45
45
 
46
46
  ## Hard Rules
@@ -54,7 +54,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
54
54
  - `target_count` 在 chat-only 启动前也是必填项,不能默认省略。
55
55
  - 当用户说“全部候选人/所有候选人”时,必须按“扫到底(unlimited)”处理,不要再追问正整数。
56
56
  - 参数名必须写 `target_count`(不要写“目标数量”等中文键名)。
57
- - 当用户选择“扫到底/全部候选人/所有候选人”时,调用参数统一写:`"target_count": "all"`。
57
+ - 当用户选择“扫到底/全部候选人/所有候选人”时,调用参数优先写:`"target_count": "all"`;`-1` 只作为兼容输入和内部 CLI 表示。
58
58
  - 禁止 agent 自行补全 `job/start_from/criteria` 并直接执行,必须由用户明确给出或确认。
59
59
  - chat-only 启动流程必须先进入聊天页并拉取岗位列表,再让用户从列表中选择 `job`。
60
60
  - 必须先用空参调用 `prepare_boss_chat_run` 获取 `job_options`;不要用 `start_boss_chat_run` 做预备调用。
package/src/boss-chat.js CHANGED
@@ -12,6 +12,8 @@ const VENDORED_BOSS_CHAT_DIR = path.join(packageRoot, "vendor", "boss-chat-cli")
12
12
  const DEFAULT_BOSS_CHAT_POLL_MS = 1500;
13
13
  const BOSS_CHAT_TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
14
14
  const CHAT_REQUIRED_FIELDS = ["job", "start_from", "target_count", "criteria"];
15
+ export const TARGET_COUNT_ACCEPTED_EXAMPLES = ["all", -1, 20, "全部候选人"];
16
+ const TARGET_COUNT_WRAPPER_KEYS = ["target_count", "targetCount", "value", "count", "limit"];
15
17
 
16
18
  function normalizeText(value) {
17
19
  return String(value || "").replace(/\s+/g, " ").trim();
@@ -34,6 +36,7 @@ function isUnlimitedTargetCountToken(value) {
34
36
  const token = normalizeText(value).toLowerCase();
35
37
  if (!token) return false;
36
38
  const compact = token.replace(/\s+/g, "");
39
+ const withoutAnnotation = compact.replace(/[((【[].*?[))】\]]/gu, "");
37
40
  const knownTokens = new Set([
38
41
  "all",
39
42
  "unlimited",
@@ -52,33 +55,81 @@ function isUnlimitedTargetCountToken(value) {
52
55
  "所有人选",
53
56
  "直到完成所有人选"
54
57
  ]);
55
- if (knownTokens.has(token) || knownTokens.has(compact)) return true;
58
+ if (knownTokens.has(token) || knownTokens.has(compact) || knownTokens.has(withoutAnnotation)) return true;
56
59
  if (/^(?:all|unlimited|infinity|inf|max|full)(?:candidate|candidates)?$/i.test(compact)) return true;
60
+ if (/^(?:all|unlimited|infinity|inf|max|full)(?:候选人|人选|牛人|人才|人员)?$/iu.test(withoutAnnotation)) return true;
57
61
  if (/^(?:全部|所有|全量|不限)(?:候选人|人选|牛人|人才|人员)?$/u.test(compact)) return true;
62
+ if (!/\d/.test(compact) && /(?:扫到底|全部候选人|所有候选人|全部人选|所有人选)/u.test(compact)) return true;
58
63
  return false;
59
64
  }
60
65
 
61
- function parseBossChatTargetCount(value) {
66
+ function getWrappedTargetCountValue(value) {
67
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
68
+ for (const key of TARGET_COUNT_WRAPPER_KEYS) {
69
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
70
+ return value[key];
71
+ }
72
+ }
73
+ return value;
74
+ }
75
+
76
+ export function getBossChatTargetCountValue(input = {}) {
77
+ if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
78
+ if (Object.prototype.hasOwnProperty.call(input, "target_count") && input.target_count !== undefined && input.target_count !== null) {
79
+ return input.target_count;
80
+ }
81
+ if (Object.prototype.hasOwnProperty.call(input, "targetCount") && input.targetCount !== undefined && input.targetCount !== null) {
82
+ return input.targetCount;
83
+ }
84
+ if (Object.prototype.hasOwnProperty.call(input, "target_count")) return input.target_count;
85
+ if (Object.prototype.hasOwnProperty.call(input, "targetCount")) return input.targetCount;
86
+ return undefined;
87
+ }
88
+
89
+ function cloneForDiagnostics(value) {
90
+ if (value === undefined) return undefined;
91
+ if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value;
92
+ try {
93
+ return JSON.parse(JSON.stringify(value));
94
+ } catch {
95
+ return String(value);
96
+ }
97
+ }
98
+
99
+ export function normalizeTargetCountInput(value) {
62
100
  if (value === undefined || value === null) {
63
101
  return {
64
102
  provided: false,
65
103
  targetCount: null,
66
- cliArg: null
104
+ cliArg: null,
105
+ publicValue: null,
106
+ rawValue: value,
107
+ parseError: null
67
108
  };
68
109
  }
69
- const raw = normalizeText(value);
110
+ const unwrapped = getWrappedTargetCountValue(value);
111
+ if (unwrapped !== value) {
112
+ return normalizeTargetCountInput(unwrapped);
113
+ }
114
+ const raw = normalizeText(unwrapped);
70
115
  if (!raw) {
71
116
  return {
72
117
  provided: false,
73
118
  targetCount: null,
74
- cliArg: null
119
+ cliArg: null,
120
+ publicValue: null,
121
+ rawValue: value,
122
+ parseError: null
75
123
  };
76
124
  }
77
125
  if (isUnlimitedTargetCountToken(raw)) {
78
126
  return {
79
127
  provided: true,
80
128
  targetCount: null,
81
- cliArg: "-1"
129
+ cliArg: "-1",
130
+ publicValue: "all",
131
+ rawValue: cloneForDiagnostics(value),
132
+ parseError: null
82
133
  };
83
134
  }
84
135
  const parsed = Number.parseInt(String(raw), 10);
@@ -86,20 +137,29 @@ function parseBossChatTargetCount(value) {
86
137
  return {
87
138
  provided: true,
88
139
  targetCount: null,
89
- cliArg: "-1"
140
+ cliArg: "-1",
141
+ publicValue: "all",
142
+ rawValue: cloneForDiagnostics(value),
143
+ parseError: null
90
144
  };
91
145
  }
92
146
  if (Number.isFinite(parsed) && parsed > 0) {
93
147
  return {
94
148
  provided: true,
95
149
  targetCount: parsed,
96
- cliArg: String(parsed)
150
+ cliArg: String(parsed),
151
+ publicValue: parsed,
152
+ rawValue: cloneForDiagnostics(value),
153
+ parseError: null
97
154
  };
98
155
  }
99
156
  return {
100
157
  provided: false,
101
158
  targetCount: null,
102
- cliArg: null
159
+ cliArg: null,
160
+ publicValue: null,
161
+ rawValue: cloneForDiagnostics(value),
162
+ parseError: "target_count must be a positive integer, -1, or one of: all, unlimited, 全部, 不限, 扫到底, 全量, 全部候选人, 所有候选人"
103
163
  };
104
164
  }
105
165
 
@@ -225,7 +285,7 @@ function normalizeBossChatStartInput(input = {}) {
225
285
  const startFromRaw = normalizeText(input.startFrom || input.start_from).toLowerCase();
226
286
  const startFrom = startFromRaw === "all" ? "all" : startFromRaw === "unread" ? "unread" : "";
227
287
  const criteria = normalizeText(input.criteria);
228
- const parsedTarget = parseBossChatTargetCount(input.targetCount ?? input.target_count);
288
+ const parsedTarget = normalizeTargetCountInput(getBossChatTargetCountValue(input));
229
289
  const port = parsePositiveInteger(input.port);
230
290
  return {
231
291
  profile,
@@ -235,6 +295,9 @@ function normalizeBossChatStartInput(input = {}) {
235
295
  targetCount: parsedTarget.targetCount,
236
296
  targetCountArg: parsedTarget.cliArg,
237
297
  targetCountProvided: parsedTarget.provided,
298
+ targetCountPublicValue: parsedTarget.publicValue,
299
+ targetCountRawValue: parsedTarget.rawValue,
300
+ targetCountParseError: parsedTarget.parseError,
238
301
  port,
239
302
  dryRun: input.dryRun === true || input.dry_run === true,
240
303
  noState: input.noState === true || input.no_state === true,
@@ -270,7 +333,7 @@ function buildTargetCountQuestionHint(item = {}) {
270
333
  { label: "全部候选人", value: "全部候选人" },
271
334
  { label: "所有候选人", value: "所有候选人" }
272
335
  ];
273
- next.examples = ["all", 20];
336
+ next.examples = TARGET_COUNT_ACCEPTED_EXAMPLES.slice();
274
337
  next.argument_name = "target_count";
275
338
  return next;
276
339
  }
@@ -290,13 +353,23 @@ function buildNextCallExample(input = {}, missingFields = []) {
290
353
  if (normalized.startFrom) sample.start_from = normalized.startFrom;
291
354
  if (normalized.criteria) sample.criteria = normalized.criteria;
292
355
  if (normalized.targetCountProvided) {
293
- sample.target_count = normalized.targetCountArg === "-1" ? "all" : normalized.targetCount;
356
+ sample.target_count = normalized.targetCountPublicValue || (normalized.targetCountArg === "-1" ? "all" : normalized.targetCount);
294
357
  } else if (missingFields.includes("target_count")) {
295
358
  sample.target_count = "all";
296
359
  }
297
360
  return Object.keys(sample).length > 0 ? sample : null;
298
361
  }
299
362
 
363
+ function buildTargetCountNeedInputDiagnostics(input = {}, missingFields = []) {
364
+ if (!Array.isArray(missingFields) || !missingFields.includes("target_count")) return {};
365
+ const normalized = normalizeBossChatStartInput(input);
366
+ return {
367
+ accepted_examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice(),
368
+ ...(normalized.targetCountRawValue !== undefined ? { received_target_count: normalized.targetCountRawValue } : {}),
369
+ ...(normalized.targetCountParseError ? { target_count_parse_error: normalized.targetCountParseError } : {})
370
+ };
371
+ }
372
+
300
373
  function buildBossChatCliArgs(command, input, resolvedConfig) {
301
374
  const args = [command, "--json"];
302
375
  if (command === "prepare-run") {
@@ -493,12 +566,14 @@ export async function startBossChatRun({ workspaceRoot, input = {} }) {
493
566
  : [];
494
567
  const normalizedPendingQuestions = normalizePendingQuestions(pendingQuestions);
495
568
  const nextCallExample = buildNextCallExample(input, missingFields);
569
+ const targetCountDiagnostics = buildTargetCountNeedInputDiagnostics(input, missingFields);
496
570
  return {
497
571
  ...prepared,
498
572
  status: "NEED_INPUT",
499
573
  required_fields: CHAT_REQUIRED_FIELDS.slice(),
500
574
  missing_fields: missingFields,
501
575
  pending_questions: normalizedPendingQuestions,
576
+ ...targetCountDiagnostics,
502
577
  ...(nextCallExample ? { next_call_example: nextCallExample } : {}),
503
578
  message: prepared?.message
504
579
  || "已获取 Boss 聊天页岗位列表,请先补齐 job / start_from / target_count / criteria。"
@@ -518,11 +593,13 @@ export async function prepareBossChatRun({ workspaceRoot, input = {} }) {
518
593
  ))
519
594
  : [];
520
595
  const nextCallExample = buildNextCallExample(input, missingFields);
596
+ const targetCountDiagnostics = buildTargetCountNeedInputDiagnostics(input, missingFields);
521
597
  return {
522
598
  ...payload,
523
599
  required_fields: CHAT_REQUIRED_FIELDS.slice(),
524
600
  missing_fields: missingFields,
525
601
  pending_questions: normalizePendingQuestions(pendingQuestions),
602
+ ...targetCountDiagnostics,
526
603
  ...(nextCallExample ? { next_call_example: nextCallExample } : {})
527
604
  };
528
605
  }
package/src/index.js CHANGED
@@ -12,6 +12,8 @@ import {
12
12
  cancelBossChatRun,
13
13
  getBossChatHealthCheck,
14
14
  getBossChatRun,
15
+ getBossChatTargetCountValue,
16
+ normalizeTargetCountInput,
15
17
  pauseBossChatRun,
16
18
  prepareBossChatRun,
17
19
  resumeBossChatRun,
@@ -119,6 +121,53 @@ function getLongRunPollAfterSec() {
119
121
  return Math.max(60, fromEnv);
120
122
  }
121
123
 
124
+ function createTargetCountInputSchema(description) {
125
+ return {
126
+ oneOf: [
127
+ {
128
+ type: "integer",
129
+ minimum: 1
130
+ },
131
+ {
132
+ type: "integer",
133
+ enum: [-1]
134
+ },
135
+ {
136
+ type: "string",
137
+ enum: ["all", "unlimited", "-1", "全部", "不限", "扫到底", "全量", "全部候选人", "所有候选人"]
138
+ },
139
+ {
140
+ type: "object",
141
+ properties: {
142
+ value: {
143
+ oneOf: [
144
+ { type: "integer", minimum: 1 },
145
+ { type: "integer", enum: [-1] },
146
+ { type: "string" }
147
+ ]
148
+ },
149
+ target_count: {
150
+ oneOf: [
151
+ { type: "integer", minimum: 1 },
152
+ { type: "integer", enum: [-1] },
153
+ { type: "string" }
154
+ ]
155
+ },
156
+ targetCount: {
157
+ oneOf: [
158
+ { type: "integer", minimum: 1 },
159
+ { type: "integer", enum: [-1] },
160
+ { type: "string" }
161
+ ]
162
+ }
163
+ },
164
+ additionalProperties: true
165
+ }
166
+ ],
167
+ description
168
+ };
169
+ }
170
+
122
171
  function getRecommendedPollAfterSec(args = {}) {
123
172
  return hasFollowUpChatRequest(args)
124
173
  ? getLongRunPollAfterSec()
@@ -387,18 +436,7 @@ function createRunInputSchema() {
387
436
  type: "string",
388
437
  enum: ["unread", "all"]
389
438
  },
390
- target_count: {
391
- oneOf: [
392
- {
393
- type: "integer",
394
- minimum: 1
395
- },
396
- {
397
- type: "string",
398
- enum: ["all", "unlimited", "全部", "不限", "扫到底", "全量", "全部候选人", "所有候选人"]
399
- }
400
- ]
401
- },
439
+ target_count: createTargetCountInputSchema("boss-chat follow-up 本次处理人数上限;支持正整数、all 或 -1(扫到底)"),
402
440
  dry_run: { type: "boolean" },
403
441
  no_state: { type: "boolean" },
404
442
  safe_pacing: { type: "boolean" },
@@ -436,19 +474,8 @@ function createBossChatStartInputSchema({ requireFullInput = false } = {}) {
436
474
  type: "string",
437
475
  description: "boss-chat 的筛选 criteria"
438
476
  },
439
- target_count: {
440
- oneOf: [
441
- {
442
- type: "integer",
443
- minimum: 1
444
- },
445
- {
446
- type: "string",
447
- enum: ["all", "unlimited", "全部", "不限", "扫到底", "全量", "全部候选人", "所有候选人"]
448
- }
449
- ],
450
- description: "本次处理人数上限;支持正整数或 all/不限/全部候选人(扫到底)"
451
- },
477
+ target_count: createTargetCountInputSchema("本次处理人数上限;支持正整数、all 或 -1(扫到底),也兼容 { value: \"all\" } 等包装对象"),
478
+ targetCount: createTargetCountInputSchema("兼容字段;优先使用 target_count。本次处理人数上限,支持正整数、all 或 -1(扫到底)"),
452
479
  port: {
453
480
  type: "integer",
454
481
  minimum: 1,
@@ -462,7 +489,11 @@ function createBossChatStartInputSchema({ requireFullInput = false } = {}) {
462
489
  additionalProperties: false
463
490
  };
464
491
  if (requireFullInput) {
465
- schema.required = ["job", "start_from", "target_count", "criteria"];
492
+ schema.required = ["job", "start_from", "criteria"];
493
+ schema.anyOf = [
494
+ { required: ["target_count"] },
495
+ { required: ["targetCount"] }
496
+ ];
466
497
  }
467
498
  return schema;
468
499
  }
@@ -727,15 +758,11 @@ function validateBossChatStartArgs(args) {
727
758
  return "criteria must be a non-empty string when provided";
728
759
  }
729
760
  }
730
- if (Object.prototype.hasOwnProperty.call(args, "target_count")) {
731
- const rawTargetCount = args.target_count;
732
- const targetCount = Number.parseInt(String(rawTargetCount), 10);
733
- const tokenAllowed =
734
- typeof rawTargetCount === "string" && isUnlimitedTargetCountToken(rawTargetCount);
735
- const numericUnlimited = Number.isFinite(targetCount) && targetCount === -1;
736
- if ((!Number.isFinite(targetCount) || targetCount <= 0) && !tokenAllowed && !numericUnlimited) {
737
- return "target_count must be a positive integer or one of: all, unlimited, 全部, 不限, 扫到底, 全量, 全部候选人, 所有候选人";
738
- }
761
+ if (
762
+ Object.prototype.hasOwnProperty.call(args, "target_count")
763
+ || Object.prototype.hasOwnProperty.call(args, "targetCount")
764
+ ) {
765
+ normalizeTargetCountInput(getBossChatTargetCountValue(args));
739
766
  }
740
767
  if (Object.prototype.hasOwnProperty.call(args, "port")) {
741
768
  const port = Number.parseInt(String(args.port), 10);
package/src/pipeline.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  import {
17
17
  cancelBossChatRun,
18
18
  getBossChatRun,
19
+ normalizeTargetCountInput,
19
20
  pauseBossChatRun,
20
21
  resumeBossChatRun,
21
22
  startBossChatRun
@@ -66,6 +67,10 @@ function parsePositiveIntegerValue(value) {
66
67
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
67
68
  }
68
69
 
70
+ function normalizePipelineTargetCountValue(value) {
71
+ return normalizeTargetCountInput(value).publicValue;
72
+ }
73
+
69
74
  function sleep(ms) {
70
75
  return new Promise((resolve) => setTimeout(resolve, ms));
71
76
  }
@@ -388,16 +393,17 @@ function normalizeFollowUpChatInput(followUp = null, defaults = null) {
388
393
  const defaultCriteria = normalizeText(defaults?.criteria || "");
389
394
  const defaultStartFromRaw = normalizeText(defaults?.start_from || "").toLowerCase();
390
395
  const defaultStartFrom = defaultStartFromRaw === "all" ? "all" : "unread";
391
- const defaultTargetCount = parsePositiveIntegerValue(defaults?.target_count);
396
+ const defaultTargetCount = normalizePipelineTargetCountValue(defaults?.target_count);
392
397
 
393
398
  const explicitCriteria = normalizeText(raw.criteria);
394
399
  const explicitStartFromRaw = normalizeText(raw.start_from).toLowerCase();
395
400
  const explicitStartFrom = explicitStartFromRaw === "all" ? "all" : explicitStartFromRaw === "unread" ? "unread" : "";
396
- const explicitTargetCount = parsePositiveIntegerValue(raw.target_count);
401
+ const explicitTarget = normalizeTargetCountInput(raw.target_count);
402
+ const explicitTargetCount = explicitTarget.publicValue;
397
403
 
398
404
  const hasExplicitCriteria = Boolean(explicitCriteria);
399
405
  const hasExplicitStartFrom = Boolean(explicitStartFrom);
400
- const hasExplicitTargetCount = Number.isInteger(explicitTargetCount) && explicitTargetCount > 0;
406
+ const hasExplicitTargetCount = explicitTarget.provided;
401
407
 
402
408
  const criteria = explicitCriteria || defaultCriteria;
403
409
  const startFrom = explicitStartFrom || defaultStartFrom;
@@ -442,8 +448,11 @@ function normalizeFollowUpChatInput(followUp = null, defaults = null) {
442
448
  missing_fields.push("follow_up.chat.target_count");
443
449
  pending_questions.push({
444
450
  field: "follow_up.chat.target_count",
445
- question: "请填写 boss-chat follow-up 本次处理人数上限(正整数,必填)。",
446
- value: summary.target_count
451
+ question: "请填写 boss-chat follow-up 本次处理人数上限(正整数,或 all/-1 表示扫到底,必填)。",
452
+ value: summary.target_count,
453
+ accepted_examples: ["all", -1, 20, "全部候选人"],
454
+ ...(explicitTarget.rawValue !== undefined ? { received_target_count: explicitTarget.rawValue } : {}),
455
+ ...(explicitTarget.parseError ? { target_count_parse_error: explicitTarget.parseError } : {})
447
456
  });
448
457
  }
449
458
 
@@ -523,7 +532,7 @@ function buildBossChatFollowUpStatus({ payload, runId, fallbackInput = null, sta
523
532
  job: normalizeText(fallbackInput?.job) || null,
524
533
  start_from: normalizeText(fallbackInput?.start_from) || null,
525
534
  criteria: normalizeText(fallbackInput?.criteria) || null,
526
- target_count: parsePositiveIntegerValue(fallbackInput?.target_count),
535
+ target_count: normalizePipelineTargetCountValue(fallbackInput?.target_count),
527
536
  port: parsePositiveIntegerValue(fallbackInput?.port),
528
537
  progress: {
529
538
  inspected: Number.isInteger(progress.inspected) ? progress.inspected : 0,