@reconcrap/boss-recommend-mcp 1.3.35 → 1.3.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -239,7 +239,9 @@ node src/cli.js chat run --job "算法工程师" --start-from unread --criteria
239
239
  - `pause_boss_chat_run`
240
240
  - `resume_boss_chat_run`
241
241
  - `cancel_boss_chat_run`
242
- - vendored `boss-chat` CLI 还支持 `--data-dir <path>` 与 `BOSS_CHAT_HOME`,优先级高于兼容旧行为的 `<cwd>/.boss-chat`
242
+ - vendored `boss-chat` CLI 还支持 `--data-dir <path>` 与 `BOSS_CHAT_HOME`,默认目录为 `~/.boss-recommend-mcp/boss-chat`(若设置 `BOSS_RECOMMEND_HOME`,则默认 `<BOSS_RECOMMEND_HOME>/boss-chat`)
243
+ - 对 `/.boss-chat`、系统目录等危险运行目录会主动拒绝启动并返回 `UNSAFE_DATA_DIR`,避免在 harness 丢参时误写根目录
244
+ - `boss_chat_health_check` 与 chat run 返回结果会包含 `data_dir` 与 `data_dir_source`,便于定位是参数/环境变量/默认路径生效
243
245
 
244
246
  chat-only 交互建议:
245
247
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.3.35",
3
+ "version": "1.3.37",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
package/src/boss-chat.js CHANGED
@@ -97,11 +97,30 @@ function isSafeBossChatLegacyWorkspaceRoot(workspaceRoot) {
97
97
  );
98
98
  }
99
99
 
