@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.
Files changed (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +2 -1
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2416 -2385
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1315 -1306
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +44 -27
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +655 -646
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +575 -351
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1616 -878
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +121 -4
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +1174 -914
  66. package/src/recommend-scheduler.js +535 -469
  67. package/src/recruit-mcp.js +2121 -2121
package/src/index.js CHANGED
@@ -880,6 +880,14 @@ function createRunInputSchema() {
880
880
  type: "string",
881
881
  enum: ["不限", "近14天没有"]
882
882
  },
883
+ current_city_only: {
884
+ type: "boolean",
885
+ description: "可选;true 表示仅推荐期望城市为当前已选城市的牛人,默认 false。不会切换城市。"
886
+ },
887
+ activity_level: {
888
+ type: "string",
889
+ description: "可选活跃度单选。标准选项:不限、刚刚活跃、今日活跃、3日内活跃、本周活跃、本月活跃。允许同义词、相对时间、英文和轻微错别字;系统会归一化为最靠近用户意图的标准选项,完全无法理解时默认 不限。"
890
+ },
883
891
  skip_recent_colleague_contacted: {
884
892
  type: "boolean",
885
893
  description: "推荐页默认 true,用于跳过近14天同事沟通过的人选。搜索页请使用 recruit 工具的 filter_recent_colleague_contacted。"
@@ -987,6 +995,21 @@ function createRunInputSchema() {
987
995
  type: "boolean",
988
996
  description: "高级测试开关;默认 false。只有显式为 true 时才允许 deterministic/local scorer、跳过筛选器、card-only、dry-run 后置动作等调试路径"
989
997
  },
998
+ debug_force_list_end_after_processed: {
999
+ type: "integer",
1000
+ minimum: 1,
1001
+ description: "仅限 debug_test_mode=true 的 live 诊断:在完成指定候选人数后的循环边界恰好一次模拟列表到底,并进入真实 Page.reload 刷新/筛选重放分支。与另外两个 debug_force_*_after_processed 参数互斥。"
1002
+ },
1003
+ debug_force_context_recovery_after_processed: {
1004
+ type: "integer",
1005
+ minimum: 1,
1006
+ description: "仅限 debug_test_mode=true 的 live 诊断:在完成指定候选人数后的循环边界恰好一次调用真实 recommend 上下文恢复与筛选重放。与另外两个 debug_force_*_after_processed 参数互斥。"
1007
+ },
1008
+ debug_force_cdp_reconnect_after_processed: {
1009
+ type: "integer",
1010
+ minimum: 1,
1011
+ description: "仅限 debug_test_mode=true 的 live 诊断:在完成指定候选人数后的循环边界恰好一次关闭当前原始 CDP 连接,随后依赖 guarded client 原生重连继续。不会执行页面脚本。与另外两个 debug_force_*_after_processed 参数互斥。"
1012
+ },
990
1013
  screening_mode: {
991
1014
  type: "string",
992
1015
  enum: ["llm", "deterministic"],
@@ -1339,10 +1362,49 @@ function createListRecommendJobsInputSchema() {
1339
1362
 
1340
1363
  function createScheduleRunInputSchema() {
1341
1364
  const base = createRunInputSchema();
1365
+ const properties = { ...base.properties };
1366
+ for (const field of [
1367
+ "debug_test_mode",
1368
+ "debug_force_list_end_after_processed",
1369
+ "debug_force_context_recovery_after_processed",
1370
+ "debug_force_cdp_reconnect_after_processed"
1371
+ ]) {
1372
+ delete properties[field];
1373
+ }
1374
+ properties.screening_mode = {
1375
+ ...properties.screening_mode,
1376
+ enum: ["llm"],
1377
+ description: "定时 Recommend 仅允许生产 LLM 筛选;deterministic/local 调试模式不可定时。"
1378
+ };
1379
+ properties.use_llm = {
1380
+ ...properties.use_llm,
1381
+ enum: [true],
1382
+ description: "定时 Recommend 仅允许 true;use_llm=false 属于调试模式。"
1383
+ };
1384
+ properties.detail_limit = {
1385
+ ...properties.detail_limit,
1386
+ minimum: 1
1387
+ };
1388
+ properties.allow_card_only_screening = {
1389
+ ...properties.allow_card_only_screening,
1390
+ enum: [false]
1391
+ };
1392
+ properties.dry_run_post_action = {
1393
+ ...properties.dry_run_post_action,
1394
+ enum: [false]
1395
+ };
1396
+ properties.no_filter = {
1397
+ ...properties.no_filter,
1398
+ enum: [false]
1399
+ };
1400
+ properties.filter_enabled = {
1401
+ ...properties.filter_enabled,
1402
+ enum: [true]
1403
+ };
1342
1404
  return {
1343
1405
  ...base,
1344
1406
  properties: {
1345
- ...base.properties,
1407
+ ...properties,
1346
1408
  schedule_id: {
1347
1409
  type: "string",
1348
1410
  description: "可选,自定义定时任务 id;默认自动生成"
@@ -1417,6 +1479,11 @@ function createCompactRunInputSchema() {
1417
1479
  },
1418
1480
  gender: { type: "string" },
1419
1481
  recent_not_view: { type: "string" },
1482
+ current_city_only: { type: "boolean" },
1483
+ activity_level: {
1484
+ type: "string",
1485
+ description: "标准选项为 不限、刚刚活跃、今日活跃、3日内活跃、本周活跃、本月活跃;非精确输入会按最靠近的用户意图归一化,无法理解时默认 不限。"
1486
+ },
1420
1487
  skip_recent_colleague_contacted: { type: "boolean" },
1421
1488
  filter_recent_colleague_contacted: {
1422
1489
  type: "boolean",
@@ -1450,6 +1517,10 @@ function createCompactRunInputSchema() {
1450
1517
  slow_live: { type: "boolean" },
1451
1518
  delay_ms: { type: "integer", minimum: 0 },
1452
1519
  detail_limit: { type: "integer", minimum: 0 },
1520
+ debug_test_mode: { type: "boolean" },
1521
+ debug_force_list_end_after_processed: { type: "integer", minimum: 1 },
1522
+ debug_force_context_recovery_after_processed: { type: "integer", minimum: 1 },
1523
+ debug_force_cdp_reconnect_after_processed: { type: "integer", minimum: 1 },
1453
1524
  execute_post_action: { type: "boolean" },
1454
1525
  no_filter: { type: "boolean" },
1455
1526
  dry_run: { type: "boolean" }
@@ -1461,10 +1532,31 @@ function createCompactRunInputSchema() {
1461
1532
 
1462
1533
  function createCompactScheduleRunInputSchema() {
1463
1534
  const base = createCompactRunInputSchema();
1535
+ const properties = { ...base.properties };
1536
+ for (const field of [
1537
+ "debug_test_mode",
1538
+ "debug_force_list_end_after_processed",
1539
+ "debug_force_context_recovery_after_processed",
1540
+ "debug_force_cdp_reconnect_after_processed"
1541
+ ]) {
1542
+ delete properties[field];
1543
+ }
1544
+ properties.detail_limit = {
1545
+ ...properties.detail_limit,
1546
+ minimum: 1
1547
+ };
1548
+ properties.no_filter = {
1549
+ ...properties.no_filter,
1550
+ enum: [false]
1551
+ };
1552
+ properties.dry_run = {
1553
+ ...properties.dry_run,
1554
+ enum: [false]
1555
+ };
1464
1556
  return {
1465
1557
  ...base,
1466
1558
  properties: {
1467
- ...base.properties,
1559
+ ...properties,
1468
1560
  schedule_id: {
1469
1561
  type: "string",
1470
1562
  description: "可选,自定义定时任务 id;默认自动生成"
@@ -1821,7 +1913,32 @@ function validateRunArgs(args) {
1821
1913
  return "arguments must be an object";
1822
1914
  }
1823
1915
  if (!args.instruction || typeof args.instruction !== "string") {
1824
- return "instruction is required and must be a string";
1916
+ return "instruction is required and must be a string";
1917
+ }
1918
+ if (
1919
+ Object.prototype.hasOwnProperty.call(args, "debug_test_mode")
1920
+ && typeof args.debug_test_mode !== "boolean"
1921
+ ) {
1922
+ return "debug_test_mode must be a boolean";
1923
+ }
1924
+ const debugBoundaryFields = [
1925
+ "debug_force_list_end_after_processed",
1926
+ "debug_force_context_recovery_after_processed",
1927
+ "debug_force_cdp_reconnect_after_processed"
1928
+ ];
1929
+ const configuredDebugBoundaries = [];
1930
+ for (const field of debugBoundaryFields) {
1931
+ if (!Object.prototype.hasOwnProperty.call(args, field)) continue;
1932
+ if (!Number.isInteger(args[field]) || args[field] <= 0) {
1933
+ return `${field} must be a positive integer`;
1934
+ }
1935
+ configuredDebugBoundaries.push(field);
1936
+ }
1937
+ if (configuredDebugBoundaries.length > 1) {
1938
+ return `${configuredDebugBoundaries.join(", ")} are mutually exclusive`;
1939
+ }
1940
+ if (configuredDebugBoundaries.length && args.debug_test_mode !== true) {
1941
+ return `${configuredDebugBoundaries[0]} requires debug_test_mode=true`;
1825
1942
  }
1826
1943
  return null;
1827
1944
  }
@@ -3111,7 +3228,7 @@ async function handleRequest(message, workspaceRoot) {
3111
3228
  );
3112
3229
  }
3113
3230
 
3114
- if ([TOOL_RUN_RECOMMEND, TOOL_START_RUN].includes(toolName)) {
3231
+ if ([TOOL_RUN_RECOMMEND, TOOL_START_RUN, TOOL_PREPARE_RUN].includes(toolName)) {
3115
3232
  const inputError = validateRunArgs(args);
3116
3233
  if (inputError) {
3117
3234
  return createJsonRpcError(id, -32602, inputError);
package/src/parser.js CHANGED
@@ -28,6 +28,50 @@ const DEGREE_ORDER = [
28
28
  ];
29
29
  const GENDER_OPTIONS = ["不限", "男", "女"];
30
30
  const RECENT_NOT_VIEW_OPTIONS = ["不限", "近14天没有"];
31
+ const ACTIVITY_LEVEL_OPTIONS = [
32
+ "不限",
33
+ "刚刚活跃",
34
+ "今日活跃",
35
+ "3日内活跃",
36
+ "本周活跃",
37
+ "本月活跃"
38
+ ];
39
+ const ACTIVITY_LEVEL_ALIASES = Object.freeze({
40
+ "不限": [
41
+ "不限", "不限制", "无要求", "没有要求", "任意", "全部", "都可以", "均可", "随便", "无所谓",
42
+ "nolimit", "norestriction", "unrestricted", "any", "all", "anytime", "nopreference", "doesntmatter", "whatever"
43
+ ],
44
+ "刚刚活跃": [
45
+ "刚刚活跃", "刚才活跃", "刚刚", "刚才", "刚上线", "刚刚上线", "当前在线", "现在在线", "正在在线", "在线", "最近活跃", "近期活跃",
46
+ "高活跃", "高活跃度", "活跃度高", "高度活跃", "非常活跃", "很活跃", "超级活跃", "极其活跃", "最活跃", "高频活跃",
47
+ "activenow", "online", "onlinenow", "justnow", "justactive", "justonline", "currentlyactive", "currentlyonline",
48
+ "recentlyactive", "activerecently", "highactivity", "highlyactive", "veryactive", "superactive", "extremelyactive", "mostactive"
49
+ ],
50
+ "今日活跃": [
51
+ "今日活跃", "今天活跃", "今日", "今天", "当日", "当天", "当天活跃", "24小时内", "近24小时", "最近24小时", "过去24小时",
52
+ "today", "activetoday", "todayactive", "daily", "dailyactive", "每日活跃", "sameday", "withinthelastday", "inthelastday", "duringthecurrentday",
53
+ "within24hours", "last24hours", "past24hours"
54
+ ],
55
+ "3日内活跃": [
56
+ "3日内活跃", "3天内活跃", "三日内活跃", "三天内活跃", "3日内", "3天内", "三日内", "三天内",
57
+ "近3日", "近3天", "最近3日", "最近3天", "过去3日", "过去3天", "昨日", "昨天", "前天", "最近几天",
58
+ "within3days", "last3days", "past3days", "recent3days", "yesterday", "daybeforeyesterday", "recentfewdays"
59
+ ],
60
+ "本周活跃": [
61
+ "本周活跃", "本舟活跃", "这周活跃", "本周", "这周", "这个星期", "本星期", "本礼拜", "这礼拜", "一周内", "近一周", "最近一周", "近7天", "最近7天", "7日内", "7天内",
62
+ "中等活跃", "中度活跃", "一般活跃", "普通活跃", "适度活跃", "活跃度一般", "中等活跃度",
63
+ "thisweek", "currentweek", "thisweekend", "weekend", "withinweek", "withinaweek", "withinthelastweek", "inthelastweek", "inthepastweek",
64
+ "duringthecurrentweek", "overthelastweek", "overthepastweek", "lastweek", "pastweek", "weekly", "recent7days", "last7days", "past7days",
65
+ "mediumactivity", "moderatelyactive", "mediumactive", "fairlyactive", "averageactivity", "averagelyactive", "normallyactive"
66
+ ],
67
+ "本月活跃": [
68
+ "本月活跃", "这个月活跃", "本月", "这个月", "当月", "一个月内", "近一个月", "最近一个月", "近30天", "最近30天", "30日内", "30天内",
69
+ "低活跃", "低活跃度", "活跃度低", "活跃度不高", "低度活跃", "不太活跃", "偶尔活跃", "较少活跃", "很少活跃", "低频活跃",
70
+ "thismonth", "currentmonth", "withinmonth", "withinamonth", "withinthelastmonth", "inthelastmonth", "inthepastmonth", "duringthecurrentmonth",
71
+ "overthelastmonth", "overthepastmonth", "lastmonth", "pastmonth", "monthly", "recent30days", "last30days", "past30days",
72
+ "lowactivity", "lowactive", "lessactive", "notveryactive", "occasionallyactive", "rarelyactive", "infrequentlyactive", "seldomactive"
73
+ ]
74
+ });
31
75
  const FILTER_CONFIRM_OPTIONS = [
32
76
  { label: "筛选项无误,继续", value: "confirm" },
33
77
  { label: "筛选项需要调整", value: "revise" }
@@ -97,10 +141,11 @@ const MAX_GREET_COUNT_PATTERNS = [
97
141
  /最多(?:打招呼|沟通|联系)\s*(\d+)\s*(?:位|人|个)?/i,
98
142
  /(?:打招呼|沟通|联系)(?:上限|最多|不超过|至多)(?:为|是|:|:)?\s*(\d+)/i
99
143
  ];
144
+ const RUN_META_FIELD_LABEL_PATTERN = "(?:页面选择|学校标签|院校标签|学历|学位|性别|是否过滤近14天看过|当前城市筛选|仅推荐本城市|仅推荐期望城市为本城市(?:的牛人)?|current[_\\s-]?city[_\\s-]?only|活跃度|活动度|activity[_\\s-]?level|筛选条件|目标筛选数|目标通过人数|通过筛选后动作|最大招呼数|最大打招呼数|岗位)";
100
145
  const CRITERIA_EXPLICIT_MARKER_PATTERN = /筛选条件\s*[::]/i;
101
- const CRITERIA_EXPLICIT_STOP_PATTERN = /(?:^|[\n;;])\s*(?:页面选择|学校标签|院校标签|学历|学位|性别|是否过滤近14天看过|目标筛选数|目标通过人数|通过筛选后动作|最大招呼数|最大打招呼数|岗位)\s*[::]/i;
146
+ const CRITERIA_EXPLICIT_STOP_PATTERN = new RegExp(`(?:^|[\\s;;])\\s*${RUN_META_FIELD_LABEL_PATTERN}\\s*[::]`, "i");
102
147
  const CRITERIA_META_FIELD_PREFIX_PATTERNS = [
103
- /^(?:页面选择|学校标签|院校标签|学历|学位|性别|是否过滤近14天看过|目标筛选数|目标通过人数|通过筛选后动作|最大招呼数|最大打招呼数|岗位)\s*(?:[::]|$)/i,
148
+ new RegExp(`^${RUN_META_FIELD_LABEL_PATTERN}\\s*(?:[::]|$)`, "i"),
104
149
  /^(?:近?14天(?:内)?(?:没有|没看过|未查看)|(?:不过滤|保留|过滤|排除)[^。;;\n]{0,12}14天)\s*(?:[::]|$)?/i,
105
150
  /^(?:目标(?:处理|筛选|通过)?(?:人数|数量)?|至少(?:处理|筛选|通过)|(?:处理|筛选|通过)\s*\d+\s*(?:位|人))(?:[::\s]|$)/i,
106
151
  /^(?:最多(?:打招呼|沟通|联系)|(?:打招呼|沟通|联系)(?:上限|最多|不超过|至多))(?:[::\s]|$)/i,
@@ -302,13 +347,244 @@ function normalizeRecentNotView(value) {
302
347
  return RECENT_NOT_VIEW_OPTIONS.includes(normalized) ? normalized : null;
303
348
  }
304
349
 
350
+ function normalizeActivityIntentText(value) {
351
+ return normalizeText(value)
352
+ .normalize("NFKC")
353
+ .toLowerCase()
354
+ .replace(/[內内]/g, "内")
355
+ .replace(/[週周]/g, "周")
356
+ .replace(/[剛刚]/g, "刚")
357
+ .replace(/[當当]/g, "当")
358
+ .replace(/[無无]/g, "无")
359
+ .replace(/[躍跃]/g, "跃");
360
+ }
361
+
362
+ function compactActivityIntentText(value) {
363
+ return normalizeActivityIntentText(value).replace(/[\s_\-—–::,,。;;、/|()[\]{}'"“”‘’]+/g, "");
364
+ }
365
+
366
+ function stripActivityIntentWrappers(value) {
367
+ let current = compactActivityIntentText(value);
368
+ for (let index = 0; index < 4; index += 1) {
369
+ const previous = current;
370
+ current = current
371
+ .replace(/^(?:please|showme|iwant|want|setto|select|choose|filterby|activitylevel|activitystatus|activity|active|活跃度单选|活跃度|活动度|请选择|请选|选择|设置为|设为|设置|筛选为|筛选|仅推荐|只推荐|推荐|仅看|只看|只要|仅要|要求|希望|我要|想要|选项为|选项是|为|是|选)/, "")
372
+ .replace(/(?:candidates?|people|users?|talents?|option|active|的牛人|牛人|的候选人|候选人|的人选|人选|的人才|人才|的用户|用户|的人|选项|档位|程度|活跃度|活跃)$/u, "")
373
+ .replace(/^(?:为|是|to|is)/, "");
374
+ if (current === previous) break;
375
+ }
376
+ return current;
377
+ }
378
+
379
+ function parseChineseActivityNumber(value) {
380
+ const normalized = String(value || "");
381
+ if (/^\d+(?:\.\d+)?$/.test(normalized)) return Number(normalized);
382
+ if (normalized === "半") return 0.5;
383
+ const englishNumbers = {
384
+ half: 0.5,
385
+ one: 1,
386
+ two: 2,
387
+ couple: 2,
388
+ three: 3,
389
+ few: 3,
390
+ four: 4,
391
+ five: 5,
392
+ six: 6,
393
+ seven: 7,
394
+ eight: 8,
395
+ nine: 9,
396
+ ten: 10,
397
+ twenty: 20,
398
+ thirty: 30
399
+ };
400
+ if (Object.prototype.hasOwnProperty.call(englishNumbers, normalized.toLowerCase())) {
401
+ return englishNumbers[normalized.toLowerCase()];
402
+ }
403
+ const digits = { 零: 0, 一: 1, 二: 2, 两: 2, 三: 3, 四: 4, 五: 5, 六: 6, 七: 7, 八: 8, 九: 9 };
404
+ if (normalized === "十") return 10;
405
+ if (normalized.includes("十")) {
406
+ const [tens, ones] = normalized.split("十");
407
+ return (tens ? digits[tens] || 0 : 1) * 10 + (ones ? digits[ones] || 0 : 0);
408
+ }
409
+ return digits[normalized] ?? null;
410
+ }
411
+
412
+ function activityLevelForRelativeDuration(value) {
413
+ const normalized = normalizeActivityIntentText(value);
414
+ const match = normalized.match(
415
+ /(?:近|最近|过去|过去的|within\s*|last\s*|past\s*)?(\d+(?:\.\d+)?|半|[一二两三四五六七八九十]+|half|one|two|couple|three|few|four|five|six|seven|eight|nine|ten|twenty|thirty)\s*(?:个)?\s*(小时|hours?|hrs?|hr|h|天|日|days?|day|d|周|星期|weeks?|week|w|月|months?|month)(?:内|以内|之内|ago)?/i
416
+ );
417
+ if (!match) return null;
418
+ const amount = parseChineseActivityNumber(match[1]);
419
+ if (!Number.isFinite(amount) || amount < 0) return null;
420
+ const unit = match[2].toLowerCase();
421
+ if (/^(?:小时|hours?|hrs?|hr|h)$/.test(unit)) {
422
+ if (amount <= 2) return "刚刚活跃";
423
+ if (amount <= 24) return "今日活跃";
424
+ return activityLevelForRelativeDuration(`${Math.ceil(amount / 24)}天`);
425
+ }
426
+ if (/^(?:天|日|days?|day|d)$/.test(unit)) {
427
+ if (amount <= 1) return "今日活跃";
428
+ const nominalWindows = [
429
+ { days: 3, activityLevel: "3日内活跃" },
430
+ { days: 7, activityLevel: "本周活跃" },
431
+ { days: 30, activityLevel: "本月活跃" }
432
+ ];
433
+ return nominalWindows.reduce((closest, candidate) => {
434
+ const closestDistance = Math.abs(amount - closest.days);
435
+ const candidateDistance = Math.abs(amount - candidate.days);
436
+ if (candidateDistance < closestDistance) return candidate;
437
+ if (candidateDistance === closestDistance && candidate.days > closest.days) return candidate;
438
+ return closest;
439
+ }, nominalWindows[0]).activityLevel;
440
+ }
441
+ if (/^(?:周|星期|weeks?|week|w)$/.test(unit)) {
442
+ return activityLevelForRelativeDuration(`${amount * 7}天`);
443
+ }
444
+ return "本月活跃";
445
+ }
446
+
447
+ function activityLevelsForSemanticPhrase(value) {
448
+ const normalized = normalizeActivityIntentText(value);
449
+ const compact = compactActivityIntentText(normalized);
450
+ const englishQualitativeText = normalized
451
+ .replace(/\bnot\s+(?:very|highly|high|super|extremely)\b/g, " low ");
452
+ const highQualitativeText = compact
453
+ .replace(/not(?:very|highly|high|super|extremely)active|nothighactivity/g, "")
454
+ .replace(/不太活跃|很少活跃|较少活跃|偶尔活跃|低频活跃|低度活跃|低活跃|活跃度不高|活跃度低/g, "");
455
+ const hasEnglishActivityContext = /\b(?:activity|active)\b/i.test(englishQualitativeText);
456
+ const hasChineseActivityContext = /活跃/.test(normalized);
457
+ const hasHighQualitativeIntent = (
458
+ /highactivity|highlyactive|veryactive|superactive|extremelyactive|mostactive|高活跃|活跃度高|高度活跃|非常活跃|很活跃|超级活跃|极其活跃|最活跃|高频活跃/.test(highQualitativeText)
459
+ || (hasEnglishActivityContext && /\b(?:high|highly|very|super|extremely|most)\b/i.test(englishQualitativeText))
460
+ || (hasChineseActivityContext && /高|非常|超级|极其|最活跃/.test(highQualitativeText))
461
+ );
462
+ const hasMediumQualitativeIntent = (
463
+ /mediumactivity|moderatelyactive|mediumactive|fairlyactive|averageactivity|averagelyactive|normallyactive|中等活跃|中度活跃|一般活跃|普通活跃|适度活跃|活跃度一般/.test(compact)
464
+ || (hasEnglishActivityContext && /\b(?:medium|moderate|moderately|fair|fairly|average|normal|normally)\b/i.test(englishQualitativeText))
465
+ || (hasChineseActivityContext && /中等|中度|一般|普通|适度/.test(normalized))
466
+ );
467
+ const hasLowQualitativeIntent = (
468
+ /lowactivity|lowactive|lessactive|notveryactive|nothighactivity|occasionallyactive|rarelyactive|infrequentlyactive|seldomactive|低活跃|活跃度低|活跃度不高|低度活跃|不太活跃|偶尔活跃|较少活跃|很少活跃|低频活跃/.test(compact)
469
+ || (hasEnglishActivityContext && /\b(?:low|less|rare|rarely|occasional|occasionally|infrequent|infrequently|seldom)\b/i.test(englishQualitativeText))
470
+ || (hasChineseActivityContext && /低|不太|偶尔|较少|很少|低频/.test(normalized))
471
+ );
472
+ const matches = [];
473
+ if (
474
+ /(?:no|without)(?:activity)?(?:limit|restriction|preference)|notrestricted|notactive|notonline|anyactivity|allactivity|dontcare|doesntmatter|不限|不限制|无要求|没有要求|任意|都可以|均可|无所谓/.test(compact)
475
+ ) {
476
+ matches.push("不限");
477
+ }
478
+ if (/justnow|rightnow|currentlyactive|currentlyonline|onlinenow|刚刚|刚才|刚上线|当前在线|现在在线/.test(compact)) {
479
+ matches.push("刚刚活跃");
480
+ }
481
+ if (hasHighQualitativeIntent) {
482
+ matches.push("刚刚活跃");
483
+ }
484
+ if (/activetoday|todayactive|today|sameday|(?:within|during|in|over)?(?:the)?(?:last|past|current)(?:one)?day|今日|今天|当日|当天/.test(compact)) {
485
+ matches.push("今日活跃");
486
+ }
487
+ if (/yesterday|daybeforeyesterday|recentfewdays|昨日|昨天|前天|最近几天/.test(compact)) {
488
+ matches.push("3日内活跃");
489
+ }
490
+ if (/(?:this|current)week|(?:within|during|in|over)?(?:the)?(?:last|past)(?:one)?week|thisweekend|weekend|本周|这周|这个星期|本星期/.test(compact)) {
491
+ matches.push("本周活跃");
492
+ }
493
+ if (hasMediumQualitativeIntent) {
494
+ matches.push("本周活跃");
495
+ }
496
+ if (/(?:this|current)month|(?:within|during|in|over)?(?:the)?(?:last|past)(?:one)?month|本月|这个月|当月/.test(compact)) {
497
+ matches.push("本月活跃");
498
+ }
499
+ if (hasLowQualitativeIntent) {
500
+ matches.push("本月活跃");
501
+ }
502
+ return uniqueList(matches);
503
+ }
504
+
505
+ function activityEditDistance(left, right) {
506
+ const a = String(left || "");
507
+ const b = String(right || "");
508
+ const previous = Array.from({ length: b.length + 1 }, (_, index) => index);
509
+ for (let leftIndex = 1; leftIndex <= a.length; leftIndex += 1) {
510
+ const current = [leftIndex];
511
+ for (let rightIndex = 1; rightIndex <= b.length; rightIndex += 1) {
512
+ current[rightIndex] = Math.min(
513
+ current[rightIndex - 1] + 1,
514
+ previous[rightIndex] + 1,
515
+ previous[rightIndex - 1] + (a[leftIndex - 1] === b[rightIndex - 1] ? 0 : 1)
516
+ );
517
+ }
518
+ previous.splice(0, previous.length, ...current);
519
+ }
520
+ return previous[b.length];
521
+ }
522
+
523
+ function resolveActivityLevelIntent(value) {
524
+ const normalized = normalizeActivityIntentText(value);
525
+ if (!normalized) return { value: "不限", recognized: false, method: "fallback" };
526
+ const compact = compactActivityIntentText(normalized);
527
+ const stripped = stripActivityIntentWrappers(compact);
528
+ const candidates = uniqueList([compact, stripped]);
529
+
530
+ for (const [activityLevel, aliases] of Object.entries(ACTIVITY_LEVEL_ALIASES)) {
531
+ const compactAliases = aliases.map((alias) => compactActivityIntentText(alias));
532
+ if (candidates.some((candidate) => compactAliases.includes(candidate))) {
533
+ return { value: activityLevel, recognized: true, method: "alias" };
534
+ }
535
+ }
536
+
537
+ const durationLevel = activityLevelForRelativeDuration(normalized)
538
+ || activityLevelForRelativeDuration(stripped);
539
+ const semanticLevels = activityLevelsForSemanticPhrase(normalized);
540
+ const inferredLevels = uniqueList([...semanticLevels, durationLevel]);
541
+ if (inferredLevels.length === 1) {
542
+ return {
543
+ value: inferredLevels[0],
544
+ recognized: true,
545
+ method: durationLevel ? "duration" : "semantic"
546
+ };
547
+ }
548
+ if (inferredLevels.length > 1) {
549
+ return { value: "不限", recognized: false, method: "ambiguous" };
550
+ }
551
+
552
+ const fuzzyMatches = [];
553
+ for (const [activityLevel, aliases] of Object.entries(ACTIVITY_LEVEL_ALIASES)) {
554
+ for (const alias of aliases) {
555
+ const compactAlias = compactActivityIntentText(alias);
556
+ if (compactAlias.length < 4) continue;
557
+ for (const candidate of candidates) {
558
+ if (candidate.length < 4 || Math.abs(candidate.length - compactAlias.length) > 1) continue;
559
+ const distance = activityEditDistance(candidate, compactAlias);
560
+ if (distance <= 1) fuzzyMatches.push({ activityLevel, distance });
561
+ }
562
+ }
563
+ }
564
+ if (fuzzyMatches.length) {
565
+ const minimumDistance = Math.min(...fuzzyMatches.map((item) => item.distance));
566
+ const closestLevels = uniqueList(
567
+ fuzzyMatches.filter((item) => item.distance === minimumDistance).map((item) => item.activityLevel)
568
+ );
569
+ if (closestLevels.length === 1) {
570
+ return { value: closestLevels[0], recognized: true, method: "fuzzy" };
571
+ }
572
+ }
573
+
574
+ return { value: "不限", recognized: false, method: "fallback" };
575
+ }
576
+
577
+ function normalizeActivityLevel(value) {
578
+ return resolveActivityLevelIntent(value).value;
579
+ }
580
+
305
581
  function normalizeBooleanOverride(value) {
306
582
  if (typeof value === "boolean") return value;
307
583
  if (typeof value === "number") return value !== 0;
308
584
  const normalized = normalizeText(value).toLowerCase();
309
585
  if (!normalized) return null;
310
- if (["true", "yes", "y", "1", "on", "enable", "enabled", "需要", "是", "开启"].includes(normalized)) return true;
311
- if (["false", "no", "n", "0", "off", "disable", "disabled", "不需要", "否", "关闭"].includes(normalized)) return false;
586
+ if (["true", "yes", "y", "1", "on", "enable", "enabled", "需要", "是", "开启", "启用", "勾选", "选中"].includes(normalized)) return true;
587
+ if (["false", "no", "n", "0", "off", "disable", "disabled", "不需要", "否", "关闭", "不启用", "不勾选", "未选中"].includes(normalized)) return false;
312
588
  return null;
313
589
  }
314
590
 
@@ -383,6 +659,65 @@ function extractRecentNotView(text) {
383
659
  return null;
384
660
  }
385
661
 
662
+ function extractStructuredRunMetaValue(rawText, labelPattern) {
663
+ const normalized = String(rawText || "").replace(/\r\n/g, "\n");
664
+ const match = normalized.match(new RegExp(
665
+ `(?:^|[\\s;;])\\s*(?:${labelPattern})\\s*[::]\\s*([\\s\\S]*?)(?=(?:[\\s;;]\\s*${RUN_META_FIELD_LABEL_PATTERN}\\s*[::])|$)`,
666
+ "i"
667
+ ));
668
+ return match ? normalizeText(String(match[1] || "").replace(/[。;;]+$/, "")) : null;
669
+ }
670
+
671
+ function extractActivityLevel(rawText) {
672
+ const structured = extractStructuredRunMetaValue(rawText, "(?:活跃度|活动度|activity[_\\s-]?level)");
673
+ if (structured !== null) {
674
+ return {
675
+ explicit: true,
676
+ raw: structured || "(空值)",
677
+ value: normalizeActivityLevel(structured)
678
+ };
679
+ }
680
+ const normalized = normalizeText(rawText);
681
+ for (const value of ACTIVITY_LEVEL_OPTIONS.filter((item) => item !== "不限")) {
682
+ if (normalized.includes(value)) {
683
+ return { explicit: false, raw: value, value };
684
+ }
685
+ }
686
+ const activityClauses = String(rawText || "")
687
+ .split(/[,,。;;\n]+/)
688
+ .map((item) => normalizeText(item))
689
+ .filter((item) => /活[跃躍越]|在线|上[线線]|activity|active|online/i.test(item));
690
+ for (const clause of activityClauses) {
691
+ const resolved = resolveActivityLevelIntent(clause);
692
+ if (resolved.recognized) {
693
+ return { explicit: false, raw: clause, value: resolved.value };
694
+ }
695
+ }
696
+ return { explicit: false, raw: null, value: null };
697
+ }
698
+
699
+ function extractCurrentCityOnly(rawText) {
700
+ const structured = extractStructuredRunMetaValue(
701
+ rawText,
702
+ "(?:current[_\\s-]?city[_\\s-]?only|当前城市筛选|仅推荐本城市|仅推荐期望城市为本城市(?:的牛人)?)"
703
+ );
704
+ const structuredValue = normalizeBooleanOverride(structured);
705
+ if (structured !== null) return structuredValue;
706
+
707
+ const normalized = normalizeText(rawText);
708
+ if (
709
+ /(?:不限|不限制)期望城市|(?:关闭|取消|禁用|去掉|取消勾选)\s*(?:当前城市筛选|仅推荐本城市|仅推荐期望城市为本城市(?:的牛人)?|只推荐期望城市(?:是|为)?本城市)/i.test(normalized)
710
+ ) {
711
+ return false;
712
+ }
713
+ if (
714
+ /\bcurrent[_\s-]?city[_\s-]?only\b|仅推荐期望城市为本城市(?:的牛人)?|只推荐期望城市(?:是|为)?本城市|仅推荐本城市/i.test(normalized)
715
+ ) {
716
+ return true;
717
+ }
718
+ return null;
719
+ }
720
+
386
721
  function extractTargetCount(text) {
387
722
  for (const pattern of TARGET_COUNT_PATTERNS) {
388
723
  const match = text.match(pattern);
@@ -411,7 +746,10 @@ function extractMaxGreetCount(text) {
411
746
 
412
747
  function extractJobSelectionHint(text) {
413
748
  const normalized = String(text || "").replace(/\r\n/g, "\n");
414
- const match = normalized.match(/(?:^|[\n;;])\s*(?:岗位|职位|job)\s*[::]\s*([^\n;;]+)/i);
749
+ const match = normalized.match(new RegExp(
750
+ `(?:^|[\\s;;])\\s*(?:岗位|职位|job)\\s*[::]\\s*([\\s\\S]*?)(?=(?:[\\s;;]\\s*${RUN_META_FIELD_LABEL_PATTERN}\\s*[::])|$)`,
751
+ "i"
752
+ ));
415
753
  if (!match?.[1]) return null;
416
754
  return normalizeText(String(match[1] || "").replace(/[。;;]+$/, "").trim());
417
755
  }
@@ -437,6 +775,20 @@ function isMetaClause(clause) {
437
775
  if (!withoutNumbering) return true;
438
776
  if (CRITERIA_META_FIELD_PREFIX_PATTERNS.some((pattern) => pattern.test(withoutNumbering))) return true;
439
777
  if (META_CLAUSE_PATTERNS.some((pattern) => pattern.test(withoutNumbering))) return true;
778
+ const activityCandidate = stripActivityIntentWrappers(withoutNumbering);
779
+ const activityIntent = resolveActivityLevelIntent(withoutNumbering);
780
+ const isKnownActivityAlias = Object.values(ACTIVITY_LEVEL_ALIASES)
781
+ .flat()
782
+ .map((item) => stripActivityIntentWrappers(item))
783
+ .includes(activityCandidate);
784
+ const isRelativeActivityDuration = /^(?:近|最近|过去|过去的|(?:(?:within|during|in|over)?(?:the)?(?:last|past)?))?(?:\d+(?:\.\d+)?|半|[一二两三四五六七八九十]+|half|one|two|couple|three|few|four|five|six|seven|eight|nine|ten|twenty|thirty)(?:个)?(?:小时|hours?|hrs?|hr|h|天|日|days?|day|d|周|星期|weeks?|week|w|月|months?|month)(?:内|以内|之内|ago)?$/i
785
+ .test(activityCandidate);
786
+ if (
787
+ activityIntent.recognized
788
+ && (isKnownActivityAlias || isRelativeActivityDuration || activityIntent.method === "fuzzy")
789
+ ) {
790
+ return true;
791
+ }
440
792
  return false;
441
793
  }
442
794
 
@@ -655,6 +1007,18 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
655
1007
  const confirmationGender = normalizeGender(confirmation?.gender_value);
656
1008
  const overrideRecentNotView = normalizeRecentNotView(overrides?.recent_not_view);
657
1009
  const confirmationRecentNotView = normalizeRecentNotView(confirmation?.recent_not_view_value);
1010
+ const hasCurrentCityOnlyOverride = Object.prototype.hasOwnProperty.call(overrides || {}, "current_city_only");
1011
+ const currentCityOnly = hasCurrentCityOnlyOverride
1012
+ ? normalizeBooleanOverride(overrides?.current_city_only) ?? false
1013
+ : extractCurrentCityOnly(rawInstruction) ?? false;
1014
+ const hasActivityLevelOverride = Object.prototype.hasOwnProperty.call(overrides || {}, "activity_level");
1015
+ const overrideActivityLevel = hasActivityLevelOverride
1016
+ ? normalizeActivityLevel(overrides?.activity_level)
1017
+ : null;
1018
+ const instructionActivityLevel = extractActivityLevel(rawInstruction);
1019
+ const activityLevel = hasActivityLevelOverride
1020
+ ? overrideActivityLevel
1021
+ : instructionActivityLevel.value || "不限";
658
1022
  const overrideCriteria = overrides?.criteria;
659
1023
  const criteriaResolution = buildCriteria({
660
1024
  instruction: text,
@@ -694,7 +1058,9 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
694
1058
  : ["不限"])
695
1059
  ),
696
1060
  gender: overrideGender || confirmationGender || extractGender(text) || "不限",
697
- recent_not_view: overrideRecentNotView || confirmationRecentNotView || extractRecentNotView(text) || "不限"
1061
+ recent_not_view: overrideRecentNotView || confirmationRecentNotView || extractRecentNotView(text) || "不限",
1062
+ current_city_only: currentCityOnly,
1063
+ activity_level: activityLevel
698
1064
  };
699
1065
  const screenParams = {
700
1066
  criteria: criteriaResolution.raw || criteriaResolution.normalized || null,
@@ -919,10 +1285,12 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
919
1285
  }
920
1286
 
921
1287
  export {
1288
+ ACTIVITY_LEVEL_OPTIONS,
922
1289
  DEGREE_OPTIONS,
923
1290
  GENDER_OPTIONS,
924
1291
  POST_ACTION_LABELS,
925
1292
  POST_ACTION_OPTIONS,
926
1293
  RECENT_NOT_VIEW_OPTIONS,
927
- SCHOOL_TAG_OPTIONS
928
- };
1294
+ SCHOOL_TAG_OPTIONS,
1295
+ normalizeActivityLevel
1296
+ };