@reconcrap/boss-recommend-mcp 2.1.14 → 2.1.15

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,15 @@ 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
+
126
175
  function normalizeStringOverride(value) {
127
176
  if (typeof value !== "string") return null;
128
177
  const normalized = value.trim();
@@ -130,17 +179,79 @@ function normalizeStringOverride(value) {
130
179
  }
131
180
 
132
181
  function normalizeSchoolsOverride(value) {
133
- if (Array.isArray(value)) return uniqueList(value.map(normalizeSchoolLabel));
134
- if (typeof value === "string") return uniqueList(value.split(/[,,]/).map(normalizeSchoolLabel));
182
+ if (Array.isArray(value)) {
183
+ return uniqueList(value.flatMap((item) => normalizeSchoolsOverride(item) || []));
184
+ }
185
+ if (typeof value === "string") return uniqueList(value.split(/[,,、|/]/).map(normalizeSchoolLabel));
135
186
  return null;
136
187
  }
137
188
 
189
+ function extractSchoolFilterExplicit(rawText) {
190
+ const value = extractFieldLineValue(rawText, [
191
+ "学校",
192
+ "院校",
193
+ "学校类型",
194
+ "院校标签",
195
+ "学校标签",
196
+ "school",
197
+ "school_tag",
198
+ "school_tags",
199
+ "schools"
200
+ ]);
201
+ if (value === null) return { explicit: false, schools: null };
202
+ return { explicit: true, schools: normalizeSchoolsOverride(value) || [] };
203
+ }
204
+
205
+ function extractRecentViewedExplicit(rawText) {
206
+ const value = extractFieldLineValue(rawText, ["只看未查看", "过滤已看", "recent_not_view", "filter_recent_viewed"]);
207
+ return value === null ? null : normalizeRecentViewedOverride(value);
208
+ }
209
+
138
210
  function normalizeDegreesOverride(value) {
139
211
  if (Array.isArray(value)) return uniqueList(value.map(normalizeText));
140
212
  if (typeof value === "string") return uniqueList(value.split(/[,,、|/]/).map(normalizeText));
141
213
  return null;
142
214
  }
143
215
 
216
+ function normalizeExperienceOverride(value) {
217
+ if (typeof value === "string") return normalizeText(value) || null;
218
+ if (Array.isArray(value)) {
219
+ const normalized = value.map(normalizeText).filter(Boolean);
220
+ return normalized.length ? normalized[0] : null;
221
+ }
222
+ if (value && typeof value === "object") return value;
223
+ return null;
224
+ }
225
+
226
+ function normalizeGenericSearchFilterOverride(value) {
227
+ if (typeof value === "string" || typeof value === "number") 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 normalizeDegreeFieldValue(value) {
237
+ const normalized = normalizeText(value);
238
+ if (!normalized) return null;
239
+ if (/^(?:不限|不限制|无限制|全部|所有|无|none|all)$/i.test(normalized)) return "不限";
240
+ return extractDegree(normalized) || normalized;
241
+ }
242
+
243
+ function normalizePostAction(value) {
244
+ const normalized = normalizeText(value).toLowerCase();
245
+ if (["", "none", "skip", "no", "不执行", "无", "什么也不做"].includes(normalized)) return "none";
246
+ if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
247
+ return POST_ACTIONS.has(normalized) ? normalized : "";
248
+ }
249
+
250
+ function parsePositiveInteger(value) {
251
+ const parsed = Number.parseInt(String(value || ""), 10);
252
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
253
+ }
254
+
144
255
  function extractKeywordExplicit(text) {
145
256
  const patterns = [
146
257
  /搜索关键词(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
@@ -169,6 +280,19 @@ function extractKeywordAuto(text) {
169
280
  return null;
170
281
  }
171
282
 
283
+ function extractJobExplicit(text) {
284
+ const patterns = [
285
+ /(?:搜索页)?(?:岗位|职位)(?:名称)?(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
286
+ /job(?:\s*title)?(?:\s*[::=]\s*|\s+is\s+)([^\n,。;;]+)/i
287
+ ];
288
+ for (const pattern of patterns) {
289
+ const match = text.match(pattern);
290
+ const job = match?.[1]?.trim();
291
+ if (job) return job;
292
+ }
293
+ return null;
294
+ }
295
+
172
296
  function extractTargetCount(text) {
173
297
  const patterns = [
174
298
  /至少筛选\s*(\d+)\s*位?/i,
@@ -186,50 +310,30 @@ function extractTargetCount(text) {
186
310
  return null;
187
311
  }
188
312
 
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();
313
+ function extractPostAction(text) {
314
+ if (/(?:什么也不做|不(?:打招呼|沟通)|只筛选|不执行)/.test(text)) return "none";
315
+ if (/(?:直接沟通|打招呼|立即沟通|greet|post_action\s*[::=]\s*greet)/i.test(text)) return "greet";
316
+ return "";
198
317
  }
199
318
 
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);
319
+ function extractTargetCountExplicit(rawText) {
320
+ const value = extractFieldLineValue(rawText, ["目标筛选人数", "目标人数", "目标通过人数", "target_count", "max_candidates"]);
321
+ return parsePositiveInteger(value);
202
322
  }
203
323
 
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
- }
324
+ function extractPostActionExplicit(rawText) {
325
+ const value = extractFieldLineValue(rawText, ["后置动作", "通过后执行动作", "post_action"]);
326
+ return normalizePostAction(value);
327
+ }
228
328
 
229
- if (!normalized.length) {
230
- return searchParams?.keyword ? `候选人需有${searchParams.keyword}相关经历` : text;
231
- }
232
- return uniqueList(normalized).join(";");
329
+ function extractExplicitCriteria(rawText) {
330
+ const normalized = String(rawText || "").replace(/\r\n/g, "\n");
331
+ const match = normalized.match(CRITERIA_MARKER_PATTERN);
332
+ if (!match) return null;
333
+ let criteria = normalized.slice(match.index + match[0].length).trim();
334
+ const trailingFieldIndex = criteria.search(CRITERIA_TRAILING_FIELD_PATTERN);
335
+ if (trailingFieldIndex > 0) criteria = criteria.slice(0, trailingFieldIndex).trim();
336
+ return normalizeCriteriaBlock(criteria);
233
337
  }
234
338
 
235
339
  function resolveKeyword(parsed, confirmation) {
@@ -251,6 +355,45 @@ function resolveKeyword(parsed, confirmation) {
251
355
  return { keyword: null, needsConfirmation: false, proposedKeyword: null };
252
356
  }
253
357
 
358
+ function resolveJob(parsed, confirmation) {
359
+ if (parsed.job_override) return parsed.job_override;
360
+ const confirmed = confirmation?.job_confirmed === true;
361
+ const value = typeof confirmation?.job_value === "string" ? confirmation.job_value.trim() : "";
362
+ if (confirmed && value) return value;
363
+ return parsed.job_explicit || null;
364
+ }
365
+
366
+ function resolvePostAction(parsed, confirmation) {
367
+ const confirmed = confirmation?.post_action_confirmed === true;
368
+ const confirmationValue = normalizePostAction(confirmation?.post_action_value);
369
+ return parsed.post_action_override
370
+ || (confirmed && confirmationValue ? confirmationValue : "")
371
+ || parsed.post_action_explicit
372
+ || "none";
373
+ }
374
+
375
+ function resolveMaxGreetCount(parsed, confirmation) {
376
+ return parsePositiveInteger(confirmation?.max_greet_count_value)
377
+ || parsePositiveInteger(parsed.max_greet_count_override)
378
+ || null;
379
+ }
380
+
381
+ function collectMissingFields(searchParams, screenParams, parsed = {}) {
382
+ const missing = [];
383
+ if (!searchParams.job) missing.push("job");
384
+ if (!searchParams.city && !parsed.city_explicit) missing.push("city");
385
+ if (!searchParams.degree && !searchParams.degrees?.length && !parsed.degree_explicit) missing.push("degree");
386
+ if (!searchParams.schools?.length && !parsed.schools_explicit) missing.push("schools");
387
+ if (!searchParams.keyword) missing.push("keyword");
388
+ if (!screenParams.criteria) missing.push("criteria");
389
+ if (!screenParams.target_count) missing.push("target_count");
390
+ return missing;
391
+ }
392
+
393
+ function collectUnresolvedMissingFields(missingFields, appliedDefaults) {
394
+ return missingFields.filter((field) => !Object.prototype.hasOwnProperty.call(appliedDefaults, field));
395
+ }
396
+
254
397
  function collectSuspiciousFields(searchParams, screenParams) {
255
398
  const suspicious = [];
256
399
  if (searchParams.city && (/\s/.test(searchParams.city) || CITY_STOP_PATTERN.test(searchParams.city) || searchParams.city.length > 8)) {
@@ -284,9 +427,27 @@ function collectSuspiciousFields(searchParams, screenParams) {
284
427
  return suspicious;
285
428
  }
286
429
 
430
+ function buildMissingFieldQuestions(missingFields = [], defaultPreview = {}) {
431
+ const questions = {
432
+ job: "请填写搜索页岗位名称(关键词输入框旁边的岗位选择)。",
433
+ city: "请填写城市;如不限城市,请明确回复不限。",
434
+ degree: "请填写学历筛选;如不限学历,请明确回复不限。",
435
+ schools: "请填写院校标签;如不限院校标签,请明确回复不限。",
436
+ keyword: "请填写搜索关键词。",
437
+ criteria: "请填写本次筛选 criteria(完整自然语言硬条件)。",
438
+ target_count: "请填写本次目标通过人数。"
439
+ };
440
+ return missingFields.map((field) => ({
441
+ field,
442
+ question: questions[field] || `请填写 ${field}。`,
443
+ value: Object.prototype.hasOwnProperty.call(defaultPreview, field) ? defaultPreview[field] : null
444
+ }));
445
+ }
446
+
287
447
  function buildDefaultPreview(missingFields, { skipKeywordDefault = false } = {}) {
288
448
  return missingFields.reduce((acc, field) => {
289
449
  if (field === "keyword" && skipKeywordDefault) return acc;
450
+ if (!["city", "degree", "schools"].includes(field)) return acc;
290
451
  acc[field] = DEFAULT_PARAM_LABELS[field];
291
452
  return acc;
292
453
  }, {});
@@ -311,14 +472,6 @@ function applyDefaults(searchParams, screenParams, missingFields, useDefaultForM
311
472
  nextSearchParams.schools = DEFAULT_PARAM_VALUES.schools.slice();
312
473
  appliedDefaults.schools = DEFAULT_PARAM_LABELS.schools;
313
474
  }
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
475
  return {
323
476
  searchParams: nextSearchParams,
324
477
  screenParams: nextScreenParams,
@@ -327,58 +480,173 @@ function applyDefaults(searchParams, screenParams, missingFields, useDefaultForM
327
480
  }
328
481
 
329
482
  export function parseRecruitInstruction({ instruction, confirmation, overrides } = {}) {
330
- const text = normalizeText(instruction);
483
+ const rawInstruction = String(instruction || "");
484
+ const text = normalizeText(rawInstruction);
485
+ const finalConfirmed = confirmation?.final_confirmed === true;
486
+ const explicitSchools = extractSchoolFilterExplicit(rawInstruction);
487
+ const explicitRecentViewed = extractRecentViewedExplicit(rawInstruction);
488
+ const explicitKeyword = extractFieldLineValue(rawInstruction, ["搜索关键词", "关键词", "keyword"]);
489
+ const explicitJob = extractFieldLineValue(rawInstruction, ["岗位", "职位", "job"]);
490
+ const explicitCity = extractFieldLineValue(rawInstruction, ["城市", "地点", "工作地", "base"]);
491
+ const explicitDegree = extractFieldLineValue(rawInstruction, ["学历", "degree"]);
492
+ const explicitExperience = extractFieldLineValue(rawInstruction, ["经验", "经验要求", "工作经验", "工作年限", "experience"]);
493
+ const explicitGender = extractFieldLineValue(rawInstruction, ["性别", "gender"]);
494
+ const explicitAge = extractFieldLineValue(rawInstruction, ["年龄", "年龄要求", "年龄范围", "age"]);
495
+ const explicitTargetCount = extractTargetCountExplicit(rawInstruction);
496
+ const explicitPostAction = extractPostActionExplicit(rawInstruction);
331
497
  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),
498
+ job_explicit: explicitJob || extractJobExplicit(text),
499
+ city: sanitizeCityCandidate(explicitCity) || extractCity(text),
500
+ city_explicit: explicitCity !== null,
501
+ degree: normalizeDegreeFieldValue(explicitDegree) || extractDegree(text),
502
+ degree_explicit: explicitDegree !== null,
503
+ experience: normalizeExperienceOverride(explicitExperience),
504
+ experience_explicit: explicitExperience !== null,
505
+ gender: normalizeGenericSearchFilterOverride(explicitGender),
506
+ gender_explicit: explicitGender !== null,
507
+ age: normalizeGenericSearchFilterOverride(explicitAge),
508
+ age_explicit: explicitAge !== null,
509
+ schools: explicitSchools.explicit ? explicitSchools.schools : extractSchools(text),
510
+ schools_explicit: explicitSchools.explicit,
511
+ filter_recent_viewed: explicitRecentViewed !== null ? explicitRecentViewed : extractRecentViewedFilter(text),
512
+ keyword_explicit: explicitKeyword || extractKeywordExplicit(text),
337
513
  keyword_auto: extractKeywordAuto(text),
338
- target_count: extractTargetCount(text)
514
+ target_count: explicitTargetCount || extractTargetCount(text),
515
+ post_action_explicit: explicitPostAction || extractPostAction(text),
516
+ criteria_explicit: extractExplicitCriteria(rawInstruction)
339
517
  };
340
518
 
341
519
  if (overrides) {
342
520
  const overrideCity = sanitizeCityCandidate(normalizeStringOverride(overrides.city));
343
521
  const overrideDegree = normalizeStringOverride(overrides.degree);
344
- const overrideDegrees = normalizeDegreesOverride(overrides.degrees);
345
- const overrideSchools = normalizeSchoolsOverride(overrides.schools);
522
+ const overrideDegrees = normalizeDegreesOverride(overrides.degrees || (Array.isArray(overrides.degree) ? overrides.degree : null));
523
+ const hasOverrideSchools = Object.prototype.hasOwnProperty.call(overrides, "schools")
524
+ || Object.prototype.hasOwnProperty.call(overrides, "school_tag")
525
+ || Object.prototype.hasOwnProperty.call(overrides, "school_tags");
526
+ const overrideSchools = normalizeSchoolsOverride(
527
+ Object.prototype.hasOwnProperty.call(overrides, "schools")
528
+ ? overrides.schools
529
+ : Object.prototype.hasOwnProperty.call(overrides, "school_tag")
530
+ ? overrides.school_tag
531
+ : overrides.school_tags
532
+ );
346
533
  const overrideKeyword = normalizeStringOverride(overrides.keyword);
347
- const overrideRecentViewed = typeof overrides.filter_recent_viewed === "boolean"
348
- ? overrides.filter_recent_viewed
349
- : null;
534
+ const overrideJob = normalizeStringOverride(overrides.job || overrides.job_title || overrides.selected_job);
535
+ const overrideCriteria = normalizeStringOverride(overrides.criteria);
536
+ const hasOverrideExperience = Object.prototype.hasOwnProperty.call(overrides, "experience")
537
+ || Object.prototype.hasOwnProperty.call(overrides, "experiences")
538
+ || Object.prototype.hasOwnProperty.call(overrides, "experience_range")
539
+ || Object.prototype.hasOwnProperty.call(overrides, "experience_start")
540
+ || Object.prototype.hasOwnProperty.call(overrides, "experience_end");
541
+ const overrideExperience = Object.prototype.hasOwnProperty.call(overrides, "experience")
542
+ ? normalizeExperienceOverride(overrides.experience)
543
+ : Object.prototype.hasOwnProperty.call(overrides, "experiences")
544
+ ? normalizeExperienceOverride(overrides.experiences)
545
+ : Object.prototype.hasOwnProperty.call(overrides, "experience_range")
546
+ ? normalizeExperienceOverride(overrides.experience_range)
547
+ : hasOverrideExperience
548
+ ? {
549
+ start: overrides.experience_start,
550
+ end: overrides.experience_end
551
+ }
552
+ : null;
553
+ const hasOverrideGender = Object.prototype.hasOwnProperty.call(overrides, "gender");
554
+ const overrideGender = hasOverrideGender ? normalizeGenericSearchFilterOverride(overrides.gender) : null;
555
+ const hasOverrideAge = Object.prototype.hasOwnProperty.call(overrides, "age")
556
+ || Object.prototype.hasOwnProperty.call(overrides, "ages")
557
+ || Object.prototype.hasOwnProperty.call(overrides, "age_range")
558
+ || Object.prototype.hasOwnProperty.call(overrides, "age_min")
559
+ || Object.prototype.hasOwnProperty.call(overrides, "age_max")
560
+ || Object.prototype.hasOwnProperty.call(overrides, "min_age")
561
+ || Object.prototype.hasOwnProperty.call(overrides, "max_age");
562
+ const overrideAge = Object.prototype.hasOwnProperty.call(overrides, "age")
563
+ ? normalizeGenericSearchFilterOverride(overrides.age)
564
+ : Object.prototype.hasOwnProperty.call(overrides, "ages")
565
+ ? normalizeGenericSearchFilterOverride(overrides.ages)
566
+ : Object.prototype.hasOwnProperty.call(overrides, "age_range")
567
+ ? normalizeGenericSearchFilterOverride(overrides.age_range)
568
+ : hasOverrideAge
569
+ ? {
570
+ min: overrides.age_min ?? overrides.min_age,
571
+ max: overrides.age_max ?? overrides.max_age
572
+ }
573
+ : null;
574
+ const overrideRecentViewed = normalizeRecentViewedOverride(
575
+ Object.prototype.hasOwnProperty.call(overrides, "filter_recent_viewed")
576
+ ? overrides.filter_recent_viewed
577
+ : overrides.recent_not_view
578
+ );
579
+ const overridePostAction = normalizePostAction(overrides.post_action);
350
580
  if (overrideCity) parsed.city = overrideCity;
351
581
  if (overrideDegree) parsed.degree = overrideDegree;
352
582
  if (overrideDegrees?.length) parsed.degrees = overrideDegrees;
353
- if (overrideSchools?.length) parsed.schools = overrideSchools;
583
+ if (Object.prototype.hasOwnProperty.call(overrides, "city")) parsed.city_explicit = true;
584
+ if (Object.prototype.hasOwnProperty.call(overrides, "degree") || Object.prototype.hasOwnProperty.call(overrides, "degrees")) {
585
+ parsed.degree_explicit = true;
586
+ }
587
+ if (hasOverrideSchools && Array.isArray(overrideSchools)) {
588
+ parsed.schools = overrideSchools;
589
+ parsed.schools_explicit = true;
590
+ }
591
+ if (hasOverrideExperience) {
592
+ parsed.experience = overrideExperience;
593
+ parsed.experience_explicit = true;
594
+ }
595
+ if (hasOverrideGender) {
596
+ parsed.gender = overrideGender;
597
+ parsed.gender_explicit = true;
598
+ }
599
+ if (hasOverrideAge) {
600
+ parsed.age = overrideAge;
601
+ parsed.age_explicit = true;
602
+ }
354
603
  if (overrideKeyword) parsed.keyword_override = overrideKeyword;
604
+ if (overrideJob) parsed.job_override = overrideJob;
605
+ if (overrideCriteria) parsed.criteria_override = overrideCriteria;
355
606
  if (overrideRecentViewed !== null) parsed.filter_recent_viewed = overrideRecentViewed;
607
+ if (overridePostAction) parsed.post_action_override = overridePostAction;
608
+ if (Number.isFinite(overrides.max_greet_count) && overrides.max_greet_count > 0) {
609
+ parsed.max_greet_count_override = Number.parseInt(String(overrides.max_greet_count), 10);
610
+ }
356
611
  if (Number.isFinite(overrides.target_count) && overrides.target_count > 0) {
357
612
  parsed.target_count = Number.parseInt(String(overrides.target_count), 10);
358
613
  }
359
614
  }
360
615
 
361
616
  const keywordResolution = resolveKeyword(parsed, confirmation);
617
+ const job = resolveJob(parsed, confirmation);
618
+ const postAction = resolvePostAction(parsed, confirmation);
619
+ const maxGreetCount = resolveMaxGreetCount(parsed, confirmation);
620
+ const confirmationCriteria = normalizeStringOverride(confirmation?.criteria_value);
362
621
  const baseSearchParams = {
622
+ job,
363
623
  city: parsed.city,
364
624
  degree: parsed.degree,
365
625
  degrees: parsed.degrees,
366
626
  schools: parsed.schools,
627
+ experience: parsed.experience,
628
+ gender: parsed.gender,
629
+ age: parsed.age,
367
630
  filter_recent_viewed: parsed.filter_recent_viewed,
368
631
  keyword: keywordResolution.keyword
369
632
  };
633
+ const criteria = parsed.criteria_override || confirmationCriteria || parsed.criteria_explicit || null;
634
+ const criteriaSource = parsed.criteria_override
635
+ ? "override"
636
+ : confirmationCriteria
637
+ ? "confirmation"
638
+ : parsed.criteria_explicit
639
+ ? "instruction_block"
640
+ : "missing";
370
641
  const baseScreenParams = {
371
- criteria: buildScreenCriteria(text, baseSearchParams),
372
- target_count: parsed.target_count
642
+ criteria,
643
+ target_count: parsed.target_count,
644
+ post_action: postAction,
645
+ max_greet_count: maxGreetCount
373
646
  };
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;
647
+ const missingBeforeDefaults = collectMissingFields(baseSearchParams, baseScreenParams, parsed);
648
+
649
+ const useDefaultForMissing = finalConfirmed || confirmation?.use_default_for_missing === true;
382
650
  const skipKeywordDefault = keywordResolution.needsConfirmation;
383
651
  const defaultPreview = buildDefaultPreview(missingBeforeDefaults, { skipKeywordDefault });
384
652
  const { searchParams, screenParams, appliedDefaults } = applyDefaults(
@@ -388,10 +656,12 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
388
656
  useDefaultForMissing,
389
657
  { skipKeywordDefault }
390
658
  );
659
+ const missingAfterDefaults = collectUnresolvedMissingFields(missingBeforeDefaults, appliedDefaults);
391
660
  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;
661
+ const needs_recent_viewed_filter_confirmation = !finalConfirmed && searchParams.filter_recent_viewed === null;
662
+ const needs_criteria_confirmation = Boolean(screenParams.criteria) && !finalConfirmed && confirmation?.criteria_confirmed !== true;
394
663
  const pending_questions = [
664
+ ...buildMissingFieldQuestions(missingAfterDefaults, defaultPreview),
395
665
  ...(needs_recent_viewed_filter_confirmation
396
666
  ? [{
397
667
  field: "filter_recent_viewed",
@@ -415,25 +685,28 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
415
685
  extracted_screen_params: baseScreenParams,
416
686
  current_search_params: searchParams,
417
687
  current_screen_params: screenParams,
418
- missing_fields: missingBeforeDefaults,
419
- has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
688
+ missing_fields: missingAfterDefaults,
689
+ missing_fields_before_defaults: missingBeforeDefaults,
690
+ has_unresolved_missing_fields: missingAfterDefaults.length > 0,
420
691
  suspicious_fields,
421
692
  pending_questions,
422
693
  default_preview: defaultPreview,
423
- applied_defaults: appliedDefaults
694
+ applied_defaults: appliedDefaults,
695
+ criteria_source: criteriaSource,
696
+ final_confirmed: finalConfirmed
424
697
  };
425
698
 
426
699
  return {
427
700
  parsed,
428
701
  searchParams,
429
702
  screenParams,
430
- missing_fields: missingBeforeDefaults,
431
- has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
703
+ missing_fields: missingAfterDefaults,
704
+ has_unresolved_missing_fields: missingAfterDefaults.length > 0,
432
705
  suspicious_fields,
433
706
  needs_keyword_confirmation: keywordResolution.needsConfirmation,
434
707
  needs_recent_viewed_filter_confirmation,
435
708
  needs_criteria_confirmation,
436
- needs_search_params_confirmation: confirmation?.search_params_confirmed !== true,
709
+ needs_search_params_confirmation: !finalConfirmed && confirmation?.search_params_confirmed !== true,
437
710
  proposed_keyword: keywordResolution.proposedKeyword,
438
711
  pending_questions,
439
712
  default_preview: defaultPreview,
@@ -447,5 +720,8 @@ export const recruitInstructionParserSemantics = Object.freeze({
447
720
  imported_at: "2026-04-30",
448
721
  default_param_values: DEFAULT_PARAM_VALUES,
449
722
  school_labels: SEARCH_SCHOOL_MAP,
450
- degree_values: Array.from(DEGREE_VALUES)
723
+ degree_values: Array.from(DEGREE_VALUES),
724
+ experience_values: ["不限", "在校/应届", "25年毕业", "26年毕业", "26年后毕业", "1-3年", "3-5年", "5-10年", "自定义"],
725
+ gender_values: ["不限", "男", "女"],
726
+ age_values: ["不限", "20-25", "25-30", "30-35", "35-40", "40-50", "50以上", "自定义"]
451
727
  });