100
- export function getBossChatDataDir() {
100
+ function isUnsafeBossChatDataDir(targetPath) {
101
+ const resolved = path.resolve(String(targetPath || ""));
102
+ return isRootDirectory(resolved) || isSystemDirectoryWorkspaceRoot(resolved);
103
+ }
104
+
105
+ function resolveBossChatDataDir() {
101
106
  if (process.env.BOSS_CHAT_HOME) {
102
- return path.resolve(process.env.BOSS_CHAT_HOME);
107
+ return {
108
+ data_dir: path.resolve(process.env.BOSS_CHAT_HOME),
109
+ data_dir_source: "env:BOSS_CHAT_HOME"
110
+ };
103
111
  }
104
- return path.join(getStateHome(), BOSS_CHAT_RUNTIME_SUBDIR);
112
+ const stateHome = getStateHome();
113
+ const source = process.env.BOSS_RECOMMEND_HOME
114
+ ? "default:env:BOSS_RECOMMEND_HOME"
115
+ : "default:user_home";
116
+ return {
117
+ data_dir: path.join(stateHome, BOSS_CHAT_RUNTIME_SUBDIR),
118
+ data_dir_source: source
119
+ };
120
+ }
121
+
122
+ export function getBossChatDataDir() {
123
+ return resolveBossChatDataDir().data_dir;
105
124
  }
106
125
 
107
126
  export function getLegacyBossChatWorkspaceDataDir(workspaceRoot) {
@@ -110,7 +129,8 @@ export function getLegacyBossChatWorkspaceDataDir(workspaceRoot) {
110
129
  }
111
130
 
112
131
  export function resolveBossChatRuntimeLayout(workspaceRoot) {
113
- const dataDir = getBossChatDataDir();
132
+ const resolvedDataDir = resolveBossChatDataDir();
133
+ const dataDir = resolvedDataDir.data_dir;
114
134
  const legacyWorkspaceDir = getLegacyBossChatWorkspaceDataDir(workspaceRoot);
115
135
  const migrationSourceDir =
116
136
  legacyWorkspaceDir && pathExists(legacyWorkspaceDir) && !pathExists(dataDir)
@@ -119,6 +139,7 @@ export function resolveBossChatRuntimeLayout(workspaceRoot) {
119
139
  return {
120
140
  workspace_root: workspaceRoot ? path.resolve(String(workspaceRoot)) : null,
121
141
  data_dir: dataDir,
142
+ data_dir_source: resolvedDataDir.data_dir_source,
122
143
  legacy_workspace_dir: legacyWorkspaceDir,
123
144
  migration_source_dir: migrationSourceDir,
124
145
  migration_pending: Boolean(migrationSourceDir),
@@ -144,6 +165,22 @@ export function ensureBossChatRuntimeReady(workspaceRoot) {
144
165
  : ""
145
166
  };
146
167
 
168
+ if (isUnsafeBossChatDataDir(runtime.data_dir)) {
169
+ return {
170
+ ...runtime,
171
+ created,
172
+ existed,
173
+ failed: [
174
+ {
175
+ path: runtime.data_dir,
176
+ message: `Refusing unsafe boss-chat runtime path: ${runtime.data_dir}. Please use BOSS_CHAT_HOME in a writable user directory.`
177
+ }
178
+ ],
179
+ migration,
180
+ blocked_reason: "UNSAFE_DATA_DIR"
181
+ };
182
+ }
183
+
147
184
  if (runtime.migration_source_dir) {
148
185
  try {
149
186
  fs.cpSync(runtime.migration_source_dir, runtime.data_dir, {
@@ -698,28 +735,39 @@ function buildBossChatCliArgs(command, input, resolvedConfig, runtimeLayout = nu
698
735
  const runId = normalizeBossChatRunId(input);
699
736
  args.push("--profile", normalizeText(input.profile) || "default");
700
737
  args.push("--run-id", runId);
701
- return args;
702
- }
703
-
738
+ return args;
739
+ }
740
+
741
+ function withRuntimeDiagnostics(payload, runtimeLayout) {
742
+ if (!payload || typeof payload !== "object") return payload;
743
+ return {
744
+ ...payload,
745
+ data_dir: runtimeLayout?.data_dir || null,
746
+ data_dir_source: runtimeLayout?.data_dir_source || null
747
+ };
748
+ }
749
+
704
750
  async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
751
+ const runtimeLayout = ensureBossChatRuntimeReady(workspaceRoot);
705
752
  const cliPath = resolveBossChatCliPath(workspaceRoot);
706
753
  if (!cliPath) {
707
- return {
754
+ return {
708
755
  ok: false,
709
756
  exitCode: -1,
710
757
  stdout: "",
711
758
  stderr: "",
712
- payload: {
713
- status: "FAILED",
714
- error: {
715
- code: "BOSS_CHAT_CLI_MISSING",
716
- message: "未找到 vendored boss-chat CLI。"
717
- }
759
+ payload: {
760
+ status: "FAILED",
761
+ error: {
762
+ code: "BOSS_CHAT_CLI_MISSING",
763
+ message: "未找到 vendored boss-chat CLI。"
764
+ },
765
+ data_dir: runtimeLayout?.data_dir || null,
766
+ data_dir_source: runtimeLayout?.data_dir_source || null
718
767
  }
719
768
  };
720
769
  }
721
770
 
722
- const runtimeLayout = ensureBossChatRuntimeReady(workspaceRoot);
723
771
  const runtimeInitFailed = runtimeLayout.failed.some((item) => item.path === runtimeLayout.data_dir)
724
772
  && !pathExists(runtimeLayout.data_dir);
725
773
  if (runtimeInitFailed) {
@@ -738,6 +786,7 @@ async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
738
786
  .join("; ") || "无法初始化 boss-chat runtime 目录。"
739
787
  },
740
788
  data_dir: runtimeLayout.data_dir,
789
+ data_dir_source: runtimeLayout.data_dir_source,
741
790
  migration: runtimeLayout.migration
742
791
  }
743
792
  };
@@ -752,12 +801,14 @@ async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
752
801
  exitCode: 1,
753
802
  stdout: "",
754
803
  stderr: "",
755
- payload: {
756
- status: "FAILED",
757
- error: configResolution.error,
758
- config_path: configResolution.config_path,
759
- config_dir: configResolution.config_dir
760
- }
804
+ payload: {
805
+ status: "FAILED",
806
+ error: configResolution.error,
807
+ config_path: configResolution.config_path,
808
+ config_dir: configResolution.config_dir,
809
+ data_dir: runtimeLayout.data_dir,
810
+ data_dir_source: runtimeLayout.data_dir_source
811
+ }
761
812
  };
762
813
  }
763
814
  }
@@ -765,12 +816,15 @@ async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
765
816
  const args = [cliPath, ...buildBossChatCliArgs(command, input, configResolution?.config || {}, runtimeLayout)];
766
817
  const cwd = path.resolve(String(workspaceRoot || process.cwd()));
767
818
  return new Promise((resolve) => {
768
- const child = spawn(process.execPath, args, {
769
- cwd,
770
- env: process.env,
771
- windowsHide: true,
772
- stdio: ["ignore", "pipe", "pipe"]
773
- });
819
+ const child = spawn(process.execPath, args, {
820
+ cwd,
821
+ env: {
822
+ ...process.env,
823
+ BOSS_CHAT_HOME: runtimeLayout.data_dir
824
+ },
825
+ windowsHide: true,
826
+ stdio: ["ignore", "pipe", "pipe"]
827
+ });
774
828
 
775
829
  let stdout = "";
776
830
  let stderr = "";
@@ -786,48 +840,53 @@ async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
786
840
  exitCode: -1,
787
841
  stdout,
788
842
  stderr,
789
- payload: {
790
- status: "FAILED",
791
- error: {
792
- code: "BOSS_CHAT_CLI_SPAWN_FAILED",
793
- message: error?.message || "无法启动 vendored boss-chat CLI。"
794
- }
795
- }
796
- });
797
- });
843
+ payload: {
844
+ status: "FAILED",
845
+ error: {
846
+ code: "BOSS_CHAT_CLI_SPAWN_FAILED",
847
+ message: error?.message || "无法启动 vendored boss-chat CLI。"
848
+ },
849
+ data_dir: runtimeLayout.data_dir,
850
+ data_dir_source: runtimeLayout.data_dir_source
851
+ }
852
+ });
853
+ });
798
854
  child.on("close", (code) => {
799
855
  const parsed = parseJsonOutput(stdout) || parseJsonOutput(stderr);
800
856
  if (parsed && typeof parsed === "object") {
801
857
  resolve({
802
858
  ok: Number(code) === 0 && String(parsed.status || "").toUpperCase() !== "FAILED",
803
- exitCode: Number.isInteger(code) ? code : 1,
804
- stdout,
805
- stderr,
806
- payload: parsed
807
- });
808
- return;
809
- }
810
- resolve({
859
+ exitCode: Number.isInteger(code) ? code : 1,
860
+ stdout,
861
+ stderr,
862
+ payload: withRuntimeDiagnostics(parsed, runtimeLayout)
863
+ });
864
+ return;
865
+ }
866
+ resolve({
811
867
  ok: Number(code) === 0,
812
868
  exitCode: Number.isInteger(code) ? code : 1,
813
869
  stdout,
814
- stderr,
815
- payload: Number(code) === 0
816
- ? {
817
- status: "OK",
818
- message: normalizeText(stdout) || `${command} 执行成功。`
819
- }
820
- : {
821
- status: "FAILED",
822
- error: {
823
- code: "BOSS_CHAT_CLI_EXECUTION_FAILED",
824
- message: normalizeText(stderr || stdout) || `${command} 执行失败。`
825
- }
826
- }
827
- });
828
- });
829
- });
830
- }
870
+ stderr,
871
+ payload: withRuntimeDiagnostics(
872
+ Number(code) === 0
873
+ ? {
874
+ status: "OK",
875
+ message: normalizeText(stdout) || `${command} 执行成功。`
876
+ }
877
+ : {
878
+ status: "FAILED",
879
+ error: {
880
+ code: "BOSS_CHAT_CLI_EXECUTION_FAILED",
881
+ message: normalizeText(stderr || stdout) || `${command} 执行失败。`
882
+ }
883
+ },
884
+ runtimeLayout
885
+ )
886
+ });
887
+ });
888
+ });
889
+ }
831
890
 
