@reconcrap/boss-recruit-mcp 1.0.1 → 1.0.2

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
@@ -91,7 +91,9 @@ boss-recruit-mcp doctor --port 9222
91
91
  "instruction": "自然语言招聘指令",
92
92
  "confirmation": {
93
93
  "keyword_confirmed": true,
94
- "keyword_value": "ai infra"
94
+ "keyword_value": "ai infra",
95
+ "search_params_confirmed": true,
96
+ "use_default_for_missing": false
95
97
  },
96
98
  "overrides": {
97
99
  "target_count": 500
@@ -102,7 +104,9 @@ boss-recruit-mcp doctor --port 9222
102
104
  ## 行为说明
103
105
 
104
106
  - 若缺 `city/degree/schools/keyword/target_count`,返回 `NEED_INPUT`
105
- - 若 keyword 由语义自动抽取(非显式给出),返回 `NEED_CONFIRMATION`
107
+ - 若 keyword 由语义自动抽取、或搜索参数仍未被用户明确确认,返回 `NEED_CONFIRMATION`
108
+ - 正式执行前应先单独做一轮参数确认,把已识别参数、待确认项、缺失项、默认值风险分开给用户确认
109
+ - 用户未补齐缺失参数时,只有在明确同意默认值及其质量风险后,才允许继续
106
110
  - 确认后自动执行:搜索 CLI -> 筛选 CLI
107
111
  - 返回摘要:目标数、已处理、通过数、耗时、输出 CSV
108
112
  - 执行前会先做本地依赖预检查,若目录 / 入口 / 配置文件缺失则返回 `PIPELINE_PREFLIGHT_FAILED`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recruit-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Unified MCP pipeline for boss-search-cli and boss-screen-cli",
5
5
  "keywords": [
6
6
  "boss",
@@ -20,3 +20,10 @@ npx @reconcrap/boss-recruit-mcp install
20
20
  - Boss 页面已登录
21
21
  - 已在用户配置中填写有效的 `baseUrl`、`apiKey`、`model`
22
22
  - 已生成 `$CODEX_HOME/boss-recruit-mcp/favorite-calibration.json`
23
+
24
+ ## 运行注意事项
25
+
26
+ - 正式开始前,必须先做一轮参数确认,分开展示已识别参数、待确认参数、缺失参数。
27
+ - 参数确认尽量复用统一模板:`已识别参数` / `待确认或待修正` / `缺失参数` / `默认值提醒` / `请用户回复`。
28
+ - 如果识别结果里出现明显脏值或可疑字段,例如“杭州筛选做过”,必须要求用户改成标准值后再继续。
29
+ - 若缺失参数仍未补齐,只能在用户明确确认接受默认值和质量风险后继续,不能静默按默认执行。
@@ -53,7 +53,39 @@
53
53
  - Input:
54
54
  - `instruction` (string, required)
55
55
  - `confirmation` (object, optional)
56
+ - `keyword_confirmed` (boolean): 是否确认关键词
57
+ - `keyword_value` (string): 用户确认或改写后的关键词
58
+ - `search_params_confirmed` (boolean): 用户是否已明确确认当前参数集
59
+ - `use_default_for_missing` (boolean): 用户是否明确同意对缺失参数使用默认值
56
60
  - `overrides` (object, optional)
61
+ - `city` (string)
62
+ - `degree` (string)
63
+ - `schools` (string[] | comma-separated string)
64
+ - `keyword` (string)
65
+ - `target_count` (number)
66
+ - Tool response 重点字段:
67
+ - `status`
68
+ - `required_confirmations`
69
+ - `review.extracted_search_params`
70
+ - `review.current_search_params`
71
+ - `review.missing_fields`
72
+ - `review.suspicious_fields`
73
+ - `review.default_preview`
74
+ - `review.applied_defaults`
75
+
76
+ ## Confirmation First
77
+
78
+ - 在任何一次正式搜索 / 筛选开始前,必须先单开一轮“参数确认对话”,不能在首轮解析后直接开跑。
79
+ - 这轮确认对话必须把当前已提取参数、疑似错误参数、缺失参数分开列给用户。
80
+ - 对于明显异常或语义可疑的提取值,不能默认接受,必须让用户二次确认后才能继续。
81
+ - 例如:
82
+ - 提取出的城市像“杭州筛选做过”这类明显脏值时,必须明确告诉用户“当前识别结果可能不正确”,并请用户改成标准城市名如“杭州”。
83
+ - 学历、学校标签、关键词、目标人数等若提取结果带噪声、过长、混入条件短语,也都要按“待确认项”处理。
84
+ - 如果用户在这轮确认后仍未补全缺失项,agent 也不能直接静默开始,必须再明确说明:
85
+ - 哪些参数仍缺失;
86
+ - 将会使用什么默认值;
87
+ - 这些默认值会降低搜索结果质量或扩大偏差。
88
+ - 只有在用户明确回复“确认按这些默认值继续”后,才允许正式执行。
57
89
 
58
90
  ## Execution Policy
59
91
 
@@ -68,22 +100,38 @@
68
100
  - 然后使用用户的 MCP 配置启动 `boss-recruit-mcp`
69
101
  3. 若缺少校准文件:
70
102
  - 明确提示用户先完成校准,不要直接调用流水线。
71
- 4. 只有当以上条件满足时,才首次调用 `run_recruit_pipeline`(只传 `instruction`)。
72
- 2. 若返回 `NEED_INPUT`:
73
- - 一次性向用户列出 `missing_fields` 所有缺失项;
74
- - 缺失项常见含义:
75
- - `city`: 城市,如“杭州”
76
- - `degree`: 学历,如“本科”“硕士及以上”
77
- - `schools`: 学校标签,如“985、211、qs100”
78
- - `target_count`: 目标筛选人数,如“10”
79
- - 用户补充后再次调用工具。
80
- 3. 若返回 `NEED_CONFIRMATION`:
103
+ 4. 只有当以上条件满足时,才首次调用 `run_recruit_pipeline`(只传 `instruction`),用于“解析”,不是立刻执行最终搜索结论。
104
+ 5. 拿到首次解析结果后,先进入单独的“参数确认对话”:
105
+ - 列出当前已提取到的参数;
106
+ - 单独标出需要用户确认的参数;
107
+ - 单独列出 `missing_fields` 中所有缺失项;
108
+ - 如果某个字段看起来明显异常、像脏字符串、或不符合标准筛选值,直接归入“待确认 / 待修正”而不是默认使用。
109
+ 6. 缺失项常见含义:
110
+ - `city`: 城市,如“杭州”
111
+ - `degree`: 学历,如“本科”“硕士及以上”
112
+ - `schools`: 学校标签,如“985、211、qs100”
113
+ - `target_count`: 目标筛选人数,如“10”
114
+ - `keyword`: 搜索关键词,如“AI infra”“推荐系统”
115
+ 7. 若返回 `NEED_INPUT`:
116
+ - 不要只问一次就结束;
117
+ - 要把缺失参数集中列出,请用户一次性补充;
118
+ - 若用户补充后仍有缺失,再次单独列出剩余缺失项;
119
+ - 若用户始终不补充,必须显式征求“是否接受默认值继续”的确认,不能直接默认执行。
120
+ 8. 若返回 `NEED_CONFIRMATION`:
81
121
  - 询问用户是否确认 `proposed_keyword`;
122
+ - 同时也要让用户确认其他已提取参数里是否有误;
82
123
  - 若确认,带 `confirmation.keyword_confirmed=true` 和 `keyword_value` 再次调用;
83
124
  - 若用户修改关键词,传用户给的新词作为 `keyword_value` 再次调用。
84
- 4. 若返回 `COMPLETED`:
125
+ 9. 当仍有缺失参数但用户想直接开始时:
126
+ - 先明确告知默认值及风险;
127
+ - 必须得到用户明确确认“可以按默认值继续”后,才能继续执行。
128
+ 10. 只有在以下条件都满足后,才允许正式开始:
129
+ - 用户已经确认已提取参数无误;
130
+ - 缺失参数已补齐,或用户已明确接受默认值;
131
+ - `NEED_CONFIRMATION` 分支中的关键词也已确认。
132
+ 11. 若返回 `COMPLETED`:
85
133
  - 向用户返回摘要:目标数、已处理、通过数、耗时、输出文件路径。
86
- 5. 若返回 `FAILED`:
134
+ 12. 若返回 `FAILED`:
87
135
  - 先提炼 `error.code`、`error.message`、`diagnostics`;
88
136
  - 如果是 `PIPELINE_PREFLIGHT_FAILED`,明确指出缺失的本地目录 / 文件;
89
137
  - 如果是 `CALIBRATION_REQUIRED`,明确提醒用户执行校准,并给出校准步骤;
@@ -96,8 +144,62 @@
96
144
  - 优先鼓励用户一次性给全这些字段:城市、学历、学校标签、目标人数、核心方向关键词。
97
145
  - 当用户提到“做过 AI infra / 推荐系统 / 搜索 / 广告 / 多模态”等经历,但没有显式写“关键词”,默认允许流水线先自动抽取,再走确认分支。
98
146
  - 当用户附带筛选要求(如“必须发表过 CCF-A 区论文”“有开源项目”“带过团队”),这些要求应该保留在 `criteria` 中,不应被误当作搜索过滤条件。
147
+ - 若参数提取结果出现明显噪声、截断、短语串接、非标准枚举值,优先视为“识别不可靠”,要求用户确认,不要为了推进流程直接采用。
148
+ - 不要把“用户没有继续回复”解释为“默认同意”;默认值只能在用户明确口头确认后使用。
149
+ - 参数确认对话里,优先采用这种结构:
150
+ - 已识别参数
151
+ - 待确认 / 待修正参数
152
+ - 缺失参数
153
+ - 若继续默认执行会采用的默认值与风险
99
154
  - 回答时不要暴露 `screening-config.json` 中的 `apiKey`、`baseUrl` 等敏感值。
100
155
 
156
+ ## Standard Confirmation Template
157
+
158
+ - 发起参数确认时,优先复用统一结构,不要每次自由发挥。
159
+ - 首轮确认模板建议按下面顺序输出:
160
+ - `已识别参数`
161
+ - `待确认 / 待修正`
162
+ - `缺失参数`
163
+ - `默认值提醒(如果适用)`
164
+ - `请用户回复`
165
+ - `已识别参数` 只放当前看起来可信的值,例如:
166
+ - 城市:杭州
167
+ - 学历:本科
168
+ - 学校标签:985 / 211 / QS100
169
+ - 关键词:AI infra
170
+ - 目标人数:10
171
+ - `待确认 / 待修正` 要明确写出“识别值 -> 疑点 -> 需要用户给出的标准值”,例如:
172
+ - 城市:当前识别为“杭州筛选做过”,这看起来混入了其他短语,请确认是否应为“杭州”
173
+ - 关键词:当前识别为“AI infra 论文”,看起来混入了附加条件,请确认是否只保留“AI infra”
174
+ - `缺失参数` 要逐项列出,不要笼统说“还差一些信息”,例如:
175
+ - 缺少城市
176
+ - 缺少目标人数
177
+ - `默认值提醒` 只在仍有缺失项时出现,且必须同时包含三部分:
178
+ - 还缺哪些参数
179
+ - 若继续会使用哪些默认值
180
+ - 会导致搜索范围变宽、相关性下降或结果偏差增大
181
+ - `请用户回复` 要求用户一次性回复完整,优先使用这种收口方式:
182
+ - 请直接按“城市 / 学历 / 学校标签 / 关键词 / 目标人数”补充或修正
183
+ - 如果你接受默认值继续,请明确回复“确认按默认值继续”
184
+ - 若用户补充后仍有缺失,再发第二轮确认时继续复用同一结构,只保留:
185
+ - 已更新的参数
186
+ - 仍待确认项
187
+ - 仍缺失项
188
+ - 默认值风险
189
+ - 若用户明确表示“不想再补充,直接开始”,也不能跳过模板;要先发一版精简确认:
190
+ - 当前仍缺失的参数
191
+ - 将采用的默认值
192
+ - 风险提示
193
+ - 明确询问“请确认是否按默认值继续”
194
+ - 不要把下面这类表达当成有效确认:
195
+ - “先这样吧”
196
+ - “你看着办”
197
+ - “差不多”
198
+ - “随便”
199
+ - 只有当用户明确确认参数无误,且对默认值给出清晰同意后,才设置:
200
+ - `confirmation.search_params_confirmed=true`
201
+ - `confirmation.use_default_for_missing=true`(如适用)
202
+
101
203
  ## Failure Handling
102
204
 
103
205
  - 不要把底层 stderr 原样大段贴给用户,只提炼关键错误和下一步。
@@ -128,4 +230,7 @@
128
230
  - 优先结构化、简洁中文输出。
129
231
  - 不展示密钥和底层敏感配置。
130
232
  - 不跳过 `NEED_CONFIRMATION` 分支。
233
+ - 正式开始前,优先给用户一轮“参数确认卡片式摘要”。
234
+ - 参数确认阶段尽量复用统一模板,减少自由表述带来的漏项。
235
+ - 若要使用默认值,必须写明“请确认是否按默认值继续”,不能模糊带过。
131
236
  - 若运行失败,优先给用户“现在卡在哪一步 + 怎么继续”。
package/src/index.js CHANGED
@@ -36,13 +36,24 @@ function createToolSchema() {
36
36
  type: "object",
37
37
  properties: {
38
38
  keyword_confirmed: { type: "boolean" },
39
- keyword_value: { type: "string" }
39
+ keyword_value: { type: "string" },
40
+ search_params_confirmed: { type: "boolean" },
41
+ use_default_for_missing: { type: "boolean" }
40
42
  },
41
43
  additionalProperties: false
42
44
  },
43
45
  overrides: {
44
46
  type: "object",
45
47
  properties: {
48
+ city: { type: "string" },
49
+ degree: { type: "string" },
50
+ schools: {
51
+ anyOf: [
52
+ { type: "array", items: { type: "string" } },
53
+ { type: "string" }
54
+ ]
55
+ },
56
+ keyword: { type: "string" },
46
57
  target_count: { type: "integer", minimum: 1 }
47
58
  },
48
59
  additionalProperties: false
package/src/parser.js CHANGED
@@ -3,6 +3,23 @@ const SEARCH_SCHOOL_MAP = {
3
3
  "211": "211院校",
4
4
  "qs100": "QS 100"
5
5
  };
6
+ const KNOWN_SCHOOL_LABELS = new Set(Object.values(SEARCH_SCHOOL_MAP));
7
+ const DEFAULT_PARAM_VALUES = {
8
+ city: null,
9
+ degree: "不限",
10
+ schools: [],
11
+ keyword: "算法工程师",
12
+ target_count: 10
13
+ };
14
+ const DEFAULT_PARAM_LABELS = {
15
+ city: "不限城市",
16
+ degree: "不限",
17
+ schools: "不限院校标签",
18
+ keyword: "算法工程师",
19
+ target_count: 10
20
+ };
21
+ const DEGREE_VALUES = new Set(["不限", "本科", "本科及以上", "硕士及以上", "博士"]);
22
+ const CITY_STOP_PATTERN = /(?:筛选|搜索|查找|找|做过|从事过|有过|相关|的人选|的人|并且|且|学历|学校|目标|必须|优先|,|。|;|;|,)/;
6
23
 
7
24
  function normalizeText(input) {
8
25
  return String(input || "").replace(/\s+/g, " ").trim();
@@ -12,16 +29,47 @@ function uniqueList(items) {
12
29
  return Array.from(new Set(items.filter(Boolean)));
13
30
  }
14
31
 
32
+ function normalizeSchoolLabel(value) {
33
+ if (typeof value !== "string") return null;
34
+ const raw = value.trim();
35
+ if (!raw) return null;
36
+
37
+ if (KNOWN_SCHOOL_LABELS.has(raw)) {
38
+ return raw;
39
+ }
40
+
41
+ const compact = raw.toLowerCase().replace(/\s+/g, "");
42
+ return SEARCH_SCHOOL_MAP[compact] || SEARCH_SCHOOL_MAP[raw] || raw;
43
+ }
44
+
45
+ function sanitizeCityCandidate(value) {
46
+ if (typeof value !== "string") return null;
47
+ let candidate = value.trim();
48
+ if (!candidate) return null;
49
+
50
+ candidate = candidate.replace(/^(在|是|为)\s*/, "").trim();
51
+ const stopIndex = candidate.search(CITY_STOP_PATTERN);
52
+ if (stopIndex >= 0) {
53
+ candidate = candidate.slice(0, stopIndex).trim();
54
+ }
55
+
56
+ candidate = candidate.replace(/[的\s]+$/g, "").trim();
57
+ return candidate || null;
58
+ }
59
+
15
60
  function extractCity(text) {
16
61
  const explicitPatterns = [
17
- /地点(?:在|是|为|:|:)?\s*([^\s,。;;、]+)/i,
18
- /城市(?:在|是|为|:|:)?\s*([^\s,。;;、]+)/i
62
+ /地点(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
63
+ /城市(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
64
+ /工作地(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
65
+ /base(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i
19
66
  ];
20
67
 
21
68
  for (const pattern of explicitPatterns) {
22
69
  const m = text.match(pattern);
23
70
  if (m && m[1]) {
24
- return m[1].trim();
71
+ const city = sanitizeCityCandidate(m[1]);
72
+ if (city) return city;
25
73
  }
26
74
  }
27
75
 
@@ -44,6 +92,24 @@ function extractSchools(text) {
44
92
  return uniqueList(schools);
45
93
  }
46
94
 
95
+ function normalizeStringOverride(value) {
96
+ if (typeof value !== "string") return null;
97
+ const normalized = value.trim();
98
+ return normalized || null;
99
+ }
100
+
101
+ function normalizeSchoolsOverride(value) {
102
+ if (Array.isArray(value)) {
103
+ return uniqueList(value.map(normalizeSchoolLabel));
104
+ }
105
+
106
+ if (typeof value === "string") {
107
+ return uniqueList(value.split(/[,,]/).map(normalizeSchoolLabel));
108
+ }
109
+
110
+ return null;
111
+ }
112
+
47
113
  function extractKeywordExplicit(text) {
48
114
  const patterns = [
49
115
  /搜索关键词(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
@@ -145,6 +211,10 @@ function buildScreenCriteria(text, searchParams) {
145
211
  }
146
212
 
147
213
  function resolveKeyword(parsed, confirmation) {
214
+ if (parsed.keyword_override) {
215
+ return { keyword: parsed.keyword_override, needsConfirmation: false, proposedKeyword: null };
216
+ }
217
+
148
218
  const explicit = parsed.keyword_explicit;
149
219
  const auto = parsed.keyword_auto;
150
220
  const confirmed = confirmation && confirmation.keyword_confirmed === true;
@@ -175,6 +245,97 @@ function resolveKeyword(parsed, confirmation) {
175
245
  return { keyword: null, needsConfirmation: false, proposedKeyword: null };
176
246
  }
177
247
 
248
+ function collectSuspiciousFields(searchParams, screenParams) {
249
+ const suspicious = [];
250
+
251
+ if (searchParams.city && (/\s/.test(searchParams.city) || CITY_STOP_PATTERN.test(searchParams.city) || searchParams.city.length > 8)) {
252
+ suspicious.push({
253
+ field: "city",
254
+ value: searchParams.city,
255
+ reason: "城市提取结果看起来包含多余短语,请确认是否为标准城市名。"
256
+ });
257
+ }
258
+
259
+ if (searchParams.degree && !DEGREE_VALUES.has(searchParams.degree)) {
260
+ suspicious.push({
261
+ field: "degree",
262
+ value: searchParams.degree,
263
+ reason: "学历提取结果不在预期枚举内,请确认。"
264
+ });
265
+ }
266
+
267
+ if (searchParams.keyword && /城市|学历|学校|目标人数|目标数量|筛选\d+位/i.test(searchParams.keyword)) {
268
+ suspicious.push({
269
+ field: "keyword",
270
+ value: searchParams.keyword,
271
+ reason: "关键词看起来混入了筛选条件,请确认是否只保留核心方向词。"
272
+ });
273
+ }
274
+
275
+ if (screenParams.target_count && (!Number.isInteger(screenParams.target_count) || screenParams.target_count <= 0)) {
276
+ suspicious.push({
277
+ field: "target_count",
278
+ value: screenParams.target_count,
279
+ reason: "目标人数不是有效正整数,请确认。"
280
+ });
281
+ }
282
+
283
+ return suspicious;
284
+ }
285
+
286
+ function buildDefaultPreview(missingFields, options = {}) {
287
+ const { skipKeywordDefault = false } = options;
288
+ return missingFields.reduce((acc, field) => {
289
+ if (field === "keyword" && skipKeywordDefault) {
290
+ return acc;
291
+ }
292
+ acc[field] = DEFAULT_PARAM_LABELS[field];
293
+ return acc;
294
+ }, {});
295
+ }
296
+
297
+ function applyDefaults(searchParams, screenParams, missingFields, useDefaultForMissing, options = {}) {
298
+ const { skipKeywordDefault = false } = options;
299
+ if (!useDefaultForMissing) {
300
+ return {
301
+ searchParams,
302
+ screenParams,
303
+ appliedDefaults: {}
304
+ };
305
+ }
306
+
307
+ const appliedDefaults = {};
308
+ const nextSearchParams = { ...searchParams };
309
+ const nextScreenParams = { ...screenParams };
310
+
311
+ if (missingFields.includes("city")) {
312
+ nextSearchParams.city = DEFAULT_PARAM_VALUES.city;
313
+ appliedDefaults.city = DEFAULT_PARAM_LABELS.city;
314
+ }
315
+ if (missingFields.includes("degree")) {
316
+ nextSearchParams.degree = DEFAULT_PARAM_VALUES.degree;
317
+ appliedDefaults.degree = DEFAULT_PARAM_LABELS.degree;
318
+ }
319
+ if (missingFields.includes("schools")) {
320
+ nextSearchParams.schools = DEFAULT_PARAM_VALUES.schools.slice();
321
+ appliedDefaults.schools = DEFAULT_PARAM_LABELS.schools;
322
+ }
323
+ if (missingFields.includes("keyword") && !skipKeywordDefault) {
324
+ nextSearchParams.keyword = DEFAULT_PARAM_VALUES.keyword;
325
+ appliedDefaults.keyword = DEFAULT_PARAM_LABELS.keyword;
326
+ }
327
+ if (missingFields.includes("target_count")) {
328
+ nextScreenParams.target_count = DEFAULT_PARAM_VALUES.target_count;
329
+ appliedDefaults.target_count = DEFAULT_PARAM_LABELS.target_count;
330
+ }
331
+
332
+ return {
333
+ searchParams: nextSearchParams,
334
+ screenParams: nextScreenParams,
335
+ appliedDefaults
336
+ };
337
+ }
338
+
178
339
  export function parseRecruitInstruction({ instruction, confirmation, overrides }) {
179
340
  const text = normalizeText(instruction);
180
341
  const parsed = {
@@ -186,36 +347,76 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
186
347
  target_count: extractTargetCount(text)
187
348
  };
188
349
 
189
- if (overrides && Number.isFinite(overrides.target_count) && overrides.target_count > 0) {
190
- parsed.target_count = Number.parseInt(String(overrides.target_count), 10);
350
+ if (overrides) {
351
+ const overrideCity = sanitizeCityCandidate(normalizeStringOverride(overrides.city));
352
+ const overrideDegree = normalizeStringOverride(overrides.degree);
353
+ const overrideSchools = normalizeSchoolsOverride(overrides.schools);
354
+ const overrideKeyword = normalizeStringOverride(overrides.keyword);
355
+
356
+ if (overrideCity) parsed.city = overrideCity;
357
+ if (overrideDegree) parsed.degree = overrideDegree;
358
+ if (overrideSchools && overrideSchools.length > 0) parsed.schools = overrideSchools;
359
+ if (overrideKeyword) parsed.keyword_override = overrideKeyword;
360
+
361
+ if (Number.isFinite(overrides.target_count) && overrides.target_count > 0) {
362
+ parsed.target_count = Number.parseInt(String(overrides.target_count), 10);
363
+ }
191
364
  }
192
365
 
193
366
  const keywordResolution = resolveKeyword(parsed, confirmation);
194
- const searchParams = {
367
+ const baseSearchParams = {
195
368
  city: parsed.city,
196
369
  degree: parsed.degree,
197
370
  schools: parsed.schools,
198
371
  keyword: keywordResolution.keyword
199
372
  };
200
373
 
201
- const screenParams = {
202
- criteria: buildScreenCriteria(text, searchParams),
374
+ const baseScreenParams = {
375
+ criteria: buildScreenCriteria(text, baseSearchParams),
203
376
  target_count: parsed.target_count
204
377
  };
205
378
 
206
- const missing = [];
207
- if (!searchParams.city) missing.push("city");
208
- if (!searchParams.degree) missing.push("degree");
209
- if (!searchParams.schools || searchParams.schools.length === 0) missing.push("schools");
210
- if (!searchParams.keyword) missing.push("keyword");
211
- if (!screenParams.target_count) missing.push("target_count");
379
+ const missingBeforeDefaults = [];
380
+ if (!baseSearchParams.city) missingBeforeDefaults.push("city");
381
+ if (!baseSearchParams.degree) missingBeforeDefaults.push("degree");
382
+ if (!baseSearchParams.schools || baseSearchParams.schools.length === 0) missingBeforeDefaults.push("schools");
383
+ if (!baseSearchParams.keyword) missingBeforeDefaults.push("keyword");
384
+ if (!baseScreenParams.target_count) missingBeforeDefaults.push("target_count");
385
+
386
+ const useDefaultForMissing = confirmation?.use_default_for_missing === true;
387
+ const skipKeywordDefault = keywordResolution.needsConfirmation;
388
+ const defaultPreview = buildDefaultPreview(missingBeforeDefaults, { skipKeywordDefault });
389
+ const { searchParams, screenParams, appliedDefaults } = applyDefaults(
390
+ baseSearchParams,
391
+ baseScreenParams,
392
+ missingBeforeDefaults,
393
+ useDefaultForMissing,
394
+ { skipKeywordDefault }
395
+ );
396
+ const suspicious_fields = collectSuspiciousFields(searchParams, screenParams);
212
397
 
213
398
  return {
214
399
  parsed,
215
400
  searchParams,
216
401
  screenParams,
217
- missing_fields: missing,
402
+ missing_fields: missingBeforeDefaults,
403
+ has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
404
+ suspicious_fields,
218
405
  needs_keyword_confirmation: keywordResolution.needsConfirmation,
219
- proposed_keyword: keywordResolution.proposedKeyword
406
+ needs_search_params_confirmation: confirmation?.search_params_confirmed !== true,
407
+ proposed_keyword: keywordResolution.proposedKeyword,
408
+ default_preview: defaultPreview,
409
+ applied_defaults: appliedDefaults,
410
+ review: {
411
+ extracted_search_params: baseSearchParams,
412
+ extracted_screen_params: baseScreenParams,
413
+ current_search_params: searchParams,
414
+ current_screen_params: screenParams,
415
+ missing_fields: missingBeforeDefaults,
416
+ has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
417
+ suspicious_fields,
418
+ default_preview: defaultPreview,
419
+ applied_defaults: appliedDefaults
420
+ }
220
421
  };
221
422
  }
package/src/pipeline.js CHANGED
@@ -1,15 +1,34 @@
1
1
  import { parseRecruitInstruction } from "./parser.js";
2
2
  import { runPipelinePreflight, runSearchCli, runScreenCli } from "./adapters.js";
3
3
 
4
+ function buildRequiredConfirmations(parsedResult) {
5
+ const confirmations = [];
6
+
7
+ if (parsedResult.needs_search_params_confirmation) {
8
+ confirmations.push("search_params");
9
+ }
10
+ if (parsedResult.needs_keyword_confirmation) {
11
+ confirmations.push("keyword");
12
+ }
13
+ if (parsedResult.has_unresolved_missing_fields) {
14
+ confirmations.push("missing_fields_or_defaults");
15
+ }
16
+
17
+ return confirmations;
18
+ }
19
+
4
20
  function buildNeedInputResponse(parsedResult) {
5
21
  return {
6
22
  status: "NEED_INPUT",
7
23
  missing_fields: parsedResult.missing_fields,
24
+ proposed_keyword: parsedResult.proposed_keyword,
25
+ required_confirmations: buildRequiredConfirmations(parsedResult),
8
26
  search_params: parsedResult.searchParams,
9
27
  screen_params: parsedResult.screenParams,
28
+ review: parsedResult.review,
10
29
  error: {
11
30
  code: "MISSING_REQUIRED_FIELDS",
12
- message: "缺少必要字段,请一次性补充缺失项后再执行。",
31
+ message: "缺少必要字段。请先补齐缺失项;若要按默认值继续,必须先明确确认默认值及其风险。",
13
32
  retryable: true
14
33
  }
15
34
  };
@@ -19,11 +38,13 @@ function buildNeedConfirmationResponse(parsedResult) {
19
38
  return {
20
39
  status: "NEED_CONFIRMATION",
21
40
  proposed_keyword: parsedResult.proposed_keyword,
41
+ required_confirmations: buildRequiredConfirmations(parsedResult),
22
42
  search_params: {
23
43
  ...parsedResult.searchParams,
24
- keyword: parsedResult.proposed_keyword
44
+ keyword: parsedResult.proposed_keyword || parsedResult.searchParams.keyword
25
45
  },
26
- screen_params: parsedResult.screenParams
46
+ screen_params: parsedResult.screenParams,
47
+ review: parsedResult.review
27
48
  };
28
49
  }
29
50
 
@@ -121,12 +142,12 @@ export async function runRecruitPipeline({
121
142
  overrides
122
143
  });
123
144
 
124
- if (parsed.needs_keyword_confirmation) {
125
- return buildNeedConfirmationResponse(parsed);
145
+ if (parsed.has_unresolved_missing_fields) {
146
+ return buildNeedInputResponse(parsed);
126
147
  }
127
148
 
128
- if (parsed.missing_fields.length > 0) {
129
- return buildNeedInputResponse(parsed);
149
+ if (parsed.needs_keyword_confirmation || parsed.needs_search_params_confirmation) {
150
+ return buildNeedConfirmationResponse(parsed);
130
151
  }
131
152
 
132
153
  const preflight = runPipelinePreflight(workspaceRoot);
@@ -9,7 +9,17 @@ function testNeedInput() {
9
9
  });
10
10
 
11
11
  assert.equal(r.needs_keyword_confirmation, true);
12
+ assert.equal(r.needs_search_params_confirmation, true);
12
13
  assert.equal(r.proposed_keyword?.toLowerCase(), "ai infra");
14
+ assert.deepEqual(
15
+ r.default_preview,
16
+ {
17
+ city: "不限城市",
18
+ degree: "不限",
19
+ schools: "不限院校标签",
20
+ target_count: 10
21
+ }
22
+ );
13
23
  }
14
24
 
15
25
  function testExampleExtraction() {
@@ -31,11 +41,16 @@ function testExampleExtraction() {
31
41
 
32
42
  const confirmed = parseRecruitInstruction({
33
43
  instruction,
34
- confirmation: { keyword_confirmed: true, keyword_value: "ai infra" },
44
+ confirmation: {
45
+ keyword_confirmed: true,
46
+ keyword_value: "ai infra",
47
+ search_params_confirmed: true
48
+ },
35
49
  overrides: null
36
50
  });
37
51
 
38
52
  assert.equal(confirmed.needs_keyword_confirmation, false);
53
+ assert.equal(confirmed.needs_search_params_confirmation, false);
39
54
  assert.equal(confirmed.searchParams.keyword, "ai infra");
40
55
  assert.equal(confirmed.missing_fields.length, 0);
41
56
  }
@@ -54,7 +69,11 @@ function testStructuredInputAndCriteriaCleanup() {
54
69
  const r = parseRecruitInstruction({
55
70
  instruction:
56
71
  "使用boss-recruit-pipeline skills帮我在boss上找做过AI infra的人选,必须发表过CCF-A区论文。城市:杭州,学历:本科,学校:985、211、qs100,目标人数:10人",
57
- confirmation: { keyword_confirmed: true, keyword_value: "AI infra" },
72
+ confirmation: {
73
+ keyword_confirmed: true,
74
+ keyword_value: "AI infra",
75
+ search_params_confirmed: true
76
+ },
58
77
  overrides: null
59
78
  });
60
79
 
@@ -67,11 +86,50 @@ function testStructuredInputAndCriteriaCleanup() {
67
86
  );
68
87
  }
69
88
 
89
+ function testCitySanitizationAndConfirmationGate() {
90
+ const r = parseRecruitInstruction({
91
+ instruction:
92
+ "在 Boss 直聘按城市杭州筛选做过 AI infra 的人选,学历为本科及以上,来自 985/211/QS100 学校,必须有 CCF-A 区会议论文,目标 10 人,关键词 AI infra。",
93
+ confirmation: null,
94
+ overrides: null
95
+ });
96
+
97
+ assert.equal(r.searchParams.city, "杭州");
98
+ assert.equal(r.needs_search_params_confirmation, true);
99
+ assert.equal(r.suspicious_fields.length, 0);
100
+ }
101
+
102
+ function testDefaultsCanOnlyApplyWhenExplicitlyRequested() {
103
+ const r = parseRecruitInstruction({
104
+ instruction: "帮我找做过推荐系统的人",
105
+ confirmation: {
106
+ keyword_confirmed: true,
107
+ keyword_value: "推荐系统",
108
+ use_default_for_missing: true,
109
+ search_params_confirmed: true
110
+ },
111
+ overrides: null
112
+ });
113
+
114
+ assert.equal(r.searchParams.city, null);
115
+ assert.equal(r.searchParams.degree, "不限");
116
+ assert.deepEqual(r.searchParams.schools, []);
117
+ assert.equal(r.screenParams.target_count, 10);
118
+ assert.deepEqual(r.applied_defaults, {
119
+ city: "不限城市",
120
+ degree: "不限",
121
+ schools: "不限院校标签",
122
+ target_count: 10
123
+ });
124
+ }
125
+
70
126
  function main() {
71
127
  testNeedInput();
72
128
  testExampleExtraction();
73
129
  testMissingFieldsBatch();
74
130
  testStructuredInputAndCriteriaCleanup();
131
+ testCitySanitizationAndConfirmationGate();
132
+ testDefaultsCanOnlyApplyWhenExplicitlyRequested();
75
133
  // eslint-disable-next-line no-console
76
134
  console.log("parser tests passed");
77
135
  }