@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23
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 -2
- package/bin/boss-recommend-mcp.js +4 -4
- package/config/screening-config.example.json +33 -33
- package/package.json +2 -1
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +42 -42
- package/skills/boss-chat/SKILL.md +106 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +219 -214
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +127 -127
- package/src/chat-runtime-config.js +775 -775
- package/src/cli.js +573 -573
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +2416 -2385
- package/src/core/capture/index.js +1201 -1201
- package/src/core/cv-acquisition/index.js +238 -238
- package/src/core/cv-capture-target/index.js +299 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +1315 -1306
- package/src/core/reporting/legacy-csv.js +334 -332
- package/src/core/run/index.js +44 -27
- package/src/core/run/timing.js +33 -33
- package/src/core/screening/index.js +2135 -2135
- package/src/core/self-heal/index.js +973 -973
- package/src/core/self-heal/viewport.js +564 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +113 -113
- package/src/domains/chat/index.js +7 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +122 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +571 -571
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +472 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +333 -333
- package/src/domains/recommend/constants.js +228 -159
- package/src/domains/recommend/detail.js +655 -646
- package/src/domains/recommend/filters.js +748 -377
- package/src/domains/recommend/index.js +4 -3
- package/src/domains/recommend/jobs.js +542 -542
- package/src/domains/recommend/location.js +736 -0
- package/src/domains/recommend/refresh.js +575 -351
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +1616 -878
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1620 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +121 -4
- package/src/parser.js +376 -8
- package/src/recommend-mcp.js +1174 -914
- package/src/recommend-scheduler.js +535 -469
- package/src/recruit-mcp.js +2121 -2121
|
@@ -1,866 +1,866 @@
|
|
|
1
|
-
const SEARCH_SCHOOL_MAP = {
|
|
2
|
-
"统招": "统招本科",
|
|
3
|
-
"统招本科": "统招本科",
|
|
4
|
-
"统招本": "统招本科",
|
|
5
|
-
"全日制本科": "统招本科",
|
|
6
|
-
"双一流": "双一流院校",
|
|
7
|
-
"双一流院校": "双一流院校",
|
|
8
|
-
"双一流学校": "双一流院校",
|
|
9
|
-
"985": "985院校",
|
|
10
|
-
"985院校": "985院校",
|
|
11
|
-
"211": "211院校",
|
|
12
|
-
"211院校": "211院校",
|
|
13
|
-
"qs": "QS 100",
|
|
14
|
-
"qs100": "QS 100",
|
|
15
|
-
"qs500": "QS 500"
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const KNOWN_SCHOOL_LABELS = new Set(Object.values(SEARCH_SCHOOL_MAP));
|
|
19
|
-
const DEFAULT_PARAM_VALUES = {
|
|
20
|
-
job: null,
|
|
21
|
-
city: null,
|
|
22
|
-
degree: "不限",
|
|
23
|
-
schools: [],
|
|
24
|
-
experience: null,
|
|
25
|
-
gender: null,
|
|
26
|
-
age: null,
|
|
27
|
-
keyword: null,
|
|
28
|
-
target_count: null,
|
|
29
|
-
criteria: null
|
|
30
|
-
};
|
|
31
|
-
const DEFAULT_PARAM_LABELS = {
|
|
32
|
-
job: "搜索页岗位未指定",
|
|
33
|
-
city: "不限城市",
|
|
34
|
-
degree: "不限",
|
|
35
|
-
schools: "不限院校标签",
|
|
36
|
-
experience: "经验要求未指定",
|
|
37
|
-
gender: "性别未指定",
|
|
38
|
-
age: "年龄要求未指定",
|
|
39
|
-
keyword: "搜索关键词未指定",
|
|
40
|
-
target_count: "目标通过人数未指定",
|
|
41
|
-
criteria: "筛选 criteria 未指定"
|
|
42
|
-
};
|
|
43
|
-
const DEGREE_VALUES = new Set(["不限", "本科", "本科及以上", "硕士及以上", "博士"]);
|
|
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|filter_recent_colleague_contacted|recent_colleague_contacted|skip_recent_colleague_contacted)\s*[::]/i;
|
|
48
|
-
const INLINE_FIELD_BOUNDARY_PATTERN = /[;;]\s*(?:岗位|职位|关键词|城市|地点|工作地|学历|学校|学校类型|院校标签|经验|经验要求|工作经验|工作年限|性别|年龄|年龄要求|年龄范围|只看未查看|过滤已看|同事近期触达|近期同事触达|同事触达|同事联系|同事沟通|同事交换简历|目标筛选人数|目标人数|休息强度|后置动作|post_action|rest_level|filter_recent_colleague_contacted|recent_colleague_contacted|skip_recent_colleague_contacted)\s*(?:\([^)]*\))?\s*[::]/i;
|
|
49
|
-
|
|
50
|
-
function normalizeText(input) {
|
|
51
|
-
return String(input || "").replace(/\s+/g, " ").trim();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function normalizeCriteriaBlock(input) {
|
|
55
|
-
const lines = String(input || "")
|
|
56
|
-
.replace(/\r\n/g, "\n")
|
|
57
|
-
.split("\n")
|
|
58
|
-
.map((line) => line.trim())
|
|
59
|
-
.filter(Boolean);
|
|
60
|
-
return lines.join("\n").trim() || null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function escapeRegExp(input) {
|
|
64
|
-
return String(input).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function extractFieldLineValue(rawText, labels = []) {
|
|
68
|
-
const lines = String(rawText || "").replace(/\r\n/g, "\n").split(/\n|[;;]/);
|
|
69
|
-
const labelPattern = labels.map(escapeRegExp).join("|");
|
|
70
|
-
if (!labelPattern) return null;
|
|
71
|
-
const pattern = new RegExp(`^\\s*(?:${labelPattern})(?:\\s*\\([^)]*\\))?\\s*[::]\\s*(.+?)\\s*$`, "i");
|
|
72
|
-
for (const line of lines) {
|
|
73
|
-
const match = line.match(pattern);
|
|
74
|
-
let value = match?.[1]?.trim();
|
|
75
|
-
const inlineBoundaryIndex = value ? value.search(INLINE_FIELD_BOUNDARY_PATTERN) : -1;
|
|
76
|
-
if (inlineBoundaryIndex >= 0) value = value.slice(0, inlineBoundaryIndex).trim();
|
|
77
|
-
if (value) return value;
|
|
78
|
-
}
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function uniqueList(items) {
|
|
83
|
-
return Array.from(new Set(items.filter(Boolean)));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function normalizeSchoolLabel(value) {
|
|
87
|
-
if (typeof value !== "string") return null;
|
|
88
|
-
const raw = value.trim();
|
|
89
|
-
if (!raw) return null;
|
|
90
|
-
if (/^(?:不限|不限制|无限制|全部|所有|无|none|all)$/i.test(raw)) return null;
|
|
91
|
-
if (KNOWN_SCHOOL_LABELS.has(raw)) return raw;
|
|
92
|
-
|
|
93
|
-
const compact = raw.toLowerCase().replace(/\s+/g, "");
|
|
94
|
-
const qsMatch = compact.match(/^qs(\d+)$/);
|
|
95
|
-
if (qsMatch) {
|
|
96
|
-
const rank = Number.parseInt(qsMatch[1], 10);
|
|
97
|
-
if (Number.isFinite(rank)) return rank > 100 ? SEARCH_SCHOOL_MAP.qs500 : SEARCH_SCHOOL_MAP.qs100;
|
|
98
|
-
}
|
|
99
|
-
return SEARCH_SCHOOL_MAP[compact] || SEARCH_SCHOOL_MAP[raw] || raw;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function sanitizeCityCandidate(value) {
|
|
103
|
-
if (typeof value !== "string") return null;
|
|
104
|
-
let candidate = value.trim();
|
|
105
|
-
if (!candidate) return null;
|
|
106
|
-
candidate = candidate.replace(/^(在|是|为)\s*/, "").trim();
|
|
107
|
-
const stopIndex = candidate.search(CITY_STOP_PATTERN);
|
|
108
|
-
if (stopIndex >= 0) candidate = candidate.slice(0, stopIndex).trim();
|
|
109
|
-
candidate = candidate.replace(/[的\s]+$/g, "").trim();
|
|
110
|
-
return candidate || null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function extractCity(text) {
|
|
114
|
-
const patterns = [
|
|
115
|
-
/地点(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
|
|
116
|
-
/城市(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
|
|
117
|
-
/工作地(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
|
|
118
|
-
/base(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i
|
|
119
|
-
];
|
|
120
|
-
for (const pattern of patterns) {
|
|
121
|
-
const match = text.match(pattern);
|
|
122
|
-
if (match?.[1]) {
|
|
123
|
-
const city = sanitizeCityCandidate(match[1]);
|
|
124
|
-
if (city) return city;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function extractDegree(text) {
|
|
131
|
-
if (/(博士及以上|博士)/.test(text)) return "博士";
|
|
132
|
-
if (/(硕士及以上|硕士以上)/.test(text)) return "硕士及以上";
|
|
133
|
-
if (/硕士/.test(text)) return "硕士";
|
|
134
|
-
if (/(本科及以上|本科以上)/.test(text)) return "本科及以上";
|
|
135
|
-
if (/本科/.test(text)) return "本科";
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function extractSchools(text) {
|
|
140
|
-
const schools = [];
|
|
141
|
-
if (/统招(?:本科)?/.test(text)) schools.push(SEARCH_SCHOOL_MAP["统招"]);
|
|
142
|
-
if (/双一流(?:院校|学校)?/.test(text)) schools.push(SEARCH_SCHOOL_MAP["双一流"]);
|
|
143
|
-
if (/(^|[^0-9])985([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["985"]);
|
|
144
|
-
if (/(^|[^0-9])211([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["211"]);
|
|
145
|
-
const qsMatches = text.matchAll(/\bqs\s*(\d+)\b/ig);
|
|
146
|
-
for (const match of qsMatches) {
|
|
147
|
-
const rank = Number.parseInt(match[1], 10);
|
|
148
|
-
if (Number.isFinite(rank)) schools.push(rank > 100 ? SEARCH_SCHOOL_MAP.qs500 : SEARCH_SCHOOL_MAP.qs100);
|
|
149
|
-
}
|
|
150
|
-
return uniqueList(schools);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function extractRecentViewedFilter(text) {
|
|
154
|
-
const negativePatterns = [
|
|
155
|
-
/(?:不|别|无需|不用|不要).{0,6}(?:过滤|排除|去掉|剔除).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
|
|
156
|
-
/(?:保留|包含).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
|
|
157
|
-
/(?:近?14天(?:内)?查看(?:过)?).{0,8}(?:不要|不用|无需|不需要|不必).{0,4}(?:过滤|排除|去掉|剔除)/i
|
|
158
|
-
];
|
|
159
|
-
if (negativePatterns.some((pattern) => pattern.test(text))) return false;
|
|
160
|
-
|
|
161
|
-
const positivePatterns = [
|
|
162
|
-
/(?:过滤|排除|去掉|剔除).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
|
|
163
|
-
/(?:近?14天(?:内)?查看(?:过)?).{0,8}(?:过滤|排除|去掉|剔除)/i
|
|
164
|
-
];
|
|
165
|
-
if (positivePatterns.some((pattern) => pattern.test(text))) return true;
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function normalizeRecentViewedOverride(value) {
|
|
170
|
-
if (typeof value === "boolean") return value;
|
|
171
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
172
|
-
if (!normalized) return null;
|
|
173
|
-
if (["true", "yes", "1", "需要过滤", "过滤", "近14天没有", "not_viewed"].includes(normalized)) return true;
|
|
174
|
-
if (["false", "no", "0", "不过滤", "不限", "none"].includes(normalized)) return false;
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function normalizeColleagueContactedFilterOverride(value) {
|
|
179
|
-
if (typeof value === "boolean") return value;
|
|
180
|
-
if (typeof value === "number") return value !== 0;
|
|
181
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
182
|
-
const compact = normalized.replace(/\s+/g, "");
|
|
183
|
-
if (!compact) return null;
|
|
184
|
-
if ([
|
|
185
|
-
"true",
|
|
186
|
-
"yes",
|
|
187
|
-
"y",
|
|
188
|
-
"1",
|
|
189
|
-
"on",
|
|
190
|
-
"enable",
|
|
191
|
-
"enabled",
|
|
192
|
-
"需要",
|
|
193
|
-
"是",
|
|
194
|
-
"开启",
|
|
195
|
-
"过滤",
|
|
196
|
-
"需要过滤",
|
|
197
|
-
"跳过",
|
|
198
|
-
"排除",
|
|
199
|
-
"剔除",
|
|
200
|
-
"近30天未和同事交换简历",
|
|
201
|
-
"未和同事交换简历",
|
|
202
|
-
"只看未和同事交换简历"
|
|
203
|
-
].includes(compact)) return true;
|
|
204
|
-
if ([
|
|
205
|
-
"false",
|
|
206
|
-
"no",
|
|
207
|
-
"n",
|
|
208
|
-
"0",
|
|
209
|
-
"off",
|
|
210
|
-
"disable",
|
|
211
|
-
"disabled",
|
|
212
|
-
"不需要",
|
|
213
|
-
"否",
|
|
214
|
-
"关闭",
|
|
215
|
-
"不限",
|
|
216
|
-
"不过滤",
|
|
217
|
-
"不跳过",
|
|
218
|
-
"不排除",
|
|
219
|
-
"保留",
|
|
220
|
-
"none",
|
|
221
|
-
"all"
|
|
222
|
-
].includes(compact)) return false;
|
|
223
|
-
if (/(?:不|别|无需|不用|不要).{0,6}(?:过滤|排除|跳过|剔除).{0,8}(?:同事|交换简历|触达|联系|沟通)/i.test(normalized)) return false;
|
|
224
|
-
if (/(?:过滤|排除|跳过|剔除).{0,8}(?:同事|交换简历|触达|联系|沟通)/i.test(normalized)) return true;
|
|
225
|
-
return normalizeBooleanOverride(value);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function normalizeBooleanOverride(value) {
|
|
229
|
-
if (typeof value === "boolean") return value;
|
|
230
|
-
if (typeof value === "number") return value !== 0;
|
|
231
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
232
|
-
if (!normalized) return null;
|
|
233
|
-
if (["true", "yes", "y", "1", "on", "enable", "enabled", "需要", "是", "开启"].includes(normalized)) return true;
|
|
234
|
-
if (["false", "no", "n", "0", "off", "disable", "disabled", "不需要", "否", "关闭"].includes(normalized)) return false;
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function normalizeStringOverride(value) {
|
|
239
|
-
if (typeof value !== "string") return null;
|
|
240
|
-
const normalized = value.trim();
|
|
241
|
-
return normalized || null;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function normalizeSchoolsOverride(value) {
|
|
245
|
-
if (Array.isArray(value)) {
|
|
246
|
-
return uniqueList(value.flatMap((item) => normalizeSchoolsOverride(item) || []));
|
|
247
|
-
}
|
|
248
|
-
if (typeof value === "string") return uniqueList(value.split(/[,,、|/]/).map(normalizeSchoolLabel));
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function extractSchoolFilterExplicit(rawText) {
|
|
253
|
-
const value = extractFieldLineValue(rawText, [
|
|
254
|
-
"学校",
|
|
255
|
-
"院校",
|
|
256
|
-
"学校类型",
|
|
257
|
-
"院校标签",
|
|
258
|
-
"学校标签",
|
|
259
|
-
"school",
|
|
260
|
-
"school_tag",
|
|
261
|
-
"school_tags",
|
|
262
|
-
"schools"
|
|
263
|
-
]);
|
|
264
|
-
if (value === null) return { explicit: false, schools: null };
|
|
265
|
-
return { explicit: true, schools: normalizeSchoolsOverride(value) || [] };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function extractRecentViewedExplicit(rawText) {
|
|
269
|
-
const value = extractFieldLineValue(rawText, ["只看未查看", "过滤已看", "recent_not_view", "filter_recent_viewed"]);
|
|
270
|
-
return value === null ? null : normalizeRecentViewedOverride(value);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function extractColleagueContactedFilterExplicit(rawText) {
|
|
274
|
-
const value = extractFieldLineValue(rawText, [
|
|
275
|
-
"同事近期触达",
|
|
276
|
-
"近期同事触达",
|
|
277
|
-
"同事触达",
|
|
278
|
-
"同事近期联系",
|
|
279
|
-
"近期同事联系",
|
|
280
|
-
"同事联系",
|
|
281
|
-
"同事近期沟通",
|
|
282
|
-
"近期同事沟通",
|
|
283
|
-
"同事沟通",
|
|
284
|
-
"同事交换简历",
|
|
285
|
-
"近期同事交换简历",
|
|
286
|
-
"近30天未和同事交换简历",
|
|
287
|
-
"filter_recent_colleague_contacted",
|
|
288
|
-
"recent_colleague_contacted",
|
|
289
|
-
"colleague_contacted_filter",
|
|
290
|
-
"colleague_contacted",
|
|
291
|
-
"skip_recent_colleague_contacted"
|
|
292
|
-
]);
|
|
293
|
-
return value === null ? null : normalizeColleagueContactedFilterOverride(value);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function normalizeDegreesOverride(value) {
|
|
297
|
-
if (Array.isArray(value)) return uniqueList(value.map(normalizeText));
|
|
298
|
-
if (typeof value === "string") return uniqueList(value.split(/[,,、|/]/).map(normalizeText));
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function normalizeExperienceOverride(value) {
|
|
303
|
-
if (typeof value === "string") return normalizeText(value) || null;
|
|
304
|
-
if (Array.isArray(value)) {
|
|
305
|
-
const normalized = value.map(normalizeText).filter(Boolean);
|
|
306
|
-
return normalized.length ? normalized[0] : null;
|
|
307
|
-
}
|
|
308
|
-
if (value && typeof value === "object") return value;
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function normalizeGenericSearchFilterOverride(value) {
|
|
313
|
-
if (typeof value === "string" || typeof value === "number") return normalizeText(value) || null;
|
|
314
|
-
if (Array.isArray(value)) {
|
|
315
|
-
const normalized = value.map(normalizeText).filter(Boolean);
|
|
316
|
-
return normalized.length ? normalized[0] : null;
|
|
317
|
-
}
|
|
318
|
-
if (value && typeof value === "object") return value;
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function normalizeDegreeFieldValue(value) {
|
|
323
|
-
const normalized = normalizeText(value);
|
|
324
|
-
if (!normalized) return null;
|
|
325
|
-
if (/^(?:不限|不限制|无限制|全部|所有|无|none|all)$/i.test(normalized)) return "不限";
|
|
326
|
-
return extractDegree(normalized) || normalized;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function normalizePostAction(value) {
|
|
330
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
331
|
-
if (["", "none", "skip", "no", "不执行", "无", "什么也不做"].includes(normalized)) return "none";
|
|
332
|
-
if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
|
|
333
|
-
return POST_ACTIONS.has(normalized) ? normalized : "";
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function parsePositiveInteger(value) {
|
|
337
|
-
const parsed = Number.parseInt(String(value || ""), 10);
|
|
338
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function extractKeywordExplicit(text) {
|
|
342
|
-
const patterns = [
|
|
343
|
-
/搜索关键词(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
|
|
344
|
-
/关键词(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
|
|
345
|
-
/keyword(?:\s*[::=]\s*|\s+is\s+)([^\n,。;;]+)/i
|
|
346
|
-
];
|
|
347
|
-
for (const pattern of patterns) {
|
|
348
|
-
const match = text.match(pattern);
|
|
349
|
-
const keyword = match?.[1]?.trim();
|
|
350
|
-
if (keyword) return keyword;
|
|
351
|
-
}
|
|
352
|
-
return null;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function extractKeywordAuto(text) {
|
|
356
|
-
const patterns = [
|
|
357
|
-
/做过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:的人选|的人|相关|并且|且|,|。|,|$)/i,
|
|
358
|
-
/有过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:经验|背景|的人选|并且|且|,|。|,|$)/i,
|
|
359
|
-
/从事过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:相关|的人选|并且|且|,|。|,|$)/i
|
|
360
|
-
];
|
|
361
|
-
for (const pattern of patterns) {
|
|
362
|
-
const match = text.match(pattern);
|
|
363
|
-
const keyword = match?.[1]?.replace(/\s+/g, " ").trim();
|
|
364
|
-
if (keyword && keyword.length >= 2) return keyword;
|
|
365
|
-
}
|
|
366
|
-
return null;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function extractJobExplicit(text) {
|
|
370
|
-
const patterns = [
|
|
371
|
-
/(?:搜索页)?(?:岗位|职位)(?:名称)?(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
|
|
372
|
-
/job(?:\s*title)?(?:\s*[::=]\s*|\s+is\s+)([^\n,。;;]+)/i
|
|
373
|
-
];
|
|
374
|
-
for (const pattern of patterns) {
|
|
375
|
-
const match = text.match(pattern);
|
|
376
|
-
const job = match?.[1]?.trim();
|
|
377
|
-
if (job) return job;
|
|
378
|
-
}
|
|
379
|
-
return null;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function extractTargetCount(text) {
|
|
383
|
-
const patterns = [
|
|
384
|
-
/至少筛选\s*(\d+)\s*位?/i,
|
|
385
|
-
/目标(?:筛选)?(?:人数|数量)?(?:为|是|:|:)?\s*(\d+)/i,
|
|
386
|
-
/目标(?:筛选)?(?:人数|数量)?\s*(\d+)\s*人/i,
|
|
387
|
-
/筛选\s*(\d+)\s*位/i
|
|
388
|
-
];
|
|
389
|
-
for (const pattern of patterns) {
|
|
390
|
-
const match = text.match(pattern);
|
|
391
|
-
if (match?.[1]) {
|
|
392
|
-
const value = Number.parseInt(match[1], 10);
|
|
393
|
-
if (Number.isFinite(value) && value > 0) return value;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function extractPostAction(text) {
|
|
400
|
-
if (/(?:什么也不做|不(?:打招呼|沟通)|只筛选|不执行)/.test(text)) return "none";
|
|
401
|
-
if (/(?:直接沟通|打招呼|立即沟通|greet|post_action\s*[::=]\s*greet)/i.test(text)) return "greet";
|
|
402
|
-
return "";
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function extractTargetCountExplicit(rawText) {
|
|
406
|
-
const value = extractFieldLineValue(rawText, ["目标筛选人数", "目标人数", "目标通过人数", "target_count", "max_candidates"]);
|
|
407
|
-
return parsePositiveInteger(value);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function extractPostActionExplicit(rawText) {
|
|
411
|
-
const value = extractFieldLineValue(rawText, ["后置动作", "通过后执行动作", "post_action"]);
|
|
412
|
-
return normalizePostAction(value);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function extractExplicitCriteria(rawText) {
|
|
416
|
-
const normalized = String(rawText || "").replace(/\r\n/g, "\n");
|
|
417
|
-
const match = normalized.match(CRITERIA_MARKER_PATTERN);
|
|
418
|
-
if (!match) return null;
|
|
419
|
-
let criteria = normalized.slice(match.index + match[0].length).trim();
|
|
420
|
-
const trailingFieldIndex = criteria.search(CRITERIA_TRAILING_FIELD_PATTERN);
|
|
421
|
-
if (trailingFieldIndex > 0) criteria = criteria.slice(0, trailingFieldIndex).trim();
|
|
422
|
-
return normalizeCriteriaBlock(criteria);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function resolveKeyword(parsed, confirmation) {
|
|
426
|
-
if (parsed.keyword_override) {
|
|
427
|
-
return { keyword: parsed.keyword_override, needsConfirmation: false, proposedKeyword: null };
|
|
428
|
-
}
|
|
429
|
-
const explicit = parsed.keyword_explicit;
|
|
430
|
-
const auto = parsed.keyword_auto;
|
|
431
|
-
const confirmed = confirmation?.keyword_confirmed === true;
|
|
432
|
-
const rejected = confirmation?.keyword_confirmed === false;
|
|
433
|
-
const value = typeof confirmation?.keyword_value === "string" ? confirmation.keyword_value.trim() : "";
|
|
434
|
-
if (confirmed && value) return { keyword: value, needsConfirmation: false, proposedKeyword: null };
|
|
435
|
-
if (explicit) return { keyword: explicit, needsConfirmation: false, proposedKeyword: null };
|
|
436
|
-
if (rejected) return { keyword: value || null, needsConfirmation: false, proposedKeyword: null };
|
|
437
|
-
if (auto) {
|
|
438
|
-
if (confirmed) return { keyword: auto, needsConfirmation: false, proposedKeyword: null };
|
|
439
|
-
return { keyword: null, needsConfirmation: true, proposedKeyword: auto };
|
|
440
|
-
}
|
|
441
|
-
return { keyword: null, needsConfirmation: false, proposedKeyword: null };
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function resolveJob(parsed, confirmation) {
|
|
445
|
-
if (parsed.job_override) return parsed.job_override;
|
|
446
|
-
const confirmed = confirmation?.job_confirmed === true;
|
|
447
|
-
const value = typeof confirmation?.job_value === "string" ? confirmation.job_value.trim() : "";
|
|
448
|
-
if (confirmed && value) return value;
|
|
449
|
-
return parsed.job_explicit || null;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function resolvePostAction(parsed, confirmation) {
|
|
453
|
-
const confirmed = confirmation?.post_action_confirmed === true;
|
|
454
|
-
const confirmationValue = normalizePostAction(confirmation?.post_action_value);
|
|
455
|
-
return parsed.post_action_override
|
|
456
|
-
|| (confirmed && confirmationValue ? confirmationValue : "")
|
|
457
|
-
|| parsed.post_action_explicit
|
|
458
|
-
|| "none";
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function resolveMaxGreetCount(parsed, confirmation) {
|
|
462
|
-
return parsePositiveInteger(confirmation?.max_greet_count_value)
|
|
463
|
-
|| parsePositiveInteger(parsed.max_greet_count_override)
|
|
464
|
-
|| null;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function collectMissingFields(searchParams, screenParams, parsed = {}) {
|
|
468
|
-
const missing = [];
|
|
469
|
-
if (!searchParams.job) missing.push("job");
|
|
470
|
-
if (!searchParams.city && !parsed.city_explicit) missing.push("city");
|
|
471
|
-
if (!searchParams.degree && !searchParams.degrees?.length && !parsed.degree_explicit) missing.push("degree");
|
|
472
|
-
if (!searchParams.schools?.length && !parsed.schools_explicit) missing.push("schools");
|
|
473
|
-
if (!searchParams.keyword) missing.push("keyword");
|
|
474
|
-
if (!screenParams.criteria) missing.push("criteria");
|
|
475
|
-
if (!screenParams.target_count) missing.push("target_count");
|
|
476
|
-
return missing;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function collectUnresolvedMissingFields(missingFields, appliedDefaults) {
|
|
480
|
-
return missingFields.filter((field) => !Object.prototype.hasOwnProperty.call(appliedDefaults, field));
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function collectSuspiciousFields(searchParams, screenParams) {
|
|
484
|
-
const suspicious = [];
|
|
485
|
-
if (searchParams.city && (/\s/.test(searchParams.city) || CITY_STOP_PATTERN.test(searchParams.city) || searchParams.city.length > 8)) {
|
|
486
|
-
suspicious.push({
|
|
487
|
-
field: "city",
|
|
488
|
-
value: searchParams.city,
|
|
489
|
-
reason: "城市提取结果看起来包含多余短语,请确认是否为标准城市名。"
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
if (searchParams.degree && !DEGREE_VALUES.has(searchParams.degree)) {
|
|
493
|
-
suspicious.push({
|
|
494
|
-
field: "degree",
|
|
495
|
-
value: searchParams.degree,
|
|
496
|
-
reason: "学历提取结果不在预期枚举内,请确认。"
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
if (searchParams.keyword && /城市|学历|学校|目标人数|目标数量|筛选\d+位/i.test(searchParams.keyword)) {
|
|
500
|
-
suspicious.push({
|
|
501
|
-
field: "keyword",
|
|
502
|
-
value: searchParams.keyword,
|
|
503
|
-
reason: "关键词看起来混入了筛选条件,请确认是否只保留核心方向词。"
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
if (screenParams.target_count && (!Number.isInteger(screenParams.target_count) || screenParams.target_count <= 0)) {
|
|
507
|
-
suspicious.push({
|
|
508
|
-
field: "target_count",
|
|
509
|
-
value: screenParams.target_count,
|
|
510
|
-
reason: "目标人数不是有效正整数,请确认。"
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
return suspicious;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function buildMissingFieldQuestions(missingFields = [], defaultPreview = {}) {
|
|
517
|
-
const questions = {
|
|
518
|
-
job: "请填写搜索页岗位名称(关键词输入框旁边的岗位选择)。",
|
|
519
|
-
city: "请填写城市;如不限城市,请明确回复不限。",
|
|
520
|
-
degree: "请填写学历筛选;如不限学历,请明确回复不限。",
|
|
521
|
-
schools: "请填写院校标签;如不限院校标签,请明确回复不限。",
|
|
522
|
-
keyword: "请填写搜索关键词。",
|
|
523
|
-
criteria: "请填写本次筛选 criteria(完整自然语言硬条件)。",
|
|
524
|
-
target_count: "请填写本次目标通过人数。"
|
|
525
|
-
};
|
|
526
|
-
return missingFields.map((field) => ({
|
|
527
|
-
field,
|
|
528
|
-
question: questions[field] || `请填写 ${field}。`,
|
|
529
|
-
value: Object.prototype.hasOwnProperty.call(defaultPreview, field) ? defaultPreview[field] : null
|
|
530
|
-
}));
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function buildDefaultPreview(missingFields, { skipKeywordDefault = false } = {}) {
|
|
534
|
-
return missingFields.reduce((acc, field) => {
|
|
535
|
-
if (field === "keyword" && skipKeywordDefault) return acc;
|
|
536
|
-
if (!["city", "degree", "schools"].includes(field)) return acc;
|
|
537
|
-
acc[field] = DEFAULT_PARAM_LABELS[field];
|
|
538
|
-
return acc;
|
|
539
|
-
}, {});
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function applyDefaults(searchParams, screenParams, missingFields, useDefaultForMissing, { skipKeywordDefault = false } = {}) {
|
|
543
|
-
if (!useDefaultForMissing) {
|
|
544
|
-
return { searchParams, screenParams, appliedDefaults: {} };
|
|
545
|
-
}
|
|
546
|
-
const appliedDefaults = {};
|
|
547
|
-
const nextSearchParams = { ...searchParams };
|
|
548
|
-
const nextScreenParams = { ...screenParams };
|
|
549
|
-
if (missingFields.includes("city")) {
|
|
550
|
-
nextSearchParams.city = DEFAULT_PARAM_VALUES.city;
|
|
551
|
-
appliedDefaults.city = DEFAULT_PARAM_LABELS.city;
|
|
552
|
-
}
|
|
553
|
-
if (missingFields.includes("degree")) {
|
|
554
|
-
nextSearchParams.degree = DEFAULT_PARAM_VALUES.degree;
|
|
555
|
-
appliedDefaults.degree = DEFAULT_PARAM_LABELS.degree;
|
|
556
|
-
}
|
|
557
|
-
if (missingFields.includes("schools")) {
|
|
558
|
-
nextSearchParams.schools = DEFAULT_PARAM_VALUES.schools.slice();
|
|
559
|
-
appliedDefaults.schools = DEFAULT_PARAM_LABELS.schools;
|
|
560
|
-
}
|
|
561
|
-
return {
|
|
562
|
-
searchParams: nextSearchParams,
|
|
563
|
-
screenParams: nextScreenParams,
|
|
564
|
-
appliedDefaults
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
export function parseRecruitInstruction({ instruction, confirmation, overrides } = {}) {
|
|
569
|
-
const rawInstruction = String(instruction || "");
|
|
570
|
-
const text = normalizeText(rawInstruction);
|
|
571
|
-
const finalConfirmed = confirmation?.final_confirmed === true;
|
|
572
|
-
const hasSkipRecentColleagueOverride = [
|
|
573
|
-
"skip_recent_colleague_contacted",
|
|
574
|
-
"filter_recent_colleague_contacted",
|
|
575
|
-
"recent_colleague_contacted",
|
|
576
|
-
"colleague_contacted_filter",
|
|
577
|
-
"colleague_contacted"
|
|
578
|
-
].some((key) => Object.prototype.hasOwnProperty.call(overrides || {}, key));
|
|
579
|
-
const confirmationSkipRecentColleagueContacted = normalizeColleagueContactedFilterOverride(
|
|
580
|
-
Object.prototype.hasOwnProperty.call(confirmation || {}, "filter_recent_colleague_contacted_value")
|
|
581
|
-
? confirmation.filter_recent_colleague_contacted_value
|
|
582
|
-
: confirmation?.skip_recent_colleague_contacted_value
|
|
583
|
-
);
|
|
584
|
-
const explicitSchools = extractSchoolFilterExplicit(rawInstruction);
|
|
585
|
-
const explicitRecentViewed = extractRecentViewedExplicit(rawInstruction);
|
|
586
|
-
const explicitColleagueContactedFilter = extractColleagueContactedFilterExplicit(rawInstruction);
|
|
587
|
-
const explicitKeyword = extractFieldLineValue(rawInstruction, ["搜索关键词", "关键词", "keyword"]);
|
|
588
|
-
const explicitJob = extractFieldLineValue(rawInstruction, ["岗位", "职位", "job"]);
|
|
589
|
-
const explicitCity = extractFieldLineValue(rawInstruction, ["城市", "地点", "工作地", "base"]);
|
|
590
|
-
const explicitDegree = extractFieldLineValue(rawInstruction, ["学历", "degree"]);
|
|
591
|
-
const explicitExperience = extractFieldLineValue(rawInstruction, ["经验", "经验要求", "工作经验", "工作年限", "experience"]);
|
|
592
|
-
const explicitGender = extractFieldLineValue(rawInstruction, ["性别", "gender"]);
|
|
593
|
-
const explicitAge = extractFieldLineValue(rawInstruction, ["年龄", "年龄要求", "年龄范围", "age"]);
|
|
594
|
-
const explicitTargetCount = extractTargetCountExplicit(rawInstruction);
|
|
595
|
-
const explicitPostAction = extractPostActionExplicit(rawInstruction);
|
|
596
|
-
const parsed = {
|
|
597
|
-
job_explicit: explicitJob || extractJobExplicit(text),
|
|
598
|
-
city: sanitizeCityCandidate(explicitCity) || extractCity(text),
|
|
599
|
-
city_explicit: explicitCity !== null,
|
|
600
|
-
degree: normalizeDegreeFieldValue(explicitDegree) || extractDegree(text),
|
|
601
|
-
degree_explicit: explicitDegree !== null,
|
|
602
|
-
experience: normalizeExperienceOverride(explicitExperience),
|
|
603
|
-
experience_explicit: explicitExperience !== null,
|
|
604
|
-
gender: normalizeGenericSearchFilterOverride(explicitGender),
|
|
605
|
-
gender_explicit: explicitGender !== null,
|
|
606
|
-
age: normalizeGenericSearchFilterOverride(explicitAge),
|
|
607
|
-
age_explicit: explicitAge !== null,
|
|
608
|
-
schools: explicitSchools.explicit ? explicitSchools.schools : extractSchools(text),
|
|
609
|
-
schools_explicit: explicitSchools.explicit,
|
|
610
|
-
filter_recent_viewed: explicitRecentViewed !== null ? explicitRecentViewed : extractRecentViewedFilter(text),
|
|
611
|
-
skip_recent_colleague_contacted: explicitColleagueContactedFilter ?? confirmationSkipRecentColleagueContacted,
|
|
612
|
-
keyword_explicit: explicitKeyword || extractKeywordExplicit(text),
|
|
613
|
-
keyword_auto: extractKeywordAuto(text),
|
|
614
|
-
target_count: explicitTargetCount || extractTargetCount(text),
|
|
615
|
-
post_action_explicit: explicitPostAction || extractPostAction(text),
|
|
616
|
-
criteria_explicit: extractExplicitCriteria(rawInstruction)
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
if (overrides) {
|
|
620
|
-
const overrideCity = sanitizeCityCandidate(normalizeStringOverride(overrides.city));
|
|
621
|
-
const overrideDegree = normalizeStringOverride(overrides.degree);
|
|
622
|
-
const overrideDegrees = normalizeDegreesOverride(overrides.degrees || (Array.isArray(overrides.degree) ? overrides.degree : null));
|
|
623
|
-
const hasOverrideSchools = Object.prototype.hasOwnProperty.call(overrides, "schools")
|
|
624
|
-
|| Object.prototype.hasOwnProperty.call(overrides, "school_tag")
|
|
625
|
-
|| Object.prototype.hasOwnProperty.call(overrides, "school_tags");
|
|
626
|
-
const overrideSchools = normalizeSchoolsOverride(
|
|
627
|
-
Object.prototype.hasOwnProperty.call(overrides, "schools")
|
|
628
|
-
? overrides.schools
|
|
629
|
-
: Object.prototype.hasOwnProperty.call(overrides, "school_tag")
|
|
630
|
-
? overrides.school_tag
|
|
631
|
-
: overrides.school_tags
|
|
632
|
-
);
|
|
633
|
-
const overrideKeyword = normalizeStringOverride(overrides.keyword);
|
|
634
|
-
const overrideJob = normalizeStringOverride(overrides.job || overrides.job_title || overrides.selected_job);
|
|
635
|
-
const overrideCriteria = normalizeStringOverride(overrides.criteria);
|
|
636
|
-
const hasOverrideExperience = Object.prototype.hasOwnProperty.call(overrides, "experience")
|
|
637
|
-
|| Object.prototype.hasOwnProperty.call(overrides, "experiences")
|
|
638
|
-
|| Object.prototype.hasOwnProperty.call(overrides, "experience_range")
|
|
639
|
-
|| Object.prototype.hasOwnProperty.call(overrides, "experience_start")
|
|
640
|
-
|| Object.prototype.hasOwnProperty.call(overrides, "experience_end");
|
|
641
|
-
const overrideExperience = Object.prototype.hasOwnProperty.call(overrides, "experience")
|
|
642
|
-
? normalizeExperienceOverride(overrides.experience)
|
|
643
|
-
: Object.prototype.hasOwnProperty.call(overrides, "experiences")
|
|
644
|
-
? normalizeExperienceOverride(overrides.experiences)
|
|
645
|
-
: Object.prototype.hasOwnProperty.call(overrides, "experience_range")
|
|
646
|
-
? normalizeExperienceOverride(overrides.experience_range)
|
|
647
|
-
: hasOverrideExperience
|
|
648
|
-
? {
|
|
649
|
-
start: overrides.experience_start,
|
|
650
|
-
end: overrides.experience_end
|
|
651
|
-
}
|
|
652
|
-
: null;
|
|
653
|
-
const hasOverrideGender = Object.prototype.hasOwnProperty.call(overrides, "gender");
|
|
654
|
-
const overrideGender = hasOverrideGender ? normalizeGenericSearchFilterOverride(overrides.gender) : null;
|
|
655
|
-
const hasOverrideAge = Object.prototype.hasOwnProperty.call(overrides, "age")
|
|
656
|
-
|| Object.prototype.hasOwnProperty.call(overrides, "ages")
|
|
657
|
-
|| Object.prototype.hasOwnProperty.call(overrides, "age_range")
|
|
658
|
-
|| Object.prototype.hasOwnProperty.call(overrides, "age_min")
|
|
659
|
-
|| Object.prototype.hasOwnProperty.call(overrides, "age_max")
|
|
660
|
-
|| Object.prototype.hasOwnProperty.call(overrides, "min_age")
|
|
661
|
-
|| Object.prototype.hasOwnProperty.call(overrides, "max_age");
|
|
662
|
-
const overrideAge = Object.prototype.hasOwnProperty.call(overrides, "age")
|
|
663
|
-
? normalizeGenericSearchFilterOverride(overrides.age)
|
|
664
|
-
: Object.prototype.hasOwnProperty.call(overrides, "ages")
|
|
665
|
-
? normalizeGenericSearchFilterOverride(overrides.ages)
|
|
666
|
-
: Object.prototype.hasOwnProperty.call(overrides, "age_range")
|
|
667
|
-
? normalizeGenericSearchFilterOverride(overrides.age_range)
|
|
668
|
-
: hasOverrideAge
|
|
669
|
-
? {
|
|
670
|
-
min: overrides.age_min ?? overrides.min_age,
|
|
671
|
-
max: overrides.age_max ?? overrides.max_age
|
|
672
|
-
}
|
|
673
|
-
: null;
|
|
674
|
-
const overrideRecentViewed = normalizeRecentViewedOverride(
|
|
675
|
-
Object.prototype.hasOwnProperty.call(overrides, "filter_recent_viewed")
|
|
676
|
-
? overrides.filter_recent_viewed
|
|
677
|
-
: overrides.recent_not_view
|
|
678
|
-
);
|
|
679
|
-
const overrideSkipRecentColleagueContacted = normalizeColleagueContactedFilterOverride(
|
|
680
|
-
Object.prototype.hasOwnProperty.call(overrides, "skip_recent_colleague_contacted")
|
|
681
|
-
? overrides.skip_recent_colleague_contacted
|
|
682
|
-
: Object.prototype.hasOwnProperty.call(overrides, "filter_recent_colleague_contacted")
|
|
683
|
-
? overrides.filter_recent_colleague_contacted
|
|
684
|
-
: Object.prototype.hasOwnProperty.call(overrides, "recent_colleague_contacted")
|
|
685
|
-
? overrides.recent_colleague_contacted
|
|
686
|
-
: Object.prototype.hasOwnProperty.call(overrides, "colleague_contacted_filter")
|
|
687
|
-
? overrides.colleague_contacted_filter
|
|
688
|
-
: overrides.colleague_contacted
|
|
689
|
-
);
|
|
690
|
-
const overridePostAction = normalizePostAction(overrides.post_action);
|
|
691
|
-
if (overrideCity) parsed.city = overrideCity;
|
|
692
|
-
if (overrideDegree) parsed.degree = overrideDegree;
|
|
693
|
-
if (overrideDegrees?.length) parsed.degrees = overrideDegrees;
|
|
694
|
-
if (Object.prototype.hasOwnProperty.call(overrides, "city")) parsed.city_explicit = true;
|
|
695
|
-
if (Object.prototype.hasOwnProperty.call(overrides, "degree") || Object.prototype.hasOwnProperty.call(overrides, "degrees")) {
|
|
696
|
-
parsed.degree_explicit = true;
|
|
697
|
-
}
|
|
698
|
-
if (hasOverrideSchools && Array.isArray(overrideSchools)) {
|
|
699
|
-
parsed.schools = overrideSchools;
|
|
700
|
-
parsed.schools_explicit = true;
|
|
701
|
-
}
|
|
702
|
-
if (hasOverrideExperience) {
|
|
703
|
-
parsed.experience = overrideExperience;
|
|
704
|
-
parsed.experience_explicit = true;
|
|
705
|
-
}
|
|
706
|
-
if (hasOverrideGender) {
|
|
707
|
-
parsed.gender = overrideGender;
|
|
708
|
-
parsed.gender_explicit = true;
|
|
709
|
-
}
|
|
710
|
-
if (hasOverrideAge) {
|
|
711
|
-
parsed.age = overrideAge;
|
|
712
|
-
parsed.age_explicit = true;
|
|
713
|
-
}
|
|
714
|
-
if (overrideKeyword) parsed.keyword_override = overrideKeyword;
|
|
715
|
-
if (overrideJob) parsed.job_override = overrideJob;
|
|
716
|
-
if (overrideCriteria) parsed.criteria_override = overrideCriteria;
|
|
717
|
-
if (overrideRecentViewed !== null) parsed.filter_recent_viewed = overrideRecentViewed;
|
|
718
|
-
if (overrideSkipRecentColleagueContacted !== null) parsed.skip_recent_colleague_contacted = overrideSkipRecentColleagueContacted;
|
|
719
|
-
if (overridePostAction) parsed.post_action_override = overridePostAction;
|
|
720
|
-
if (Number.isFinite(overrides.max_greet_count) && overrides.max_greet_count > 0) {
|
|
721
|
-
parsed.max_greet_count_override = Number.parseInt(String(overrides.max_greet_count), 10);
|
|
722
|
-
}
|
|
723
|
-
if (Number.isFinite(overrides.target_count) && overrides.target_count > 0) {
|
|
724
|
-
parsed.target_count = Number.parseInt(String(overrides.target_count), 10);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const keywordResolution = resolveKeyword(parsed, confirmation);
|
|
729
|
-
const job = resolveJob(parsed, confirmation);
|
|
730
|
-
const postAction = resolvePostAction(parsed, confirmation);
|
|
731
|
-
const maxGreetCount = resolveMaxGreetCount(parsed, confirmation);
|
|
732
|
-
const confirmationCriteria = normalizeStringOverride(confirmation?.criteria_value);
|
|
733
|
-
const skipRecentColleagueContacted = typeof parsed.skip_recent_colleague_contacted === "boolean"
|
|
734
|
-
? parsed.skip_recent_colleague_contacted
|
|
735
|
-
: finalConfirmed
|
|
736
|
-
? false
|
|
737
|
-
: null;
|
|
738
|
-
const baseSearchParams = {
|
|
739
|
-
job,
|
|
740
|
-
city: parsed.city,
|
|
741
|
-
degree: parsed.degree,
|
|
742
|
-
degrees: parsed.degrees,
|
|
743
|
-
schools: parsed.schools,
|
|
744
|
-
experience: parsed.experience,
|
|
745
|
-
gender: parsed.gender,
|
|
746
|
-
age: parsed.age,
|
|
747
|
-
filter_recent_viewed: parsed.filter_recent_viewed,
|
|
748
|
-
skip_recent_colleague_contacted: skipRecentColleagueContacted,
|
|
749
|
-
keyword: keywordResolution.keyword
|
|
750
|
-
};
|
|
751
|
-
const criteria = parsed.criteria_override || confirmationCriteria || parsed.criteria_explicit || null;
|
|
752
|
-
const criteriaSource = parsed.criteria_override
|
|
753
|
-
? "override"
|
|
754
|
-
: confirmationCriteria
|
|
755
|
-
? "confirmation"
|
|
756
|
-
: parsed.criteria_explicit
|
|
757
|
-
? "instruction_block"
|
|
758
|
-
: "missing";
|
|
759
|
-
const baseScreenParams = {
|
|
760
|
-
criteria,
|
|
761
|
-
target_count: parsed.target_count,
|
|
762
|
-
post_action: postAction,
|
|
763
|
-
max_greet_count: maxGreetCount,
|
|
764
|
-
skip_recent_colleague_contacted: skipRecentColleagueContacted === true,
|
|
765
|
-
search_exchange_resume_filter_days: 30
|
|
766
|
-
};
|
|
767
|
-
const missingBeforeDefaults = collectMissingFields(baseSearchParams, baseScreenParams, parsed);
|
|
768
|
-
|
|
769
|
-
const useDefaultForMissing = finalConfirmed || confirmation?.use_default_for_missing === true;
|
|
770
|
-
const skipKeywordDefault = keywordResolution.needsConfirmation;
|
|
771
|
-
const defaultPreview = buildDefaultPreview(missingBeforeDefaults, { skipKeywordDefault });
|
|
772
|
-
const { searchParams, screenParams, appliedDefaults } = applyDefaults(
|
|
773
|
-
baseSearchParams,
|
|
774
|
-
baseScreenParams,
|
|
775
|
-
missingBeforeDefaults,
|
|
776
|
-
useDefaultForMissing,
|
|
777
|
-
{ skipKeywordDefault }
|
|
778
|
-
);
|
|
779
|
-
const missingAfterDefaults = collectUnresolvedMissingFields(missingBeforeDefaults, appliedDefaults);
|
|
780
|
-
const suspicious_fields = collectSuspiciousFields(searchParams, screenParams);
|
|
781
|
-
const needs_recent_viewed_filter_confirmation = !finalConfirmed && searchParams.filter_recent_viewed === null;
|
|
782
|
-
const needs_skip_recent_colleague_contacted_confirmation = (
|
|
783
|
-
!finalConfirmed
|
|
784
|
-
&& !hasSkipRecentColleagueOverride
|
|
785
|
-
&& confirmationSkipRecentColleagueContacted === null
|
|
786
|
-
&& confirmation?.skip_recent_colleague_contacted_confirmed !== true
|
|
787
|
-
&& confirmation?.filter_recent_colleague_contacted_confirmed !== true
|
|
788
|
-
);
|
|
789
|
-
const needs_criteria_confirmation = Boolean(screenParams.criteria) && !finalConfirmed && confirmation?.criteria_confirmed !== true;
|
|
790
|
-
const pending_questions = [
|
|
791
|
-
...buildMissingFieldQuestions(missingAfterDefaults, defaultPreview),
|
|
792
|
-
...(needs_recent_viewed_filter_confirmation
|
|
793
|
-
? [{
|
|
794
|
-
field: "filter_recent_viewed",
|
|
795
|
-
question: "是否需要过滤近14天查看过的人选?",
|
|
796
|
-
options: [
|
|
797
|
-
{ label: "需要过滤", value: true },
|
|
798
|
-
{ label: "不过滤", value: false }
|
|
799
|
-
]
|
|
800
|
-
}]
|
|
801
|
-
: []),
|
|
802
|
-
...(needs_skip_recent_colleague_contacted_confirmation
|
|
803
|
-
? [{
|
|
804
|
-
field: "filter_recent_colleague_contacted",
|
|
805
|
-
question: "是否过滤近期已被同事触达的人选?开启后搜索页会勾选 Boss 的“近30天未和同事交换简历”。",
|
|
806
|
-
value: true,
|
|
807
|
-
options: [
|
|
808
|
-
{ label: "过滤", value: true },
|
|
809
|
-
{ label: "不过滤", value: false }
|
|
810
|
-
]
|
|
811
|
-
}]
|
|
812
|
-
: []),
|
|
813
|
-
...(needs_criteria_confirmation
|
|
814
|
-
? [{
|
|
815
|
-
field: "criteria",
|
|
816
|
-
question: "请确认筛选 criteria 是否准确无误(尤其是硬性约束条件)?",
|
|
817
|
-
value: baseScreenParams.criteria
|
|
818
|
-
}]
|
|
819
|
-
: [])
|
|
820
|
-
];
|
|
821
|
-
const review = {
|
|
822
|
-
extracted_search_params: baseSearchParams,
|
|
823
|
-
extracted_screen_params: baseScreenParams,
|
|
824
|
-
current_search_params: searchParams,
|
|
825
|
-
current_screen_params: screenParams,
|
|
826
|
-
missing_fields: missingAfterDefaults,
|
|
827
|
-
missing_fields_before_defaults: missingBeforeDefaults,
|
|
828
|
-
has_unresolved_missing_fields: missingAfterDefaults.length > 0,
|
|
829
|
-
suspicious_fields,
|
|
830
|
-
pending_questions,
|
|
831
|
-
default_preview: defaultPreview,
|
|
832
|
-
applied_defaults: appliedDefaults,
|
|
833
|
-
criteria_source: criteriaSource,
|
|
834
|
-
final_confirmed: finalConfirmed
|
|
835
|
-
};
|
|
836
|
-
|
|
837
|
-
return {
|
|
838
|
-
parsed,
|
|
839
|
-
searchParams,
|
|
840
|
-
screenParams,
|
|
841
|
-
missing_fields: missingAfterDefaults,
|
|
842
|
-
has_unresolved_missing_fields: missingAfterDefaults.length > 0,
|
|
843
|
-
suspicious_fields,
|
|
844
|
-
needs_keyword_confirmation: keywordResolution.needsConfirmation,
|
|
845
|
-
needs_recent_viewed_filter_confirmation,
|
|
846
|
-
needs_skip_recent_colleague_contacted_confirmation,
|
|
847
|
-
needs_criteria_confirmation,
|
|
848
|
-
needs_search_params_confirmation: !finalConfirmed && confirmation?.search_params_confirmed !== true,
|
|
849
|
-
proposed_keyword: keywordResolution.proposedKeyword,
|
|
850
|
-
pending_questions,
|
|
851
|
-
default_preview: defaultPreview,
|
|
852
|
-
applied_defaults: appliedDefaults,
|
|
853
|
-
review
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
export const recruitInstructionParserSemantics = Object.freeze({
|
|
858
|
-
source: "boss-recruit-mcp/src/parser.js",
|
|
859
|
-
imported_at: "2026-04-30",
|
|
860
|
-
default_param_values: DEFAULT_PARAM_VALUES,
|
|
861
|
-
school_labels: SEARCH_SCHOOL_MAP,
|
|
862
|
-
degree_values: Array.from(DEGREE_VALUES),
|
|
863
|
-
experience_values: ["不限", "在校/应届", "25年毕业", "26年毕业", "26年后毕业", "1-3年", "3-5年", "5-10年", "自定义"],
|
|
864
|
-
gender_values: ["不限", "男", "女"],
|
|
865
|
-
age_values: ["不限", "20-25", "25-30", "30-35", "35-40", "40-50", "50以上", "自定义"]
|
|
866
|
-
});
|
|
1
|
+
const SEARCH_SCHOOL_MAP = {
|
|
2
|
+
"统招": "统招本科",
|
|
3
|
+
"统招本科": "统招本科",
|
|
4
|
+
"统招本": "统招本科",
|
|
5
|
+
"全日制本科": "统招本科",
|
|
6
|
+
"双一流": "双一流院校",
|
|
7
|
+
"双一流院校": "双一流院校",
|
|
8
|
+
"双一流学校": "双一流院校",
|
|
9
|
+
"985": "985院校",
|
|
10
|
+
"985院校": "985院校",
|
|
11
|
+
"211": "211院校",
|
|
12
|
+
"211院校": "211院校",
|
|
13
|
+
"qs": "QS 100",
|
|
14
|
+
"qs100": "QS 100",
|
|
15
|
+
"qs500": "QS 500"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const KNOWN_SCHOOL_LABELS = new Set(Object.values(SEARCH_SCHOOL_MAP));
|
|
19
|
+
const DEFAULT_PARAM_VALUES = {
|
|
20
|
+
job: null,
|
|
21
|
+
city: null,
|
|
22
|
+
degree: "不限",
|
|
23
|
+
schools: [],
|
|
24
|
+
experience: null,
|
|
25
|
+
gender: null,
|
|
26
|
+
age: null,
|
|
27
|
+
keyword: null,
|
|
28
|
+
target_count: null,
|
|
29
|
+
criteria: null
|
|
30
|
+
};
|
|
31
|
+
const DEFAULT_PARAM_LABELS = {
|
|
32
|
+
job: "搜索页岗位未指定",
|
|
33
|
+
city: "不限城市",
|
|
34
|
+
degree: "不限",
|
|
35
|
+
schools: "不限院校标签",
|
|
36
|
+
experience: "经验要求未指定",
|
|
37
|
+
gender: "性别未指定",
|
|
38
|
+
age: "年龄要求未指定",
|
|
39
|
+
keyword: "搜索关键词未指定",
|
|
40
|
+
target_count: "目标通过人数未指定",
|
|
41
|
+
criteria: "筛选 criteria 未指定"
|
|
42
|
+
};
|
|
43
|
+
const DEGREE_VALUES = new Set(["不限", "本科", "本科及以上", "硕士及以上", "博士"]);
|
|
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|filter_recent_colleague_contacted|recent_colleague_contacted|skip_recent_colleague_contacted)\s*[::]/i;
|
|
48
|
+
const INLINE_FIELD_BOUNDARY_PATTERN = /[;;]\s*(?:岗位|职位|关键词|城市|地点|工作地|学历|学校|学校类型|院校标签|经验|经验要求|工作经验|工作年限|性别|年龄|年龄要求|年龄范围|只看未查看|过滤已看|同事近期触达|近期同事触达|同事触达|同事联系|同事沟通|同事交换简历|目标筛选人数|目标人数|休息强度|后置动作|post_action|rest_level|filter_recent_colleague_contacted|recent_colleague_contacted|skip_recent_colleague_contacted)\s*(?:\([^)]*\))?\s*[::]/i;
|
|
49
|
+
|
|
50
|
+
function normalizeText(input) {
|
|
51
|
+
return String(input || "").replace(/\s+/g, " ").trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeCriteriaBlock(input) {
|
|
55
|
+
const lines = String(input || "")
|
|
56
|
+
.replace(/\r\n/g, "\n")
|
|
57
|
+
.split("\n")
|
|
58
|
+
.map((line) => line.trim())
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
return lines.join("\n").trim() || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function escapeRegExp(input) {
|
|
64
|
+
return String(input).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractFieldLineValue(rawText, labels = []) {
|
|
68
|
+
const lines = String(rawText || "").replace(/\r\n/g, "\n").split(/\n|[;;]/);
|
|
69
|
+
const labelPattern = labels.map(escapeRegExp).join("|");
|
|
70
|
+
if (!labelPattern) return null;
|
|
71
|
+
const pattern = new RegExp(`^\\s*(?:${labelPattern})(?:\\s*\\([^)]*\\))?\\s*[::]\\s*(.+?)\\s*$`, "i");
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
const match = line.match(pattern);
|
|
74
|
+
let value = match?.[1]?.trim();
|
|
75
|
+
const inlineBoundaryIndex = value ? value.search(INLINE_FIELD_BOUNDARY_PATTERN) : -1;
|
|
76
|
+
if (inlineBoundaryIndex >= 0) value = value.slice(0, inlineBoundaryIndex).trim();
|
|
77
|
+
if (value) return value;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function uniqueList(items) {
|
|
83
|
+
return Array.from(new Set(items.filter(Boolean)));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeSchoolLabel(value) {
|
|
87
|
+
if (typeof value !== "string") return null;
|
|
88
|
+
const raw = value.trim();
|
|
89
|
+
if (!raw) return null;
|
|
90
|
+
if (/^(?:不限|不限制|无限制|全部|所有|无|none|all)$/i.test(raw)) return null;
|
|
91
|
+
if (KNOWN_SCHOOL_LABELS.has(raw)) return raw;
|
|
92
|
+
|
|
93
|
+
const compact = raw.toLowerCase().replace(/\s+/g, "");
|
|
94
|
+
const qsMatch = compact.match(/^qs(\d+)$/);
|
|
95
|
+
if (qsMatch) {
|
|
96
|
+
const rank = Number.parseInt(qsMatch[1], 10);
|
|
97
|
+
if (Number.isFinite(rank)) return rank > 100 ? SEARCH_SCHOOL_MAP.qs500 : SEARCH_SCHOOL_MAP.qs100;
|
|
98
|
+
}
|
|
99
|
+
return SEARCH_SCHOOL_MAP[compact] || SEARCH_SCHOOL_MAP[raw] || raw;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function sanitizeCityCandidate(value) {
|
|
103
|
+
if (typeof value !== "string") return null;
|
|
104
|
+
let candidate = value.trim();
|
|
105
|
+
if (!candidate) return null;
|
|
106
|
+
candidate = candidate.replace(/^(在|是|为)\s*/, "").trim();
|
|
107
|
+
const stopIndex = candidate.search(CITY_STOP_PATTERN);
|
|
108
|
+
if (stopIndex >= 0) candidate = candidate.slice(0, stopIndex).trim();
|
|
109
|
+
candidate = candidate.replace(/[的\s]+$/g, "").trim();
|
|
110
|
+
return candidate || null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function extractCity(text) {
|
|
114
|
+
const patterns = [
|
|
115
|
+
/地点(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
|
|
116
|
+
/城市(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
|
|
117
|
+
/工作地(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
|
|
118
|
+
/base(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i
|
|
119
|
+
];
|
|
120
|
+
for (const pattern of patterns) {
|
|
121
|
+
const match = text.match(pattern);
|
|
122
|
+
if (match?.[1]) {
|
|
123
|
+
const city = sanitizeCityCandidate(match[1]);
|
|
124
|
+
if (city) return city;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function extractDegree(text) {
|
|
131
|
+
if (/(博士及以上|博士)/.test(text)) return "博士";
|
|
132
|
+
if (/(硕士及以上|硕士以上)/.test(text)) return "硕士及以上";
|
|
133
|
+
if (/硕士/.test(text)) return "硕士";
|
|
134
|
+
if (/(本科及以上|本科以上)/.test(text)) return "本科及以上";
|
|
135
|
+
if (/本科/.test(text)) return "本科";
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function extractSchools(text) {
|
|
140
|
+
const schools = [];
|
|
141
|
+
if (/统招(?:本科)?/.test(text)) schools.push(SEARCH_SCHOOL_MAP["统招"]);
|
|
142
|
+
if (/双一流(?:院校|学校)?/.test(text)) schools.push(SEARCH_SCHOOL_MAP["双一流"]);
|
|
143
|
+
if (/(^|[^0-9])985([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["985"]);
|
|
144
|
+
if (/(^|[^0-9])211([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["211"]);
|
|
145
|
+
const qsMatches = text.matchAll(/\bqs\s*(\d+)\b/ig);
|
|
146
|
+
for (const match of qsMatches) {
|
|
147
|
+
const rank = Number.parseInt(match[1], 10);
|
|
148
|
+
if (Number.isFinite(rank)) schools.push(rank > 100 ? SEARCH_SCHOOL_MAP.qs500 : SEARCH_SCHOOL_MAP.qs100);
|
|
149
|
+
}
|
|
150
|
+
return uniqueList(schools);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function extractRecentViewedFilter(text) {
|
|
154
|
+
const negativePatterns = [
|
|
155
|
+
/(?:不|别|无需|不用|不要).{0,6}(?:过滤|排除|去掉|剔除).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
|
|
156
|
+
/(?:保留|包含).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
|
|
157
|
+
/(?:近?14天(?:内)?查看(?:过)?).{0,8}(?:不要|不用|无需|不需要|不必).{0,4}(?:过滤|排除|去掉|剔除)/i
|
|
158
|
+
];
|
|
159
|
+
if (negativePatterns.some((pattern) => pattern.test(text))) return false;
|
|
160
|
+
|
|
161
|
+
const positivePatterns = [
|
|
162
|
+
/(?:过滤|排除|去掉|剔除).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
|
|
163
|
+
/(?:近?14天(?:内)?查看(?:过)?).{0,8}(?:过滤|排除|去掉|剔除)/i
|
|
164
|
+
];
|
|
165
|
+
if (positivePatterns.some((pattern) => pattern.test(text))) return true;
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizeRecentViewedOverride(value) {
|
|
170
|
+
if (typeof value === "boolean") return value;
|
|
171
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
172
|
+
if (!normalized) return null;
|
|
173
|
+
if (["true", "yes", "1", "需要过滤", "过滤", "近14天没有", "not_viewed"].includes(normalized)) return true;
|
|
174
|
+
if (["false", "no", "0", "不过滤", "不限", "none"].includes(normalized)) return false;
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeColleagueContactedFilterOverride(value) {
|
|
179
|
+
if (typeof value === "boolean") return value;
|
|
180
|
+
if (typeof value === "number") return value !== 0;
|
|
181
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
182
|
+
const compact = normalized.replace(/\s+/g, "");
|
|
183
|
+
if (!compact) return null;
|
|
184
|
+
if ([
|
|
185
|
+
"true",
|
|
186
|
+
"yes",
|
|
187
|
+
"y",
|
|
188
|
+
"1",
|
|
189
|
+
"on",
|
|
190
|
+
"enable",
|
|
191
|
+
"enabled",
|
|
192
|
+
"需要",
|
|
193
|
+
"是",
|
|
194
|
+
"开启",
|
|
195
|
+
"过滤",
|
|
196
|
+
"需要过滤",
|
|
197
|
+
"跳过",
|
|
198
|
+
"排除",
|
|
199
|
+
"剔除",
|
|
200
|
+
"近30天未和同事交换简历",
|
|
201
|
+
"未和同事交换简历",
|
|
202
|
+
"只看未和同事交换简历"
|
|
203
|
+
].includes(compact)) return true;
|
|
204
|
+
if ([
|
|
205
|
+
"false",
|
|
206
|
+
"no",
|
|
207
|
+
"n",
|
|
208
|
+
"0",
|
|
209
|
+
"off",
|
|
210
|
+
"disable",
|
|
211
|
+
"disabled",
|
|
212
|
+
"不需要",
|
|
213
|
+
"否",
|
|
214
|
+
"关闭",
|
|
215
|
+
"不限",
|
|
216
|
+
"不过滤",
|
|
217
|
+
"不跳过",
|
|
218
|
+
"不排除",
|
|
219
|
+
"保留",
|
|
220
|
+
"none",
|
|
221
|
+
"all"
|
|
222
|
+
].includes(compact)) return false;
|
|
223
|
+
if (/(?:不|别|无需|不用|不要).{0,6}(?:过滤|排除|跳过|剔除).{0,8}(?:同事|交换简历|触达|联系|沟通)/i.test(normalized)) return false;
|
|
224
|
+
if (/(?:过滤|排除|跳过|剔除).{0,8}(?:同事|交换简历|触达|联系|沟通)/i.test(normalized)) return true;
|
|
225
|
+
return normalizeBooleanOverride(value);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function normalizeBooleanOverride(value) {
|
|
229
|
+
if (typeof value === "boolean") return value;
|
|
230
|
+
if (typeof value === "number") return value !== 0;
|
|
231
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
232
|
+
if (!normalized) return null;
|
|
233
|
+
if (["true", "yes", "y", "1", "on", "enable", "enabled", "需要", "是", "开启"].includes(normalized)) return true;
|
|
234
|
+
if (["false", "no", "n", "0", "off", "disable", "disabled", "不需要", "否", "关闭"].includes(normalized)) return false;
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeStringOverride(value) {
|
|
239
|
+
if (typeof value !== "string") return null;
|
|
240
|
+
const normalized = value.trim();
|
|
241
|
+
return normalized || null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeSchoolsOverride(value) {
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
return uniqueList(value.flatMap((item) => normalizeSchoolsOverride(item) || []));
|
|
247
|
+
}
|
|
248
|
+
if (typeof value === "string") return uniqueList(value.split(/[,,、|/]/).map(normalizeSchoolLabel));
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function extractSchoolFilterExplicit(rawText) {
|
|
253
|
+
const value = extractFieldLineValue(rawText, [
|
|
254
|
+
"学校",
|
|
255
|
+
"院校",
|
|
256
|
+
"学校类型",
|
|
257
|
+
"院校标签",
|
|
258
|
+
"学校标签",
|
|
259
|
+
"school",
|
|
260
|
+
"school_tag",
|
|
261
|
+
"school_tags",
|
|
262
|
+
"schools"
|
|
263
|
+
]);
|
|
264
|
+
if (value === null) return { explicit: false, schools: null };
|
|
265
|
+
return { explicit: true, schools: normalizeSchoolsOverride(value) || [] };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function extractRecentViewedExplicit(rawText) {
|
|
269
|
+
const value = extractFieldLineValue(rawText, ["只看未查看", "过滤已看", "recent_not_view", "filter_recent_viewed"]);
|
|
270
|
+
return value === null ? null : normalizeRecentViewedOverride(value);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function extractColleagueContactedFilterExplicit(rawText) {
|
|
274
|
+
const value = extractFieldLineValue(rawText, [
|
|
275
|
+
"同事近期触达",
|
|
276
|
+
"近期同事触达",
|
|
277
|
+
"同事触达",
|
|
278
|
+
"同事近期联系",
|
|
279
|
+
"近期同事联系",
|
|
280
|
+
"同事联系",
|
|
281
|
+
"同事近期沟通",
|
|
282
|
+
"近期同事沟通",
|
|
283
|
+
"同事沟通",
|
|
284
|
+
"同事交换简历",
|
|
285
|
+
"近期同事交换简历",
|
|
286
|
+
"近30天未和同事交换简历",
|
|
287
|
+
"filter_recent_colleague_contacted",
|
|
288
|
+
"recent_colleague_contacted",
|
|
289
|
+
"colleague_contacted_filter",
|
|
290
|
+
"colleague_contacted",
|
|
291
|
+
"skip_recent_colleague_contacted"
|
|
292
|
+
]);
|
|
293
|
+
return value === null ? null : normalizeColleagueContactedFilterOverride(value);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function normalizeDegreesOverride(value) {
|
|
297
|
+
if (Array.isArray(value)) return uniqueList(value.map(normalizeText));
|
|
298
|
+
if (typeof value === "string") return uniqueList(value.split(/[,,、|/]/).map(normalizeText));
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function normalizeExperienceOverride(value) {
|
|
303
|
+
if (typeof value === "string") return normalizeText(value) || null;
|
|
304
|
+
if (Array.isArray(value)) {
|
|
305
|
+
const normalized = value.map(normalizeText).filter(Boolean);
|
|
306
|
+
return normalized.length ? normalized[0] : null;
|
|
307
|
+
}
|
|
308
|
+
if (value && typeof value === "object") return value;
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function normalizeGenericSearchFilterOverride(value) {
|
|
313
|
+
if (typeof value === "string" || typeof value === "number") return normalizeText(value) || null;
|
|
314
|
+
if (Array.isArray(value)) {
|
|
315
|
+
const normalized = value.map(normalizeText).filter(Boolean);
|
|
316
|
+
return normalized.length ? normalized[0] : null;
|
|
317
|
+
}
|
|
318
|
+
if (value && typeof value === "object") return value;
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function normalizeDegreeFieldValue(value) {
|
|
323
|
+
const normalized = normalizeText(value);
|
|
324
|
+
if (!normalized) return null;
|
|
325
|
+
if (/^(?:不限|不限制|无限制|全部|所有|无|none|all)$/i.test(normalized)) return "不限";
|
|
326
|
+
return extractDegree(normalized) || normalized;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function normalizePostAction(value) {
|
|
330
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
331
|
+
if (["", "none", "skip", "no", "不执行", "无", "什么也不做"].includes(normalized)) return "none";
|
|
332
|
+
if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
|
|
333
|
+
return POST_ACTIONS.has(normalized) ? normalized : "";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function parsePositiveInteger(value) {
|
|
337
|
+
const parsed = Number.parseInt(String(value || ""), 10);
|
|
338
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function extractKeywordExplicit(text) {
|
|
342
|
+
const patterns = [
|
|
343
|
+
/搜索关键词(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
|
|
344
|
+
/关键词(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
|
|
345
|
+
/keyword(?:\s*[::=]\s*|\s+is\s+)([^\n,。;;]+)/i
|
|
346
|
+
];
|
|
347
|
+
for (const pattern of patterns) {
|
|
348
|
+
const match = text.match(pattern);
|
|
349
|
+
const keyword = match?.[1]?.trim();
|
|
350
|
+
if (keyword) return keyword;
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function extractKeywordAuto(text) {
|
|
356
|
+
const patterns = [
|
|
357
|
+
/做过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:的人选|的人|相关|并且|且|,|。|,|$)/i,
|
|
358
|
+
/有过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:经验|背景|的人选|并且|且|,|。|,|$)/i,
|
|
359
|
+
/从事过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:相关|的人选|并且|且|,|。|,|$)/i
|
|
360
|
+
];
|
|
361
|
+
for (const pattern of patterns) {
|
|
362
|
+
const match = text.match(pattern);
|
|
363
|
+
const keyword = match?.[1]?.replace(/\s+/g, " ").trim();
|
|
364
|
+
if (keyword && keyword.length >= 2) return keyword;
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function extractJobExplicit(text) {
|
|
370
|
+
const patterns = [
|
|
371
|
+
/(?:搜索页)?(?:岗位|职位)(?:名称)?(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
|
|
372
|
+
/job(?:\s*title)?(?:\s*[::=]\s*|\s+is\s+)([^\n,。;;]+)/i
|
|
373
|
+
];
|
|
374
|
+
for (const pattern of patterns) {
|
|
375
|
+
const match = text.match(pattern);
|
|
376
|
+
const job = match?.[1]?.trim();
|
|
377
|
+
if (job) return job;
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function extractTargetCount(text) {
|
|
383
|
+
const patterns = [
|
|
384
|
+
/至少筛选\s*(\d+)\s*位?/i,
|
|
385
|
+
/目标(?:筛选)?(?:人数|数量)?(?:为|是|:|:)?\s*(\d+)/i,
|
|
386
|
+
/目标(?:筛选)?(?:人数|数量)?\s*(\d+)\s*人/i,
|
|
387
|
+
/筛选\s*(\d+)\s*位/i
|
|
388
|
+
];
|
|
389
|
+
for (const pattern of patterns) {
|
|
390
|
+
const match = text.match(pattern);
|
|
391
|
+
if (match?.[1]) {
|
|
392
|
+
const value = Number.parseInt(match[1], 10);
|
|
393
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function extractPostAction(text) {
|
|
400
|
+
if (/(?:什么也不做|不(?:打招呼|沟通)|只筛选|不执行)/.test(text)) return "none";
|
|
401
|
+
if (/(?:直接沟通|打招呼|立即沟通|greet|post_action\s*[::=]\s*greet)/i.test(text)) return "greet";
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function extractTargetCountExplicit(rawText) {
|
|
406
|
+
const value = extractFieldLineValue(rawText, ["目标筛选人数", "目标人数", "目标通过人数", "target_count", "max_candidates"]);
|
|
407
|
+
return parsePositiveInteger(value);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function extractPostActionExplicit(rawText) {
|
|
411
|
+
const value = extractFieldLineValue(rawText, ["后置动作", "通过后执行动作", "post_action"]);
|
|
412
|
+
return normalizePostAction(value);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function extractExplicitCriteria(rawText) {
|
|
416
|
+
const normalized = String(rawText || "").replace(/\r\n/g, "\n");
|
|
417
|
+
const match = normalized.match(CRITERIA_MARKER_PATTERN);
|
|
418
|
+
if (!match) return null;
|
|
419
|
+
let criteria = normalized.slice(match.index + match[0].length).trim();
|
|
420
|
+
const trailingFieldIndex = criteria.search(CRITERIA_TRAILING_FIELD_PATTERN);
|
|
421
|
+
if (trailingFieldIndex > 0) criteria = criteria.slice(0, trailingFieldIndex).trim();
|
|
422
|
+
return normalizeCriteriaBlock(criteria);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function resolveKeyword(parsed, confirmation) {
|
|
426
|
+
if (parsed.keyword_override) {
|
|
427
|
+
return { keyword: parsed.keyword_override, needsConfirmation: false, proposedKeyword: null };
|
|
428
|
+
}
|
|
429
|
+
const explicit = parsed.keyword_explicit;
|
|
430
|
+
const auto = parsed.keyword_auto;
|
|
431
|
+
const confirmed = confirmation?.keyword_confirmed === true;
|
|
432
|
+
const rejected = confirmation?.keyword_confirmed === false;
|
|
433
|
+
const value = typeof confirmation?.keyword_value === "string" ? confirmation.keyword_value.trim() : "";
|
|
434
|
+
if (confirmed && value) return { keyword: value, needsConfirmation: false, proposedKeyword: null };
|
|
435
|
+
if (explicit) return { keyword: explicit, needsConfirmation: false, proposedKeyword: null };
|
|
436
|
+
if (rejected) return { keyword: value || null, needsConfirmation: false, proposedKeyword: null };
|
|
437
|
+
if (auto) {
|
|
438
|
+
if (confirmed) return { keyword: auto, needsConfirmation: false, proposedKeyword: null };
|
|
439
|
+
return { keyword: null, needsConfirmation: true, proposedKeyword: auto };
|
|
440
|
+
}
|
|
441
|
+
return { keyword: null, needsConfirmation: false, proposedKeyword: null };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function resolveJob(parsed, confirmation) {
|
|
445
|
+
if (parsed.job_override) return parsed.job_override;
|
|
446
|
+
const confirmed = confirmation?.job_confirmed === true;
|
|
447
|
+
const value = typeof confirmation?.job_value === "string" ? confirmation.job_value.trim() : "";
|
|
448
|
+
if (confirmed && value) return value;
|
|
449
|
+
return parsed.job_explicit || null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function resolvePostAction(parsed, confirmation) {
|
|
453
|
+
const confirmed = confirmation?.post_action_confirmed === true;
|
|
454
|
+
const confirmationValue = normalizePostAction(confirmation?.post_action_value);
|
|
455
|
+
return parsed.post_action_override
|
|
456
|
+
|| (confirmed && confirmationValue ? confirmationValue : "")
|
|
457
|
+
|| parsed.post_action_explicit
|
|
458
|
+
|| "none";
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function resolveMaxGreetCount(parsed, confirmation) {
|
|
462
|
+
return parsePositiveInteger(confirmation?.max_greet_count_value)
|
|
463
|
+
|| parsePositiveInteger(parsed.max_greet_count_override)
|
|
464
|
+
|| null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function collectMissingFields(searchParams, screenParams, parsed = {}) {
|
|
468
|
+
const missing = [];
|
|
469
|
+
if (!searchParams.job) missing.push("job");
|
|
470
|
+
if (!searchParams.city && !parsed.city_explicit) missing.push("city");
|
|
471
|
+
if (!searchParams.degree && !searchParams.degrees?.length && !parsed.degree_explicit) missing.push("degree");
|
|
472
|
+
if (!searchParams.schools?.length && !parsed.schools_explicit) missing.push("schools");
|
|
473
|
+
if (!searchParams.keyword) missing.push("keyword");
|
|
474
|
+
if (!screenParams.criteria) missing.push("criteria");
|
|
475
|
+
if (!screenParams.target_count) missing.push("target_count");
|
|
476
|
+
return missing;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function collectUnresolvedMissingFields(missingFields, appliedDefaults) {
|
|
480
|
+
return missingFields.filter((field) => !Object.prototype.hasOwnProperty.call(appliedDefaults, field));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function collectSuspiciousFields(searchParams, screenParams) {
|
|
484
|
+
const suspicious = [];
|
|
485
|
+
if (searchParams.city && (/\s/.test(searchParams.city) || CITY_STOP_PATTERN.test(searchParams.city) || searchParams.city.length > 8)) {
|
|
486
|
+
suspicious.push({
|
|
487
|
+
field: "city",
|
|
488
|
+
value: searchParams.city,
|
|
489
|
+
reason: "城市提取结果看起来包含多余短语,请确认是否为标准城市名。"
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if (searchParams.degree && !DEGREE_VALUES.has(searchParams.degree)) {
|
|
493
|
+
suspicious.push({
|
|
494
|
+
field: "degree",
|
|
495
|
+
value: searchParams.degree,
|
|
496
|
+
reason: "学历提取结果不在预期枚举内,请确认。"
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
if (searchParams.keyword && /城市|学历|学校|目标人数|目标数量|筛选\d+位/i.test(searchParams.keyword)) {
|
|
500
|
+
suspicious.push({
|
|
501
|
+
field: "keyword",
|
|
502
|
+
value: searchParams.keyword,
|
|
503
|
+
reason: "关键词看起来混入了筛选条件,请确认是否只保留核心方向词。"
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
if (screenParams.target_count && (!Number.isInteger(screenParams.target_count) || screenParams.target_count <= 0)) {
|
|
507
|
+
suspicious.push({
|
|
508
|
+
field: "target_count",
|
|
509
|
+
value: screenParams.target_count,
|
|
510
|
+
reason: "目标人数不是有效正整数,请确认。"
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return suspicious;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function buildMissingFieldQuestions(missingFields = [], defaultPreview = {}) {
|
|
517
|
+
const questions = {
|
|
518
|
+
job: "请填写搜索页岗位名称(关键词输入框旁边的岗位选择)。",
|
|
519
|
+
city: "请填写城市;如不限城市,请明确回复不限。",
|
|
520
|
+
degree: "请填写学历筛选;如不限学历,请明确回复不限。",
|
|
521
|
+
schools: "请填写院校标签;如不限院校标签,请明确回复不限。",
|
|
522
|
+
keyword: "请填写搜索关键词。",
|
|
523
|
+
criteria: "请填写本次筛选 criteria(完整自然语言硬条件)。",
|
|
524
|
+
target_count: "请填写本次目标通过人数。"
|
|
525
|
+
};
|
|
526
|
+
return missingFields.map((field) => ({
|
|
527
|
+
field,
|
|
528
|
+
question: questions[field] || `请填写 ${field}。`,
|
|
529
|
+
value: Object.prototype.hasOwnProperty.call(defaultPreview, field) ? defaultPreview[field] : null
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function buildDefaultPreview(missingFields, { skipKeywordDefault = false } = {}) {
|
|
534
|
+
return missingFields.reduce((acc, field) => {
|
|
535
|
+
if (field === "keyword" && skipKeywordDefault) return acc;
|
|
536
|
+
if (!["city", "degree", "schools"].includes(field)) return acc;
|
|
537
|
+
acc[field] = DEFAULT_PARAM_LABELS[field];
|
|
538
|
+
return acc;
|
|
539
|
+
}, {});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function applyDefaults(searchParams, screenParams, missingFields, useDefaultForMissing, { skipKeywordDefault = false } = {}) {
|
|
543
|
+
if (!useDefaultForMissing) {
|
|
544
|
+
return { searchParams, screenParams, appliedDefaults: {} };
|
|
545
|
+
}
|
|
546
|
+
const appliedDefaults = {};
|
|
547
|
+
const nextSearchParams = { ...searchParams };
|
|
548
|
+
const nextScreenParams = { ...screenParams };
|
|
549
|
+
if (missingFields.includes("city")) {
|
|
550
|
+
nextSearchParams.city = DEFAULT_PARAM_VALUES.city;
|
|
551
|
+
appliedDefaults.city = DEFAULT_PARAM_LABELS.city;
|
|
552
|
+
}
|
|
553
|
+
if (missingFields.includes("degree")) {
|
|
554
|
+
nextSearchParams.degree = DEFAULT_PARAM_VALUES.degree;
|
|
555
|
+
appliedDefaults.degree = DEFAULT_PARAM_LABELS.degree;
|
|
556
|
+
}
|
|
557
|
+
if (missingFields.includes("schools")) {
|
|
558
|
+
nextSearchParams.schools = DEFAULT_PARAM_VALUES.schools.slice();
|
|
559
|
+
appliedDefaults.schools = DEFAULT_PARAM_LABELS.schools;
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
searchParams: nextSearchParams,
|
|
563
|
+
screenParams: nextScreenParams,
|
|
564
|
+
appliedDefaults
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function parseRecruitInstruction({ instruction, confirmation, overrides } = {}) {
|
|
569
|
+
const rawInstruction = String(instruction || "");
|
|
570
|
+
const text = normalizeText(rawInstruction);
|
|
571
|
+
const finalConfirmed = confirmation?.final_confirmed === true;
|
|
572
|
+
const hasSkipRecentColleagueOverride = [
|
|
573
|
+
"skip_recent_colleague_contacted",
|
|
574
|
+
"filter_recent_colleague_contacted",
|
|
575
|
+
"recent_colleague_contacted",
|
|
576
|
+
"colleague_contacted_filter",
|
|
577
|
+
"colleague_contacted"
|
|
578
|
+
].some((key) => Object.prototype.hasOwnProperty.call(overrides || {}, key));
|
|
579
|
+
const confirmationSkipRecentColleagueContacted = normalizeColleagueContactedFilterOverride(
|
|
580
|
+
Object.prototype.hasOwnProperty.call(confirmation || {}, "filter_recent_colleague_contacted_value")
|
|
581
|
+
? confirmation.filter_recent_colleague_contacted_value
|
|
582
|
+
: confirmation?.skip_recent_colleague_contacted_value
|
|
583
|
+
);
|
|
584
|
+
const explicitSchools = extractSchoolFilterExplicit(rawInstruction);
|
|
585
|
+
const explicitRecentViewed = extractRecentViewedExplicit(rawInstruction);
|
|
586
|
+
const explicitColleagueContactedFilter = extractColleagueContactedFilterExplicit(rawInstruction);
|
|
587
|
+
const explicitKeyword = extractFieldLineValue(rawInstruction, ["搜索关键词", "关键词", "keyword"]);
|
|
588
|
+
const explicitJob = extractFieldLineValue(rawInstruction, ["岗位", "职位", "job"]);
|
|
589
|
+
const explicitCity = extractFieldLineValue(rawInstruction, ["城市", "地点", "工作地", "base"]);
|
|
590
|
+
const explicitDegree = extractFieldLineValue(rawInstruction, ["学历", "degree"]);
|
|
591
|
+
const explicitExperience = extractFieldLineValue(rawInstruction, ["经验", "经验要求", "工作经验", "工作年限", "experience"]);
|
|
592
|
+
const explicitGender = extractFieldLineValue(rawInstruction, ["性别", "gender"]);
|
|
593
|
+
const explicitAge = extractFieldLineValue(rawInstruction, ["年龄", "年龄要求", "年龄范围", "age"]);
|
|
594
|
+
const explicitTargetCount = extractTargetCountExplicit(rawInstruction);
|
|
595
|
+
const explicitPostAction = extractPostActionExplicit(rawInstruction);
|
|
596
|
+
const parsed = {
|
|
597
|
+
job_explicit: explicitJob || extractJobExplicit(text),
|
|
598
|
+
city: sanitizeCityCandidate(explicitCity) || extractCity(text),
|
|
599
|
+
city_explicit: explicitCity !== null,
|
|
600
|
+
degree: normalizeDegreeFieldValue(explicitDegree) || extractDegree(text),
|
|
601
|
+
degree_explicit: explicitDegree !== null,
|
|
602
|
+
experience: normalizeExperienceOverride(explicitExperience),
|
|
603
|
+
experience_explicit: explicitExperience !== null,
|
|
604
|
+
gender: normalizeGenericSearchFilterOverride(explicitGender),
|
|
605
|
+
gender_explicit: explicitGender !== null,
|
|
606
|
+
age: normalizeGenericSearchFilterOverride(explicitAge),
|
|
607
|
+
age_explicit: explicitAge !== null,
|
|
608
|
+
schools: explicitSchools.explicit ? explicitSchools.schools : extractSchools(text),
|
|
609
|
+
schools_explicit: explicitSchools.explicit,
|
|
610
|
+
filter_recent_viewed: explicitRecentViewed !== null ? explicitRecentViewed : extractRecentViewedFilter(text),
|
|
611
|
+
skip_recent_colleague_contacted: explicitColleagueContactedFilter ?? confirmationSkipRecentColleagueContacted,
|
|
612
|
+
keyword_explicit: explicitKeyword || extractKeywordExplicit(text),
|
|
613
|
+
keyword_auto: extractKeywordAuto(text),
|
|
614
|
+
target_count: explicitTargetCount || extractTargetCount(text),
|
|
615
|
+
post_action_explicit: explicitPostAction || extractPostAction(text),
|
|
616
|
+
criteria_explicit: extractExplicitCriteria(rawInstruction)
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
if (overrides) {
|
|
620
|
+
const overrideCity = sanitizeCityCandidate(normalizeStringOverride(overrides.city));
|
|
621
|
+
const overrideDegree = normalizeStringOverride(overrides.degree);
|
|
622
|
+
const overrideDegrees = normalizeDegreesOverride(overrides.degrees || (Array.isArray(overrides.degree) ? overrides.degree : null));
|
|
623
|
+
const hasOverrideSchools = Object.prototype.hasOwnProperty.call(overrides, "schools")
|
|
624
|
+
|| Object.prototype.hasOwnProperty.call(overrides, "school_tag")
|
|
625
|
+
|| Object.prototype.hasOwnProperty.call(overrides, "school_tags");
|
|
626
|
+
const overrideSchools = normalizeSchoolsOverride(
|
|
627
|
+
Object.prototype.hasOwnProperty.call(overrides, "schools")
|
|
628
|
+
? overrides.schools
|
|
629
|
+
: Object.prototype.hasOwnProperty.call(overrides, "school_tag")
|
|
630
|
+
? overrides.school_tag
|
|
631
|
+
: overrides.school_tags
|
|
632
|
+
);
|
|
633
|
+
const overrideKeyword = normalizeStringOverride(overrides.keyword);
|
|
634
|
+
const overrideJob = normalizeStringOverride(overrides.job || overrides.job_title || overrides.selected_job);
|
|
635
|
+
const overrideCriteria = normalizeStringOverride(overrides.criteria);
|
|
636
|
+
const hasOverrideExperience = Object.prototype.hasOwnProperty.call(overrides, "experience")
|
|
637
|
+
|| Object.prototype.hasOwnProperty.call(overrides, "experiences")
|
|
638
|
+
|| Object.prototype.hasOwnProperty.call(overrides, "experience_range")
|
|
639
|
+
|| Object.prototype.hasOwnProperty.call(overrides, "experience_start")
|
|
640
|
+
|| Object.prototype.hasOwnProperty.call(overrides, "experience_end");
|
|
641
|
+
const overrideExperience = Object.prototype.hasOwnProperty.call(overrides, "experience")
|
|
642
|
+
? normalizeExperienceOverride(overrides.experience)
|
|
643
|
+
: Object.prototype.hasOwnProperty.call(overrides, "experiences")
|
|
644
|
+
? normalizeExperienceOverride(overrides.experiences)
|
|
645
|
+
: Object.prototype.hasOwnProperty.call(overrides, "experience_range")
|
|
646
|
+
? normalizeExperienceOverride(overrides.experience_range)
|
|
647
|
+
: hasOverrideExperience
|
|
648
|
+
? {
|
|
649
|
+
start: overrides.experience_start,
|
|
650
|
+
end: overrides.experience_end
|
|
651
|
+
}
|
|
652
|
+
: null;
|
|
653
|
+
const hasOverrideGender = Object.prototype.hasOwnProperty.call(overrides, "gender");
|
|
654
|
+
const overrideGender = hasOverrideGender ? normalizeGenericSearchFilterOverride(overrides.gender) : null;
|
|
655
|
+
const hasOverrideAge = Object.prototype.hasOwnProperty.call(overrides, "age")
|
|
656
|
+
|| Object.prototype.hasOwnProperty.call(overrides, "ages")
|
|
657
|
+
|| Object.prototype.hasOwnProperty.call(overrides, "age_range")
|
|
658
|
+
|| Object.prototype.hasOwnProperty.call(overrides, "age_min")
|
|
659
|
+
|| Object.prototype.hasOwnProperty.call(overrides, "age_max")
|
|
660
|
+
|| Object.prototype.hasOwnProperty.call(overrides, "min_age")
|
|
661
|
+
|| Object.prototype.hasOwnProperty.call(overrides, "max_age");
|
|
662
|
+
const overrideAge = Object.prototype.hasOwnProperty.call(overrides, "age")
|
|
663
|
+
? normalizeGenericSearchFilterOverride(overrides.age)
|
|
664
|
+
: Object.prototype.hasOwnProperty.call(overrides, "ages")
|
|
665
|
+
? normalizeGenericSearchFilterOverride(overrides.ages)
|
|
666
|
+
: Object.prototype.hasOwnProperty.call(overrides, "age_range")
|
|
667
|
+
? normalizeGenericSearchFilterOverride(overrides.age_range)
|
|
668
|
+
: hasOverrideAge
|
|
669
|
+
? {
|
|
670
|
+
min: overrides.age_min ?? overrides.min_age,
|
|
671
|
+
max: overrides.age_max ?? overrides.max_age
|
|
672
|
+
}
|
|
673
|
+
: null;
|
|
674
|
+
const overrideRecentViewed = normalizeRecentViewedOverride(
|
|
675
|
+
Object.prototype.hasOwnProperty.call(overrides, "filter_recent_viewed")
|
|
676
|
+
? overrides.filter_recent_viewed
|
|
677
|
+
: overrides.recent_not_view
|
|
678
|
+
);
|
|
679
|
+
const overrideSkipRecentColleagueContacted = normalizeColleagueContactedFilterOverride(
|
|
680
|
+
Object.prototype.hasOwnProperty.call(overrides, "skip_recent_colleague_contacted")
|
|
681
|
+
? overrides.skip_recent_colleague_contacted
|
|
682
|
+
: Object.prototype.hasOwnProperty.call(overrides, "filter_recent_colleague_contacted")
|
|
683
|
+
? overrides.filter_recent_colleague_contacted
|
|
684
|
+
: Object.prototype.hasOwnProperty.call(overrides, "recent_colleague_contacted")
|
|
685
|
+
? overrides.recent_colleague_contacted
|
|
686
|
+
: Object.prototype.hasOwnProperty.call(overrides, "colleague_contacted_filter")
|
|
687
|
+
? overrides.colleague_contacted_filter
|
|
688
|
+
: overrides.colleague_contacted
|
|
689
|
+
);
|
|
690
|
+
const overridePostAction = normalizePostAction(overrides.post_action);
|
|
691
|
+
if (overrideCity) parsed.city = overrideCity;
|
|
692
|
+
if (overrideDegree) parsed.degree = overrideDegree;
|
|
693
|
+
if (overrideDegrees?.length) parsed.degrees = overrideDegrees;
|
|
694
|
+
if (Object.prototype.hasOwnProperty.call(overrides, "city")) parsed.city_explicit = true;
|
|
695
|
+
if (Object.prototype.hasOwnProperty.call(overrides, "degree") || Object.prototype.hasOwnProperty.call(overrides, "degrees")) {
|
|
696
|
+
parsed.degree_explicit = true;
|
|
697
|
+
}
|
|
698
|
+
if (hasOverrideSchools && Array.isArray(overrideSchools)) {
|
|
699
|
+
parsed.schools = overrideSchools;
|
|
700
|
+
parsed.schools_explicit = true;
|
|
701
|
+
}
|
|
702
|
+
if (hasOverrideExperience) {
|
|
703
|
+
parsed.experience = overrideExperience;
|
|
704
|
+
parsed.experience_explicit = true;
|
|
705
|
+
}
|
|
706
|
+
if (hasOverrideGender) {
|
|
707
|
+
parsed.gender = overrideGender;
|
|
708
|
+
parsed.gender_explicit = true;
|
|
709
|
+
}
|
|
710
|
+
if (hasOverrideAge) {
|
|
711
|
+
parsed.age = overrideAge;
|
|
712
|
+
parsed.age_explicit = true;
|
|
713
|
+
}
|
|
714
|
+
if (overrideKeyword) parsed.keyword_override = overrideKeyword;
|
|
715
|
+
if (overrideJob) parsed.job_override = overrideJob;
|
|
716
|
+
if (overrideCriteria) parsed.criteria_override = overrideCriteria;
|
|
717
|
+
if (overrideRecentViewed !== null) parsed.filter_recent_viewed = overrideRecentViewed;
|
|
718
|
+
if (overrideSkipRecentColleagueContacted !== null) parsed.skip_recent_colleague_contacted = overrideSkipRecentColleagueContacted;
|
|
719
|
+
if (overridePostAction) parsed.post_action_override = overridePostAction;
|
|
720
|
+
if (Number.isFinite(overrides.max_greet_count) && overrides.max_greet_count > 0) {
|
|
721
|
+
parsed.max_greet_count_override = Number.parseInt(String(overrides.max_greet_count), 10);
|
|
722
|
+
}
|
|
723
|
+
if (Number.isFinite(overrides.target_count) && overrides.target_count > 0) {
|
|
724
|
+
parsed.target_count = Number.parseInt(String(overrides.target_count), 10);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const keywordResolution = resolveKeyword(parsed, confirmation);
|
|
729
|
+
const job = resolveJob(parsed, confirmation);
|
|
730
|
+
const postAction = resolvePostAction(parsed, confirmation);
|
|
731
|
+
const maxGreetCount = resolveMaxGreetCount(parsed, confirmation);
|
|
732
|
+
const confirmationCriteria = normalizeStringOverride(confirmation?.criteria_value);
|
|
733
|
+
const skipRecentColleagueContacted = typeof parsed.skip_recent_colleague_contacted === "boolean"
|
|
734
|
+
? parsed.skip_recent_colleague_contacted
|
|
735
|
+
: finalConfirmed
|
|
736
|
+
? false
|
|
737
|
+
: null;
|
|
738
|
+
const baseSearchParams = {
|
|
739
|
+
job,
|
|
740
|
+
city: parsed.city,
|
|
741
|
+
degree: parsed.degree,
|
|
742
|
+
degrees: parsed.degrees,
|
|
743
|
+
schools: parsed.schools,
|
|
744
|
+
experience: parsed.experience,
|
|
745
|
+
gender: parsed.gender,
|
|
746
|
+
age: parsed.age,
|
|
747
|
+
filter_recent_viewed: parsed.filter_recent_viewed,
|
|
748
|
+
skip_recent_colleague_contacted: skipRecentColleagueContacted,
|
|
749
|
+
keyword: keywordResolution.keyword
|
|
750
|
+
};
|
|
751
|
+
const criteria = parsed.criteria_override || confirmationCriteria || parsed.criteria_explicit || null;
|
|
752
|
+
const criteriaSource = parsed.criteria_override
|
|
753
|
+
? "override"
|
|
754
|
+
: confirmationCriteria
|
|
755
|
+
? "confirmation"
|
|
756
|
+
: parsed.criteria_explicit
|
|
757
|
+
? "instruction_block"
|
|
758
|
+
: "missing";
|
|
759
|
+
const baseScreenParams = {
|
|
760
|
+
criteria,
|
|
761
|
+
target_count: parsed.target_count,
|
|
762
|
+
post_action: postAction,
|
|
763
|
+
max_greet_count: maxGreetCount,
|
|
764
|
+
skip_recent_colleague_contacted: skipRecentColleagueContacted === true,
|
|
765
|
+
search_exchange_resume_filter_days: 30
|
|
766
|
+
};
|
|
767
|
+
const missingBeforeDefaults = collectMissingFields(baseSearchParams, baseScreenParams, parsed);
|
|
768
|
+
|
|
769
|
+
const useDefaultForMissing = finalConfirmed || confirmation?.use_default_for_missing === true;
|
|
770
|
+
const skipKeywordDefault = keywordResolution.needsConfirmation;
|
|
771
|
+
const defaultPreview = buildDefaultPreview(missingBeforeDefaults, { skipKeywordDefault });
|
|
772
|
+
const { searchParams, screenParams, appliedDefaults } = applyDefaults(
|
|
773
|
+
baseSearchParams,
|
|
774
|
+
baseScreenParams,
|
|
775
|
+
missingBeforeDefaults,
|
|
776
|
+
useDefaultForMissing,
|
|
777
|
+
{ skipKeywordDefault }
|
|
778
|
+
);
|
|
779
|
+
const missingAfterDefaults = collectUnresolvedMissingFields(missingBeforeDefaults, appliedDefaults);
|
|
780
|
+
const suspicious_fields = collectSuspiciousFields(searchParams, screenParams);
|
|
781
|
+
const needs_recent_viewed_filter_confirmation = !finalConfirmed && searchParams.filter_recent_viewed === null;
|
|
782
|
+
const needs_skip_recent_colleague_contacted_confirmation = (
|
|
783
|
+
!finalConfirmed
|
|
784
|
+
&& !hasSkipRecentColleagueOverride
|
|
785
|
+
&& confirmationSkipRecentColleagueContacted === null
|
|
786
|
+
&& confirmation?.skip_recent_colleague_contacted_confirmed !== true
|
|
787
|
+
&& confirmation?.filter_recent_colleague_contacted_confirmed !== true
|
|
788
|
+
);
|
|
789
|
+
const needs_criteria_confirmation = Boolean(screenParams.criteria) && !finalConfirmed && confirmation?.criteria_confirmed !== true;
|
|
790
|
+
const pending_questions = [
|
|
791
|
+
...buildMissingFieldQuestions(missingAfterDefaults, defaultPreview),
|
|
792
|
+
...(needs_recent_viewed_filter_confirmation
|
|
793
|
+
? [{
|
|
794
|
+
field: "filter_recent_viewed",
|
|
795
|
+
question: "是否需要过滤近14天查看过的人选?",
|
|
796
|
+
options: [
|
|
797
|
+
{ label: "需要过滤", value: true },
|
|
798
|
+
{ label: "不过滤", value: false }
|
|
799
|
+
]
|
|
800
|
+
}]
|
|
801
|
+
: []),
|
|
802
|
+
...(needs_skip_recent_colleague_contacted_confirmation
|
|
803
|
+
? [{
|
|
804
|
+
field: "filter_recent_colleague_contacted",
|
|
805
|
+
question: "是否过滤近期已被同事触达的人选?开启后搜索页会勾选 Boss 的“近30天未和同事交换简历”。",
|
|
806
|
+
value: true,
|
|
807
|
+
options: [
|
|
808
|
+
{ label: "过滤", value: true },
|
|
809
|
+
{ label: "不过滤", value: false }
|
|
810
|
+
]
|
|
811
|
+
}]
|
|
812
|
+
: []),
|
|
813
|
+
...(needs_criteria_confirmation
|
|
814
|
+
? [{
|
|
815
|
+
field: "criteria",
|
|
816
|
+
question: "请确认筛选 criteria 是否准确无误(尤其是硬性约束条件)?",
|
|
817
|
+
value: baseScreenParams.criteria
|
|
818
|
+
}]
|
|
819
|
+
: [])
|
|
820
|
+
];
|
|
821
|
+
const review = {
|
|
822
|
+
extracted_search_params: baseSearchParams,
|
|
823
|
+
extracted_screen_params: baseScreenParams,
|
|
824
|
+
current_search_params: searchParams,
|
|
825
|
+
current_screen_params: screenParams,
|
|
826
|
+
missing_fields: missingAfterDefaults,
|
|
827
|
+
missing_fields_before_defaults: missingBeforeDefaults,
|
|
828
|
+
has_unresolved_missing_fields: missingAfterDefaults.length > 0,
|
|
829
|
+
suspicious_fields,
|
|
830
|
+
pending_questions,
|
|
831
|
+
default_preview: defaultPreview,
|
|
832
|
+
applied_defaults: appliedDefaults,
|
|
833
|
+
criteria_source: criteriaSource,
|
|
834
|
+
final_confirmed: finalConfirmed
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
parsed,
|
|
839
|
+
searchParams,
|
|
840
|
+
screenParams,
|
|
841
|
+
missing_fields: missingAfterDefaults,
|
|
842
|
+
has_unresolved_missing_fields: missingAfterDefaults.length > 0,
|
|
843
|
+
suspicious_fields,
|
|
844
|
+
needs_keyword_confirmation: keywordResolution.needsConfirmation,
|
|
845
|
+
needs_recent_viewed_filter_confirmation,
|
|
846
|
+
needs_skip_recent_colleague_contacted_confirmation,
|
|
847
|
+
needs_criteria_confirmation,
|
|
848
|
+
needs_search_params_confirmation: !finalConfirmed && confirmation?.search_params_confirmed !== true,
|
|
849
|
+
proposed_keyword: keywordResolution.proposedKeyword,
|
|
850
|
+
pending_questions,
|
|
851
|
+
default_preview: defaultPreview,
|
|
852
|
+
applied_defaults: appliedDefaults,
|
|
853
|
+
review
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
export const recruitInstructionParserSemantics = Object.freeze({
|
|
858
|
+
source: "boss-recruit-mcp/src/parser.js",
|
|
859
|
+
imported_at: "2026-04-30",
|
|
860
|
+
default_param_values: DEFAULT_PARAM_VALUES,
|
|
861
|
+
school_labels: SEARCH_SCHOOL_MAP,
|
|
862
|
+
degree_values: Array.from(DEGREE_VALUES),
|
|
863
|
+
experience_values: ["不限", "在校/应届", "25年毕业", "26年毕业", "26年后毕业", "1-3年", "3-5年", "5-10年", "自定义"],
|
|
864
|
+
gender_values: ["不限", "男", "女"],
|
|
865
|
+
age_values: ["不限", "20-25", "25-30", "30-35", "35-40", "40-50", "50以上", "自定义"]
|
|
866
|
+
});
|