@reconcrap/boss-recommend-mcp 2.0.6 → 2.0.8

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.
@@ -1,4 +1,9 @@
1
1
  import { createRunLifecycleManager } from "../../core/run/index.js";
2
+ import {
3
+ addTiming,
4
+ imageEvidenceFilePath,
5
+ measureTiming
6
+ } from "../../core/run/timing.js";
2
7
  import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
3
8
  import {
4
9
  compactCvAcquisitionState,
@@ -19,7 +24,13 @@ import {
19
24
  resetInfiniteListForRefreshRound
20
25
  } from "../../core/infinite-list/index.js";
21
26
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
22
- import { screenCandidate } from "../../core/screening/index.js";
27
+ import {
28
+ callScreeningLlm,
29
+ compactScreeningLlmResult,
30
+ createFailedLlmScreeningResult,
31
+ llmResultToScreening,
32
+ screenCandidate
33
+ } from "../../core/screening/index.js";
23
34
  import {
24
35
  closeRecruitDetail,
25
36
  createRecruitDetailNetworkRecorder,
@@ -72,10 +83,22 @@ function compactDetail(detailResult) {
72
83
  parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
73
84
  cv_acquisition: detailResult.cv_acquisition || null,
74
85
  image_evidence: summarizeImageEvidence(detailResult.image_evidence),
86
+ llm_screening: compactScreeningLlmResult(detailResult.llm_result),
75
87
  close_result: detailResult.close_result
76
88
  };
77
89
  }
78
90
 
91
+ function normalizeScreeningMode(value) {
92
+ const normalized = String(value || "llm").trim().toLowerCase();
93
+ return ["deterministic", "local", "local_scorer"].includes(normalized)
94
+ ? "deterministic"
95
+ : "llm";
96
+ }
97
+
98
+ function createMissingLlmConfigResult() {
99
+ return createFailedLlmScreeningResult(new Error("LLM screening config is required for production search runs"));
100
+ }
101
+
79
102
  function normalizeSearchParams(searchParams = {}) {
80
103
  return normalizeRecruitSearchParams(searchParams);
81
104
  }
@@ -110,7 +133,7 @@ export async function runRecruitWorkflow({
110
133
  criteria = "",
111
134
  searchParams = {},
112
135
  maxCandidates = 5,
113
- detailLimit = 1,
136
+ detailLimit = null,
114
137
  closeDetail = true,
115
138
  delayMs = 0,
116
139
  cardTimeoutMs = 90000,
@@ -127,12 +150,20 @@ export async function runRecruitWorkflow({
127
150
  listFallbackPoint = null,
128
151
  refreshOnEnd = true,
129
152
  maxRefreshRounds = 2,
130
- refreshResetSettleMs = 5000
153
+ refreshResetSettleMs = 5000,
154
+ screeningMode = "llm",
155
+ llmConfig = null,
156
+ llmTimeoutMs = 120000,
157
+ llmImageLimit = 8,
158
+ llmImageDetail = "high",
159
+ imageOutputDir = ""
131
160
  } = {}, runControl) {
132
161
  if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
133
162
  const normalizedSearchParams = normalizeSearchParams(searchParams);
163
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
164
+ const useLlmScreening = normalizedScreeningMode !== "deterministic";
134
165
  const limit = Math.max(1, Number(maxCandidates) || 1);
135
- const detailCountLimit = Math.max(0, Number(detailLimit) || 0);
166
+ const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
136
167
  const networkRecorder = detailCountLimit > 0
137
168
  ? createRecruitDetailNetworkRecorder(client)
138
169
  : null;
@@ -222,6 +253,8 @@ export async function runRecruitWorkflow({
222
253
  screened: 0,
223
254
  detail_opened: 0,
224
255
  passed: 0,
256
+ screening_mode: normalizedScreeningMode,
257
+ llm_screened: 0,
225
258
  unique_seen: compactInfiniteListState(listState).seen_count,
226
259
  scroll_count: 0,
227
260
  refresh_rounds: 0,
@@ -231,12 +264,14 @@ export async function runRecruitWorkflow({
231
264
  });
232
265
 
233
266
  while (results.length < limit) {
267
+ const candidateStarted = Date.now();
268
+ const timings = {};
234
269
  await runControl.waitIfPaused();
235
270
  runControl.throwIfCanceled();
236
271
  runControl.setPhase("recruit:candidate");
237
272
  rootState = await ensureRecruitViewport(rootState, "candidate_loop");
238
273
 
239
- const nextCandidateResult = await getNextInfiniteListCandidate({
274
+ const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
240
275
  client,
241
276
  state: listState,
242
277
  maxScrolls: listMaxScrolls,
@@ -264,7 +299,7 @@ export async function runRecruitWorkflow({
264
299
  search_params: normalizedSearchParams
265
300
  }
266
301
  })
267
- });
302
+ }));
268
303
  if (!nextCandidateResult.ok) {
269
304
  listEndReason = nextCandidateResult.reason || "list_exhausted";
270
305
  if (
@@ -298,6 +333,7 @@ export async function runRecruitWorkflow({
298
333
  screened: results.length,
299
334
  detail_opened: results.filter((item) => item.detail).length,
300
335
  passed: results.filter((item) => item.screening.passed).length,
336
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
301
337
  unique_seen: compactInfiniteListState(listState).seen_count,
302
338
  scroll_count: compactInfiniteListState(listState).scroll_count,
303
339
  refresh_rounds: refreshRounds,
@@ -345,8 +381,10 @@ export async function runRecruitWorkflow({
345
381
  rootState = await ensureRecruitViewport(rootState, "detail");
346
382
  networkRecorder.clear();
347
383
  const openedDetail = await openRecruitCardDetail(client, cardNodeId);
384
+ addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
385
+ addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
348
386
  const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
349
- const networkWait = await waitForCvNetworkEvents(
387
+ const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
350
388
  waitForRecruitDetailNetworkEvents,
351
389
  networkRecorder,
352
390
  {
@@ -355,7 +393,10 @@ export async function runRecruitWorkflow({
355
393
  requireLoaded: true,
356
394
  intervalMs: 120
357
395
  }
358
- );
396
+ ));
397
+ if (networkWait?.elapsed_ms != null) {
398
+ timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
399
+ }
359
400
  detailResult = await extractRecruitDetailCandidate(client, {
360
401
  cardCandidate,
361
402
  cardNodeId,
@@ -377,7 +418,13 @@ export async function runRecruitWorkflow({
377
418
  || openedDetail.detail_state?.resumeIframe?.node_id
378
419
  || null;
379
420
  if (captureNodeId) {
380
- imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
421
+ imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
422
+ filePath: imageEvidenceFilePath({
423
+ imageOutputDir,
424
+ domain: "recruit",
425
+ runId: runControl?.runId,
426
+ index
427
+ }),
381
428
  padding: 4,
382
429
  maxScreenshots: maxImagePages,
383
430
  wheelDeltaY: imageWheelDeltaY,
@@ -389,7 +436,7 @@ export async function runRecruitWorkflow({
389
436
  run_candidate_index: index,
390
437
  candidate_key: candidateKey
391
438
  }
392
- });
439
+ }));
393
440
  source = "image";
394
441
  recordCvImageFallback(cvAcquisitionState, {
395
442
  parsedNetworkProfileCount,
@@ -408,7 +455,7 @@ export async function runRecruitWorkflow({
408
455
 
409
456
  let closeResult = null;
410
457
  if (closeDetail) {
411
- closeResult = await closeRecruitDetail(client);
458
+ closeResult = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
412
459
  }
413
460
  detailResult.close_result = closeResult;
414
461
  detailResult.image_evidence = imageEvidence;
@@ -426,14 +473,43 @@ export async function runRecruitWorkflow({
426
473
  await runControl.waitIfPaused();
427
474
  runControl.throwIfCanceled();
428
475
  runControl.setPhase("recruit:screening");
429
- const screening = screenCandidate(screeningCandidate, { criteria });
476
+ let llmResult = null;
477
+ if (useLlmScreening) {
478
+ if (!llmConfig) {
479
+ llmResult = createMissingLlmConfigResult();
480
+ } else {
481
+ try {
482
+ const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
483
+ ? "vision_model_ms"
484
+ : "text_model_ms";
485
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
486
+ candidate: screeningCandidate,
487
+ criteria,
488
+ config: llmConfig,
489
+ timeoutMs: llmTimeoutMs,
490
+ imageEvidence: detailResult?.image_evidence || null,
491
+ maxImages: llmImageLimit,
492
+ imageDetail: llmImageDetail
493
+ }));
494
+ } catch (error) {
495
+ llmResult = createFailedLlmScreeningResult(error);
496
+ }
497
+ }
498
+ if (detailResult) detailResult.llm_result = llmResult;
499
+ }
500
+ const screening = useLlmScreening
501
+ ? llmResultToScreening(llmResult, screeningCandidate)
502
+ : screenCandidate(screeningCandidate, { criteria });
503
+ timings.total_ms = Date.now() - candidateStarted;
430
504
  const compactResult = {
431
505
  index,
432
506
  candidate_key: candidateKey,
433
507
  card_node_id: cardNodeId,
434
508
  candidate: compactCandidate(screeningCandidate),
435
509
  detail: compactDetail(detailResult),
436
- screening: compactScreening(screening)
510
+ llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
511
+ screening: compactScreening(screening),
512
+ timings
437
513
  };
438
514
  results.push(compactResult);
439
515
  markInfiniteListCandidateProcessed(listState, candidateKey, {
@@ -450,6 +526,7 @@ export async function runRecruitWorkflow({
450
526
  screened: results.length,
451
527
  detail_opened: results.filter((item) => item.detail).length,
452
528
  passed: results.filter((item) => item.screening.passed).length,
529
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
453
530
  unique_seen: compactInfiniteListState(listState).seen_count,
454
531
  scroll_count: compactInfiniteListState(listState).scroll_count,
455
532
  refresh_rounds: refreshRounds,
@@ -461,7 +538,9 @@ export async function runRecruitWorkflow({
461
538
  last_candidate_key: candidateKey,
462
539
  last_score: screening.score
463
540
  });
541
+ const checkpointStarted = Date.now();
464
542
  runControl.checkpoint({
543
+ results,
465
544
  last_candidate: {
466
545
  id: screeningCandidate.id || null,
467
546
  key: candidateKey,
@@ -470,12 +549,17 @@ export async function runRecruitWorkflow({
470
549
  status: screening.status,
471
550
  passed: screening.passed,
472
551
  score: screening.score
473
- }
552
+ },
553
+ llm_screening: compactScreeningLlmResult(llmResult)
474
554
  }
475
555
  });
556
+ addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
476
557
 
477
558
  if (delayMs > 0) {
559
+ const sleepStarted = Date.now();
478
560
  await runControl.sleep(delayMs);
561
+ addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
562
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
479
563
  }
480
564
  }
481
565
 
@@ -496,6 +580,7 @@ export async function runRecruitWorkflow({
496
580
  processed: results.length,
497
581
  screened: results.length,
498
582
  detail_opened: results.filter((item) => item.detail).length,
583
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
499
584
  passed: results.filter((item) => item.screening.passed).length,
500
585
  results
501
586
  };
@@ -514,7 +599,7 @@ export function createRecruitRunService({
514
599
  criteria = "",
515
600
  searchParams = {},
516
601
  maxCandidates = 5,
517
- detailLimit = 1,
602
+ detailLimit = null,
518
603
  closeDetail = true,
519
604
  delayMs = 0,
520
605
  cardTimeoutMs = 90000,
@@ -532,10 +617,19 @@ export function createRecruitRunService({
532
617
  refreshOnEnd = true,
533
618
  maxRefreshRounds = 2,
534
619
  refreshResetSettleMs = 5000,
620
+ screeningMode = "llm",
621
+ llmConfig = null,
622
+ llmTimeoutMs = 120000,
623
+ llmImageLimit = 8,
624
+ llmImageDetail = "high",
625
+ imageOutputDir = "",
535
626
  name = "recruit-domain-run"
536
627
  } = {}) {
537
628
  if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
538
629
  const normalizedSearchParams = normalizeSearchParams(searchParams);
630
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
631
+ const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
632
+ const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
539
633
  return manager.startRun({
540
634
  name,
541
635
  context: {
@@ -544,7 +638,7 @@ export function createRecruitRunService({
544
638
  criteria_present: Boolean(criteria),
545
639
  search_params: normalizedSearchParams,
546
640
  max_candidates: maxCandidates,
547
- detail_limit: detailLimit,
641
+ detail_limit: normalizedDetailLimit,
548
642
  close_detail: closeDetail,
549
643
  reset_before_search: resetBeforeSearch,
550
644
  reset_timeout_ms: resetTimeoutMs,
@@ -559,14 +653,21 @@ export function createRecruitRunService({
559
653
  list_fallback_point: listFallbackPoint,
560
654
  refresh_on_end: refreshOnEnd,
561
655
  max_refresh_rounds: maxRefreshRounds,
562
- refresh_reset_settle_ms: refreshResetSettleMs
656
+ refresh_reset_settle_ms: refreshResetSettleMs,
657
+ screening_mode: normalizedScreeningMode,
658
+ llm_configured: Boolean(llmConfig),
659
+ llm_timeout_ms: llmTimeoutMs,
660
+ llm_image_limit: llmImageLimit,
661
+ llm_image_detail: llmImageDetail,
662
+ image_output_dir: imageOutputDir || ""
563
663
  },
564
664
  progress: {
565
665
  card_count: 0,
566
- target_count: Math.max(1, Number(maxCandidates) || 1),
666
+ target_count: candidateLimit,
567
667
  processed: 0,
568
668
  screened: 0,
569
669
  detail_opened: 0,
670
+ llm_screened: 0,
570
671
  passed: 0
571
672
  },
572
673
  checkpoint: {},
@@ -576,7 +677,7 @@ export function createRecruitRunService({
576
677
  criteria,
577
678
  searchParams: normalizedSearchParams,
578
679
  maxCandidates,
579
- detailLimit,
680
+ detailLimit: normalizedDetailLimit,
580
681
  closeDetail,
581
682
  delayMs,
582
683
  cardTimeoutMs,
@@ -593,7 +694,13 @@ export function createRecruitRunService({
593
694
  listFallbackPoint,
594
695
  refreshOnEnd,
595
696
  maxRefreshRounds,
596
- refreshResetSettleMs
697
+ refreshResetSettleMs,
698
+ screeningMode: normalizedScreeningMode,
699
+ llmConfig,
700
+ llmTimeoutMs,
701
+ llmImageLimit,
702
+ llmImageDetail,
703
+ imageOutputDir
597
704
  }, runControl)
598
705
  });
599
706
  }
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,14 @@ 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",
1051
+ imageOutputDir: resolveBossConfiguredOutputDir("", getRunsDir()),
1001
1052
  name: "mcp-recommend-pipeline-run",
1002
1053
  parsed
1003
1054
  };
@@ -1054,6 +1105,30 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
1054
1105
  if (gate) return gate;
1055
1106
  const configResolution = resolveBossScreeningConfig(workspaceRoot);
1056
1107
  const normalized = normalizeRecommendStartInput(args, parsed, configResolution);
1108
+ const debugTestOptions = collectRecommendDebugTestOptions(args, normalized);
1109
+ if (debugTestOptions.length && !isDebugTestMode(args)) {
1110
+ return {
1111
+ status: "FAILED",
1112
+ error: {
1113
+ code: "DEBUG_TEST_MODE_REQUIRED",
1114
+ message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
1115
+ retryable: false
1116
+ },
1117
+ debug_test_options: debugTestOptions
1118
+ };
1119
+ }
1120
+ if (normalized.screeningMode === "llm" && !configResolution.ok) {
1121
+ return {
1122
+ status: "FAILED",
1123
+ error: {
1124
+ code: "SCREEN_CONFIG_ERROR",
1125
+ message: configResolution.error?.message || "screening-config.json is required for LLM screening.",
1126
+ retryable: true
1127
+ },
1128
+ config_path: configResolution.config_path || null,
1129
+ candidate_paths: configResolution.candidate_paths || []
1130
+ };
1131
+ }
1057
1132
 
1058
1133
  let session;
1059
1134
  try {
@@ -1085,7 +1160,7 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
1085
1160
 
1086
1161
  let started;
1087
1162
  try {
1088
- started = recommendRunService.startRecommendRun(getRunOptions(args, parsed, normalized, session));
1163
+ started = recommendRunService.startRecommendRun(getRunOptions(args, parsed, normalized, session, configResolution));
1089
1164
  } catch (error) {
1090
1165
  await session.close?.();
1091
1166
  return {