@reconcrap/boss-recommend-mcp 1.1.1 → 1.1.3

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-recommend-mcp",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
package/src/adapters.js CHANGED
@@ -16,6 +16,7 @@ const screenConfigTemplateDefaults = {
16
16
  apiKey: "replace-with-openai-api-key",
17
17
  model: "gpt-4.1-mini"
18
18
  };
19
+ const DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS = 24 * 60 * 60 * 1000;
19
20
 
20
21
  function getCodexHome() {
21
22
  return process.env.CODEX_HOME
@@ -830,7 +831,39 @@ function parseJsonOutput(text) {
830
831
  return null;
831
832
  }
832
833
 
833
- function parseScreenProgressLine(line, currentProgress = {}) {
834
+ function createScreenProgressTracker(currentTracker = {}) {
835
+ const outcome = String(currentTracker.outcome || "").trim();
836
+ return {
837
+ candidate_index: Number.isInteger(currentTracker.candidate_index) ? currentTracker.candidate_index : null,
838
+ outcome: outcome === "pass" || outcome === "skip" ? outcome : null,
839
+ action_failed: currentTracker.action_failed === true
840
+ };
841
+ }
842
+
843
+ function finalizeCandidateProgress(progress, tracker) {
844
+ if (!Number.isInteger(tracker.candidate_index)) {
845
+ return false;
846
+ }
847
+
848
+ let changed = false;
849
+ if (tracker.action_failed === true) {
850
+ progress.skipped += 1;
851
+ changed = true;
852
+ } else if (tracker.outcome === "pass") {
853
+ progress.passed += 1;
854
+ changed = true;
855
+ } else if (tracker.outcome === "skip") {
856
+ progress.skipped += 1;
857
+ changed = true;
858
+ }
859
+
860
+ tracker.candidate_index = null;
861
+ tracker.outcome = null;
862
+ tracker.action_failed = false;
863
+ return changed;
864
+ }
865
+
866
+ function parseScreenProgressLine(line, currentProgress = {}, currentTracker = {}) {
834
867
  const normalizedLine = String(line || "").replace(/\s+/g, " ").trim();
835
868
  if (!normalizedLine) return null;
836
869
 
@@ -840,23 +873,56 @@ function parseScreenProgressLine(line, currentProgress = {}) {
840
873
  skipped: Number.isInteger(currentProgress.skipped) ? currentProgress.skipped : 0,
841
874
  greet_count: Number.isInteger(currentProgress.greet_count) ? currentProgress.greet_count : 0
842
875
  };
876
+ const nextTracker = createScreenProgressTracker(currentTracker);
843
877
 
844
878
  let changed = false;
845
879
  const processedMatch = normalizedLine.match(/处理第\s*(\d+)\s*位候选人/u);
846
880
  if (processedMatch) {
881
+ if (finalizeCandidateProgress(nextProgress, nextTracker)) {
882
+ changed = true;
883
+ }
847
884
  const processed = Number.parseInt(processedMatch[1], 10);
848
885
  if (Number.isInteger(processed) && processed >= 0 && processed !== nextProgress.processed) {
849
886
  nextProgress.processed = processed;
850
887
  changed = true;
851
888
  }
889
+ nextTracker.candidate_index = processed;
890
+ nextTracker.outcome = null;
891
+ nextTracker.action_failed = false;
852
892
  }
853
893
 
854
894
  if (/筛选结果:\s*通过/u.test(normalizedLine)) {
855
- nextProgress.passed += 1;
856
- changed = true;
895
+ if (nextTracker.outcome !== "pass" || nextTracker.action_failed) {
896
+ changed = true;
897
+ }
898
+ nextTracker.outcome = "pass";
899
+ nextTracker.action_failed = false;
857
900
  } else if (/筛选结果:\s*不通过/u.test(normalizedLine)) {
858
- nextProgress.skipped += 1;
859
- changed = true;
901
+ if (nextTracker.outcome !== "skip" || nextTracker.action_failed) {
902
+ changed = true;
903
+ }
904
+ nextTracker.outcome = "skip";
905
+ nextTracker.action_failed = false;
906
+ }
907
+
908
+ if (/候选人处理失败\s*:/u.test(normalizedLine)) {
909
+ if (!nextTracker.action_failed) {
910
+ changed = true;
911
+ }
912
+ nextTracker.action_failed = true;
913
+ }
914
+
915
+ if (/^\[关闭详情\].*成功/u.test(normalizedLine)) {
916
+ if (finalizeCandidateProgress(nextProgress, nextTracker)) {
917
+ changed = true;
918
+ }
919
+ }
920
+
921
+ const finalStateLine = /Process timed out after|status"\s*:\s*"(?:COMPLETED|PAUSED|FAILED)"/iu.test(normalizedLine);
922
+ if (finalStateLine) {
923
+ if (finalizeCandidateProgress(nextProgress, nextTracker)) {
924
+ changed = true;
925
+ }
860
926
  }
861
927
 
862
928
  const greetMatch = normalizedLine.match(/greet[_\s-]*count\s*[:=]\s*(\d+)/iu);
@@ -871,7 +937,34 @@ function parseScreenProgressLine(line, currentProgress = {}) {
871
937
  if (!changed) return null;
872
938
  return {
873
939
  line: normalizedLine,
874
- progress: nextProgress
940
+ progress: nextProgress,
941
+ tracker: nextTracker
942
+ };
943
+ }
944
+
945
+ function resolveRecommendScreenTimeoutMs(runtime = null) {
946
+ const runtimeTimeoutMs = parsePositiveInteger(runtime?.timeoutMs);
947
+ const envTimeoutMs = parsePositiveInteger(process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS);
948
+ return runtimeTimeoutMs || envTimeoutMs || DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS;
949
+ }
950
+
951
+ function buildRecommendScreenProcessError(result, screenTimeoutMs) {
952
+ if (result.code === 0) return null;
953
+ if (result.error_code === "TIMEOUT") {
954
+ return {
955
+ code: "TIMEOUT",
956
+ message: `推荐页筛选命令执行超时(${screenTimeoutMs}ms)。`
957
+ };
958
+ }
959
+ if (result.error_code === "ABORTED") {
960
+ return {
961
+ code: "PROCESS_ABORTED",
962
+ message: "推荐页筛选命令已取消。"
963
+ };
964
+ }
965
+ return {
966
+ code: "RECOMMEND_SCREEN_FAILED",
967
+ message: "推荐页筛选命令执行失败。"
875
968
  };
876
969
  }
877
970
 
@@ -1683,21 +1776,24 @@ export async function runRecommendScreenCli({ workspaceRoot, screenParams, resum
1683
1776
  skipped: 0,
1684
1777
  greet_count: 0
1685
1778
  };
1779
+ let inferredTracker = createScreenProgressTracker();
1780
+ const screenTimeoutMs = resolveRecommendScreenTimeoutMs(runtime);
1686
1781
 
1687
1782
  const result = await runProcess({
1688
1783
  command: "node",
1689
1784
  args,
1690
1785
  cwd: screenDir,
1691
- timeoutMs: 60 * 60 * 1000,
1786
+ timeoutMs: screenTimeoutMs,
1692
1787
  heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
1693
1788
  signal: runtime?.signal,
1694
1789
  onOutput: (event) => {
1695
1790
  safeInvokeCallback(runtime?.onOutput, event);
1696
1791
  },
1697
1792
  onLine: (event) => {
1698
- const parsed = parseScreenProgressLine(event?.line, inferredProgress);
1793
+ const parsed = parseScreenProgressLine(event?.line, inferredProgress, inferredTracker);
1699
1794
  if (!parsed) return;
1700
1795
  inferredProgress = parsed.progress;
1796
+ inferredTracker = parsed.tracker;
1701
1797
  safeInvokeCallback(runtime?.onProgress, {
1702
1798
  ...inferredProgress,
1703
1799
  line: parsed.line
@@ -1731,24 +1827,14 @@ export async function runRecommendScreenCli({ workspaceRoot, screenParams, resum
1731
1827
  stderr: result.stderr,
1732
1828
  structured,
1733
1829
  summary,
1734
- error: structured?.error || missingOutputError || (
1735
- result.code === 0
1736
- ? null
1737
- : result.error_code === "ABORTED"
1738
- ? {
1739
- code: "PROCESS_ABORTED",
1740
- message: "推荐页筛选命令已取消。"
1741
- }
1742
- : {
1743
- code: "RECOMMEND_SCREEN_FAILED",
1744
- message: "推荐页筛选命令执行失败。"
1745
- }
1746
- )
1830
+ error: structured?.error || missingOutputError || buildRecommendScreenProcessError(result, screenTimeoutMs)
1747
1831
  };
1748
1832
  }
1749
1833
 
1750
1834
  export const __testables = {
1751
1835
  runProcess,
1752
1836
  parseJsonOutput,
1753
- parseScreenProgressLine
1837
+ parseScreenProgressLine,
1838
+ resolveRecommendScreenTimeoutMs,
1839
+ buildRecommendScreenProcessError
1754
1840
  };
package/src/index.js CHANGED
@@ -203,7 +203,7 @@ function createRunInputSchema() {
203
203
  post_action_confirmed: { type: "boolean" },
204
204
  post_action_value: {
205
205
  type: "string",
206
- enum: ["favorite", "greet"]
206
+ enum: ["favorite", "greet", "none"]
207
207
  },
208
208
  final_confirmed: { type: "boolean" },
209
209
  job_confirmed: { type: "boolean" },
@@ -267,7 +267,7 @@ function createRunInputSchema() {
267
267
  max_greet_count: { type: "integer", minimum: 1 },
268
268
  post_action: {
269
269
  type: "string",
270
- enum: ["favorite", "greet"]
270
+ enum: ["favorite", "greet", "none"]
271
271
  }
272
272
  },
273
273
  additionalProperties: false
package/src/parser.js CHANGED
@@ -28,10 +28,11 @@ const DEGREE_ORDER = [
28
28
  ];
29
29
  const GENDER_OPTIONS = ["不限", "男", "女"];
30
30
  const RECENT_NOT_VIEW_OPTIONS = ["不限", "近14天没有"];
31
- const POST_ACTION_OPTIONS = ["favorite", "greet"];
31
+ const POST_ACTION_OPTIONS = ["favorite", "greet", "none"];
32
32
  const POST_ACTION_LABELS = {
33
33
  favorite: "收藏",
34
- greet: "直接沟通"
34
+ greet: "直接沟通",
35
+ none: "什么也不做"
35
36
  };
36
37
  const LEADING_NOISE_PATTERNS = [
37
38
  /^使用boss-recommend-pipeline skills/i,
@@ -90,7 +91,7 @@ const FILTER_CLAUSE_PATTERNS = [
90
91
  /近?14天(?:内)?没有|近?14天(?:内)?没看过|近?14天(?:内)?未查看|过滤[^。;;\n]{0,12}14天|排除[^。;;\n]{0,12}14天/i,
91
92
  /目标(?:处理|筛选)?(?:人数|数量)?|至少(?:处理|筛选)|(?:处理|筛选)\s*\d+\s*(?:位|人)/i,
92
93
  /最多(?:打招呼|沟通|联系)|(?:打招呼|沟通|联系)(?:上限|最多|不超过|至多)/i,
93
- /收藏|打招呼|直接沟通/i
94
+ /收藏|打招呼|直接沟通|什么也不做|不做任何操作|不操作|仅筛选|只筛选/i
94
95
  ];
95
96
  const META_CLAUSE_PATTERNS = [
96
97
  /推荐页|推荐页面|boss推荐/i,
@@ -192,7 +193,10 @@ function expandDegreeAtOrAbove(value) {
192
193
  function parseDegreeSelectionsFromText(text) {
193
194
  const normalizedText = normalizeText(text);
194
195
  if (!normalizedText) return [];
195
- if (/(?:学历|学位|教育)[^。;;\n]{0,6}不限|不限[^。;;\n]{0,6}(?:学历|学位|教育)/i.test(normalizedText)) {
196
+ if (
197
+ /(?:学历|学位|教育)(?:要求)?\s*(?:[::]\s*)?(?:不限|不限制|无要求)|(?:不限|不限制)\s*(?:[::]\s*)?(?:学历|学位|教育)(?:要求)?/i
198
+ .test(normalizedText)
199
+ ) {
196
200
  return ["不限"];
197
201
  }
198
202
 
@@ -256,6 +260,9 @@ function normalizePostAction(value) {
256
260
  if (!normalized) return null;
257
261
  if (["favorite", "fav", "收藏"].includes(normalized)) return "favorite";
258
262
  if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
263
+ if (["none", "noop", "no-op", "什么也不做", "不做任何操作", "不操作", "仅筛选", "只筛选"].includes(normalized)) {
264
+ return "none";
265
+ }
259
266
  return null;
260
267
  }
261
268
 
@@ -374,6 +381,8 @@ function resolvePostAction({ instruction, confirmation, overrides }) {
374
381
  ? "favorite"
375
382
  : /打招呼|直接沟通|沟通/.test(instruction)
376
383
  ? "greet"
384
+ : /什么也不做|不做任何操作|不操作|仅筛选|只筛选/.test(instruction)
385
+ ? "none"
377
386
  : null;
378
387
  const proposed = overrideValue || confirmationValue || instructionValue || null;
379
388
 
@@ -531,41 +540,25 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
531
540
  invalidOverrideSchoolTags: schoolTagAudit.invalid,
532
541
  maxGreetCountResolution
533
542
  });
534
- const hasSchoolTagSignal = Boolean(
535
- (Array.isArray(overrideSchoolTag) && overrideSchoolTag.length > 0)
536
- || (Array.isArray(confirmationSchoolTag) && confirmationSchoolTag.length > 0)
537
- || (Array.isArray(detectedSchoolTags) && detectedSchoolTags.length > 0)
538
- );
539
- const hasDegreeSignal = Boolean(
540
- (Array.isArray(overrideDegrees) && overrideDegrees.length > 0)
541
- || (Array.isArray(confirmationDegrees) && confirmationDegrees.length > 0)
542
- || (Array.isArray(detectedDegrees) && detectedDegrees.length > 0)
543
- );
544
- const hasGenderSignal = Boolean(
545
- overrideGender
546
- || confirmationGender
547
- || extractGender(text)
548
- );
549
- const hasRecentNotViewSignal = Boolean(
550
- overrideRecentNotView
551
- || confirmationRecentNotView
552
- || extractRecentNotView(text)
553
- );
543
+ const hasConfirmedSchoolTagValue = Array.isArray(confirmationSchoolTag) && confirmationSchoolTag.length > 0;
544
+ const hasConfirmedDegreeValue = Array.isArray(confirmationDegrees) && confirmationDegrees.length > 0;
545
+ const hasConfirmedGenderValue = Boolean(confirmationGender);
546
+ const hasConfirmedRecentNotViewValue = Boolean(confirmationRecentNotView);
554
547
  const needs_school_tag_confirmation = (
555
548
  confirmation?.school_tag_confirmed !== true
556
- || !hasSchoolTagSignal
549
+ || !hasConfirmedSchoolTagValue
557
550
  );
558
551
  const needs_degree_confirmation = (
559
552
  confirmation?.degree_confirmed !== true
560
- || !hasDegreeSignal
553
+ || !hasConfirmedDegreeValue
561
554
  );
562
555
  const needs_gender_confirmation = (
563
556
  confirmation?.gender_confirmed !== true
564
- || !hasGenderSignal
557
+ || !hasConfirmedGenderValue
565
558
  );
566
559
  const needs_recent_not_view_confirmation = (
567
560
  confirmation?.recent_not_view_confirmed !== true
568
- || !hasRecentNotViewSignal
561
+ || !hasConfirmedRecentNotViewValue
569
562
  );
570
563
  const needs_filters_confirmation = (
571
564
  confirmation?.filters_confirmed !== true
@@ -656,7 +649,8 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
656
649
  value: postActionResolution.proposed_post_action,
657
650
  options: [
658
651
  { label: POST_ACTION_LABELS.favorite, value: "favorite" },
659
- { label: POST_ACTION_LABELS.greet, value: "greet" }
652
+ { label: POST_ACTION_LABELS.greet, value: "greet" },
653
+ { label: POST_ACTION_LABELS.none, value: "none" }
660
654
  ]
661
655
  });
662
656
  }
package/src/pipeline.js CHANGED
@@ -959,7 +959,7 @@ export async function runRecommendPipeline(
959
959
  });
960
960
 
961
961
  return {
962
- status: "COMPLETED",
962
+ status: "COMPLETED",
963
963
  search_params: parsed.searchParams,
964
964
  screen_params: parsed.screenParams,
965
965
  result: {
@@ -976,8 +976,10 @@ export async function runRecommendPipeline(
976
976
  post_action: parsed.screenParams.post_action,
977
977
  max_greet_count: parsed.screenParams.max_greet_count,
978
978
  greet_count: screenSummary.greet_count ?? 0,
979
- greet_limit_fallback_count: screenSummary.greet_limit_fallback_count ?? 0
980
- },
981
- message: "Recommend 流水线已完成。post_action 在运行开始时已一次性确认;若选择打招呼并设置上限,超出上限后会自动改为收藏。"
982
- };
983
- }
979
+ greet_limit_fallback_count: screenSummary.greet_limit_fallback_count ?? 0
980
+ },
981
+ message: parsed.screenParams.post_action === "none"
982
+ ? "Recommend 流水线已完成。本次 post_action=none:符合条件的人选仅记录到 CSV,不执行收藏或打招呼。"
983
+ : "Recommend 流水线已完成。post_action 在运行开始时已一次性确认;若选择打招呼并设置上限,超出上限后会自动改为收藏。"
984
+ };
985
+ }
@@ -4,7 +4,13 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { runRecommendScreenCli, __testables as adapterTestables } from "./adapters.js";
6
6
 
7
- const { runProcess, parseJsonOutput } = adapterTestables;
7
+ const {
8
+ runProcess,
9
+ parseJsonOutput,
10
+ parseScreenProgressLine,
11
+ resolveRecommendScreenTimeoutMs,
12
+ buildRecommendScreenProcessError
13
+ } = adapterTestables;
8
14
 
9
15
  async function testRunProcessHeartbeatAndOutput() {
10
16
  const heartbeats = [];
@@ -57,6 +63,50 @@ function testParsePausedStructuredOutput() {
57
63
  assert.equal(parsed?.result?.processed_count, 3);
58
64
  }
59
65
 
66
+ function testParseScreenProgressLineShouldCountFavoriteFailureAsSkipped() {
67
+ let progress = { processed: 0, passed: 0, skipped: 0, greet_count: 0 };
68
+ let tracker = {};
69
+ const feed = (line) => {
70
+ const parsed = parseScreenProgressLine(line, progress, tracker);
71
+ if (!parsed) return;
72
+ progress = parsed.progress;
73
+ tracker = parsed.tracker;
74
+ };
75
+
76
+ feed("处理第 1 位候选人: 甲");
77
+ feed("筛选结果: 通过");
78
+ feed("[关闭详情] 成功: no popup or detail signal visible");
79
+ feed("处理第 2 位候选人: 乙");
80
+ feed("筛选结果: 通过");
81
+ feed("候选人处理失败: FAVORITE_BUTTON_FAILED");
82
+ feed("[关闭详情] 成功: no popup or detail signal visible");
83
+
84
+ assert.equal(progress.processed, 2);
85
+ assert.equal(progress.passed, 1);
86
+ assert.equal(progress.skipped, 1);
87
+ }
88
+
89
+ function testResolveScreenTimeoutDefaultsTo24Hours() {
90
+ const previous = process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS;
91
+ delete process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS;
92
+ try {
93
+ assert.equal(resolveRecommendScreenTimeoutMs(null), 24 * 60 * 60 * 1000);
94
+ assert.equal(resolveRecommendScreenTimeoutMs({ timeoutMs: 1234 }), 1234);
95
+ } finally {
96
+ if (previous === undefined) {
97
+ delete process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS;
98
+ } else {
99
+ process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS = previous;
100
+ }
101
+ }
102
+ }
103
+
104
+ function testBuildRecommendScreenProcessErrorMapsTimeout() {
105
+ const error = buildRecommendScreenProcessError({ code: -1, error_code: "TIMEOUT" }, 86400000);
106
+ assert.equal(error?.code, "TIMEOUT");
107
+ assert.equal(String(error?.message || "").includes("86400000"), true);
108
+ }
109
+
60
110
  async function testResumeRequiresCheckpointFile() {
61
111
  const previousHome = process.env.BOSS_RECOMMEND_HOME;
62
112
  const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-resume-"));
@@ -103,6 +153,9 @@ async function main() {
103
153
  await testRunProcessHeartbeatAndOutput();
104
154
  await testRunProcessAbortSignal();
105
155
  testParsePausedStructuredOutput();
156
+ testParseScreenProgressLineShouldCountFavoriteFailureAsSkipped();
157
+ testResolveScreenTimeoutDefaultsTo24Hours();
158
+ testBuildRecommendScreenProcessErrorMapsTimeout();
106
159
  await testResumeRequiresCheckpointFile();
107
160
  console.log("adapters runtime tests passed");
108
161
  }
@@ -29,9 +29,13 @@ function testConfirmedPostActionAndOverrides() {
29
29
  confirmation: {
30
30
  filters_confirmed: true,
31
31
  school_tag_confirmed: true,
32
+ school_tag_value: ["211"],
32
33
  degree_confirmed: true,
34
+ degree_value: ["本科"],
33
35
  gender_confirmed: true,
36
+ gender_value: "女",
34
37
  recent_not_view_confirmed: true,
38
+ recent_not_view_value: "近14天没有",
35
39
  criteria_confirmed: true,
36
40
  target_count_confirmed: true,
37
41
  target_count_value: 12,
@@ -67,6 +71,30 @@ function testConfirmedPostActionAndOverrides() {
67
71
  assert.equal(result.needs_max_greet_count_confirmation, false);
68
72
  }
69
73
 
74
+ function testMissingRecentNotViewValueShouldRequireReconfirmation() {
75
+ const result = parseRecommendInstruction({
76
+ instruction: "推荐页筛选985男生,近14天没有,有销售经验,符合标准收藏",
77
+ confirmation: {
78
+ filters_confirmed: true,
79
+ school_tag_confirmed: true,
80
+ school_tag_value: ["985"],
81
+ degree_confirmed: true,
82
+ degree_value: ["本科"],
83
+ gender_confirmed: true,
84
+ gender_value: "男",
85
+ recent_not_view_confirmed: true,
86
+ criteria_confirmed: true,
87
+ target_count_confirmed: true,
88
+ post_action_confirmed: true,
89
+ post_action_value: "favorite"
90
+ },
91
+ overrides: null
92
+ });
93
+
94
+ assert.equal(result.needs_recent_not_view_confirmation, true);
95
+ assert.equal(result.pending_questions.some((q) => q.field === "recent_not_view"), true);
96
+ }
97
+
70
98
  function testFilterConfirmedWithoutExplicitValuesShouldRequireReconfirmation() {
71
99
  const result = parseRecommendInstruction({
72
100
  instruction: "通过boss推荐skill帮我找人",
@@ -172,6 +200,17 @@ function testDegreeAtOrAboveExpansion() {
172
200
  assert.deepEqual(result.searchParams.degree, ["大专", "本科", "硕士", "博士"]);
173
201
  }
174
202
 
203
+ function testDegreeShouldNotBeOverwrittenBySchoolTagUnlimitedClause() {
204
+ const result = parseRecommendInstruction({
205
+ instruction: "学校标签不限,学历要求大专及以上,性别不限,过滤近14天已看",
206
+ confirmation: null,
207
+ overrides: null
208
+ });
209
+
210
+ assert.deepEqual(result.searchParams.school_tag, ["不限"]);
211
+ assert.deepEqual(result.searchParams.degree, ["大专", "本科", "硕士", "博士"]);
212
+ }
213
+
175
214
  function testDegreeExplicitListOnly() {
176
215
  const result = parseRecommendInstruction({
177
216
  instruction: "推荐页筛选大专、本科,近14天没有,有Agent经验",
@@ -390,6 +429,31 @@ function testTargetCountCanBeSkippedAfterConfirmation() {
390
429
  assert.equal(result.screenParams.target_count, null);
391
430
  }
392
431
 
432
+ function testPostActionNoneCanBeConfirmed() {
433
+ const result = parseRecommendInstruction({
434
+ instruction: "推荐页筛选211女生,近14天没有,有AI经验,符合标准什么也不做",
435
+ confirmation: {
436
+ filters_confirmed: true,
437
+ school_tag_confirmed: true,
438
+ school_tag_value: ["211"],
439
+ degree_confirmed: true,
440
+ degree_value: ["本科"],
441
+ gender_confirmed: true,
442
+ gender_value: "女",
443
+ recent_not_view_confirmed: true,
444
+ recent_not_view_value: "近14天没有",
445
+ criteria_confirmed: true,
446
+ target_count_confirmed: true,
447
+ post_action_confirmed: true,
448
+ post_action_value: "none"
449
+ },
450
+ overrides: null
451
+ });
452
+
453
+ assert.equal(result.screenParams.post_action, "none");
454
+ assert.equal(result.needs_post_action_confirmation, false);
455
+ }
456
+
393
457
  function testJobSelectionHintCanComeFromOverrides() {
394
458
  const result = parseRecommendInstruction({
395
459
  instruction: "推荐页筛选211女生,有算法经验,符合标准收藏",
@@ -415,11 +479,13 @@ function testMcpMentionShouldStayInCriteria() {
415
479
  function main() {
416
480
  testNeedConfirmationIncludesPostAction();
417
481
  testConfirmedPostActionAndOverrides();
482
+ testMissingRecentNotViewValueShouldRequireReconfirmation();
418
483
  testFilterConfirmedWithoutExplicitValuesShouldRequireReconfirmation();
419
484
  testFilterConfirmedWithExplicitConfirmationValuesShouldNotFallbackToUnlimited();
420
485
  testMultipleSchoolTagsMarkedSuspicious();
421
486
  testDegreeCanBeExtracted();
422
487
  testDegreeAtOrAboveExpansion();
488
+ testDegreeShouldNotBeOverwrittenBySchoolTagUnlimitedClause();
423
489
  testDegreeExplicitListOnly();
424
490
  testDegreeOverrideCanBeArray();
425
491
  testSchoolTagOverrideCanBeArray();
@@ -433,6 +499,7 @@ function main() {
433
499
  testGreetAutoFilledMaxGreetCountShouldRequireReconfirmation();
434
500
  testTargetCountNeedsConfirmationEvenWhenOptional();
435
501
  testTargetCountCanBeSkippedAfterConfirmation();
502
+ testPostActionNoneCanBeConfirmed();
436
503
  testJobSelectionHintCanComeFromOverrides();
437
504
  console.log("parser tests passed");
438
505
  }
@@ -31,6 +31,9 @@ function normalizePostAction(value) {
31
31
  if (!normalized) return null;
32
32
  if (["favorite", "fav", "收藏"].includes(normalized)) return "favorite";
33
33
  if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
34
+ if (["none", "noop", "no-op", "什么也不做", "不做任何操作", "不操作", "仅筛选", "只筛选"].includes(normalized)) {
35
+ return "none";
36
+ }
34
37
  return null;
35
38
  }
36
39
 
@@ -210,10 +213,11 @@ async function promptMissingInputs(args) {
210
213
  if (!(args.postActionConfirmed === true && args.postAction)) {
211
214
  args.postAction = await askWithValidation(
212
215
  ask,
213
- "本次通过人选统一执行什么动作?请输入 1(收藏) 2(直接沟通): ",
216
+ "本次通过人选统一执行什么动作?请输入 1(收藏) / 2(直接沟通) / 3(什么也不做): ",
214
217
  (value) => {
215
218
  if (value === "1") return "favorite";
216
219
  if (value === "2") return "greet";
220
+ if (value === "3") return "none";
217
221
  return null;
218
222
  }
219
223
  );
@@ -289,9 +293,10 @@ async function promptPostAction() {
289
293
  });
290
294
  const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
291
295
  try {
292
- const answer = normalizeText(await ask("本次通过人选统一执行什么动作?请输入 1(收藏) 2(直接沟通): "));
296
+ const answer = normalizeText(await ask("本次通过人选统一执行什么动作?请输入 1(收藏) / 2(直接沟通) / 3(什么也不做): "));
293
297
  if (answer === "1") return "favorite";
294
298
  if (answer === "2") return "greet";
299
+ if (answer === "3") return "none";
295
300
  throw new Error("INVALID_POST_ACTION_CONFIRMATION");
296
301
  } finally {
297
302
  rl.close();
@@ -1955,7 +1960,9 @@ class RecommendScreenCli {
1955
1960
  }
1956
1961
  const actionResult = effectiveAction === "favorite"
1957
1962
  ? await this.favoriteCandidate()
1958
- : await this.greetCandidate();
1963
+ : effectiveAction === "greet"
1964
+ ? await this.greetCandidate()
1965
+ : { actionTaken: "none" };
1959
1966
  if (actionResult.actionTaken === "greet") {
1960
1967
  this.greetCount += 1;
1961
1968
  }
@@ -2050,7 +2057,7 @@ async function main() {
2050
2057
  console.log(JSON.stringify({
2051
2058
  status: "COMPLETED",
2052
2059
  result: {
2053
- usage: "node boss-recommend-screen-cli.cjs --criteria \"有 MCP 开发经验\" --post-action greet --max-greet-count 10 --post-action-confirmed true --baseurl <url> --apikey <key> --model <model> --port 9222 --output <csv-path> --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
2060
+ usage: "node boss-recommend-screen-cli.cjs --criteria \"有 MCP 开发经验\" --post-action <favorite|greet|none> --max-greet-count 10 --post-action-confirmed true --baseurl <url> --apikey <key> --model <model> --port 9222 --output <csv-path> --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
2054
2061
  }
2055
2062
  }));
2056
2063
  return;