@reconcrap/boss-recommend-mcp 2.0.47 → 2.0.49

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.
Files changed (55) hide show
  1. package/bin/boss-recommend-mcp.js +4 -4
  2. package/config/screening-config.example.json +27 -27
  3. package/package.json +1 -1
  4. package/scripts/postinstall.cjs +44 -44
  5. package/skills/boss-chat/README.md +39 -39
  6. package/skills/boss-chat/SKILL.md +93 -93
  7. package/skills/boss-recommend-pipeline/README.md +12 -12
  8. package/skills/boss-recommend-pipeline/SKILL.md +180 -180
  9. package/skills/boss-recruit-pipeline/README.md +17 -17
  10. package/skills/boss-recruit-pipeline/SKILL.md +58 -58
  11. package/src/chat-mcp.js +1780 -1780
  12. package/src/chat-runtime-config.js +749 -749
  13. package/src/cli.js +3054 -3054
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +1586 -1453
  16. package/src/core/capture/index.js +1201 -1201
  17. package/src/core/cv-acquisition/index.js +238 -238
  18. package/src/core/cv-capture-target/index.js +299 -299
  19. package/src/core/greet-quota/index.js +54 -54
  20. package/src/core/infinite-list/index.js +1326 -1326
  21. package/src/core/reporting/legacy-csv.js +341 -341
  22. package/src/core/run/timing.js +33 -33
  23. package/src/core/self-heal/index.js +973 -973
  24. package/src/core/self-heal/viewport.js +564 -564
  25. package/src/domains/chat/cards.js +137 -137
  26. package/src/domains/chat/constants.js +221 -221
  27. package/src/domains/chat/detail.js +1668 -1668
  28. package/src/domains/chat/index.js +7 -7
  29. package/src/domains/chat/jobs.js +592 -592
  30. package/src/domains/chat/page-guard.js +98 -98
  31. package/src/domains/chat/roots.js +56 -56
  32. package/src/domains/chat/run-service.js +1977 -1977
  33. package/src/domains/recommend/actions.js +457 -457
  34. package/src/domains/recommend/cards.js +243 -243
  35. package/src/domains/recommend/constants.js +165 -165
  36. package/src/domains/recommend/detail.js +1 -1
  37. package/src/domains/recommend/filters.js +610 -610
  38. package/src/domains/recommend/index.js +10 -10
  39. package/src/domains/recommend/jobs.js +378 -316
  40. package/src/domains/recommend/refresh.js +491 -472
  41. package/src/domains/recommend/roots.js +80 -80
  42. package/src/domains/recommend/run-service.js +50 -29
  43. package/src/domains/recommend/scopes.js +246 -246
  44. package/src/domains/recruit/actions.js +277 -277
  45. package/src/domains/recruit/cards.js +74 -74
  46. package/src/domains/recruit/constants.js +167 -167
  47. package/src/domains/recruit/detail.js +461 -461
  48. package/src/domains/recruit/index.js +9 -9
  49. package/src/domains/recruit/instruction-parser.js +451 -451
  50. package/src/domains/recruit/refresh.js +44 -44
  51. package/src/domains/recruit/roots.js +68 -68
  52. package/src/domains/recruit/run-service.js +1207 -1207
  53. package/src/domains/recruit/search.js +1202 -1202
  54. package/src/recommend-mcp.js +22 -22
  55. package/src/recruit-mcp.js +1338 -1338
