@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.
- package/README.md +5 -5
- package/package.json +1 -1
- package/skills/boss-chat/README.md +2 -2
- package/skills/boss-chat/SKILL.md +7 -7
- package/skills/boss-recruit-pipeline/SKILL.md +23 -1
- package/src/chat-mcp.js +70 -73
- package/src/domains/chat/detail.js +79 -47
- package/src/domains/chat/run-service.js +400 -158
- package/src/domains/recruit/constants.js +65 -0
- package/src/domains/recruit/instruction-parser.js +362 -86
- package/src/domains/recruit/run-service.js +281 -10
- package/src/domains/recruit/search.js +2076 -298
- package/src/index.js +18 -12
- package/src/recommend-mcp.js +77 -8
- package/src/recruit-mcp.js +228 -3
|
@@ -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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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))
|
|
134
|
-
|
|
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
|
|
190
|
-
return
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
201
|
-
|
|
319
|
+
function extractTargetCountExplicit(rawText) {
|
|
320
|
+
const value = extractFieldLineValue(rawText, ["目标筛选人数", "目标人数", "目标通过人数", "target_count", "max_candidates"]);
|
|
321
|
+
return parsePositiveInteger(value);
|
|
202
322
|
}
|
|
203
323
|
|
|
204
|
-
function
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
376
|
-
|
|
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:
|
|
419
|
-
|
|
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:
|
|
431
|
-
has_unresolved_missing_fields:
|
|
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
|
});
|