@reconcrap/boss-recommend-mcp 2.0.6 → 2.0.7

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.
@@ -21,7 +21,13 @@ import {
21
21
  resetInfiniteListForRefreshRound
22
22
  } from "../../core/infinite-list/index.js";
23
23
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
24
- import { screenCandidate } from "../../core/screening/index.js";
24
+ import {
25
+ callScreeningLlm,
26
+ compactScreeningLlmResult,
27
+ createFailedLlmScreeningResult,
28
+ llmResultToScreening,
29
+ screenCandidate
30
+ } from "../../core/screening/index.js";
25
31
  import {
26
32
  closeRecommendDetail,
27
33
  createRecommendDetailNetworkRecorder,
@@ -165,10 +171,22 @@ function compactDetail(detailResult) {
165
171
  parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
166
172
  cv_acquisition: detailResult.cv_acquisition || null,
167
173
  image_evidence: summarizeImageEvidence(detailResult.image_evidence),
174
+ llm_screening: compactScreeningLlmResult(detailResult.llm_result),
168
175
  close_result: detailResult.close_result
169
176
  };
170
177
  }
171
178
 
179
+ function normalizeScreeningMode(value) {
180
+ const normalized = String(value || "llm").trim().toLowerCase();
181
+ return ["deterministic", "local", "local_scorer"].includes(normalized)
182
+ ? "deterministic"
183
+ : "llm";
184
+ }
185
+
186
+ function createMissingLlmConfigResult() {
187
+ return createFailedLlmScreeningResult(new Error("LLM screening config is required for production recommend runs"));
188
+ }
189
+
172
190
  function compactActionDiscovery(discovery) {
173
191
  if (!discovery) return null;
174
192
  return {
@@ -354,13 +372,20 @@ export async function runRecommendWorkflow({
354
372
  executePostAction = true,
355
373
  actionTimeoutMs = 8000,
356
374
  actionIntervalMs = 500,
357
- actionAfterClickDelayMs = 900
375
+ actionAfterClickDelayMs = 900,
376
+ screeningMode = "llm",
377
+ llmConfig = null,
378
+ llmTimeoutMs = 120000,
379
+ llmImageLimit = 8,
380
+ llmImageDetail = "high"
358
381
  } = {}, runControl) {
359
382
  if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
360
383
  const normalizedFilter = normalizeFilter(filter);
361
384
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
362
385
  const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
363
386
  const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
387
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
388
+ const useLlmScreening = normalizedScreeningMode !== "deterministic";
364
389
  const postActionEnabled = normalizedPostAction !== "none";
365
390
  const limit = Math.max(1, Number(maxCandidates) || 1);
366
391
  const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
@@ -484,6 +509,8 @@ export async function runRecommendWorkflow({
484
509
  passed: 0,
485
510
  greet_count: 0,
486
511
  post_action_clicked: 0,
512
+ screening_mode: normalizedScreeningMode,
513
+ llm_screened: 0,
487
514
  unique_seen: compactInfiniteListState(listState).seen_count,
488
515
  scroll_count: 0,
489
516
  refresh_rounds: 0,
@@ -562,6 +589,7 @@ export async function runRecommendWorkflow({
562
589
  screened: results.length,
563
590
  detail_opened: results.filter((item) => item.detail).length,
564
591
  passed: results.filter((item) => item.screening.passed).length,
592
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
565
593
  unique_seen: compactInfiniteListState(listState).seen_count,
566
594
  scroll_count: compactInfiniteListState(listState).scroll_count,
567
595
  refresh_rounds: refreshRounds,
@@ -696,7 +724,30 @@ export async function runRecommendWorkflow({
696
724
  await runControl.waitIfPaused();
697
725
  runControl.throwIfCanceled();
698
726
  runControl.setPhase("recommend:screening");
699
- const screening = screenCandidate(screeningCandidate, { criteria });
727
+ let llmResult = null;
728
+ if (useLlmScreening) {
729
+ if (!llmConfig) {
730
+ llmResult = createMissingLlmConfigResult();
731
+ } else {
732
+ try {
733
+ llmResult = await callScreeningLlm({
734
+ candidate: screeningCandidate,
735
+ criteria,
736
+ config: llmConfig,
737
+ timeoutMs: llmTimeoutMs,
738
+ imageEvidence: detailResult?.image_evidence || null,
739
+ maxImages: llmImageLimit,
740
+ imageDetail: llmImageDetail
741
+ });
742
+ } catch (error) {
743
+ llmResult = createFailedLlmScreeningResult(error);
744
+ }
745
+ }
746
+ if (detailResult) detailResult.llm_result = llmResult;
747
+ }
748
+ const screening = useLlmScreening
749
+ ? llmResultToScreening(llmResult, screeningCandidate)
750
+ : screenCandidate(screeningCandidate, { criteria });
700
751
  let actionDiscovery = null;
701
752
  let postActionResult = null;
702
753
  if (postActionEnabled && detailResult) {
@@ -731,6 +782,7 @@ export async function runRecommendWorkflow({
731
782
  card_node_id: cardNodeId,
732
783
  candidate: compactCandidate(screeningCandidate),
733
784
  detail: compactDetail(detailResult),
785
+ llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
734
786
  screening: compactScreening(screening),
735
787
  action_discovery: compactActionDiscovery(actionDiscovery),
736
788
  post_action: postActionResult
@@ -750,6 +802,7 @@ export async function runRecommendWorkflow({
750
802
  screened: results.length,
751
803
  detail_opened: results.filter((item) => item.detail).length,
752
804
  passed: results.filter((item) => item.screening.passed).length,
805
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
753
806
  greet_count: greetCount,
754
807
  post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
755
808
  unique_seen: compactInfiniteListState(listState).seen_count,
@@ -764,6 +817,7 @@ export async function runRecommendWorkflow({
764
817
  last_score: screening.score
765
818
  });
766
819
  runControl.checkpoint({
820
+ results,
767
821
  last_candidate: {
768
822
  id: screeningCandidate.id || null,
769
823
  key: candidateKey,
@@ -773,6 +827,7 @@ export async function runRecommendWorkflow({
773
827
  passed: screening.passed,
774
828
  score: screening.score
775
829
  },
830
+ llm_screening: compactScreeningLlmResult(llmResult),
776
831
  post_action: postActionResult
777
832
  }
778
833
  });
@@ -806,6 +861,7 @@ export async function runRecommendWorkflow({
806
861
  processed: results.length,
807
862
  screened: results.length,
808
863
  detail_opened: results.filter((item) => item.detail).length,
864
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
809
865
  passed: results.filter((item) => item.screening.passed).length,
810
866
  greet_count: greetCount,
811
867
  post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
@@ -851,6 +907,11 @@ export function createRecommendRunService({
851
907
  actionTimeoutMs = 8000,
852
908
  actionIntervalMs = 500,
853
909
  actionAfterClickDelayMs = 900,
910
+ screeningMode = "llm",
911
+ llmConfig = null,
912
+ llmTimeoutMs = 120000,
913
+ llmImageLimit = 8,
914
+ llmImageDetail = "high",
854
915
  name = "recommend-domain-run"
855
916
  } = {}) {
856
917
  if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
@@ -858,6 +919,7 @@ export function createRecommendRunService({
858
919
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
859
920
  const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
860
921
  const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
922
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
861
923
  const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
862
924
  const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
863
925
  return manager.startRun({
@@ -888,7 +950,12 @@ export function createRecommendRunService({
888
950
  post_action: normalizedPostAction,
889
951
  max_greet_count: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
890
952
  execute_post_action: Boolean(executePostAction),
891
- action_timeout_ms: actionTimeoutMs
953
+ action_timeout_ms: actionTimeoutMs,
954
+ screening_mode: normalizedScreeningMode,
955
+ llm_configured: Boolean(llmConfig),
956
+ llm_timeout_ms: llmTimeoutMs,
957
+ llm_image_limit: llmImageLimit,
958
+ llm_image_detail: llmImageDetail
892
959
  },
893
960
  progress: {
894
961
  card_count: 0,
@@ -896,6 +963,7 @@ export function createRecommendRunService({
896
963
  processed: 0,
897
964
  screened: 0,
898
965
  detail_opened: 0,
966
+ llm_screened: 0,
899
967
  passed: 0,
900
968
  greet_count: 0,
901
969
  post_action_clicked: 0
@@ -931,7 +999,12 @@ export function createRecommendRunService({
931
999
  executePostAction,
932
1000
  actionTimeoutMs,
933
1001
  actionIntervalMs,
934
- actionAfterClickDelayMs
1002
+ actionAfterClickDelayMs,
1003
+ screeningMode: normalizedScreeningMode,
1004
+ llmConfig,
1005
+ llmTimeoutMs,
1006
+ llmImageLimit,
1007
+ llmImageDetail
935
1008
  }, runControl)
936
1009
  });
937
1010
  }
@@ -19,7 +19,13 @@ import {
19
19
  resetInfiniteListForRefreshRound
20
20
  } from "../../core/infinite-list/index.js";
21
21
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
22
- import { screenCandidate } from "../../core/screening/index.js";
22
+ import {
23
+ callScreeningLlm,
24
+ compactScreeningLlmResult,
25
+ createFailedLlmScreeningResult,
26
+ llmResultToScreening,
27
+ screenCandidate
28
+ } from "../../core/screening/index.js";
23
29
  import {
24
30
  closeRecruitDetail,
25
31
  createRecruitDetailNetworkRecorder,
@@ -72,10 +78,22 @@ function compactDetail(detailResult) {
72
78
  parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
73
79
  cv_acquisition: detailResult.cv_acquisition || null,
74
80
  image_evidence: summarizeImageEvidence(detailResult.image_evidence),
81
+ llm_screening: compactScreeningLlmResult(detailResult.llm_result),
75
82
  close_result: detailResult.close_result
76
83
  };
77
84
  }
78
85
 
86
+ function normalizeScreeningMode(value) {
87
+ const normalized = String(value || "llm").trim().toLowerCase();
88
+ return ["deterministic", "local", "local_scorer"].includes(normalized)
89
+ ? "deterministic"
90
+ : "llm";
91
+ }
92
+
93
+ function createMissingLlmConfigResult() {
94
+ return createFailedLlmScreeningResult(new Error("LLM screening config is required for production search runs"));
95
+ }
96
+
79
97
  function normalizeSearchParams(searchParams = {}) {
80
98
  return normalizeRecruitSearchParams(searchParams);
81
99
  }
@@ -110,7 +128,7 @@ export async function runRecruitWorkflow({
110
128
  criteria = "",
111
129
  searchParams = {},
112
130
  maxCandidates = 5,
113
- detailLimit = 1,
131
+ detailLimit = null,
114
132
  closeDetail = true,
115
133
  delayMs = 0,
116
134
  cardTimeoutMs = 90000,
@@ -127,12 +145,19 @@ export async function runRecruitWorkflow({
127
145
  listFallbackPoint = null,
128
146
  refreshOnEnd = true,
129
147
  maxRefreshRounds = 2,
130
- refreshResetSettleMs = 5000
148
+ refreshResetSettleMs = 5000,
149
+ screeningMode = "llm",
150
+ llmConfig = null,
151
+ llmTimeoutMs = 120000,
152
+ llmImageLimit = 8,
153
+ llmImageDetail = "high"
131
154
  } = {}, runControl) {
132
155
  if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
133
156
  const normalizedSearchParams = normalizeSearchParams(searchParams);
157
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
158
+ const useLlmScreening = normalizedScreeningMode !== "deterministic";
134
159
  const limit = Math.max(1, Number(maxCandidates) || 1);
135
- const detailCountLimit = Math.max(0, Number(detailLimit) || 0);
160
+ const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
136
161
  const networkRecorder = detailCountLimit > 0
137
162
  ? createRecruitDetailNetworkRecorder(client)
138
163
  : null;
@@ -222,6 +247,8 @@ export async function runRecruitWorkflow({
222
247
  screened: 0,
223
248
  detail_opened: 0,
224
249
  passed: 0,
250
+ screening_mode: normalizedScreeningMode,
251
+ llm_screened: 0,
225
252
  unique_seen: compactInfiniteListState(listState).seen_count,
226
253
  scroll_count: 0,
227
254
  refresh_rounds: 0,
@@ -298,6 +325,7 @@ export async function runRecruitWorkflow({
298
325
  screened: results.length,
299
326
  detail_opened: results.filter((item) => item.detail).length,
300
327
  passed: results.filter((item) => item.screening.passed).length,
328
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
301
329
  unique_seen: compactInfiniteListState(listState).seen_count,
302
330
  scroll_count: compactInfiniteListState(listState).scroll_count,
303
331
  refresh_rounds: refreshRounds,
@@ -426,13 +454,37 @@ export async function runRecruitWorkflow({
426
454
  await runControl.waitIfPaused();
427
455
  runControl.throwIfCanceled();
428
456
  runControl.setPhase("recruit:screening");
429
- const screening = screenCandidate(screeningCandidate, { criteria });
457
+ let llmResult = null;
458
+ if (useLlmScreening) {
459
+ if (!llmConfig) {
460
+ llmResult = createMissingLlmConfigResult();
461
+ } else {
462
+ try {
463
+ llmResult = await callScreeningLlm({
464
+ candidate: screeningCandidate,
465
+ criteria,
466
+ config: llmConfig,
467
+ timeoutMs: llmTimeoutMs,
468
+ imageEvidence: detailResult?.image_evidence || null,
469
+ maxImages: llmImageLimit,
470
+ imageDetail: llmImageDetail
471
+ });
472
+ } catch (error) {
473
+ llmResult = createFailedLlmScreeningResult(error);
474
+ }
475
+ }
476
+ if (detailResult) detailResult.llm_result = llmResult;
477
+ }
478
+ const screening = useLlmScreening
479
+ ? llmResultToScreening(llmResult, screeningCandidate)
480
+ : screenCandidate(screeningCandidate, { criteria });
430
481
  const compactResult = {
431
482
  index,
432
483
  candidate_key: candidateKey,
433
484
  card_node_id: cardNodeId,
434
485
  candidate: compactCandidate(screeningCandidate),
435
486
  detail: compactDetail(detailResult),
487
+ llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
436
488
  screening: compactScreening(screening)
437
489
  };
438
490
  results.push(compactResult);
@@ -450,6 +502,7 @@ export async function runRecruitWorkflow({
450
502
  screened: results.length,
451
503
  detail_opened: results.filter((item) => item.detail).length,
452
504
  passed: results.filter((item) => item.screening.passed).length,
505
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
453
506
  unique_seen: compactInfiniteListState(listState).seen_count,
454
507
  scroll_count: compactInfiniteListState(listState).scroll_count,
455
508
  refresh_rounds: refreshRounds,
@@ -462,6 +515,7 @@ export async function runRecruitWorkflow({
462
515
  last_score: screening.score
463
516
  });
464
517
  runControl.checkpoint({
518
+ results,
465
519
  last_candidate: {
466
520
  id: screeningCandidate.id || null,
467
521
  key: candidateKey,
@@ -470,7 +524,8 @@ export async function runRecruitWorkflow({
470
524
  status: screening.status,
471
525
  passed: screening.passed,
472
526
  score: screening.score
473
- }
527
+ },
528
+ llm_screening: compactScreeningLlmResult(llmResult)
474
529
  }
475
530
  });
476
531
 
@@ -496,6 +551,7 @@ export async function runRecruitWorkflow({
496
551
  processed: results.length,
497
552
  screened: results.length,
498
553
  detail_opened: results.filter((item) => item.detail).length,
554
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
499
555
  passed: results.filter((item) => item.screening.passed).length,
500
556
  results
501
557
  };
@@ -514,7 +570,7 @@ export function createRecruitRunService({
514
570
  criteria = "",
515
571
  searchParams = {},
516
572
  maxCandidates = 5,
517
- detailLimit = 1,
573
+ detailLimit = null,
518
574
  closeDetail = true,
519
575
  delayMs = 0,
520
576
  cardTimeoutMs = 90000,
@@ -532,10 +588,18 @@ export function createRecruitRunService({
532
588
  refreshOnEnd = true,
533
589
  maxRefreshRounds = 2,
534
590
  refreshResetSettleMs = 5000,
591
+ screeningMode = "llm",
592
+ llmConfig = null,
593
+ llmTimeoutMs = 120000,
594
+ llmImageLimit = 8,
595
+ llmImageDetail = "high",
535
596
  name = "recruit-domain-run"
536
597
  } = {}) {
537
598
  if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
538
599
  const normalizedSearchParams = normalizeSearchParams(searchParams);
600
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
601
+ const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
602
+ const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
539
603
  return manager.startRun({
540
604
  name,
541
605
  context: {
@@ -544,7 +608,7 @@ export function createRecruitRunService({
544
608
  criteria_present: Boolean(criteria),
545
609
  search_params: normalizedSearchParams,
546
610
  max_candidates: maxCandidates,
547
- detail_limit: detailLimit,
611
+ detail_limit: normalizedDetailLimit,
548
612
  close_detail: closeDetail,
549
613
  reset_before_search: resetBeforeSearch,
550
614
  reset_timeout_ms: resetTimeoutMs,
@@ -559,14 +623,20 @@ export function createRecruitRunService({
559
623
  list_fallback_point: listFallbackPoint,
560
624
  refresh_on_end: refreshOnEnd,
561
625
  max_refresh_rounds: maxRefreshRounds,
562
- refresh_reset_settle_ms: refreshResetSettleMs
626
+ refresh_reset_settle_ms: refreshResetSettleMs,
627
+ screening_mode: normalizedScreeningMode,
628
+ llm_configured: Boolean(llmConfig),
629
+ llm_timeout_ms: llmTimeoutMs,
630
+ llm_image_limit: llmImageLimit,
631
+ llm_image_detail: llmImageDetail
563
632
  },
564
633
  progress: {
565
634
  card_count: 0,
566
- target_count: Math.max(1, Number(maxCandidates) || 1),
635
+ target_count: candidateLimit,
567
636
  processed: 0,
568
637
  screened: 0,
569
638
  detail_opened: 0,
639
+ llm_screened: 0,
570
640
  passed: 0
571
641
  },
572
642
  checkpoint: {},
@@ -576,7 +646,7 @@ export function createRecruitRunService({
576
646
  criteria,
577
647
  searchParams: normalizedSearchParams,
578
648
  maxCandidates,
579
- detailLimit,
649
+ detailLimit: normalizedDetailLimit,
580
650
  closeDetail,
581
651
  delayMs,
582
652
  cardTimeoutMs,
@@ -593,7 +663,12 @@ export function createRecruitRunService({
593
663
  listFallbackPoint,
594
664
  refreshOnEnd,
595
665
  maxRefreshRounds,
596
- refreshResetSettleMs
666
+ refreshResetSettleMs,
667
+ screeningMode: normalizedScreeningMode,
668
+ llmConfig,
669
+ llmTimeoutMs,
670
+ llmImageLimit,
671
+ llmImageDetail
597
672
  }, runControl)
598
673
  });
599
674
  }
package/src/index.js CHANGED
@@ -541,11 +541,38 @@ function createRunInputSchema() {
541
541
  detail_limit: {
542
542
  type: "integer",
543
543
  minimum: 0,
544
- description: "打开详情/CV 的人数上限;默认跟随 target_count/max_candidates。生产筛选不应传 0;只有 allow_card_only_screening=true 时才会接受 0 作为调试卡片-only 模式"
544
+ description: "打开详情/CV 的人数上限;默认跟随 target_count/max_candidates。生产筛选不应传 0;detail_limit=0 需要 debug_test_mode=true allow_card_only_screening=true"
545
545
  },
546
546
  allow_card_only_screening: {
547
547
  type: "boolean",
548
- description: "高级调试开关;默认 false。只有显式为 true 时,recommend 才会尊重 detail_limit=0 并只用卡片信息筛选"
548
+ description: "高级调试开关;默认 false。只有同时显式 debug_test_mode=true 时,recommend 才会尊重 detail_limit=0"
549
+ },
550
+ debug_test_mode: {
551
+ type: "boolean",
552
+ description: "高级测试开关;默认 false。只有显式为 true 时才允许 deterministic/local scorer、跳过筛选器、card-only、dry-run 后置动作等调试路径"
553
+ },
554
+ screening_mode: {
555
+ type: "string",
556
+ enum: ["llm", "deterministic"],
557
+ description: "筛选引擎;默认 llm。deterministic 仅限 debug_test_mode=true 的明确测试场景"
558
+ },
559
+ use_llm: {
560
+ type: "boolean",
561
+ description: "兼容字段;默认 true。use_llm=false 等同 deterministic,仅限 debug_test_mode=true"
562
+ },
563
+ llm_timeout_ms: {
564
+ type: "integer",
565
+ minimum: 1000,
566
+ description: "可选,单个候选人的 LLM 调用超时"
567
+ },
568
+ llm_image_limit: {
569
+ type: "integer",
570
+ minimum: 1,
571
+ description: "可选,传给 LLM 的图片简历截图页数上限"
572
+ },
573
+ llm_image_detail: {
574
+ type: "string",
575
+ description: "可选,图片输入 detail,默认 high"
549
576
  },
550
577
  delay_ms: {
551
578
  type: "integer",
@@ -577,11 +604,11 @@ function createRunInputSchema() {
577
604
  },
578
605
  no_filter: {
579
606
  type: "boolean",
580
- description: "开发/live gate 专用:跳过本次筛选器点击,默认 false"
607
+ description: "开发/live gate 专用:跳过本次筛选器点击,默认 false;正式 run 需要 debug_test_mode=true 才允许"
581
608
  },
582
609
  filter_enabled: {
583
610
  type: "boolean",
584
- description: "开发/live gate 专用:false 时跳过本次筛选器点击"
611
+ description: "开发/live gate 专用:false 时跳过本次筛选器点击;正式 run 需要 debug_test_mode=true 才允许"
585
612
  },
586
613
  refresh_on_end: {
587
614
  type: "boolean",
@@ -672,7 +699,16 @@ function createBossChatStartInputSchema({ requireFullInput = false } = {}) {
672
699
  },
673
700
  use_llm: {
674
701
  type: "boolean",
675
- description: "可选,是否使用 screening-config.json 中的 LLM 配置筛选;求简历任务默认使用"
702
+ description: "可选,默认 true。use_llm=false 等同 deterministic/local scorer,仅限 debug_test_mode=true 的明确测试场景"
703
+ },
704
+ screening_mode: {
705
+ type: "string",
706
+ enum: ["llm", "deterministic"],
707
+ description: "筛选引擎;默认 llm。deterministic 仅限 debug_test_mode=true"
708
+ },
709
+ debug_test_mode: {
710
+ type: "boolean",
711
+ description: "高级测试开关;默认 false。只有显式为 true 时才允许 deterministic/local scorer、detail_limit=0、dry-run 求简历等调试路径"
676
712
  },
677
713
  request_cv: {
678
714
  type: "boolean",
@@ -703,6 +739,15 @@ function createBossChatStartInputSchema({ requireFullInput = false } = {}) {
703
739
  minimum: 1000,
704
740
  description: "可选,单个候选人的 LLM 调用超时"
705
741
  },
742
+ llm_image_limit: {
743
+ type: "integer",
744
+ minimum: 1,
745
+ description: "可选,传给 LLM 的图片简历截图页数上限"
746
+ },
747
+ llm_image_detail: {
748
+ type: "string",
749
+ description: "可选,图片输入 detail,默认 high"
750
+ },
706
751
  online_resume_button_timeout_ms: {
707
752
  type: "integer",
708
753
  minimum: 1000,
@@ -84,9 +84,38 @@ function parseNonNegativeInteger(raw, fallback) {
84
84
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
85
85
  }
86
86
 
87
+ function isDebugTestMode(args = {}) {
88
+ return args.debug_test_mode === true || args.allow_debug_test_mode === true;
89
+ }
90
+
91
+ function normalizeScreeningModeArg(args = {}) {
92
+ const raw = normalizeText(args.screening_mode || args.screeningMode || "");
93
+ if (args.use_llm === false) return "deterministic";
94
+ return ["deterministic", "local", "local_scorer"].includes(raw.toLowerCase())
95
+ ? "deterministic"
96
+ : "llm";
97
+ }
98
+
99
+ function collectRecommendDebugTestOptions(args = {}, normalized = {}) {
100
+ const reasons = [];
101
+ if (normalizeScreeningModeArg(args) === "deterministic") reasons.push("deterministic_screening");
102
+ if (args.allow_card_only_screening === true) reasons.push("allow_card_only_screening");
103
+ if (parseNonNegativeInteger(args.detail_limit, null) === 0) reasons.push("detail_limit=0");
104
+ if (args.no_filter === true) reasons.push("no_filter");
105
+ if (args.filter_enabled === false) reasons.push("filter_enabled=false");
106
+ if (args.dry_run_post_action === true) reasons.push("dry_run_post_action");
107
+ if (args.execute_post_action === false && normalized.postAction && normalized.postAction !== "none") {
108
+ reasons.push("execute_post_action=false");
109
+ }
110
+ return reasons;
111
+ }
112
+
87
113
  function resolveRecommendDetailLimit(args = {}, normalized = {}) {
88
114
  const fallback = parsePositiveInteger(normalized.targetCount, 5);
89
115
  const requested = parseNonNegativeInteger(args.detail_limit, fallback);
116
+ if (requested === 0 && !isDebugTestMode(args)) {
117
+ return fallback;
118
+ }
90
119
  if (requested === 0 && args.allow_card_only_screening !== true) {
91
120
  return fallback;
92
121
  }
@@ -270,8 +299,15 @@ function ensureRecommendRunArtifacts(snapshot) {
270
299
  if (meta) meta.checkpointPath = artifacts.checkpoint_path;
271
300
 
272
301
  const summary = snapshot?.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
273
- if (summary) {
274
- const rows = Array.isArray(summary.results) ? summary.results : [];
302
+ const checkpointResults = Array.isArray(checkpoint.results) ? checkpoint.results : [];
303
+ const artifactSummary = summary || (checkpointResults.length ? {
304
+ domain: "recommend",
305
+ partial: true,
306
+ partial_reason: snapshot?.status || snapshot?.state || "non_terminal",
307
+ results: checkpointResults
308
+ } : null);
309
+ if (artifactSummary) {
310
+ const rows = Array.isArray(artifactSummary.results) ? artifactSummary.results : [];
275
311
  writeRecommendLegacyCsvAtomic(artifacts.output_csv, rows, snapshot, meta);
276
312
  writeJsonAtomic(artifacts.report_json, {
277
313
  run_id: snapshot.runId || snapshot.run_id,
@@ -280,7 +316,7 @@ function ensureRecommendRunArtifacts(snapshot) {
280
316
  progress: snapshot.progress || {},
281
317
  context: snapshot.context || {},
282
318
  checkpoint,
283
- summary,
319
+ summary: artifactSummary,
284
320
  generated_at: new Date().toISOString()
285
321
  });
286
322
  if (meta) {
@@ -297,6 +333,12 @@ function buildLegacyRecommendResult(snapshot) {
297
333
  const artifacts = ensureRecommendRunArtifacts(snapshot);
298
334
  const meta = getRecommendRunMeta(snapshot.runId);
299
335
  const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
336
+ const checkpoint = snapshot.checkpoint && typeof snapshot.checkpoint === "object" ? snapshot.checkpoint : {};
337
+ const resultRows = Array.isArray(summary?.results)
338
+ ? summary.results
339
+ : Array.isArray(checkpoint.results)
340
+ ? checkpoint.results
341
+ : [];
300
342
  const progress = normalizeLegacyProgress(snapshot.progress, summary);
301
343
  const targetCount = Number.isInteger(progress.target_count)
302
344
  ? progress.target_count
@@ -341,7 +383,7 @@ function buildLegacyRecommendResult(snapshot) {
341
383
  screen_params: clonePlain(meta.parsed?.screenParams || {}, {}),
342
384
  target_count_semantics: TARGET_COUNT_SEMANTICS,
343
385
  error: snapshot.error || null,
344
- results: Array.isArray(summary?.results) ? summary.results : []
386
+ results: resultRows
345
387
  };
346
388
  }
347
389
 
@@ -958,11 +1000,12 @@ function normalizeRecommendStartInput(args = {}, parsed, configResolution = null
958
1000
  postAction: parsed.screenParams?.post_action || "none",
959
1001
  maxGreetCount: Number.isInteger(parsed.screenParams?.max_greet_count)
960
1002
  ? parsed.screenParams.max_greet_count
961
- : null
1003
+ : null,
1004
+ screeningMode: normalizeScreeningModeArg(args)
962
1005
  };
963
1006
  }
964
1007
 
965
- function getRunOptions(args, parsed, normalized, session) {
1008
+ function getRunOptions(args, parsed, normalized, session, configResolution = null) {
966
1009
  const slowLive = args.slow_live === true;
967
1010
  const executePostAction = args.dry_run_post_action === true
968
1011
  ? false
@@ -998,6 +1041,13 @@ function getRunOptions(args, parsed, normalized, session) {
998
1041
  actionTimeoutMs: parsePositiveInteger(args.action_timeout_ms, slowLive ? 12000 : 8000),
999
1042
  actionIntervalMs: parsePositiveInteger(args.action_interval_ms, 500),
1000
1043
  actionAfterClickDelayMs: parseNonNegativeInteger(args.action_after_click_delay_ms, slowLive ? 1200 : 900),
1044
+ screeningMode: normalized.screeningMode,
1045
+ llmConfig: normalized.screeningMode === "llm" && configResolution?.ok ? {
1046
+ ...configResolution.config
1047
+ } : null,
1048
+ llmTimeoutMs: parsePositiveInteger(args.llm_timeout_ms, slowLive ? 180000 : 120000),
1049
+ llmImageLimit: parsePositiveInteger(args.llm_image_limit, 8),
1050
+ llmImageDetail: normalizeText(args.llm_image_detail) || "high",
1001
1051
  name: "mcp-recommend-pipeline-run",
1002
1052
  parsed
1003
1053
  };
@@ -1054,6 +1104,30 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
1054
1104
  if (gate) return gate;
1055
1105
  const configResolution = resolveBossScreeningConfig(workspaceRoot);
1056
1106
  const normalized = normalizeRecommendStartInput(args, parsed, configResolution);
1107
+ const debugTestOptions = collectRecommendDebugTestOptions(args, normalized);
1108
+ if (debugTestOptions.length && !isDebugTestMode(args)) {
1109
+ return {
1110
+ status: "FAILED",
1111
+ error: {
1112
+ code: "DEBUG_TEST_MODE_REQUIRED",
1113
+ message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
1114
+ retryable: false
1115
+ },
1116
+ debug_test_options: debugTestOptions
1117
+ };
1118
+ }
1119
+ if (normalized.screeningMode === "llm" && !configResolution.ok) {
1120
+ return {
1121
+ status: "FAILED",
1122
+ error: {
1123
+ code: "SCREEN_CONFIG_ERROR",
1124
+ message: configResolution.error?.message || "screening-config.json is required for LLM screening.",
1125
+ retryable: true
1126
+ },
1127
+ config_path: configResolution.config_path || null,
1128
+ candidate_paths: configResolution.candidate_paths || []
1129
+ };
1130
+ }
1057
1131
 
1058
1132
  let session;
1059
1133
  try {
@@ -1085,7 +1159,7 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
1085
1159
 
1086
1160
  let started;
1087
1161
  try {
1088
- started = recommendRunService.startRecommendRun(getRunOptions(args, parsed, normalized, session));
1162
+ started = recommendRunService.startRecommendRun(getRunOptions(args, parsed, normalized, session, configResolution));
1089
1163
  } catch (error) {
1090
1164
  await session.close?.();
1091
1165
  return {