@reconcrap/boss-recommend-mcp 2.0.5 → 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.
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/chat-mcp.js +59 -18
- package/src/core/reporting/legacy-csv.js +2 -2
- package/src/core/screening/index.js +48 -0
- package/src/domains/chat/run-service.js +93 -40
- package/src/domains/recommend/detail.js +189 -6
- package/src/domains/recommend/run-service.js +92 -9
- package/src/domains/recruit/run-service.js +87 -12
- package/src/index.js +50 -5
- package/src/recommend-mcp.js +81 -7
- package/src/recruit-mcp.js +103 -8
|
@@ -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 {
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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: "
|
|
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,
|
package/src/recommend-mcp.js
CHANGED
|
@@ -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
|
-
|
|
274
|
-
|
|
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:
|
|
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 {
|
package/src/recruit-mcp.js
CHANGED
|
@@ -73,6 +73,27 @@ function parseNonNegativeInteger(raw, fallback) {
|
|
|
73
73
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
function isDebugTestMode(args = {}) {
|
|
77
|
+
return args.debug_test_mode === true || args.allow_debug_test_mode === true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeScreeningModeArg(args = {}) {
|
|
81
|
+
const raw = normalizeText(args.screening_mode || args.screeningMode || "");
|
|
82
|
+
if (args.use_llm === false) return "deterministic";
|
|
83
|
+
return ["deterministic", "local", "local_scorer"].includes(raw.toLowerCase())
|
|
84
|
+
? "deterministic"
|
|
85
|
+
: "llm";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function collectRecruitDebugTestOptions(args = {}) {
|
|
89
|
+
const reasons = [];
|
|
90
|
+
if (normalizeScreeningModeArg(args) === "deterministic") reasons.push("deterministic_screening");
|
|
91
|
+
if (parseNonNegativeInteger(args.detail_limit, null) === 0) reasons.push("detail_limit=0");
|
|
92
|
+
if (args.dry_run_post_action === true) reasons.push("dry_run_post_action");
|
|
93
|
+
if (args.execute_post_action === false) reasons.push("execute_post_action=false");
|
|
94
|
+
return reasons;
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
function methodSummary(methodLog = []) {
|
|
77
98
|
const summary = {};
|
|
78
99
|
for (const entry of methodLog || []) {
|
|
@@ -203,8 +224,15 @@ function ensureRecruitRunArtifacts(snapshot) {
|
|
|
203
224
|
if (meta) meta.checkpointPath = artifacts.checkpoint_path;
|
|
204
225
|
|
|
205
226
|
const summary = snapshot?.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
|
|
206
|
-
|
|
207
|
-
|
|
227
|
+
const checkpointResults = Array.isArray(checkpoint.results) ? checkpoint.results : [];
|
|
228
|
+
const artifactSummary = summary || (checkpointResults.length ? {
|
|
229
|
+
domain: "recruit",
|
|
230
|
+
partial: true,
|
|
231
|
+
partial_reason: snapshot?.status || snapshot?.state || "non_terminal",
|
|
232
|
+
results: checkpointResults
|
|
233
|
+
} : null);
|
|
234
|
+
if (artifactSummary) {
|
|
235
|
+
const rows = Array.isArray(artifactSummary.results) ? artifactSummary.results : [];
|
|
208
236
|
writeRecruitLegacyCsvAtomic(artifacts.output_csv, rows, snapshot, meta);
|
|
209
237
|
writeJsonAtomic(artifacts.report_json, {
|
|
210
238
|
run_id: snapshot.runId || snapshot.run_id,
|
|
@@ -213,7 +241,7 @@ function ensureRecruitRunArtifacts(snapshot) {
|
|
|
213
241
|
progress: snapshot.progress || {},
|
|
214
242
|
context: snapshot.context || {},
|
|
215
243
|
checkpoint,
|
|
216
|
-
summary,
|
|
244
|
+
summary: artifactSummary,
|
|
217
245
|
generated_at: new Date().toISOString()
|
|
218
246
|
});
|
|
219
247
|
if (meta) {
|
|
@@ -270,6 +298,12 @@ function buildLegacyRunResult(snapshot) {
|
|
|
270
298
|
const artifacts = ensureRecruitRunArtifacts(snapshot);
|
|
271
299
|
const meta = getRecruitRunMeta(snapshot.runId);
|
|
272
300
|
const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
|
|
301
|
+
const checkpoint = snapshot.checkpoint && typeof snapshot.checkpoint === "object" ? snapshot.checkpoint : {};
|
|
302
|
+
const resultRows = Array.isArray(summary?.results)
|
|
303
|
+
? summary.results
|
|
304
|
+
: Array.isArray(checkpoint.results)
|
|
305
|
+
? checkpoint.results
|
|
306
|
+
: [];
|
|
273
307
|
const progress = normalizeLegacyProgress(snapshot.progress, summary);
|
|
274
308
|
const targetCount = Number.isInteger(progress.target_count)
|
|
275
309
|
? progress.target_count
|
|
@@ -302,7 +336,8 @@ function buildLegacyRunResult(snapshot) {
|
|
|
302
336
|
|| null,
|
|
303
337
|
completion_reason: completionReason(snapshot.status),
|
|
304
338
|
target_count_semantics: TARGET_COUNT_SEMANTICS,
|
|
305
|
-
run_id: snapshot.runId
|
|
339
|
+
run_id: snapshot.runId,
|
|
340
|
+
results: resultRows
|
|
306
341
|
};
|
|
307
342
|
}
|
|
308
343
|
|
|
@@ -387,7 +422,34 @@ export function createRecruitPipelineInputSchema() {
|
|
|
387
422
|
detail_limit: {
|
|
388
423
|
type: "integer",
|
|
389
424
|
minimum: 0,
|
|
390
|
-
description: "
|
|
425
|
+
description: "打开详情/CV 的人数上限;默认跟随 max_candidates。detail_limit=0 属于调试路径,需要 debug_test_mode=true"
|
|
426
|
+
},
|
|
427
|
+
debug_test_mode: {
|
|
428
|
+
type: "boolean",
|
|
429
|
+
description: "高级测试开关;默认 false。只有显式为 true 时才允许 deterministic/local scorer、detail_limit=0 等调试路径"
|
|
430
|
+
},
|
|
431
|
+
screening_mode: {
|
|
432
|
+
type: "string",
|
|
433
|
+
enum: ["llm", "deterministic"],
|
|
434
|
+
description: "筛选引擎;默认 llm。deterministic 仅限 debug_test_mode=true"
|
|
435
|
+
},
|
|
436
|
+
use_llm: {
|
|
437
|
+
type: "boolean",
|
|
438
|
+
description: "兼容字段;默认 true。use_llm=false 等同 deterministic,仅限 debug_test_mode=true"
|
|
439
|
+
},
|
|
440
|
+
llm_timeout_ms: {
|
|
441
|
+
type: "integer",
|
|
442
|
+
minimum: 1000,
|
|
443
|
+
description: "可选,单个候选人的 LLM 调用超时"
|
|
444
|
+
},
|
|
445
|
+
llm_image_limit: {
|
|
446
|
+
type: "integer",
|
|
447
|
+
minimum: 1,
|
|
448
|
+
description: "可选,传给 LLM 的图片简历截图页数上限"
|
|
449
|
+
},
|
|
450
|
+
llm_image_detail: {
|
|
451
|
+
type: "string",
|
|
452
|
+
description: "可选,图片输入 detail,默认 high"
|
|
391
453
|
},
|
|
392
454
|
delay_ms: {
|
|
393
455
|
type: "integer",
|
|
@@ -751,22 +813,30 @@ async function connectRecruitChromeSession({
|
|
|
751
813
|
};
|
|
752
814
|
}
|
|
753
815
|
|
|
754
|
-
function getRunOptions(args, parsed, session) {
|
|
816
|
+
function getRunOptions(args, parsed, session, configResolution = null) {
|
|
755
817
|
const slowLive = args.slow_live === true;
|
|
756
818
|
const targetCount = parsePositiveInteger(args.max_candidates, parsed.screenParams.target_count || 10);
|
|
819
|
+
const screeningMode = normalizeScreeningModeArg(args);
|
|
757
820
|
return {
|
|
758
821
|
client: session.client,
|
|
759
822
|
targetUrl: RECRUIT_TARGET_URL,
|
|
760
823
|
criteria: parsed.screenParams.criteria,
|
|
761
824
|
searchParams: parsed.searchParams,
|
|
762
825
|
maxCandidates: targetCount,
|
|
763
|
-
detailLimit: parseNonNegativeInteger(args.detail_limit,
|
|
826
|
+
detailLimit: parseNonNegativeInteger(args.detail_limit, targetCount),
|
|
764
827
|
closeDetail: true,
|
|
765
828
|
delayMs: Math.max(0, parsePositiveInteger(args.delay_ms, 0)),
|
|
766
829
|
cardTimeoutMs: slowLive ? 180000 : 90000,
|
|
767
830
|
resetBeforeSearch: args.reset_search !== false,
|
|
768
831
|
resetTimeoutMs: slowLive ? 300000 : 180000,
|
|
769
832
|
cityOptionTimeoutMs: slowLive ? 60000 : 30000,
|
|
833
|
+
screeningMode,
|
|
834
|
+
llmConfig: screeningMode === "llm" && configResolution?.ok ? {
|
|
835
|
+
...configResolution.config
|
|
836
|
+
} : null,
|
|
837
|
+
llmTimeoutMs: parsePositiveInteger(args.llm_timeout_ms, slowLive ? 180000 : 120000),
|
|
838
|
+
llmImageLimit: parsePositiveInteger(args.llm_image_limit, 8),
|
|
839
|
+
llmImageDetail: normalizeText(args.llm_image_detail) || "high",
|
|
770
840
|
name: "mcp-recruit-pipeline-run"
|
|
771
841
|
};
|
|
772
842
|
}
|
|
@@ -814,6 +884,31 @@ async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "" }
|
|
|
814
884
|
const gate = evaluateRecruitPipelineGate(parsed);
|
|
815
885
|
if (gate) return gate;
|
|
816
886
|
const configResolution = resolveBossScreeningConfig(workspaceRoot);
|
|
887
|
+
const screeningMode = normalizeScreeningModeArg(args);
|
|
888
|
+
const debugTestOptions = collectRecruitDebugTestOptions(args);
|
|
889
|
+
if (debugTestOptions.length && !isDebugTestMode(args)) {
|
|
890
|
+
return {
|
|
891
|
+
status: "FAILED",
|
|
892
|
+
error: {
|
|
893
|
+
code: "DEBUG_TEST_MODE_REQUIRED",
|
|
894
|
+
message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
|
|
895
|
+
retryable: false
|
|
896
|
+
},
|
|
897
|
+
debug_test_options: debugTestOptions
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
if (screeningMode === "llm" && !configResolution.ok) {
|
|
901
|
+
return {
|
|
902
|
+
status: "FAILED",
|
|
903
|
+
error: {
|
|
904
|
+
code: "SCREEN_CONFIG_ERROR",
|
|
905
|
+
message: configResolution.error?.message || "screening-config.json is required for LLM screening.",
|
|
906
|
+
retryable: true
|
|
907
|
+
},
|
|
908
|
+
config_path: configResolution.config_path || null,
|
|
909
|
+
candidate_paths: configResolution.candidate_paths || []
|
|
910
|
+
};
|
|
911
|
+
}
|
|
817
912
|
const host = normalizeText(args.host) || DEFAULT_RECRUIT_HOST;
|
|
818
913
|
const port = parsePositiveInteger(
|
|
819
914
|
args.port,
|
|
@@ -850,7 +945,7 @@ async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "" }
|
|
|
850
945
|
|
|
851
946
|
let started;
|
|
852
947
|
try {
|
|
853
|
-
started = recruitRunService.startRecruitRun(getRunOptions(args, parsed, session));
|
|
948
|
+
started = recruitRunService.startRecruitRun(getRunOptions(args, parsed, session, configResolution));
|
|
854
949
|
} catch (error) {
|
|
855
950
|
await session.close?.();
|
|
856
951
|
return {
|