@reconcrap/boss-recruit-mcp 1.0.19 → 1.0.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recruit-mcp",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "description": "Unified MCP pipeline for boss-search-cli and boss-screen-cli",
5
5
  "keywords": [
6
6
  "boss",
@@ -192,6 +192,9 @@ description: "Use when users ask to recruit candidates on Boss Zhipin via the bo
192
192
  - 同时也要让用户确认其他已提取参数里是否有误;
193
193
  - 必须单独确认筛选 `criteria` 是否准确(尤其学历/学校/论文等硬性条件不能丢失);
194
194
  - 若 `required_confirmations` 或 `pending_questions` 里包含 `filter_recent_viewed`,必须明确补问:是否需要过滤近 14 天查看过的人选;
195
+ - 若用户回复中没有明确给出 `filter_recent_viewed` 的是/否,禁止直接再次调用流水线;必须先追问一个单点问题并拿到布尔值(需要过滤/不过滤);
196
+ - 对于多轮对话,必须维护一个“已确认参数快照”(city/degree/schools/keyword/filter_recent_viewed/target_count/criteria);后续每次重试都要把快照写入 `overrides` 与 `confirmation`,不能只重述自然语言 instruction;
197
+ - 当用户已用结构化格式(如“关键词:…,城市:…”)确认过参数后,后续调用不得丢弃这些字段;若缺字段,优先沿用快照值并提示用户可修改;
195
198
  - 若确认,带 `confirmation.keyword_confirmed=true` 和 `keyword_value` 再次调用;
196
199
  - 若用户修改关键词,传用户给的新词作为 `keyword_value` 再次调用;
197
200
  - 对“是否过滤近 14 天查看”这个问题,需把用户选择写入 `overrides.filter_recent_viewed=true/false` 再次调用。
package/src/cli.js CHANGED
@@ -133,39 +133,14 @@ function writeInstalledSkillVersion(version) {
133
133
  fs.writeFileSync(markerPath, `${version}\n`, "utf8");
134
134
  }
135
135
 
136
- function getChromeUserDataDir(port, options = {}) {
137
- const rawProvided = options.userDataDir ?? options["user-data-dir"];
138
- const provided = typeof rawProvided === "string" ? rawProvided.trim() : "";
139
- const basePath = provided || resolveDefaultChromeUserDataDir(port);
140
- const targetPath = path.resolve(basePath);
141
- ensureDir(targetPath);
142
- return targetPath;
143
- }
144
-
145
- function getSharedChromeUserDataDir(port) {
146
- return path.join(getCodexHome(), "boss-mcp", `chrome-profile-${port}`);
147
- }
148
-
149
- function getLegacyRecruitChromeUserDataDir(port) {
150
- return path.join(getCodexHome(), "boss-recruit-mcp", `chrome-profile-${port}`);
151
- }
152
-
153
- function getLegacyRecommendChromeUserDataDir(port) {
154
- return path.join(os.homedir(), ".boss-recommend-mcp", `chrome-profile-${port}`);
155
- }
156
-
157
- function resolveDefaultChromeUserDataDir(port) {
158
- const sharedPath = getSharedChromeUserDataDir(port);
159
- if (pathExists(sharedPath)) {
160
- return sharedPath;
161
- }
162
- const legacyPaths = [
163
- getLegacyRecruitChromeUserDataDir(port),
164
- getLegacyRecommendChromeUserDataDir(port)
165
- ];
166
- const legacyExisting = legacyPaths.find((candidate) => pathExists(candidate));
167
- return legacyExisting || sharedPath;
168
- }
136
+ function getChromeUserDataDir(port, options = {}) {
137
+ const rawProvided = options.userDataDir ?? options["user-data-dir"];
138
+ const provided = typeof rawProvided === "string" ? rawProvided.trim() : "";
139
+ const basePath = provided || path.join(getCodexHome(), "boss-recruit-mcp", `chrome-profile-${port}`);
140
+ const targetPath = path.resolve(basePath);
141
+ ensureDir(targetPath);
142
+ return targetPath;
143
+ }
169
144
 
170
145
  function parseOptions(args) {
171
146
  const options = {};
package/src/parser.js CHANGED
@@ -31,6 +31,8 @@ const DEFAULT_PARAM_LABELS = {
31
31
  };
32
32
  const DEGREE_VALUES = new Set(["不限", "本科", "本科及以上", "硕士及以上", "博士"]);
33
33
  const CITY_STOP_PATTERN = /(?:筛选|搜索|查找|找|做过|从事过|有过|相关|的人选|的人|并且|且|学历|学校|目标|必须|优先|,|。|;|;|,)/;
34
+ const SEARCH_INTENT_LEADING_PATTERN = /^(?:帮我|请)?(?:在boss上|在boss直聘上)?(?:找|筛选|搜索|查找)/i;
35
+ const SEARCH_INTENT_META_PATTERN = /(?:城市|地点|学历|学校|目标|人数|候选人|人选|岗位|职位|做过|有过|从事过|本科|硕士|博士)/i;
34
36
 
35
37
  function normalizeText(input) {
36
38
  return String(input || "").replace(/\s+/g, " ").trim();
@@ -91,6 +93,19 @@ function extractCity(text) {
91
93
  }
92
94
  }
93
95
 
96
+ const implicitPatterns = [
97
+ /(?:^|[,,。;;\n])(?:帮我|请)?(?:在boss上|在boss直聘上)?(?:找|筛选|搜索|查找)\s*([^\s,。;;、]{2,12}?)(?=(?:本科|硕士|博士|做过|有过|从事过|的人选|的人|候选人|工程师|研究员|岗位|职位|,|。|;|;|,|$))/i,
98
+ /(?:^|[,,。;;\n])在\s*([^\s,。;;、]{2,12}?)(?=(?:招|招聘|找|筛选|搜索|查找|本科|硕士|博士|做过|有过|从事过|的人选|的人|候选人|工程师|研究员|岗位|职位|,|。|;|;|,|$))/i
99
+ ];
100
+
101
+ for (const pattern of implicitPatterns) {
102
+ const m = text.match(pattern);
103
+ if (m && m[1]) {
104
+ const city = sanitizeCityCandidate(m[1]);
105
+ if (city) return city;
106
+ }
107
+ }
108
+
94
109
  return null;
95
110
  }
96
111
 
@@ -183,9 +198,9 @@ function extractKeywordExplicit(text) {
183
198
 
184
199
  function extractKeywordAuto(text) {
185
200
  const patterns = [
186
- /做过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:的人选|的人|相关|并且|且|,|。|,|$)/i,
187
- /有过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:经验|背景|的人选|并且|且|,|。|,|$)/i,
188
- /从事过\s*([A-Za-z0-9+#./\-\s]{2,40}?)(?:相关|的人选|并且|且|,|。|,|$)/i
201
+ /做(?:过)?\s*([\u4e00-\u9fa5A-Za-z0-9+#./\-\s]{2,40}?)(?:的人选|的人|相关|并且|且|,|。|,|$)/i,
202
+ /有过\s*([\u4e00-\u9fa5A-Za-z0-9+#./\-\s]{2,40}?)(?:经验|背景|的人选|并且|且|,|。|,|$)/i,
203
+ /从事过\s*([\u4e00-\u9fa5A-Za-z0-9+#./\-\s]{2,40}?)(?:相关|的人选|并且|且|,|。|,|$)/i
189
204
  ];
190
205
 
191
206
  for (const pattern of patterns) {
@@ -216,6 +231,21 @@ function extractTargetCount(text) {
216
231
  return null;
217
232
  }
218
233
 
234
+ function extractExplicitCriteria(text) {
235
+ const patterns = [
236
+ /(?:筛选条件|筛选要求|硬性条件|筛选标准|要求)(?:为|是|:|:)\s*([^\n]+)/i
237
+ ];
238
+
239
+ for (const pattern of patterns) {
240
+ const m = text.match(pattern);
241
+ if (m && m[1]) {
242
+ const criteria = m[1].trim();
243
+ if (criteria) return criteria;
244
+ }
245
+ }
246
+ return null;
247
+ }
248
+
219
249
  function sanitizeClause(clause) {
220
250
  return clause
221
251
  .replace(/^使用boss-recruit-pipeline skills/i, "")
@@ -232,6 +262,11 @@ function isCountPlanningClause(clause) {
232
262
  }
233
263
 
234
264
  function buildScreenCriteria(text, searchParams) {
265
+ const explicitCriteria = extractExplicitCriteria(text);
266
+ if (explicitCriteria) {
267
+ return explicitCriteria;
268
+ }
269
+
235
270
  const clauses = text
236
271
  .split(/[,,。;;\n]/)
237
272
  .map((s) => sanitizeClause(s))
@@ -242,6 +277,7 @@ function buildScreenCriteria(text, searchParams) {
242
277
  if (/地点|城市/.test(clause)) return false;
243
278
  if (/近?14天(?:内)?查看(?:过)?|过滤近14天查看/.test(clause)) return false;
244
279
  if (isCountPlanningClause(clause)) return false;
280
+ if (SEARCH_INTENT_LEADING_PATTERN.test(clause) && SEARCH_INTENT_META_PATTERN.test(clause)) return false;
245
281
  return true;
246
282
  });
247
283
 
@@ -443,7 +479,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
443
479
  if (!baseSearchParams.city) missingBeforeDefaults.push("city");
444
480
  if (!baseSearchParams.degree) missingBeforeDefaults.push("degree");
445
481
  if (!baseSearchParams.schools || baseSearchParams.schools.length === 0) missingBeforeDefaults.push("schools");
446
- if (!baseSearchParams.keyword) missingBeforeDefaults.push("keyword");
482
+ if (!baseSearchParams.keyword && !keywordResolution.needsConfirmation) missingBeforeDefaults.push("keyword");
447
483
  if (!baseScreenParams.target_count) missingBeforeDefaults.push("target_count");
448
484
 
449
485
  const useDefaultForMissing = confirmation?.use_default_for_missing === true;
@@ -302,6 +302,39 @@ function testCriteriaConfirmationPrompt() {
302
302
  assert.equal(confirmedCriteria.needs_criteria_confirmation, false);
303
303
  }
304
304
 
305
+ function testNaturalSentenceExtractsCityAndKeywordWithoutMissing() {
306
+ const r = parseRecruitInstruction({
307
+ instruction: "找杭州本科及以上做算法的人,本科学校必须是211或qs100院校,有CCF-A论文或会议成果,目标3人",
308
+ confirmation: null,
309
+ overrides: null
310
+ });
311
+
312
+ assert.equal(r.searchParams.city, "杭州");
313
+ assert.equal(r.proposed_keyword, "算法");
314
+ assert.deepEqual(r.missing_fields, []);
315
+ assert.equal(r.has_unresolved_missing_fields, false);
316
+ assert.equal(r.needs_keyword_confirmation, true);
317
+ assert.match(r.screenParams.criteria, /CCF-A论文/);
318
+ }
319
+
320
+ function testExplicitCriteriaTextIsPreserved() {
321
+ const r = parseRecruitInstruction({
322
+ instruction:
323
+ "关键词:算法,城市:杭州,学历:本科及以上,学校:985、211、qs100,目标人数:3人,筛选条件:必须有CCF-A论文或者会议成果,本科学校必须至少是211或者qs100院校",
324
+ confirmation: {
325
+ keyword_confirmed: true,
326
+ keyword_value: "算法",
327
+ search_params_confirmed: true
328
+ },
329
+ overrides: null
330
+ });
331
+
332
+ assert.equal(
333
+ r.screenParams.criteria,
334
+ "必须有CCF-A论文或者会议成果,本科学校必须至少是211或者qs100院校"
335
+ );
336
+ }
337
+
305
338
  function main() {
306
339
  testNeedInput();
307
340
  testExampleExtraction();
@@ -314,6 +347,8 @@ function main() {
314
347
  testDefaultsCanOnlyApplyWhenExplicitlyRequested();
315
348
  testRecentViewedFilterPromptAndNegativeOverride();
316
349
  testCriteriaConfirmationPrompt();
350
+ testNaturalSentenceExtractsCityAndKeywordWithoutMissing();
351
+ testExplicitCriteriaTextIsPreserved();
317
352
  // eslint-disable-next-line no-console
318
353
  console.log("parser tests passed");
319
354
  }