@@ -1,451 +1,451 @@
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
- city: null,
21
- degree: "不限",
22
- schools: [],
23
- keyword: "算法工程师",
24
- target_count: 10
25
- };
26
- const DEFAULT_PARAM_LABELS = {
27
- city: "不限城市",
28
- degree: "不限",
29
- schools: "不限院校标签",
30
- keyword: "算法工程师",
31
- target_count: 10
32
- };
33
- const DEGREE_VALUES = new Set(["不限", "本科", "本科及以上", "硕士及以上", "博士"]);
34
- const CITY_STOP_PATTERN = /(?:筛选|搜索|查找|找|做过|从事过|有过|相关|的人选|的人|并且|且|学历|学校|目标|必须|优先|,|。|;|;|,)/;
35
-
36
- function normalizeText(input) {
37
- return String(input || "").replace(/\s+/g, " ").trim();
38
- }
39
-
40
- function uniqueList(items) {
41
- return Array.from(new Set(items.filter(Boolean)));
42
- }
43
-
44
- function normalizeSchoolLabel(value) {
45
- if (typeof value !== "string") return null;
46
- const raw = value.trim();
47
- if (!raw) return null;
48
- if (KNOWN_SCHOOL_LABELS.has(raw)) return raw;
49
-
50
- const compact = raw.toLowerCase().replace(/\s+/g, "");
51
- const qsMatch = compact.match(/^qs(\d+)$/);
52
- if (qsMatch) {
53
- const rank = Number.parseInt(qsMatch[1], 10);
54
- if (Number.isFinite(rank)) return rank > 100 ? SEARCH_SCHOOL_MAP.qs500 : SEARCH_SCHOOL_MAP.qs100;
55
- }
56
- return SEARCH_SCHOOL_MAP[compact] || SEARCH_SCHOOL_MAP[raw] || raw;
57
- }
58
-
59
- function sanitizeCityCandidate(value) {
60
- if (typeof value !== "string") return null;
61
- let candidate = value.trim();
62
- if (!candidate) return null;
63
- candidate = candidate.replace(/^(在|是|为)\s*/, "").trim();
64
- const stopIndex = candidate.search(CITY_STOP_PATTERN);
65
- if (stopIndex >= 0) candidate = candidate.slice(0, stopIndex).trim();
66
- candidate = candidate.replace(/[的\s]+$/g, "").trim();
67
- return candidate || null;
68
- }
69
-
70
- function extractCity(text) {
71
- const patterns = [
72
- /地点(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
73
- /城市(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
74
- /工作地(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
75
- /base(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i
76
- ];
77
- for (const pattern of patterns) {
78
- const match = text.match(pattern);
79
- if (match?.[1]) {
80
- const city = sanitizeCityCandidate(match[1]);
81
- if (city) return city;
82
- }
83
- }
84
- return null;
85
- }
86
-
87
- function extractDegree(text) {
88
- if (/(博士及以上|博士)/.test(text)) return "博士";
89
- if (/(硕士及以上|硕士以上)/.test(text)) return "硕士及以上";
90
- if (/硕士/.test(text)) return "硕士";
91
- if (/(本科及以上|本科以上)/.test(text)) return "本科及以上";
92
- if (/本科/.test(text)) return "本科";
93
- return null;
94
- }
95
-
96
- function extractSchools(text) {
97
- const schools = [];
98
- if (/统招(?:本科)?/.test(text)) schools.push(SEARCH_SCHOOL_MAP["统招"]);
99
- if (/双一流(?:院校|学校)?/.test(text)) schools.push(SEARCH_SCHOOL_MAP["双一流"]);
100
- if (/(^|[^0-9])985([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["985"]);
101
- if (/(^|[^0-9])211([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["211"]);
102
- const qsMatches = text.matchAll(/\bqs\s*(\d+)\b/ig);
103
- for (const match of qsMatches) {
104
- const rank = Number.parseInt(match[1], 10);
105
- if (Number.isFinite(rank)) schools.push(rank > 100 ? SEARCH_SCHOOL_MAP.qs500 : SEARCH_SCHOOL_MAP.qs100);
106
- }
107
- return uniqueList(schools);
108
- }
109
-
110
- function extractRecentViewedFilter(text) {
111
- const negativePatterns = [
112
- /(?:不|别|无需|不用|不要).{0,6}(?:过滤|排除|去掉|剔除).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
113
- /(?:保留|包含).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
114
- /(?:近?14天(?:内)?查看(?:过)?).{0,8}(?:不要|不用|无需|不需要|不必).{0,4}(?:过滤|排除|去掉|剔除)/i
115
- ];
116
- if (negativePatterns.some((pattern) => pattern.test(text))) return false;
117
-
118
- const positivePatterns = [
119
- /(?:过滤|排除|去掉|剔除).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
120
- /(?:近?14天(?:内)?查看(?:过)?).{0,8}(?:过滤|排除|去掉|剔除)/i
121
- ];
122
- if (positivePatterns.some((pattern) => pattern.test(text))) return true;
123
- return null;
124
- }
125
-
126
- function normalizeStringOverride(value) {
127
- if (typeof value !== "string") return null;
128
- const normalized = value.trim();
129
- return normalized || null;
130
- }
131
-
132
- function normalizeSchoolsOverride(value) {
133
- if (Array.isArray(value)) return uniqueList(value.map(normalizeSchoolLabel));
134
- if (typeof value === "string") return uniqueList(value.split(/[,,]/).map(normalizeSchoolLabel));
135
- return null;
136
- }
137
-
138
- function normalizeDegreesOverride(value) {
139
- if (Array.isArray(value)) return uniqueList(value.map(normalizeText));
140
- if (typeof value === "string") return uniqueList(value.split(/[,,、|/]/).map(normalizeText));
141
- return null;
142
- }
143
-
144
- function extractKeywordExplicit(text) {
145
- const patterns = [
146
- /搜索关键词(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
147
- /关键词(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
148
- /keyword(?:\s*[::=]\s*|\s+is\s+)([^\n,。;;]+)/i
149
- ];
150
- for (const pattern of patterns) {
151
- const match = text.match(pattern);
152
- const keyword = match?.[1]?.trim();
153
- if (keyword) return keyword;
154
- }
155
- return null;
156
- }
157
-
158
- function extractKeywordAuto(text) {
159
- const patterns = [
160
- /做过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:的人选|的人|相关|并且|且|,|。|,|$)/i,
161
- /有过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:经验|背景|的人选|并且|且|,|。|,|$)/i,
162
- /从事过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:相关|的人选|并且|且|,|。|,|$)/i
163
- ];
164
- for (const pattern of patterns) {
165
- const match = text.match(pattern);
166
- const keyword = match?.[1]?.replace(/\s+/g, " ").trim();
167
- if (keyword && keyword.length >= 2) return keyword;
168
- }
169
- return null;
170
- }
171
-
172
- function extractTargetCount(text) {
173
- const patterns = [
174
- /至少筛选\s*(\d+)\s*位?/i,
175
- /目标(?:筛选)?(?:人数|数量)?(?:为|是|:|:)?\s*(\d+)/i,
176
- /目标(?:筛选)?(?:人数|数量)?\s*(\d+)\s*人/i,
177
- /筛选\s*(\d+)\s*位/i
178
- ];
179
- for (const pattern of patterns) {
180
- const match = text.match(pattern);
181
- if (match?.[1]) {
182
- const value = Number.parseInt(match[1], 10);
183
- if (Number.isFinite(value) && value > 0) return value;
184
- }
185
- }
186
- return null;
187
- }
188
-
189
- function sanitizeClause(clause) {
190
- return clause
191
- .replace(/^使用boss-recruit-pipeline skills/i, "")
192
- .replace(/^帮我(?:在boss上)?(?:找|筛选)/i, "")
193
- .replace(/^请(?:在boss上)?(?:帮我)?(?:找|筛选)/i, "")
194
- .replace(/^在boss上(?:帮我)?(?:找|筛选)/i, "")
195
- .replace(/的人选$/, "")
196
- .replace(/的人$/, "")
197
- .trim();
198
- }
199
-
200
- function isCountPlanningClause(clause) {
201
- return /(?:目标(?:筛选)?(?:人数|数量)?|至少筛选|筛选\s*\d+\s*位|输出\s*\d+\s*(?:位|个|个人选|个候选人)?|最终输出\s*\d+\s*(?:位|个|个人选|个候选人)?|处理\s*\d+\s*(?:位|人)|(?:浏览|拉取|抓取).*(?:至少\s*)?\d+\s*(?:位|个|个人选|个候选人)?|最匹配.*\d+\s*(?:位|个|个人选|个候选人)?)/i.test(clause);
202
- }
203
-
204
- function buildScreenCriteria(text, searchParams) {
205
- const clauses = text
206
- .split(/[,,。;;\n]/)
207
- .map((clause) => sanitizeClause(clause))
208
- .filter(Boolean);
209
-
210
- const normalized = clauses
211
- .filter((clause) => {
212
- if (/搜索关键词|关键词|keyword/i.test(clause)) return false;
213
- if (/地点|城市/.test(clause)) return false;
214
- if (/近?14天(?:内)?查看(?:过)?|过滤近14天查看/.test(clause)) return false;
215
- if (isCountPlanningClause(clause)) return false;
216
- return true;
217
- })
218
- .map((clause) => clause.replace(/\s+/g, " ").trim())
219
- .filter(Boolean);
220
-
221
- if (searchParams?.keyword) {
222
- const keywordClause = `候选人需有${searchParams.keyword}相关经历`;
223
- const alreadyCovered = normalized.some((clause) =>
224
- clause.toLowerCase().includes(String(searchParams.keyword).toLowerCase())
225
- );
226
- if (!alreadyCovered) normalized.unshift(keywordClause);
227
- }
228
-
229
- if (!normalized.length) {
230
- return searchParams?.keyword ? `候选人需有${searchParams.keyword}相关经历` : text;
231
- }
232
- return uniqueList(normalized).join(";");
233
- }
234
-
235
- function resolveKeyword(parsed, confirmation) {
236
- if (parsed.keyword_override) {
237
- return { keyword: parsed.keyword_override, needsConfirmation: false, proposedKeyword: null };
238
- }
239
- const explicit = parsed.keyword_explicit;
240
- const auto = parsed.keyword_auto;
241
- const confirmed = confirmation?.keyword_confirmed === true;
242
- const rejected = confirmation?.keyword_confirmed === false;
243
- const value = typeof confirmation?.keyword_value === "string" ? confirmation.keyword_value.trim() : "";
244
- if (confirmed && value) return { keyword: value, needsConfirmation: false, proposedKeyword: null };
245
- if (explicit) return { keyword: explicit, needsConfirmation: false, proposedKeyword: null };
246
- if (rejected) return { keyword: value || null, needsConfirmation: false, proposedKeyword: null };
247
- if (auto) {
248
- if (confirmed) return { keyword: auto, needsConfirmation: false, proposedKeyword: null };
249
- return { keyword: null, needsConfirmation: true, proposedKeyword: auto };
250
- }
251
- return { keyword: null, needsConfirmation: false, proposedKeyword: null };
252
- }
253
-
254
- function collectSuspiciousFields(searchParams, screenParams) {
255
- const suspicious = [];
256
- if (searchParams.city && (/\s/.test(searchParams.city) || CITY_STOP_PATTERN.test(searchParams.city) || searchParams.city.length > 8)) {
257
- suspicious.push({
258
- field: "city",
259
- value: searchParams.city,
260
- reason: "城市提取结果看起来包含多余短语,请确认是否为标准城市名。"
261
- });
262
- }
263
- if (searchParams.degree && !DEGREE_VALUES.has(searchParams.degree)) {
264
- suspicious.push({
265
- field: "degree",
266
- value: searchParams.degree,
267
- reason: "学历提取结果不在预期枚举内,请确认。"
268
- });
269
- }
270
- if (searchParams.keyword && /城市|学历|学校|目标人数|目标数量|筛选\d+位/i.test(searchParams.keyword)) {
271
- suspicious.push({
272
- field: "keyword",
273
- value: searchParams.keyword,
274
- reason: "关键词看起来混入了筛选条件,请确认是否只保留核心方向词。"
275
- });
276
- }
277
- if (screenParams.target_count && (!Number.isInteger(screenParams.target_count) || screenParams.target_count <= 0)) {
278
- suspicious.push({
279
- field: "target_count",
280
- value: screenParams.target_count,
281
- reason: "目标人数不是有效正整数,请确认。"
282
- });
283
- }
284
- return suspicious;
285
- }
286
-
287
- function buildDefaultPreview(missingFields, { skipKeywordDefault = false } = {}) {
288
- return missingFields.reduce((acc, field) => {
289
- if (field === "keyword" && skipKeywordDefault) return acc;
290
- acc[field] = DEFAULT_PARAM_LABELS[field];
291
- return acc;
292
- }, {});
293
- }
294
-
295
- function applyDefaults(searchParams, screenParams, missingFields, useDefaultForMissing, { skipKeywordDefault = false } = {}) {
296
- if (!useDefaultForMissing) {
297
- return { searchParams, screenParams, appliedDefaults: {} };
298
- }
299
- const appliedDefaults = {};
300
- const nextSearchParams = { ...searchParams };
301
- const nextScreenParams = { ...screenParams };
302
- if (missingFields.includes("city")) {
303
- nextSearchParams.city = DEFAULT_PARAM_VALUES.city;
304
- appliedDefaults.city = DEFAULT_PARAM_LABELS.city;
305
- }
306
- if (missingFields.includes("degree")) {
307
- nextSearchParams.degree = DEFAULT_PARAM_VALUES.degree;
308
- appliedDefaults.degree = DEFAULT_PARAM_LABELS.degree;
309
- }
310
- if (missingFields.includes("schools")) {
311
- nextSearchParams.schools = DEFAULT_PARAM_VALUES.schools.slice();
312
- appliedDefaults.schools = DEFAULT_PARAM_LABELS.schools;
313
- }
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
- return {
323
- searchParams: nextSearchParams,
324
- screenParams: nextScreenParams,
325
- appliedDefaults
326
- };
327
- }
328
-
329
- export function parseRecruitInstruction({ instruction, confirmation, overrides } = {}) {
330
- const text = normalizeText(instruction);
331
- const parsed = {
332
- city: extractCity(text),
333
- degree: extractDegree(text),
334
- schools: extractSchools(text),
335
- filter_recent_viewed: extractRecentViewedFilter(text),
336
- keyword_explicit: extractKeywordExplicit(text),
337
- keyword_auto: extractKeywordAuto(text),
338
- target_count: extractTargetCount(text)
339
- };
340
-
341
- if (overrides) {
342
- const overrideCity = sanitizeCityCandidate(normalizeStringOverride(overrides.city));
343
- const overrideDegree = normalizeStringOverride(overrides.degree);
344
- const overrideDegrees = normalizeDegreesOverride(overrides.degrees);
345
- const overrideSchools = normalizeSchoolsOverride(overrides.schools);
346
- const overrideKeyword = normalizeStringOverride(overrides.keyword);
347
- const overrideRecentViewed = typeof overrides.filter_recent_viewed === "boolean"
348
- ? overrides.filter_recent_viewed
349
- : null;
350
- if (overrideCity) parsed.city = overrideCity;
351
- if (overrideDegree) parsed.degree = overrideDegree;
352
- if (overrideDegrees?.length) parsed.degrees = overrideDegrees;
353
- if (overrideSchools?.length) parsed.schools = overrideSchools;
354
- if (overrideKeyword) parsed.keyword_override = overrideKeyword;
355
- if (overrideRecentViewed !== null) parsed.filter_recent_viewed = overrideRecentViewed;
356
- if (Number.isFinite(overrides.target_count) && overrides.target_count > 0) {
357
- parsed.target_count = Number.parseInt(String(overrides.target_count), 10);
358
- }
359
- }
360
-
361
- const keywordResolution = resolveKeyword(parsed, confirmation);
362
- const baseSearchParams = {
363
- city: parsed.city,
364
- degree: parsed.degree,
365
- degrees: parsed.degrees,
366
- schools: parsed.schools,
367
- filter_recent_viewed: parsed.filter_recent_viewed,
368
- keyword: keywordResolution.keyword
369
- };
370
- const baseScreenParams = {
371
- criteria: buildScreenCriteria(text, baseSearchParams),
372
- target_count: parsed.target_count
373
- };
374
- const missingBeforeDefaults = [];
375
- if (!baseSearchParams.city) missingBeforeDefaults.push("city");
376
- if (!baseSearchParams.degree) missingBeforeDefaults.push("degree");
377
- if (!baseSearchParams.schools?.length) missingBeforeDefaults.push("schools");
378
- if (!baseSearchParams.keyword) missingBeforeDefaults.push("keyword");
379
- if (!baseScreenParams.target_count) missingBeforeDefaults.push("target_count");
380
-
381
- const useDefaultForMissing = confirmation?.use_default_for_missing === true;
382
- const skipKeywordDefault = keywordResolution.needsConfirmation;
383
- const defaultPreview = buildDefaultPreview(missingBeforeDefaults, { skipKeywordDefault });
384
- const { searchParams, screenParams, appliedDefaults } = applyDefaults(
385
- baseSearchParams,
386
- baseScreenParams,
387
- missingBeforeDefaults,
388
- useDefaultForMissing,
389
- { skipKeywordDefault }
390
- );
391
- 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;
394
- const pending_questions = [
395
- ...(needs_recent_viewed_filter_confirmation
396
- ? [{
397
- field: "filter_recent_viewed",
398
- question: "是否需要过滤近14天查看过的人选?",
399
- options: [
400
- { label: "需要过滤", value: true },
401
- { label: "不过滤", value: false }
402
- ]
403
- }]
404
- : []),
405
- ...(needs_criteria_confirmation
406
- ? [{
407
- field: "criteria",
408
- question: "请确认筛选 criteria 是否准确无误(尤其是硬性约束条件)?",
409
- value: baseScreenParams.criteria
410
- }]
411
- : [])
412
- ];
413
- const review = {
414
- extracted_search_params: baseSearchParams,
415
- extracted_screen_params: baseScreenParams,
416
- current_search_params: searchParams,
417
- current_screen_params: screenParams,
418
- missing_fields: missingBeforeDefaults,
419
- has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
420
- suspicious_fields,
421
- pending_questions,
422
- default_preview: defaultPreview,
423
- applied_defaults: appliedDefaults
424
- };
425
-
426
- return {
427
- parsed,
428
- searchParams,
429
- screenParams,
430
- missing_fields: missingBeforeDefaults,
431
- has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
432
- suspicious_fields,
433
- needs_keyword_confirmation: keywordResolution.needsConfirmation,
434
- needs_recent_viewed_filter_confirmation,
435
- needs_criteria_confirmation,
436
- needs_search_params_confirmation: confirmation?.search_params_confirmed !== true,
437
- proposed_keyword: keywordResolution.proposedKeyword,
438
- pending_questions,
439
- default_preview: defaultPreview,
440
- applied_defaults: appliedDefaults,
441
- review
442
- };
443
- }
444
-
445
- export const recruitInstructionParserSemantics = Object.freeze({
446
- source: "boss-recruit-mcp/src/parser.js",
447
- imported_at: "2026-04-30",
448
- default_param_values: DEFAULT_PARAM_VALUES,
449
- school_labels: SEARCH_SCHOOL_MAP,
450
- degree_values: Array.from(DEGREE_VALUES)
451
- });
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
+ city: null,
21
+ degree: "不限",
22
+ schools: [],
23
+ keyword: "算法工程师",
24
+ target_count: 10
25
+ };
26
+ const DEFAULT_PARAM_LABELS = {
27
+ city: "不限城市",
28
+ degree: "不限",
29
+ schools: "不限院校标签",
30
+ keyword: "算法工程师",
31
+ target_count: 10
32
+ };
33
+ const DEGREE_VALUES = new Set(["不限", "本科", "本科及以上", "硕士及以上", "博士"]);
34
+ const CITY_STOP_PATTERN = /(?:筛选|搜索|查找|找|做过|从事过|有过|相关|的人选|的人|并且|且|学历|学校|目标|必须|优先|,|。|;|;|,)/;
35
+
36
+ function normalizeText(input) {
37
+ return String(input || "").replace(/\s+/g, " ").trim();
38
+ }
39
+
40
+ function uniqueList(items) {
41
+ return Array.from(new Set(items.filter(Boolean)));
42
+ }
43
+
44
+ function normalizeSchoolLabel(value) {
45
+ if (typeof value !== "string") return null;
46
+ const raw = value.trim();
47
+ if (!raw) return null;
48
+ if (KNOWN_SCHOOL_LABELS.has(raw)) return raw;
49
+
50
+ const compact = raw.toLowerCase().replace(/\s+/g, "");
51
+ const qsMatch = compact.match(/^qs(\d+)$/);
52
+ if (qsMatch) {
53
+ const rank = Number.parseInt(qsMatch[1], 10);
54
+ if (Number.isFinite(rank)) return rank > 100 ? SEARCH_SCHOOL_MAP.qs500 : SEARCH_SCHOOL_MAP.qs100;
55
+ }
56
+ return SEARCH_SCHOOL_MAP[compact] || SEARCH_SCHOOL_MAP[raw] || raw;
57
+ }
58
+
59
+ function sanitizeCityCandidate(value) {
60
+ if (typeof value !== "string") return null;
61
+ let candidate = value.trim();
62
+ if (!candidate) return null;
63
+ candidate = candidate.replace(/^(在|是|为)\s*/, "").trim();
64
+ const stopIndex = candidate.search(CITY_STOP_PATTERN);
65
+ if (stopIndex >= 0) candidate = candidate.slice(0, stopIndex).trim();
66
+ candidate = candidate.replace(/[的\s]+$/g, "").trim();
67
+ return candidate || null;
68
+ }
69
+
70
+ function extractCity(text) {
71
+ const patterns = [
72
+ /地点(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
73
+ /城市(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
74
+ /工作地(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i,
75
+ /base(?:在|是|为|:|:)?\s*([^\n,。;;、]+)/i
76
+ ];
77
+ for (const pattern of patterns) {
78
+ const match = text.match(pattern);
79
+ if (match?.[1]) {
80
+ const city = sanitizeCityCandidate(match[1]);
81
+ if (city) return city;
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+
87
+ function extractDegree(text) {
88
+ if (/(博士及以上|博士)/.test(text)) return "博士";
89
+ if (/(硕士及以上|硕士以上)/.test(text)) return "硕士及以上";
90
+ if (/硕士/.test(text)) return "硕士";
91
+ if (/(本科及以上|本科以上)/.test(text)) return "本科及以上";
92
+ if (/本科/.test(text)) return "本科";
93
+ return null;
94
+ }
95
+
96
+ function extractSchools(text) {
97
+ const schools = [];
98
+ if (/统招(?:本科)?/.test(text)) schools.push(SEARCH_SCHOOL_MAP["统招"]);
99
+ if (/双一流(?:院校|学校)?/.test(text)) schools.push(SEARCH_SCHOOL_MAP["双一流"]);
100
+ if (/(^|[^0-9])985([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["985"]);
101
+ if (/(^|[^0-9])211([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["211"]);
102
+ const qsMatches = text.matchAll(/\bqs\s*(\d+)\b/ig);
103
+ for (const match of qsMatches) {
104
+ const rank = Number.parseInt(match[1], 10);
105
+ if (Number.isFinite(rank)) schools.push(rank > 100 ? SEARCH_SCHOOL_MAP.qs500 : SEARCH_SCHOOL_MAP.qs100);
106
+ }
107
+ return uniqueList(schools);
108
+ }
109
+
110
+ function extractRecentViewedFilter(text) {
111
+ const negativePatterns = [
112
+ /(?:不|别|无需|不用|不要).{0,6}(?:过滤|排除|去掉|剔除).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
113
+ /(?:保留|包含).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
114
+ /(?:近?14天(?:内)?查看(?:过)?).{0,8}(?:不要|不用|无需|不需要|不必).{0,4}(?:过滤|排除|去掉|剔除)/i
115
+ ];
116
+ if (negativePatterns.some((pattern) => pattern.test(text))) return false;
117
+
118
+ const positivePatterns = [
119
+ /(?:过滤|排除|去掉|剔除).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
120
+ /(?:近?14天(?:内)?查看(?:过)?).{0,8}(?:过滤|排除|去掉|剔除)/i
121
+ ];
122
+ if (positivePatterns.some((pattern) => pattern.test(text))) return true;
123
+ return null;
124
+ }
125
+
126
+ function normalizeStringOverride(value) {
127
+ if (typeof value !== "string") return null;
128
+ const normalized = value.trim();
129
+ return normalized || null;
130
+ }
131
+
132
+ function normalizeSchoolsOverride(value) {
133
+ if (Array.isArray(value)) return uniqueList(value.map(normalizeSchoolLabel));
134
+ if (typeof value === "string") return uniqueList(value.split(/[,,]/).map(normalizeSchoolLabel));
135
+ return null;
136
+ }
137
+
138
+ function normalizeDegreesOverride(value) {
139
+ if (Array.isArray(value)) return uniqueList(value.map(normalizeText));
140
+ if (typeof value === "string") return uniqueList(value.split(/[,,、|/]/).map(normalizeText));
141
+ return null;
142
+ }
143
+
144
+ function extractKeywordExplicit(text) {
145
+ const patterns = [
146
+ /搜索关键词(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
147
+ /关键词(?:为|是|:|:)?\s*([^\n,。;;]+)/i,
148
+ /keyword(?:\s*[::=]\s*|\s+is\s+)([^\n,。;;]+)/i
149
+ ];
150
+ for (const pattern of patterns) {
151
+ const match = text.match(pattern);
152
+ const keyword = match?.[1]?.trim();
153
+ if (keyword) return keyword;
154
+ }
155
+ return null;
156
+ }
157
+
158
+ function extractKeywordAuto(text) {
159
+ const patterns = [
160
+ /做过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:的人选|的人|相关|并且|且|,|。|,|$)/i,
161
+ /有过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:经验|背景|的人选|并且|且|,|。|,|$)/i,
162
+ /从事过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:相关|的人选|并且|且|,|。|,|$)/i
163
+ ];
164
+ for (const pattern of patterns) {
165
+ const match = text.match(pattern);
166
+ const keyword = match?.[1]?.replace(/\s+/g, " ").trim();
167
+ if (keyword && keyword.length >= 2) return keyword;
168
+ }
169
+ return null;
170
+ }
171
+
172
+ function extractTargetCount(text) {
173
+ const patterns = [
174
+ /至少筛选\s*(\d+)\s*位?/i,
175
+ /目标(?:筛选)?(?:人数|数量)?(?:为|是|:|:)?\s*(\d+)/i,
176
+ /目标(?:筛选)?(?:人数|数量)?\s*(\d+)\s*人/i,
177
+ /筛选\s*(\d+)\s*位/i
178
+ ];
179
+ for (const pattern of patterns) {
180
+ const match = text.match(pattern);
181
+ if (match?.[1]) {
182
+ const value = Number.parseInt(match[1], 10);
183
+ if (Number.isFinite(value) && value > 0) return value;
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+
189
+ function sanitizeClause(clause) {
190
+ return clause
191
+ .replace(/^使用boss-recruit-pipeline skills/i, "")
192
+ .replace(/^帮我(?:在boss上)?(?:找|筛选)/i, "")
193
+ .replace(/^请(?:在boss上)?(?:帮我)?(?:找|筛选)/i, "")
194
+ .replace(/^在boss上(?:帮我)?(?:找|筛选)/i, "")
195
+ .replace(/的人选$/, "")
196
+ .replace(/的人$/, "")
197
+ .trim();
198
+ }
199
+
200
+ function isCountPlanningClause(clause) {
201
+ return /(?:目标(?:筛选)?(?:人数|数量)?|至少筛选|筛选\s*\d+\s*位|输出\s*\d+\s*(?:位|个|个人选|个候选人)?|最终输出\s*\d+\s*(?:位|个|个人选|个候选人)?|处理\s*\d+\s*(?:位|人)|(?:浏览|拉取|抓取).*(?:至少\s*)?\d+\s*(?:位|个|个人选|个候选人)?|最匹配.*\d+\s*(?:位|个|个人选|个候选人)?)/i.test(clause);
202
+ }
203
+
204
+ function buildScreenCriteria(text, searchParams) {
205
+ const clauses = text
206
+ .split(/[,,。;;\n]/)
207
+ .map((clause) => sanitizeClause(clause))
208
+ .filter(Boolean);
209
+
210
+ const normalized = clauses
211
+ .filter((clause) => {
212
+ if (/搜索关键词|关键词|keyword/i.test(clause)) return false;
213
+ if (/地点|城市/.test(clause)) return false;
214
+ if (/近?14天(?:内)?查看(?:过)?|过滤近14天查看/.test(clause)) return false;
215
+ if (isCountPlanningClause(clause)) return false;
216
+ return true;
217
+ })
218
+ .map((clause) => clause.replace(/\s+/g, " ").trim())
219
+ .filter(Boolean);
220
+
221
+ if (searchParams?.keyword) {
222
+ const keywordClause = `候选人需有${searchParams.keyword}相关经历`;
223
+ const alreadyCovered = normalized.some((clause) =>
224
+ clause.toLowerCase().includes(String(searchParams.keyword).toLowerCase())
225
+ );
226
+ if (!alreadyCovered) normalized.unshift(keywordClause);
227
+ }
228
+
229
+ if (!normalized.length) {
230
+ return searchParams?.keyword ? `候选人需有${searchParams.keyword}相关经历` : text;
231
+ }
232
+ return uniqueList(normalized).join(";");
233
+ }
234
+
235
+ function resolveKeyword(parsed, confirmation) {
236
+ if (parsed.keyword_override) {
237
+ return { keyword: parsed.keyword_override, needsConfirmation: false, proposedKeyword: null };
238
+ }
239
+ const explicit = parsed.keyword_explicit;
240
+ const auto = parsed.keyword_auto;
241
+ const confirmed = confirmation?.keyword_confirmed === true;
242
+ const rejected = confirmation?.keyword_confirmed === false;
243
+ const value = typeof confirmation?.keyword_value === "string" ? confirmation.keyword_value.trim() : "";
244
+ if (confirmed && value) return { keyword: value, needsConfirmation: false, proposedKeyword: null };
245
+ if (explicit) return { keyword: explicit, needsConfirmation: false, proposedKeyword: null };
246
+ if (rejected) return { keyword: value || null, needsConfirmation: false, proposedKeyword: null };
247
+ if (auto) {
248
+ if (confirmed) return { keyword: auto, needsConfirmation: false, proposedKeyword: null };
249
+ return { keyword: null, needsConfirmation: true, proposedKeyword: auto };
250
+ }
251
+ return { keyword: null, needsConfirmation: false, proposedKeyword: null };
252
+ }
253
+
254
+ function collectSuspiciousFields(searchParams, screenParams) {
255
+ const suspicious = [];
256
+ if (searchParams.city && (/\s/.test(searchParams.city) || CITY_STOP_PATTERN.test(searchParams.city) || searchParams.city.length > 8)) {
257
+ suspicious.push({
258
+ field: "city",
259
+ value: searchParams.city,
260
+ reason: "城市提取结果看起来包含多余短语,请确认是否为标准城市名。"
261
+ });
262
+ }
263
+ if (searchParams.degree && !DEGREE_VALUES.has(searchParams.degree)) {
264
+ suspicious.push({
265
+ field: "degree",
266
+ value: searchParams.degree,
267
+ reason: "学历提取结果不在预期枚举内,请确认。"
268
+ });
269
+ }
270
+ if (searchParams.keyword && /城市|学历|学校|目标人数|目标数量|筛选\d+位/i.test(searchParams.keyword)) {
271
+ suspicious.push({
272
+ field: "keyword",
273
+ value: searchParams.keyword,
274
+ reason: "关键词看起来混入了筛选条件,请确认是否只保留核心方向词。"
275
+ });
276
+ }
277
+ if (screenParams.target_count && (!Number.isInteger(screenParams.target_count) || screenParams.target_count <= 0)) {
278
+ suspicious.push({
279
+ field: "target_count",
280
+ value: screenParams.target_count,
281
+ reason: "目标人数不是有效正整数,请确认。"
282
+ });
283
+ }
284
+ return suspicious;
285
+ }
286
+
287
+ function buildDefaultPreview(missingFields, { skipKeywordDefault = false } = {}) {
288
+ return missingFields.reduce((acc, field) => {
289
+ if (field === "keyword" && skipKeywordDefault) return acc;
290
+ acc[field] = DEFAULT_PARAM_LABELS[field];
291
+ return acc;
292
+ }, {});
293
+ }
294
+
295
+ function applyDefaults(searchParams, screenParams, missingFields, useDefaultForMissing, { skipKeywordDefault = false } = {}) {
296
+ if (!useDefaultForMissing) {
297
+ return { searchParams, screenParams, appliedDefaults: {} };
298
+ }
299
+ const appliedDefaults = {};
300
+ const nextSearchParams = { ...searchParams };
301
+ const nextScreenParams = { ...screenParams };
302
+ if (missingFields.includes("city")) {
303
+ nextSearchParams.city = DEFAULT_PARAM_VALUES.city;
304
+ appliedDefaults.city = DEFAULT_PARAM_LABELS.city;
305
+ }
306
+ if (missingFields.includes("degree")) {
307
+ nextSearchParams.degree = DEFAULT_PARAM_VALUES.degree;
308
+ appliedDefaults.degree = DEFAULT_PARAM_LABELS.degree;
309
+ }
310
+ if (missingFields.includes("schools")) {
311
+ nextSearchParams.schools = DEFAULT_PARAM_VALUES.schools.slice();
312
+ appliedDefaults.schools = DEFAULT_PARAM_LABELS.schools;
313
+ }
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
+ return {
323
+ searchParams: nextSearchParams,
324
+ screenParams: nextScreenParams,
325
+ appliedDefaults
326
+ };
327
+ }
328
+
329
+ export function parseRecruitInstruction({ instruction, confirmation, overrides } = {}) {
330
+ const text = normalizeText(instruction);
331
+ const parsed = {
332
+ city: extractCity(text),
333
+ degree: extractDegree(text),
334
+ schools: extractSchools(text),
335
+ filter_recent_viewed: extractRecentViewedFilter(text),
336
+ keyword_explicit: extractKeywordExplicit(text),
337
+ keyword_auto: extractKeywordAuto(text),
338
+ target_count: extractTargetCount(text)
339
+ };
340
+
341
+ if (overrides) {
342
+ const overrideCity = sanitizeCityCandidate(normalizeStringOverride(overrides.city));
343
+ const overrideDegree = normalizeStringOverride(overrides.degree);
344
+ const overrideDegrees = normalizeDegreesOverride(overrides.degrees);
345
+ const overrideSchools = normalizeSchoolsOverride(overrides.schools);
346
+ const overrideKeyword = normalizeStringOverride(overrides.keyword);
347
+ const overrideRecentViewed = typeof overrides.filter_recent_viewed === "boolean"
348
+ ? overrides.filter_recent_viewed
349
+ : null;
350
+ if (overrideCity) parsed.city = overrideCity;
351
+ if (overrideDegree) parsed.degree = overrideDegree;
352
+ if (overrideDegrees?.length) parsed.degrees = overrideDegrees;
353
+ if (overrideSchools?.length) parsed.schools = overrideSchools;
354
+ if (overrideKeyword) parsed.keyword_override = overrideKeyword;
355
+ if (overrideRecentViewed !== null) parsed.filter_recent_viewed = overrideRecentViewed;
356
+ if (Number.isFinite(overrides.target_count) && overrides.target_count > 0) {
357
+ parsed.target_count = Number.parseInt(String(overrides.target_count), 10);
358
+ }
359
+ }
360
+
361
+ const keywordResolution = resolveKeyword(parsed, confirmation);
362
+ const baseSearchParams = {
363
+ city: parsed.city,
364
+ degree: parsed.degree,
365
+ degrees: parsed.degrees,
366
+ schools: parsed.schools,
367
+ filter_recent_viewed: parsed.filter_recent_viewed,
368
+ keyword: keywordResolution.keyword
369
+ };
370
+ const baseScreenParams = {
371
+ criteria: buildScreenCriteria(text, baseSearchParams),
372
+ target_count: parsed.target_count
373
+ };
374
+ const missingBeforeDefaults = [];
375
+ if (!baseSearchParams.city) missingBeforeDefaults.push("city");
376
+ if (!baseSearchParams.degree) missingBeforeDefaults.push("degree");
377
+ if (!baseSearchParams.schools?.length) missingBeforeDefaults.push("schools");
378
+ if (!baseSearchParams.keyword) missingBeforeDefaults.push("keyword");
379
+ if (!baseScreenParams.target_count) missingBeforeDefaults.push("target_count");
380
+
381
+ const useDefaultForMissing = confirmation?.use_default_for_missing === true;
382
+ const skipKeywordDefault = keywordResolution.needsConfirmation;
383
+ const defaultPreview = buildDefaultPreview(missingBeforeDefaults, { skipKeywordDefault });
384
+ const { searchParams, screenParams, appliedDefaults } = applyDefaults(
385
+ baseSearchParams,
386
+ baseScreenParams,
387
+ missingBeforeDefaults,
388
+ useDefaultForMissing,
389
+ { skipKeywordDefault }
390
+ );
391
+ 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;
394
+ const pending_questions = [
395
+ ...(needs_recent_viewed_filter_confirmation
396
+ ? [{
397
+ field: "filter_recent_viewed",
398
+ question: "是否需要过滤近14天查看过的人选?",
399
+ options: [
400
+ { label: "需要过滤", value: true },
401
+ { label: "不过滤", value: false }
402
+ ]
403
+ }]
404
+ : []),
405
+ ...(needs_criteria_confirmation
406
+ ? [{
407
+ field: "criteria",
408
+ question: "请确认筛选 criteria 是否准确无误(尤其是硬性约束条件)?",
409
+ value: baseScreenParams.criteria
410
+ }]
411
+ : [])
412
+ ];
413
+ const review = {
414
+ extracted_search_params: baseSearchParams,
415
+ extracted_screen_params: baseScreenParams,
416
+ current_search_params: searchParams,
417
+ current_screen_params: screenParams,
418
+ missing_fields: missingBeforeDefaults,
419
+ has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
420
+ suspicious_fields,
421
+ pending_questions,
422
+ default_preview: defaultPreview,
423
+ applied_defaults: appliedDefaults
424
+ };
425
+
426
+ return {
427
+ parsed,
428
+ searchParams,
429
+ screenParams,
430
+ missing_fields: missingBeforeDefaults,
431
+ has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
432
+ suspicious_fields,
433
+ needs_keyword_confirmation: keywordResolution.needsConfirmation,
434
+ needs_recent_viewed_filter_confirmation,
435
+ needs_criteria_confirmation,
436
+ needs_search_params_confirmation: confirmation?.search_params_confirmed !== true,
437
+ proposed_keyword: keywordResolution.proposedKeyword,
438
+ pending_questions,
439
+ default_preview: defaultPreview,
440
+ applied_defaults: appliedDefaults,
441
+ review
442
+ };
443
+ }
444
+
445
+ export const recruitInstructionParserSemantics = Object.freeze({
446
+ source: "boss-recruit-mcp/src/parser.js",
447
+ imported_at: "2026-04-30",
448
+ default_param_values: DEFAULT_PARAM_VALUES,
449
+ school_labels: SEARCH_SCHOOL_MAP,
450
+ degree_values: Array.from(DEGREE_VALUES)
451
+ });