832
891
  export function getBossChatHealthCheck(workspaceRoot, input = {}) {
833
892
  const cliDir = resolveBossChatCliDir(workspaceRoot);
@@ -844,6 +903,7 @@ export function getBossChatHealthCheck(workspaceRoot, input = {}) {
844
903
  message: "未找到 vendored boss-chat CLI。"
845
904
  },
846
905
  data_dir: runtimeLayout.data_dir,
906
+ data_dir_source: runtimeLayout.data_dir_source,
847
907
  legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
848
908
  migration_pending: runtimeLayout.migration_pending
849
909
  };
@@ -857,6 +917,7 @@ export function getBossChatHealthCheck(workspaceRoot, input = {}) {
857
917
  cli_dir: cliDir,
858
918
  cli_path: cliPath,
859
919
  data_dir: runtimeLayout.data_dir,
920
+ data_dir_source: runtimeLayout.data_dir_source,
860
921
  legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
861
922
  migration_pending: runtimeLayout.migration_pending
862
923
  };
@@ -870,6 +931,7 @@ export function getBossChatHealthCheck(workspaceRoot, input = {}) {
870
931
  debug_port: resolvedPort,
871
932
  shared_llm_config: true,
872
933
  data_dir: runtimeLayout.data_dir,
934
+ data_dir_source: runtimeLayout.data_dir_source,
873
935
  legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
874
936
  migration_source_dir: runtimeLayout.migration_source_dir,
875
937
  migration_pending: runtimeLayout.migration_pending
package/src/pipeline.js CHANGED
@@ -30,6 +30,15 @@ const SEARCH_NO_IFRAME_RETRY_DELAY_MS = 1200;
30
30
  const MAX_SEARCH_FILTER_AUTO_RETRY_ATTEMPTS = 2;
31
31
  const SEARCH_FILTER_AUTO_RETRY_DELAY_MS = 1200;
32
32
  const BOSS_CHAT_FOLLOW_UP_POLL_MS = 1500;
33
+ const FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN = "passed_count";
34
+ const FOLLOW_UP_TARGET_COUNT_PASSED_LABEL = "通过筛选数";
35
+ const FOLLOW_UP_TARGET_COUNT_PASSED_ALIASES = new Set([
36
+ FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN,
37
+ "passed",
38
+ "通过筛选数",
39
+ "筛选通过数",
40
+ "通过数"
41
+ ]);
33
42
  const SEARCH_FILTER_RETRY_TOKENS = [
34
43
  "FILTER_CONFIRM_FAILED",
35
44
  "FILTER_DOM_CLASS_VERIFY_FAILED",
@@ -72,6 +81,75 @@ function normalizePipelineTargetCountValue(value) {
72
81
  return normalizeTargetCountInput(value).publicValue;
73
82
  }
74
83
 
84
+ function isFollowUpPassedTargetCountToken(value) {
85
+ const normalized = normalizeText(value).toLowerCase().replace(/\s+/g, "");
86
+ if (!normalized) return false;
87
+ return FOLLOW_UP_TARGET_COUNT_PASSED_ALIASES.has(normalized);
88
+ }
89
+
90
+ function normalizeFollowUpTargetCountInput(value) {
91
+ const normalized = normalizeTargetCountInput(value);
92
+ if (normalized.provided) {
93
+ return {
94
+ ...normalized,
95
+ launchValue: normalized.publicValue,
96
+ passedCountMode: false
97
+ };
98
+ }
99
+ if (isFollowUpPassedTargetCountToken(value)) {
100
+ return {
101
+ provided: true,
102
+ targetCount: null,
103
+ cliArg: null,
104
+ publicValue: FOLLOW_UP_TARGET_COUNT_PASSED_LABEL,
105
+ rawValue: value,
106
+ parseError: null,
107
+ launchValue: FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN,
108
+ passedCountMode: true
109
+ };
110
+ }
111
+ return {
112
+ ...normalized,
113
+ launchValue: null,
114
+ passedCountMode: false
115
+ };
116
+ }
117
+
118
+ function resolveFollowUpChatTargetCountForLaunch(targetCount, recommendSummary = null) {
119
+ if (isFollowUpPassedTargetCountToken(targetCount)) {
120
+ const passedCount = parsePositiveIntegerValue(recommendSummary?.passed_count);
121
+ if (passedCount) {
122
+ return {
123
+ ok: true,
124
+ target_count: passedCount,
125
+ resolved_from: FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN,
126
+ passed_count: recommendSummary?.passed_count ?? null
127
+ };
128
+ }
129
+ return {
130
+ ok: false,
131
+ code: "FOLLOW_UP_TARGET_COUNT_PASSED_UNAVAILABLE",
132
+ message: "boss-chat follow-up 选择了“通过筛选数”,但本次通过人数为空或 0。请改为正整数,或填写 all(扫到底)。",
133
+ passed_count: recommendSummary?.passed_count ?? null
134
+ };
135
+ }
136
+ const normalized = normalizeTargetCountInput(targetCount);
137
+ if (normalized.provided) {
138
+ return {
139
+ ok: true,
140
+ target_count: normalized.publicValue,
141
+ resolved_from: "explicit",
142
+ passed_count: recommendSummary?.passed_count ?? null
143
+ };
144
+ }
145
+ return {
146
+ ok: false,
147
+ code: "FOLLOW_UP_TARGET_COUNT_INVALID",
148
+ message: normalized.parseError || "boss-chat follow-up target_count 无效。",
149
+ passed_count: recommendSummary?.passed_count ?? null
150
+ };
151
+ }
152
+
75
153
  function sleep(ms) {
76
154
  return new Promise((resolve) => setTimeout(resolve, ms));
77
155
  }
@@ -394,13 +472,12 @@ function normalizeFollowUpChatInput(followUp = null, defaults = null) {
394
472
  const defaultCriteria = normalizeText(defaults?.criteria || "");
395
473
  const defaultStartFromRaw = normalizeText(defaults?.start_from || "").toLowerCase();
396
474
  const defaultStartFrom = defaultStartFromRaw === "all" ? "all" : "unread";
397
- const defaultTargetCount = normalizePipelineTargetCountValue(defaults?.target_count);
398
475
 
399
476
  const explicitCriteria = normalizeText(raw.criteria);
400
477
  const explicitStartFromRaw = normalizeText(raw.start_from).toLowerCase();
401
478
  const explicitStartFrom = explicitStartFromRaw === "all" ? "all" : explicitStartFromRaw === "unread" ? "unread" : "";
402
- const explicitTarget = normalizeTargetCountInput(raw.target_count);
403
- const explicitTargetCount = explicitTarget.publicValue;
479
+ const explicitTarget = normalizeFollowUpTargetCountInput(raw.target_count);
480
+ const explicitTargetCount = explicitTarget.launchValue;
404
481
 
405
482
  const hasExplicitCriteria = Boolean(explicitCriteria);
406
483
  const hasExplicitStartFrom = Boolean(explicitStartFrom);
@@ -408,14 +485,15 @@ function normalizeFollowUpChatInput(followUp = null, defaults = null) {
408
485
 
409
486
  const criteria = explicitCriteria || defaultCriteria;
410
487
  const startFrom = explicitStartFrom || defaultStartFrom;
411
- const targetCount = explicitTargetCount || defaultTargetCount;
488
+ const targetCount = explicitTargetCount || FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN;
489
+ const targetCountSummaryValue = explicitTarget.publicValue || FOLLOW_UP_TARGET_COUNT_PASSED_LABEL;
412
490
 
413
491
  const profile = normalizeText(raw.profile) || "default";
414
492
  const summary = {
415
493
  profile,
416
494
  criteria: criteria || null,
417
495
  start_from: startFrom || null,
418
- target_count: targetCount,
496
+ target_count: targetCountSummaryValue,
419
497
  dry_run: raw.dry_run === true,
420
498
  no_state: raw.no_state === true,
421
499
  safe_pacing: typeof raw.safe_pacing === "boolean" ? raw.safe_pacing : null,
@@ -446,21 +524,39 @@ function normalizeFollowUpChatInput(followUp = null, defaults = null) {
446
524
  });
447
525
  }
448
526
  if (!hasExplicitTargetCount) {
449
- const targetCountHints = buildTargetCountCompatibilityHints({
450
- argumentName: "follow_up.chat.target_count",
451
- recommendedArgumentPatch: {
452
- follow_up: {
453
- chat: {
454
- target_count: "all"
455
- }
527
+ const recommendedTargetCountPatch = {
528
+ follow_up: {
529
+ chat: {
530
+ target_count: FOLLOW_UP_TARGET_COUNT_PASSED_LABEL
456
531
  }
457
532
  }
533
+ };
534
+ const targetCountHints = buildTargetCountCompatibilityHints({
535
+ argumentName: "follow_up.chat.target_count",
536
+ recommendedArgumentPatch: recommendedTargetCountPatch
458
537
  });
459
538
  missing_fields.push("follow_up.chat.target_count");
460
539
  pending_questions.push({
461
540
  ...targetCountHints,
541
+ answer_format: `follow_up.chat.target_count = "${FOLLOW_UP_TARGET_COUNT_PASSED_LABEL}" | 正整数 | "all"`,
542
+ recommended_value: FOLLOW_UP_TARGET_COUNT_PASSED_LABEL,
543
+ recommended_argument_patch: recommendedTargetCountPatch,
544
+ canonical_passed_count_value: FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN,
545
+ accepted_examples: [
546
+ FOLLOW_UP_TARGET_COUNT_PASSED_LABEL,
547
+ ...targetCountHints.accepted_examples
548
+ ],
549
+ options: [
550
+ {
551
+ label: `通过筛选数(推荐)`,
552
+ value: FOLLOW_UP_TARGET_COUNT_PASSED_LABEL,
553
+ canonical_value: FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN,
554
+ argument_patch: recommendedTargetCountPatch
555
+ },
556
+ ...(Array.isArray(targetCountHints.options) ? targetCountHints.options : [])
557
+ ],
462
558
  field: "follow_up.chat.target_count",
463
- question: "请填写 boss-chat follow-up 本次处理人数上限。若扫到底,请在 follow_up.chat.target_count 里字面填写 \"all\"。",
559
+ question: "请填写 boss-chat follow-up 目标人数。默认建议填写“通过筛选数”;若要扫到底,请在 follow_up.chat.target_count 里字面填写 \"all\"。",
464
560
  value: summary.target_count,
465
561
  ...(explicitTarget.rawValue !== undefined ? { received_target_count: explicitTarget.rawValue } : {}),
466
562
  ...(explicitTarget.parseError ? { target_count_parse_error: explicitTarget.parseError } : {})
@@ -857,10 +953,32 @@ async function runBossChatFollowUpPhase({
857
953
  cancelChatRun
858
954
  }) {
859
955
  const recommendSummary = recommendResult?.result || null;
860
- const resolvedChatInput = buildResolvedFollowUpChatInput(followUpChat, {
956
+ const requestedChatInput = buildResolvedFollowUpChatInput(followUpChat, {
861
957
  selectedJob,
862
958
  debugPort
863
959
  });
960
+ const targetCountResolution = resolveFollowUpChatTargetCountForLaunch(
961
+ requestedChatInput.target_count,
962
+ recommendSummary
963
+ );
964
+ if (!targetCountResolution.ok) {
965
+ return buildFollowUpFailedResponse(
966
+ targetCountResolution.code || "BOSS_CHAT_FOLLOW_UP_TARGET_COUNT_INVALID",
967
+ targetCountResolution.message || "boss-chat follow-up target_count 无法解析。",
968
+ recommendResult,
969
+ {
970
+ enabled: true,
971
+ input: requestedChatInput,
972
+ target_count_resolution: targetCountResolution
973
+ }
974
+ );
975
+ }
976
+ const resolvedChatInput = {
977
+ ...requestedChatInput,
978
+ target_count: targetCountResolution.target_count,
979
+ target_count_source: targetCountResolution.resolved_from,
980
+ target_count_requested: requestedChatInput.target_count
981
+ };
864
982
  let chatRunId = normalizeText(resume?.chat_run_id || "");
865
983
  const resumeFromChatPhase = resume?.resume === true && normalizeText(resume?.follow_up_phase) === "chat_followup";
866
984
  let pauseRequested = false;
@@ -244,6 +244,7 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
244
244
  assert.equal(health.status, "OK");
245
245
  assert.equal(health.shared_llm_config, true);
246
246
  assert.equal(health.debug_port, 9666);
247
+ assert.equal(health.data_dir_source, "env:BOSS_CHAT_HOME");
247
248
  assert.equal(health.data_dir, getTestChatDataDir(workspaceRoot));
248
249
  assert.equal(health.legacy_workspace_dir, path.join(workspaceRoot, ".boss-chat"));
249
250
  assert.equal(health.migration_pending, false);
@@ -526,18 +527,45 @@ function testVendorBossChatCliShouldResolveExplicitDataDir() {
526
527
  const cwd = path.join(path.parse(process.cwd()).root, "workspace");
527
528
  const args = vendorCliTestables.parseArgs(["start-run", "--data-dir", "/tmp/boss-chat-data"]);
528
529
  assert.equal(args.dataDir, "/tmp/boss-chat-data");
530
+ const explicitResolved = vendorCliTestables.resolveDataDirDetails(args, { BOSS_CHAT_HOME: "/tmp/ignored" }, cwd);
531
+ assert.equal(explicitResolved.source, "arg:data-dir");
532
+ assert.equal(explicitResolved.path, path.resolve("/tmp/boss-chat-data"));
529
533
  assert.equal(
530
534
  vendorCliTestables.resolveDataDir(args, { BOSS_CHAT_HOME: "/tmp/ignored" }, cwd),
531
535
  path.resolve("/tmp/boss-chat-data")
532
536
  );
537
+ const envResolved = vendorCliTestables.resolveDataDirDetails({}, { BOSS_CHAT_HOME: "/tmp/from-env" }, cwd);
538
+ assert.equal(envResolved.source, "env:BOSS_CHAT_HOME");
539
+ assert.equal(envResolved.path, path.resolve("/tmp/from-env"));
533
540
  assert.equal(
534
541
  vendorCliTestables.resolveDataDir({}, { BOSS_CHAT_HOME: "/tmp/from-env" }, cwd),
535
542
  path.resolve("/tmp/from-env")
536
543
  );
544
+ const defaultResolved = vendorCliTestables.resolveDataDirDetails({}, {}, cwd);
545
+ assert.equal(defaultResolved.source, "default:user_home");
546
+ assert.equal(defaultResolved.path, path.join(os.homedir(), ".boss-recommend-mcp", "boss-chat"));
537
547
  assert.equal(
538
548
  vendorCliTestables.resolveDataDir({}, {}, cwd),
539
- path.join(path.resolve(cwd), ".boss-chat")
549
+ path.join(os.homedir(), ".boss-recommend-mcp", "boss-chat")
540
550
  );
551
+
552
+ const unsafeRoot = vendorCliTestables.validateDataDir(path.parse(process.cwd()).root);
553
+ assert.equal(unsafeRoot.ok, false);
554
+ assert.equal(unsafeRoot.code, "UNSAFE_DATA_DIR");
555
+ assert.equal(unsafeRoot.message.includes("Refusing unsafe boss-chat data dir"), true);
556
+
557
+ const safePath = vendorCliTestables.validateDataDir(path.join(os.homedir(), ".boss-recommend-mcp", "boss-chat"));
558
+ assert.equal(safePath.ok, true);
559
+ }
560
+
561
+ function testVendorBossChatCliShouldUseRecommendHomeForDefaultDataDir() {
562
+ const resolved = vendorCliTestables.resolveDataDirDetails(
563
+ {},
564
+ { BOSS_RECOMMEND_HOME: "/tmp/recommend-home" },
565
+ path.join(path.parse(process.cwd()).root, "workspace")
566
+ );
567
+ assert.equal(resolved.source, "default:env:BOSS_RECOMMEND_HOME");
568
+ assert.equal(resolved.path, path.resolve("/tmp/recommend-home/boss-chat"));
541
569
  }
542
570
 
543
571
  async function testBossChatPageShouldTreatBlankChatShellAsOnChatPage() {
@@ -3019,6 +3047,7 @@ async function main() {
3019
3047
  await testBossChatMcpToolsShouldValidateAndRoute();
3020
3048
  await testBossChatCliShouldSupportRunAndFollowUpParsing();
3021
3049
  testVendorBossChatCliShouldResolveExplicitDataDir();
3050
+ testVendorBossChatCliShouldUseRecommendHomeForDefaultDataDir();
3022
3051
  await testVendorBossChatCliShouldWaitForHydratedChatShell();
3023
3052
  await testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile();
3024
3053
  testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig();
@@ -2183,8 +2183,9 @@ async function testFollowUpChatMissingFieldsShouldExposeRecommendDefaults() {
2183
2183
  const targetCountQuestion = result.pending_questions.find((item) => item.field === "follow_up.chat.target_count");
2184
2184
  assert.equal(criteriaQuestion?.value, "默认沿用 recommend 的筛选条件");
2185
2185
  assert.equal(startFromQuestion?.value, "unread");
2186
- assert.equal(targetCountQuestion?.value, 18);
2187
- assert.equal(targetCountQuestion?.recommended_argument_patch?.follow_up?.chat?.target_count, "all");
2186
+ assert.equal(targetCountQuestion?.value, "通过筛选数");
2187
+ assert.equal(targetCountQuestion?.recommended_argument_patch?.follow_up?.chat?.target_count, "通过筛选数");
2188
+ assert.equal(targetCountQuestion?.options?.some((item) => item.label.includes("通过筛选数(推荐)")), true);
2188
2189
  assert.equal(targetCountQuestion?.options?.some((item) => item.label.includes('follow_up.chat.target_count="all"')), true);
2189
2190
  }
2190
2191
 
@@ -2225,7 +2226,9 @@ async function testFollowUpChatMissingTargetCountShouldNeedInput() {
2225
2226
  assert.equal(result.missing_fields.includes("follow_up.chat.target_count"), true);
2226
2227
  const targetQuestion = result.pending_questions.find((item) => item.field === "follow_up.chat.target_count");
2227
2228
  assert.equal(Boolean(targetQuestion), true);
2228
- assert.equal(targetQuestion.recommended_argument_patch?.follow_up?.chat?.target_count, "all");
2229
+ assert.equal(targetQuestion.recommended_argument_patch?.follow_up?.chat?.target_count, "通过筛选数");
2230
+ assert.equal(targetQuestion.options?.some((item) => item.value === "通过筛选数"), true);
2231
+ assert.equal(targetQuestion.options?.some((item) => item.value === "all"), true);
2229
2232
  }
2230
2233
 
2231
2234
  async function testFollowUpChatInvalidTargetCountShouldNeedInputWithDiagnostics() {
@@ -2248,7 +2251,8 @@ async function testFollowUpChatInvalidTargetCountShouldNeedInputWithDiagnostics(
2248
2251
  assert.equal(targetQuestion?.received_target_count, "not a target");
2249
2252
  assert.equal(Boolean(targetQuestion?.target_count_parse_error), true);
2250
2253
  assert.equal(targetQuestion?.accepted_examples.includes("all"), true);
2251
- assert.equal(targetQuestion?.recommended_argument_patch?.follow_up?.chat?.target_count, "all");
2254
+ assert.equal(targetQuestion?.accepted_examples.includes("通过筛选数"), true);
2255
+ assert.equal(targetQuestion?.recommended_argument_patch?.follow_up?.chat?.target_count, "通过筛选数");
2252
2256
  }
2253
2257
 
2254
2258
  async function testFollowUpChatAllTargetCountShouldLaunchUnlimited() {
@@ -2317,6 +2321,63 @@ async function testFollowUpChatAllTargetCountShouldLaunchUnlimited() {
2317
2321
  assert.equal(result.follow_up?.chat?.target_count, "all");
2318
2322
  }
2319
2323
 
2324
+ async function testFollowUpChatPassedTargetCountShouldLaunchWithPassedCount() {
2325
+ let capturedChatInput = null;
2326
+ const result = await runRecommendPipeline(
2327
+ {
2328
+ workspaceRoot: process.cwd(),
2329
+ instruction: "test",
2330
+ confirmation: createJobConfirmedConfirmation(),
2331
+ overrides: {},
2332
+ followUp: createFollowUpChat({ target_count: "通过筛选数" })
2333
+ },
2334
+ {
2335
+ parseRecommendInstruction: () => createParsed(),
2336
+ runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9555 }),
2337
+ ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
2338
+ listRecommendJobs: async () => createJobListResult(),
2339
+ runRecommendSearchCli: async () => ({
2340
+ ok: true,
2341
+ summary: {
2342
+ candidate_count: 6,
2343
+ applied_filters: {},
2344
+ page_state: {}
2345
+ }
2346
+ }),
2347
+ runRecommendScreenCli: async () => ({
2348
+ ok: true,
2349
+ summary: {
2350
+ processed_count: 6,
2351
+ passed_count: 2,
2352
+ skipped_count: 0
2353
+ }
2354
+ }),
2355
+ startBossChatRun: async ({ input }) => {
2356
+ capturedChatInput = input;
2357
+ return {
2358
+ status: "ACCEPTED",
2359
+ run_id: "chat-run-pass-count",
2360
+ message: "chat started"
2361
+ };
2362
+ },
2363
+ getBossChatRun: async () => ({
2364
+ status: "COMPLETED",
2365
+ run: {
2366
+ runId: "chat-run-pass-count",
2367
+ state: "completed",
2368
+ lastMessage: "chat completed",
2369
+ progress: { processed: 2, matched: 2 }
2370
+ }
2371
+ })
2372
+ }
2373
+ );
2374
+
2375
+ assert.equal(result.status, "COMPLETED");
2376
+ assert.equal(capturedChatInput?.target_count, 2);
2377
+ assert.equal(result.follow_up?.chat?.input?.target_count, 2);
2378
+ assert.equal(result.follow_up?.chat?.input?.target_count_requested, "passed_count");
2379
+ }
2380
+
2320
2381
  async function testFinalReviewShouldIncludeFollowUpChatSummary() {
2321
2382
  const result = await runRecommendPipeline(
2322
2383
  {
@@ -2576,6 +2637,7 @@ async function main() {
2576
2637
  await testFinalReviewShouldIncludeFollowUpChatSummary();
2577
2638
  await testCompletedPipelineShouldRunChatFollowUp();
2578
2639
  await testFollowUpChatAllTargetCountShouldLaunchUnlimited();
2640
+ await testFollowUpChatPassedTargetCountShouldLaunchWithPassedCount();
2579
2641
  await testCompletedPipelineShouldFailWhenChatLaunchFails();
2580
2642
  await testCompletedPipelineShouldFailWhenChatRunFails();
2581
2643
  console.log("pipeline tests passed");
@@ -1,8 +1,9 @@
1
- #!/usr/bin/env node
2
- import { spawn } from 'node:child_process';
3
- import { appendFile, mkdir } from 'node:fs/promises';
4
- import path from 'node:path';
5
- import process from 'node:process';
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { appendFile, mkdir } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
6
7
  import * as readlineCore from 'node:readline';
7
8
  import readline from 'node:readline/promises';
8
9
  import util from 'node:util';
@@ -301,12 +302,82 @@ function parseArgs(argv) {
301
302
  return args;
302
303
  }
303
304
 
304
- function resolveDataDir(args = {}, env = process.env, cwd = process.cwd()) {
305
+ function isRootDirectory(targetPath) {
306
+ const resolved = path.resolve(String(targetPath || ''));
307
+ const parsed = path.parse(resolved);
308
+ return resolved.toLowerCase() === String(parsed.root || '').toLowerCase();
309
+ }
310
+
311
+ function isSystemDirectoryPath(targetPath) {
312
+ const resolved = path.resolve(String(targetPath || ''));
313
+ const normalized = resolved.replace(/\\/g, '/').toLowerCase();
314
+ if (process.platform === 'win32') {
315
+ return (
316
+ normalized.endsWith('/windows')
317
+ || normalized.endsWith('/windows/system32')
318
+ || normalized.endsWith('/windows/syswow64')
319
+ || normalized.endsWith('/program files')
320
+ || normalized.endsWith('/program files (x86)')
321
+ );
322
+ }
323
+ return (
324
+ normalized === '/system'
325
+ || normalized.startsWith('/system/')
326
+ || normalized === '/usr'
327
+ || normalized.startsWith('/usr/')
328
+ || normalized === '/bin'
329
+ || normalized.startsWith('/bin/')
330
+ || normalized === '/sbin'
331
+ || normalized.startsWith('/sbin/')
332
+ );
333
+ }
334
+
335
+ function resolveDefaultDataDir(env = process.env) {
336
+ const stateHomeRaw = String(env?.BOSS_RECOMMEND_HOME || '').trim();
337
+ const stateHome = stateHomeRaw
338
+ ? path.resolve(stateHomeRaw)
339
+ : path.join(os.homedir(), '.boss-recommend-mcp');
340
+ const source = stateHomeRaw
341
+ ? 'default:env:BOSS_RECOMMEND_HOME'
342
+ : 'default:user_home';
343
+ return {
344
+ path: path.join(stateHome, 'boss-chat'),
345
+ source,
346
+ };
347
+ }
348
+
349
+ function resolveDataDirDetails(args = {}, env = process.env, cwd = process.cwd()) {
305
350
  const explicit = String(args?.dataDir || '').trim();
306
- if (explicit) return path.resolve(explicit);
351
+ if (explicit) {
352
+ return {
353
+ path: path.resolve(explicit),
354
+ source: 'arg:data-dir',
355
+ };
356
+ }
307
357
  const fromEnv = String(env?.BOSS_CHAT_HOME || '').trim();
308
- if (fromEnv) return path.resolve(fromEnv);
309
- return path.join(path.resolve(String(cwd || process.cwd())), '.boss-chat');
358
+ if (fromEnv) {
359
+ return {
360
+ path: path.resolve(fromEnv),
361
+ source: 'env:BOSS_CHAT_HOME',
362
+ };
363
+ }
364
+ return resolveDefaultDataDir(env);
365
+ }
366
+
367
+ function resolveDataDir(args = {}, env = process.env, cwd = process.cwd()) {
368
+ return resolveDataDirDetails(args, env, cwd).path;
369
+ }
370
+
371
+ function validateDataDir(targetPath) {
372
+ const resolved = path.resolve(String(targetPath || ''));
373
+ if (isRootDirectory(resolved) || isSystemDirectoryPath(resolved)) {
374
+ return {
375
+ ok: false,
376
+ code: 'UNSAFE_DATA_DIR',
377
+ message: `Refusing unsafe boss-chat data dir: ${resolved}. Please use --data-dir or BOSS_CHAT_HOME to a writable user directory.`,
378
+ };
379
+ }
380
+ return { ok: true };
310
381
  }
311
382
 
312
383
  function printUsage() {
@@ -323,7 +394,7 @@ function printUsage() {
323
394
  console.log('');
324
395
  console.log('Common options:');
325
396
  console.log(' --profile <name> Profile name (default: default)');
326
- console.log(' --data-dir <path> Runtime data dir (default: $BOSS_CHAT_HOME or <cwd>/.boss-chat)');
397
+ console.log(' --data-dir <path> Runtime data dir (default: $BOSS_CHAT_HOME or ~/.boss-recommend-mcp/boss-chat)');
327
398
  console.log(' --json JSON output for agent integration');
328
399
  console.log(' --run-id <id> Target async run_id (for get/pause/resume/cancel)');
329
400
  console.log('');
@@ -1527,7 +1598,27 @@ async function executeRunCommand(args, dataDir) {
1527
1598
 
1528
1599
  async function main() {
1529
1600
  const args = parseArgs(process.argv.slice(2));
1530
- const dataDir = resolveDataDir(args);
1601
+ const resolvedDataDir = resolveDataDirDetails(args);
1602
+ const dataDir = resolvedDataDir.path;
1603
+ const dataDirValidation = validateDataDir(dataDir);
1604
+ if (!dataDirValidation.ok) {
1605
+ const failedPayload = {
1606
+ status: 'FAILED',
1607
+ error: {
1608
+ code: dataDirValidation.code,
1609
+ message: dataDirValidation.message,
1610
+ },
1611
+ data_dir: dataDir,
1612
+ data_dir_source: resolvedDataDir.source,
1613
+ };
1614
+ if (args.json) {
1615
+ console.log(JSON.stringify(failedPayload));
1616
+ } else {
1617
+ console.error(failedPayload.error.message);
1618
+ }
1619
+ process.exitCode = 1;
1620
+ return;
1621
+ }
1531
1622
  await mkdir(dataDir, { recursive: true });
1532
1623
 
1533
1624
  if (args.command === 'help') {
@@ -1562,19 +1653,28 @@ async function main() {
1562
1653
  break;
1563
1654
  default:
1564
1655
  printUsage();
1565
- process.exitCode = 1;
1566
- return;
1567
- }
1568
-
1569
- outputCommandResult(args, payload);
1570
- if (payload?.status === 'FAILED') {
1571
- process.exitCode = 1;
1572
- }
1573
- }
1656
+ process.exitCode = 1;
1657
+ return;
1658
+ }
1659
+
1660
+ if (payload && typeof payload === 'object') {
1661
+ payload = {
1662
+ ...payload,
1663
+ data_dir: dataDir,
1664
+ data_dir_source: resolvedDataDir.source,
1665
+ };
1666
+ }
1667
+ outputCommandResult(args, payload);
1668
+ if (payload?.status === 'FAILED') {
1669
+ process.exitCode = 1;
1670
+ }
1671
+ }
1574
1672
 
1575
1673
  export const __testables = {
1576
1674
  parseArgs,
1675
+ resolveDataDirDetails,
1577
1676
  resolveDataDir,
1677
+ validateDataDir,
1578
1678
  connectBossChatPage,
1579
1679
  hasHydratedChatShell,
1580
1680
  promptRunProfile,