@reconcrap/boss-recommend-mcp 2.1.14 → 2.1.16

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.
@@ -17,26 +17,65 @@ const SEARCH_SCHOOL_MAP = {
17
17
 
18
18
  const KNOWN_SCHOOL_LABELS = new Set(Object.values(SEARCH_SCHOOL_MAP));
19
19
  const DEFAULT_PARAM_VALUES = {
20
+ job: null,
20
21
  city: null,
21
22
  degree: "不限",
22
23
  schools: [],
23
- keyword: "算法工程师",
24
- target_count: 10
24
+ experience: null,
25
+ gender: null,
26
+ age: null,
27
+ keyword: null,
28
+ target_count: null,
29
+ criteria: null
25
30
  };
26
31
  const DEFAULT_PARAM_LABELS = {
32
+ job: "搜索页岗位未指定",
27
33
  city: "不限城市",
28
34
  degree: "不限",
29
35
  schools: "不限院校标签",
30
- keyword: "算法工程师",
31
- target_count: 10
36
+ experience: "经验要求未指定",
37
+ gender: "性别未指定",
38
+ age: "年龄要求未指定",
39
+ keyword: "搜索关键词未指定",
40
+ target_count: "目标通过人数未指定",
41
+ criteria: "筛选 criteria 未指定"
32
42
  };
33
43
  const DEGREE_VALUES = new Set(["不限", "本科", "本科及以上", "硕士及以上", "博士"]);
34
- const CITY_STOP_PATTERN = /(?:筛选|搜索|查找|找|做过|从事过|有过|相关|的人选|的人|并且|且|学历|学校|目标|必须|优先|,|。|;|;|,)/;
44
+ const CITY_STOP_PATTERN = /(?:筛选|搜索|查找|找|做过|从事过|有过|相关|的人选|的人|并且|且|学历|学校|经验|性别|年龄|目标|必须|优先|,|。|;|;|,)/;
45
+ const POST_ACTIONS = new Set(["none", "greet"]);
46
+ const CRITERIA_MARKER_PATTERN = /(?:筛选条件|筛选标准|筛选要求|筛选规则|硬性条件|硬条件|criteria)\s*[::]/i;
47
+ const CRITERIA_TRAILING_FIELD_PATTERN = /\n\s*(?:岗位|职位|关键词|城市|地点|工作地|学历|学校类型|院校标签|经验|经验要求|工作经验|工作年限|性别|年龄|年龄要求|年龄范围|只看未查看|目标筛选人数|目标人数|休息强度|后置动作|post_action|rest_level)\s*[::]/i;
35
48
 
36
49
  function normalizeText(input) {
37
50
  return String(input || "").replace(/\s+/g, " ").trim();
38
51
  }
39
52
 
53
+ function normalizeCriteriaBlock(input) {
54
+ const lines = String(input || "")
55
+ .replace(/\r\n/g, "\n")
56
+ .split("\n")
57
+ .map((line) => line.trim())
58
+ .filter(Boolean);
59
+ return lines.join("\n").trim() || null;
60
+ }
61
+
62
+ function escapeRegExp(input) {
63
+ return String(input).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
64
+ }
65
+
66
+ function extractFieldLineValue(rawText, labels = []) {
67
+ const lines = String(rawText || "").replace(/\r\n/g, "\n").split("\n");
68
+ const labelPattern = labels.map(escapeRegExp).join("|");
69
+ if (!labelPattern) return null;
70
+ const pattern = new RegExp(`^\\s*(?:${labelPattern})(?:\\s*\\([^)]*\\))?\\s*[::]\\s*(.+?)\\s*$`, "i");
71
+ for (const line of lines) {
72
+ const match = line.match(pattern);
73
+ const value = match?.[1]?.trim();
74
+ if (value) return value;
75
+ }
76
+ return null;
77
+ }
78
+
40
79
  function uniqueList(items) {
41
80
  return Array.from(new Set(items.filter(Boolean)));
42
81
  }
@@ -45,6 +84,7 @@ function normalizeSchoolLabel(value) {
45
84
  if (typeof value !== "string") return null;
46
85
  const raw = value.trim();
47
86
  if (!raw) return null;
87
+ if (/^(?:不限|不限制|无限制|全部|所有|无|none|all)$/i.test(raw)) return null;
48
88
  if (KNOWN_SCHOOL_LABELS.has(raw)) return raw;
49
89
 
50
90
  const compact = raw.toLowerCase().replace(/\s+/g, "");
@@ -123,6 +163,25 @@ function extractRecentViewedFilter(text) {
123
163
  return null;
124
164
  }
125
165
 
166
+ function normalizeRecentViewedOverride(value) {
167
+ if (typeof value === "boolean") return value;
168
+ const normalized = normalizeText(value).toLowerCase();
169
+ if (!normalized) return null;
170
+ if (["true", "yes", "1", "需要过滤", "过滤", "近14天没有", "not_viewed"].includes(normalized)) return true;
171
+ if (["false", "no", "0", "不过滤", "不限", "none"].includes(normalized)) return false;
172
+ return null;
173
+ }
174
+
175
+ function normalizeBooleanOverride(value) {
176
+ if (typeof value === "boolean") return value;
177
+ if (typeof value === "number") return value !== 0;
178
+ const normalized = normalizeText(value).toLowerCase();
179
+ if (!normalized) return null;
180
+ if (["true", "yes", "y", "1", "on", "enable", "enabled", "需要", "是", "开启"].includes(normalized)) return true;
181
+ if (["false", "no", "n", "0", "off", "disable", "disabled", "不需要", "否", "关闭"].includes(normalized)) return false;
182
+ return null;
183
+ }
184
+
126
185
  function normalizeStringOverride(value) {
127
186
  if (typeof value !== "string") return null;
128
187
  const normalized = value.trim();
@@ -130,17 +189,79 @@ function normalizeStringOverride(value) {
130
189
  }
131
190
 
132
191
  function normalizeSchoolsOverride(value) {
133
- if (Array.isArray(value)) return uniqueList(value.map(normalizeSchoolLabel));
134
- if (typeof value === "string") return uniqueList(value.split(/[,,]/).map(normalizeSchoolLabel));
192
+ if (Array.isArray(value)) {
193
+ return uniqueList(value.flatMap((item) => normalizeSchoolsOverride(item) || []));
194
+ }
195
+ if (typeof value === "string") return uniqueList(value.split(/[,,、|/]/).map(normalizeSchoolLabel));
135
196
  return null;
136
197
  }
137
198
 
199
+ function extractSchoolFilterExplicit(rawText) {
200
+ const value = extractFieldLineValue(rawText, [
201
+ "学校",
202
+ "院校",
203
+ "学校类型",
204
+ "院校标签",
205
+ "学校标签",
206
+ "school",
207
+ "school_tag",
208
+ "school_tags",
209
+ "schools"
210
+ ]);
211
+ if (value === null) return { explicit: false, schools: null };
212
+ return { explicit: true, schools: normalizeSchoolsOverride(value) || [] };
213
+ }
214
+
215
+ function extractRecentViewedExplicit(rawText) {
216
+ const value = extractFieldLineValue(rawText, ["只看未查看", "过滤已看", "recent_not_view", "filter_recent_viewed"]);
217
+ return value === null ? null : normalizeRecentViewedOverride(value);
218
+ }
219
+
138
220
  function normalizeDegreesOverride(value) {
139
221
  if (Array.isArray(value)) return uniqueList(value.map(normalizeText));
140
222
  if (typeof value === "string") return uniqueList(value.split(/[,,、|/]/).map(normalizeText));
141
223
  return null;
142
224
  }
143
225
 
226
+ function normalizeExperienceOverride(value) {
227
+ if (typeof value === "string") return normalizeText(value) || null;
228
+ if (Array.isArray(value)) {
229
+ const normalized = value.map(normalizeText).filter(Boolean);
230
+ return normalized.length ? normalized[0] : null;
231
+ }
232
+ if (value && typeof value === "object") return value;
233
+ return null;
234
+ }
235
+
236
+ function normalizeGenericSearchFilterOverride(value) {
237
+ if (typeof value === "string" || typeof value === "number") return normalizeText(value) || null;
238
+ if (Array.isArray(value)) {
239
+ const normalized = value.map(normalizeText).filter(Boolean);
240
+ return normalized.length ? normalized[0] : null;
241
+ }
242
+ if (value && typeof value === "object") return value;
243
+ return null;
244
+ }
245
+
246
+ function normalizeDegreeFieldValue(value) {
247
+ const normalized = normalizeText(value);
248
+ if (!normalized) return null;
249
+ if (/^(?:不限|不限制|无限制|全部|所有|无|none|all)$/i.test(normalized)) return "不限";
250
+ return extractDegree(normalized) || normalized;
251
+ }
252
+
253
+ function normalizePostAction(value) {
254
+ const normalized = normalizeText(value).toLowerCase();
255
+ if (["", "none", "skip", "no", "不执行", "无", "什么也不做"].includes(normalized)) return "none";
256
+ if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
257
+ return POST_ACTIONS.has(normalized) ? normalized : "";
258
+ }
259
+
260
+ function parsePositiveInteger(value) {
261
+ const parsed = Number.parseInt(String(value || ""), 10);
262
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
263
+ }
264
+
144
265
  function extractKeywordExplicit(text) {
145
266
  const patterns = [
146
267
  /搜索关键词(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
@@ -169,6 +290,19 @@ function extractKeywordAuto(text) {
169
290
  return null;
170
291
  }
171
292
 
293
+ function extractJobExplicit(text) {
294
+ const patterns = [
295
+ /(?:搜索页)?(?:岗位|职位)(?:名称)?(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
296
+ /job(?:\s*title)?(?:\s*[::=]\s*|\s+is\s+)([^\n,。;;]+)/i
297
+ ];
298
+ for (const pattern of patterns) {
299
+ const match = text.match(pattern);
300
+ const job = match?.[1]?.trim();
301
+ if (job) return job;
302
+ }
303
+ return null;
304
+ }
305
+
172
306
  function extractTargetCount(text) {
173
307
  const patterns = [
174
308
  /至少筛选\s*(\d+)\s*位?/i,
@@ -186,50 +320,30 @@ function extractTargetCount(text) {
186
320
  return null;
187
321
  }
188
322
 
189
- function sanitizeClause(clause) {
190
- return clause
191
- .replace(/^使用boss-recruit-pipeline skills/i, "")
192
- .replace(/^帮我(?:在boss上)?(?:找|筛选)/i, "")
193
- .replace(/^请(?:在boss上)?(?:帮我)?(?:找|筛选)/i, "")
194
- .replace(/^在boss上(?:帮我)?(?:找|筛选)/i, "")
195
- .replace(/的人选$/, "")
196
- .replace(/的人$/, "")
197
- .trim();
323
+ function extractPostAction(text) {
324
+ if (/(?:什么也不做|不(?:打招呼|沟通)|只筛选|不执行)/.test(text)) return "none";
325
+ if (/(?:直接沟通|打招呼|立即沟通|greet|post_action\s*[::=]\s*greet)/i.test(text)) return "greet";
326
+ return "";
198
327
  }
199
328
 
200
- function isCountPlanningClause(clause) {
201
- return /(?:目标(?:筛选)?(?:人数|数量)?|至少筛选|筛选\s*\d+\s*位|输出\s*\d+\s*(?:位|个|个人选|个候选人)?|最终输出\s*\d+\s*(?:位|个|个人选|个候选人)?|处理\s*\d+\s*(?:位|人)|(?:浏览|拉取|抓取).*(?:至少\s*)?\d+\s*(?:位|个|个人选|个候选人)?|最匹配.*\d+\s*(?:位|个|个人选|个候选人)?)/i.test(clause);
329
+ function extractTargetCountExplicit(rawText) {
330
+ const value = extractFieldLineValue(rawText, ["目标筛选人数", "目标人数", "目标通过人数", "target_count", "max_candidates"]);
331
+ return parsePositiveInteger(value);
202
332
  }
203
333
 
204
- function buildScreenCriteria(text, searchParams) {
205
- const clauses = text
206
- .split(/[,,。;;\n]/)
207
- .map((clause) => sanitizeClause(clause))
208
- .filter(Boolean);
209
-
210
- const normalized = clauses
211
- .filter((clause) => {
212
- if (/搜索关键词|关键词|keyword/i.test(clause)) return false;
213
- if (/地点|城市/.test(clause)) return false;
214
- if (/近?14天(?:内)?查看(?:过)?|过滤近14天查看/.test(clause)) return false;
215
- if (isCountPlanningClause(clause)) return false;
216
- return true;
217
- })
218
- .map((clause) => clause.replace(/\s+/g, " ").trim())
219
- .filter(Boolean);
220
-
221
- if (searchParams?.keyword) {
222
- const keywordClause = `候选人需有${searchParams.keyword}相关经历`;
223
- const alreadyCovered = normalized.some((clause) =>
224
- clause.toLowerCase().includes(String(searchParams.keyword).toLowerCase())
225
- );
226
- if (!alreadyCovered) normalized.unshift(keywordClause);
227
- }
334
+ function extractPostActionExplicit(rawText) {
335
+ const value = extractFieldLineValue(rawText, ["后置动作", "通过后执行动作", "post_action"]);
336
+ return normalizePostAction(value);
337
+ }
228
338
 
229
- if (!normalized.length) {
230
- return searchParams?.keyword ? `候选人需有${searchParams.keyword}相关经历` : text;
231
- }
232
- return uniqueList(normalized).join(";");
339
+ function extractExplicitCriteria(rawText) {
340
+ const normalized = String(rawText || "").replace(/\r\n/g, "\n");
341
+ const match = normalized.match(CRITERIA_MARKER_PATTERN);
342
+ if (!match) return null;
343
+ let criteria = normalized.slice(match.index + match[0].length).trim();
344
+ const trailingFieldIndex = criteria.search(CRITERIA_TRAILING_FIELD_PATTERN);
345
+ if (trailingFieldIndex > 0) criteria = criteria.slice(0, trailingFieldIndex).trim();
346
+ return normalizeCriteriaBlock(criteria);
233
347
  }
234
348
 
235
349
  function resolveKeyword(parsed, confirmation) {
@@ -251,6 +365,45 @@ function resolveKeyword(parsed, confirmation) {
251
365
  return { keyword: null, needsConfirmation: false, proposedKeyword: null };
252
366
  }
253
367
 
368
+ function resolveJob(parsed, confirmation) {
369
+ if (parsed.job_override) return parsed.job_override;
370
+ const confirmed = confirmation?.job_confirmed === true;
371
+ const value = typeof confirmation?.job_value === "string" ? confirmation.job_value.trim() : "";
372
+ if (confirmed && value) return value;
373
+ return parsed.job_explicit || null;
374
+ }
375
+
376
+ function resolvePostAction(parsed, confirmation) {
377
+ const confirmed = confirmation?.post_action_confirmed === true;
378
+ const confirmationValue = normalizePostAction(confirmation?.post_action_value);
379
+ return parsed.post_action_override
380
+ || (confirmed && confirmationValue ? confirmationValue : "")
381
+ || parsed.post_action_explicit
382
+ || "none";
383
+ }
384
+
385
+ function resolveMaxGreetCount(parsed, confirmation) {
386
+ return parsePositiveInteger(confirmation?.max_greet_count_value)
387
+ || parsePositiveInteger(parsed.max_greet_count_override)
388
+ || null;
389
+ }
390
+
391
+ function collectMissingFields(searchParams, screenParams, parsed = {}) {
392
+ const missing = [];
393
+ if (!searchParams.job) missing.push("job");
394
+ if (!searchParams.city && !parsed.city_explicit) missing.push("city");
395
+ if (!searchParams.degree && !searchParams.degrees?.length && !parsed.degree_explicit) missing.push("degree");
396
+ if (!searchParams.schools?.length && !parsed.schools_explicit) missing.push("schools");
397
+ if (!searchParams.keyword) missing.push("keyword");
398
+ if (!screenParams.criteria) missing.push("criteria");
399
+ if (!screenParams.target_count) missing.push("target_count");
400
+ return missing;
401
+ }
402
+
403
+ function collectUnresolvedMissingFields(missingFields, appliedDefaults) {
404
+ return missingFields.filter((field) => !Object.prototype.hasOwnProperty.call(appliedDefaults, field));
405
+ }
406
+
254
407
  function collectSuspiciousFields(searchParams, screenParams) {
255
408
  const suspicious = [];
256
409
  if (searchParams.city && (/\s/.test(searchParams.city) || CITY_STOP_PATTERN.test(searchParams.city) || searchParams.city.length > 8)) {
@@ -284,9 +437,27 @@ function collectSuspiciousFields(searchParams, screenParams) {
284
437
  return suspicious;
285
438
  }
286
439
 
440
+ function buildMissingFieldQuestions(missingFields = [], defaultPreview = {}) {
441
+ const questions = {
442
+ job: "请填写搜索页岗位名称(关键词输入框旁边的岗位选择)。",
443
+ city: "请填写城市;如不限城市,请明确回复不限。",
444
+ degree: "请填写学历筛选;如不限学历,请明确回复不限。",
445
+ schools: "请填写院校标签;如不限院校标签,请明确回复不限。",
446
+ keyword: "请填写搜索关键词。",
447
+ criteria: "请填写本次筛选 criteria(完整自然语言硬条件)。",
448
+ target_count: "请填写本次目标通过人数。"
449
+ };
450
+ return missingFields.map((field) => ({
451
+ field,
452
+ question: questions[field] || `请填写 ${field}。`,
453
+ value: Object.prototype.hasOwnProperty.call(defaultPreview, field) ? defaultPreview[field] : null
454
+ }));
455
+ }
456
+
287
457
  function buildDefaultPreview(missingFields, { skipKeywordDefault = false } = {}) {
288
458
  return missingFields.reduce((acc, field) => {
289
459
  if (field === "keyword" && skipKeywordDefault) return acc;
460
+ if (!["city", "degree", "schools"].includes(field)) return acc;
290
461
  acc[field] = DEFAULT_PARAM_LABELS[field];
291
462
  return acc;
292
463
  }, {});
@@ -311,14 +482,6 @@ function applyDefaults(searchParams, screenParams, missingFields, useDefaultForM
311
482
  nextSearchParams.schools = DEFAULT_PARAM_VALUES.schools.slice();
312
483
  appliedDefaults.schools = DEFAULT_PARAM_LABELS.schools;
313
484
  }
314
- if (missingFields.includes("keyword") && !skipKeywordDefault) {
315
- nextSearchParams.keyword = DEFAULT_PARAM_VALUES.keyword;
316
- appliedDefaults.keyword = DEFAULT_PARAM_LABELS.keyword;
317
- }
318
- if (missingFields.includes("target_count")) {
319
- nextScreenParams.target_count = DEFAULT_PARAM_VALUES.target_count;
320
- appliedDefaults.target_count = DEFAULT_PARAM_LABELS.target_count;
321
- }
322
485
  return {
323
486
  searchParams: nextSearchParams,
324
487
  screenParams: nextScreenParams,
@@ -327,58 +490,186 @@ function applyDefaults(searchParams, screenParams, missingFields, useDefaultForM
327
490
  }
328
491
 
329
492
  export function parseRecruitInstruction({ instruction, confirmation, overrides } = {}) {
330
- const text = normalizeText(instruction);
493
+ const rawInstruction = String(instruction || "");
494
+ const text = normalizeText(rawInstruction);
495
+ const finalConfirmed = confirmation?.final_confirmed === true;
496
+ const hasSkipRecentColleagueOverride = Object.prototype.hasOwnProperty.call(
497
+ overrides || {},
498
+ "skip_recent_colleague_contacted"
499
+ );
500
+ const confirmationSkipRecentColleagueContacted = normalizeBooleanOverride(
501
+ confirmation?.skip_recent_colleague_contacted_value
502
+ );
503
+ const explicitSchools = extractSchoolFilterExplicit(rawInstruction);
504
+ const explicitRecentViewed = extractRecentViewedExplicit(rawInstruction);
505
+ const explicitKeyword = extractFieldLineValue(rawInstruction, ["搜索关键词", "关键词", "keyword"]);
506
+ const explicitJob = extractFieldLineValue(rawInstruction, ["岗位", "职位", "job"]);
507
+ const explicitCity = extractFieldLineValue(rawInstruction, ["城市", "地点", "工作地", "base"]);
508
+ const explicitDegree = extractFieldLineValue(rawInstruction, ["学历", "degree"]);
509
+ const explicitExperience = extractFieldLineValue(rawInstruction, ["经验", "经验要求", "工作经验", "工作年限", "experience"]);
510
+ const explicitGender = extractFieldLineValue(rawInstruction, ["性别", "gender"]);
511
+ const explicitAge = extractFieldLineValue(rawInstruction, ["年龄", "年龄要求", "年龄范围", "age"]);
512
+ const explicitTargetCount = extractTargetCountExplicit(rawInstruction);
513
+ const explicitPostAction = extractPostActionExplicit(rawInstruction);
331
514
  const parsed = {
332
- city: extractCity(text),
333
- degree: extractDegree(text),
334
- schools: extractSchools(text),
335
- filter_recent_viewed: extractRecentViewedFilter(text),
336
- keyword_explicit: extractKeywordExplicit(text),
515
+ job_explicit: explicitJob || extractJobExplicit(text),
516
+ city: sanitizeCityCandidate(explicitCity) || extractCity(text),
517
+ city_explicit: explicitCity !== null,
518
+ degree: normalizeDegreeFieldValue(explicitDegree) || extractDegree(text),
519
+ degree_explicit: explicitDegree !== null,
520
+ experience: normalizeExperienceOverride(explicitExperience),
521
+ experience_explicit: explicitExperience !== null,
522
+ gender: normalizeGenericSearchFilterOverride(explicitGender),
523
+ gender_explicit: explicitGender !== null,
524
+ age: normalizeGenericSearchFilterOverride(explicitAge),
525
+ age_explicit: explicitAge !== null,
526
+ schools: explicitSchools.explicit ? explicitSchools.schools : extractSchools(text),
527
+ schools_explicit: explicitSchools.explicit,
528
+ filter_recent_viewed: explicitRecentViewed !== null ? explicitRecentViewed : extractRecentViewedFilter(text),
529
+ skip_recent_colleague_contacted: confirmationSkipRecentColleagueContacted ?? true,
530
+ keyword_explicit: explicitKeyword || extractKeywordExplicit(text),
337
531
  keyword_auto: extractKeywordAuto(text),
338
- target_count: extractTargetCount(text)
532
+ target_count: explicitTargetCount || extractTargetCount(text),
533
+ post_action_explicit: explicitPostAction || extractPostAction(text),
534
+ criteria_explicit: extractExplicitCriteria(rawInstruction)
339
535
  };
340
536
 
341
537
  if (overrides) {
342
538
  const overrideCity = sanitizeCityCandidate(normalizeStringOverride(overrides.city));
343
539
  const overrideDegree = normalizeStringOverride(overrides.degree);
344
- const overrideDegrees = normalizeDegreesOverride(overrides.degrees);
345
- const overrideSchools = normalizeSchoolsOverride(overrides.schools);
540
+ const overrideDegrees = normalizeDegreesOverride(overrides.degrees || (Array.isArray(overrides.degree) ? overrides.degree : null));
541
+ const hasOverrideSchools = Object.prototype.hasOwnProperty.call(overrides, "schools")
542
+ || Object.prototype.hasOwnProperty.call(overrides, "school_tag")
543
+ || Object.prototype.hasOwnProperty.call(overrides, "school_tags");
544
+ const overrideSchools = normalizeSchoolsOverride(
545
+ Object.prototype.hasOwnProperty.call(overrides, "schools")
546
+ ? overrides.schools
547
+ : Object.prototype.hasOwnProperty.call(overrides, "school_tag")
548
+ ? overrides.school_tag
549
+ : overrides.school_tags
550
+ );
346
551
  const overrideKeyword = normalizeStringOverride(overrides.keyword);
347
- const overrideRecentViewed = typeof overrides.filter_recent_viewed === "boolean"
348
- ? overrides.filter_recent_viewed
349
- : null;
552
+ const overrideJob = normalizeStringOverride(overrides.job || overrides.job_title || overrides.selected_job);
553
+ const overrideCriteria = normalizeStringOverride(overrides.criteria);
554
+ const hasOverrideExperience = Object.prototype.hasOwnProperty.call(overrides, "experience")
555
+ || Object.prototype.hasOwnProperty.call(overrides, "experiences")
556
+ || Object.prototype.hasOwnProperty.call(overrides, "experience_range")
557
+ || Object.prototype.hasOwnProperty.call(overrides, "experience_start")
558
+ || Object.prototype.hasOwnProperty.call(overrides, "experience_end");
559
+ const overrideExperience = Object.prototype.hasOwnProperty.call(overrides, "experience")
560
+ ? normalizeExperienceOverride(overrides.experience)
561
+ : Object.prototype.hasOwnProperty.call(overrides, "experiences")
562
+ ? normalizeExperienceOverride(overrides.experiences)
563
+ : Object.prototype.hasOwnProperty.call(overrides, "experience_range")
564
+ ? normalizeExperienceOverride(overrides.experience_range)
565
+ : hasOverrideExperience
566
+ ? {
567
+ start: overrides.experience_start,
568
+ end: overrides.experience_end
569
+ }
570
+ : null;
571
+ const hasOverrideGender = Object.prototype.hasOwnProperty.call(overrides, "gender");
572
+ const overrideGender = hasOverrideGender ? normalizeGenericSearchFilterOverride(overrides.gender) : null;
573
+ const hasOverrideAge = Object.prototype.hasOwnProperty.call(overrides, "age")
574
+ || Object.prototype.hasOwnProperty.call(overrides, "ages")
575
+ || Object.prototype.hasOwnProperty.call(overrides, "age_range")
576
+ || Object.prototype.hasOwnProperty.call(overrides, "age_min")
577
+ || Object.prototype.hasOwnProperty.call(overrides, "age_max")
578
+ || Object.prototype.hasOwnProperty.call(overrides, "min_age")
579
+ || Object.prototype.hasOwnProperty.call(overrides, "max_age");
580
+ const overrideAge = Object.prototype.hasOwnProperty.call(overrides, "age")
581
+ ? normalizeGenericSearchFilterOverride(overrides.age)
582
+ : Object.prototype.hasOwnProperty.call(overrides, "ages")
583
+ ? normalizeGenericSearchFilterOverride(overrides.ages)
584
+ : Object.prototype.hasOwnProperty.call(overrides, "age_range")
585
+ ? normalizeGenericSearchFilterOverride(overrides.age_range)
586
+ : hasOverrideAge
587
+ ? {
588
+ min: overrides.age_min ?? overrides.min_age,
589
+ max: overrides.age_max ?? overrides.max_age
590
+ }
591
+ : null;
592
+ const overrideRecentViewed = normalizeRecentViewedOverride(
593
+ Object.prototype.hasOwnProperty.call(overrides, "filter_recent_viewed")
594
+ ? overrides.filter_recent_viewed
595
+ : overrides.recent_not_view
596
+ );
597
+ const overrideSkipRecentColleagueContacted = normalizeBooleanOverride(overrides.skip_recent_colleague_contacted);
598
+ const overridePostAction = normalizePostAction(overrides.post_action);
350
599
  if (overrideCity) parsed.city = overrideCity;
351
600
  if (overrideDegree) parsed.degree = overrideDegree;
352
601
  if (overrideDegrees?.length) parsed.degrees = overrideDegrees;
353
- if (overrideSchools?.length) parsed.schools = overrideSchools;
602
+ if (Object.prototype.hasOwnProperty.call(overrides, "city")) parsed.city_explicit = true;
603
+ if (Object.prototype.hasOwnProperty.call(overrides, "degree") || Object.prototype.hasOwnProperty.call(overrides, "degrees")) {
604
+ parsed.degree_explicit = true;
605
+ }
606
+ if (hasOverrideSchools && Array.isArray(overrideSchools)) {
607
+ parsed.schools = overrideSchools;
608
+ parsed.schools_explicit = true;
609
+ }
610
+ if (hasOverrideExperience) {
611
+ parsed.experience = overrideExperience;
612
+ parsed.experience_explicit = true;
613
+ }
614
+ if (hasOverrideGender) {
615
+ parsed.gender = overrideGender;
616
+ parsed.gender_explicit = true;
617
+ }
618
+ if (hasOverrideAge) {
619
+ parsed.age = overrideAge;
620
+ parsed.age_explicit = true;
621
+ }
354
622
  if (overrideKeyword) parsed.keyword_override = overrideKeyword;
623
+ if (overrideJob) parsed.job_override = overrideJob;
624
+ if (overrideCriteria) parsed.criteria_override = overrideCriteria;
355
625
  if (overrideRecentViewed !== null) parsed.filter_recent_viewed = overrideRecentViewed;
626
+ if (overrideSkipRecentColleagueContacted !== null) parsed.skip_recent_colleague_contacted = overrideSkipRecentColleagueContacted;
627
+ if (overridePostAction) parsed.post_action_override = overridePostAction;
628
+ if (Number.isFinite(overrides.max_greet_count) && overrides.max_greet_count > 0) {
629
+ parsed.max_greet_count_override = Number.parseInt(String(overrides.max_greet_count), 10);
630
+ }
356
631
  if (Number.isFinite(overrides.target_count) && overrides.target_count > 0) {
357
632
  parsed.target_count = Number.parseInt(String(overrides.target_count), 10);
358
633
  }
359
634
  }
360
635
 
361
636
  const keywordResolution = resolveKeyword(parsed, confirmation);
637
+ const job = resolveJob(parsed, confirmation);
638
+ const postAction = resolvePostAction(parsed, confirmation);
639
+ const maxGreetCount = resolveMaxGreetCount(parsed, confirmation);
640
+ const confirmationCriteria = normalizeStringOverride(confirmation?.criteria_value);
362
641
  const baseSearchParams = {
642
+ job,
363
643
  city: parsed.city,
364
644
  degree: parsed.degree,
365
645
  degrees: parsed.degrees,
366
646
  schools: parsed.schools,
647
+ experience: parsed.experience,
648
+ gender: parsed.gender,
649
+ age: parsed.age,
367
650
  filter_recent_viewed: parsed.filter_recent_viewed,
651
+ skip_recent_colleague_contacted: parsed.skip_recent_colleague_contacted !== false,
368
652
  keyword: keywordResolution.keyword
369
653
  };
654
+ const criteria = parsed.criteria_override || confirmationCriteria || parsed.criteria_explicit || null;
655
+ const criteriaSource = parsed.criteria_override
656
+ ? "override"
657
+ : confirmationCriteria
658
+ ? "confirmation"
659
+ : parsed.criteria_explicit
660
+ ? "instruction_block"
661
+ : "missing";
370
662
  const baseScreenParams = {
371
- criteria: buildScreenCriteria(text, baseSearchParams),
372
- target_count: parsed.target_count
663
+ criteria,
664
+ target_count: parsed.target_count,
665
+ post_action: postAction,
666
+ max_greet_count: maxGreetCount,
667
+ skip_recent_colleague_contacted: parsed.skip_recent_colleague_contacted !== false,
668
+ search_exchange_resume_filter_days: 30
373
669
  };
374
- const missingBeforeDefaults = [];
375
- if (!baseSearchParams.city) missingBeforeDefaults.push("city");
376
- if (!baseSearchParams.degree) missingBeforeDefaults.push("degree");
377
- if (!baseSearchParams.schools?.length) missingBeforeDefaults.push("schools");
378
- if (!baseSearchParams.keyword) missingBeforeDefaults.push("keyword");
379
- if (!baseScreenParams.target_count) missingBeforeDefaults.push("target_count");
380
-
381
- const useDefaultForMissing = confirmation?.use_default_for_missing === true;
670
+ const missingBeforeDefaults = collectMissingFields(baseSearchParams, baseScreenParams, parsed);
671
+
672
+ const useDefaultForMissing = finalConfirmed || confirmation?.use_default_for_missing === true;
382
673
  const skipKeywordDefault = keywordResolution.needsConfirmation;
383
674
  const defaultPreview = buildDefaultPreview(missingBeforeDefaults, { skipKeywordDefault });
384
675
  const { searchParams, screenParams, appliedDefaults } = applyDefaults(
@@ -388,10 +679,18 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
388
679
  useDefaultForMissing,
389
680
  { skipKeywordDefault }
390
681
  );
682
+ const missingAfterDefaults = collectUnresolvedMissingFields(missingBeforeDefaults, appliedDefaults);
391
683
  const suspicious_fields = collectSuspiciousFields(searchParams, screenParams);
392
- const needs_recent_viewed_filter_confirmation = searchParams.filter_recent_viewed === null;
393
- const needs_criteria_confirmation = confirmation?.criteria_confirmed !== true;
684
+ const needs_recent_viewed_filter_confirmation = !finalConfirmed && searchParams.filter_recent_viewed === null;
685
+ const needs_skip_recent_colleague_contacted_confirmation = (
686
+ !finalConfirmed
687
+ && !hasSkipRecentColleagueOverride
688
+ && confirmationSkipRecentColleagueContacted === null
689
+ && confirmation?.skip_recent_colleague_contacted_confirmed !== true
690
+ );
691
+ const needs_criteria_confirmation = Boolean(screenParams.criteria) && !finalConfirmed && confirmation?.criteria_confirmed !== true;
394
692
  const pending_questions = [
693
+ ...buildMissingFieldQuestions(missingAfterDefaults, defaultPreview),
395
694
  ...(needs_recent_viewed_filter_confirmation
396
695
  ? [{
397
696
  field: "filter_recent_viewed",
@@ -402,6 +701,17 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
402
701
  ]
403
702
  }]
404
703
  : []),
704
+ ...(needs_skip_recent_colleague_contacted_confirmation
705
+ ? [{
706
+ field: "skip_recent_colleague_contacted",
707
+ question: "是否跳过近期已被同事触达的人选?搜索页会开启 Boss 的“近30天未和同事交换简历”过滤。",
708
+ value: true,
709
+ options: [
710
+ { label: "跳过(推荐)", value: true },
711
+ { label: "不跳过", value: false }
712
+ ]
713
+ }]
714
+ : []),
405
715
  ...(needs_criteria_confirmation
406
716
  ? [{
407
717
  field: "criteria",
@@ -415,25 +725,29 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
415
725
  extracted_screen_params: baseScreenParams,
416
726
  current_search_params: searchParams,
417
727
  current_screen_params: screenParams,
418
- missing_fields: missingBeforeDefaults,
419
- has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
728
+ missing_fields: missingAfterDefaults,
729
+ missing_fields_before_defaults: missingBeforeDefaults,
730
+ has_unresolved_missing_fields: missingAfterDefaults.length > 0,
420
731
  suspicious_fields,
421
732
  pending_questions,
422
733
  default_preview: defaultPreview,
423
- applied_defaults: appliedDefaults
734
+ applied_defaults: appliedDefaults,
735
+ criteria_source: criteriaSource,
736
+ final_confirmed: finalConfirmed
424
737
  };
425
738
 
426
739
  return {
427
740
  parsed,
428
741
  searchParams,
429
742
  screenParams,
430
- missing_fields: missingBeforeDefaults,
431
- has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
743
+ missing_fields: missingAfterDefaults,
744
+ has_unresolved_missing_fields: missingAfterDefaults.length > 0,
432
745
  suspicious_fields,
433
746
  needs_keyword_confirmation: keywordResolution.needsConfirmation,
434
747
  needs_recent_viewed_filter_confirmation,
748
+ needs_skip_recent_colleague_contacted_confirmation,
435
749
  needs_criteria_confirmation,
436
- needs_search_params_confirmation: confirmation?.search_params_confirmed !== true,
750
+ needs_search_params_confirmation: !finalConfirmed && confirmation?.search_params_confirmed !== true,
437
751
  proposed_keyword: keywordResolution.proposedKeyword,
438
752
  pending_questions,
439
753
  default_preview: defaultPreview,
@@ -447,5 +761,8 @@ export const recruitInstructionParserSemantics = Object.freeze({
447
761
  imported_at: "2026-04-30",
448
762
  default_param_values: DEFAULT_PARAM_VALUES,
449
763
  school_labels: SEARCH_SCHOOL_MAP,
450
- degree_values: Array.from(DEGREE_VALUES)
764
+ degree_values: Array.from(DEGREE_VALUES),
765
+ experience_values: ["不限", "在校/应届", "25年毕业", "26年毕业", "26年后毕业", "1-3年", "3-5年", "5-10年", "自定义"],
766
+ gender_values: ["不限", "男", "女"],
767
+ age_values: ["不限", "20-25", "25-30", "30-35", "35-40", "40-50", "50以上", "自定义"]
451
768
  });