@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 +1 -0
- package/package.json +1 -1
- package/skills/boss-chat/README.md +7 -0
- package/skills/boss-chat/SKILL.md +9 -0
- package/src/boss-chat.js +52 -3
- package/src/cli.js +9 -1
- package/src/index.js +16 -6
- package/src/test-boss-chat.js +54 -0
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
|
@@ -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
|
-
|
|
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
|
-
]
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
]
|
|
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")) {
|
package/src/test-boss-chat.js
CHANGED
|
@@ -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
|
|