@reconcrap/boss-recommend-mcp 1.3.14 → 1.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/parser.js
CHANGED
|
@@ -86,26 +86,33 @@ const RECENT_NOT_VIEW_NEGATIVE_PATTERNS = [
|
|
|
86
86
|
/保留[^。;;\n]{0,12}14天/i
|
|
87
87
|
];
|
|
88
88
|
const TARGET_COUNT_PATTERNS = [
|
|
89
|
+
/目标筛选数(?:量)?(?:为|是|:|:)?\s*(\d+)/i,
|
|
90
|
+
/目标通过数(?:量)?(?:为|是|:|:)?\s*(\d+)/i,
|
|
89
91
|
/目标(?:处理|筛选|通过)?(?:人数|数量)?(?:为|是|:|:)?\s*(\d+)/i,
|
|
90
92
|
/至少(?:处理|筛选|通过)\s*(\d+)\s*(?:位|人)/i,
|
|
91
93
|
/(?:处理|筛选|通过)\s*(\d+)\s*(?:位|人)/i
|
|
92
94
|
];
|
|
93
95
|
const MAX_GREET_COUNT_PATTERNS = [
|
|
96
|
+
/最大招呼数(?:量)?(?:为|是|:|:)?\s*(\d+)/i,
|
|
97
|
+
/最大(?:打招呼|招呼|沟通|联系)(?:人数|数量|数)?(?:为|是|:|:)?\s*(\d+)/i,
|
|
94
98
|
/最多(?:打招呼|沟通|联系)\s*(\d+)\s*(?:位|人|个)?/i,
|
|
95
99
|
/(?:打招呼|沟通|联系)(?:上限|最多|不超过|至多)(?:为|是|:|:)?\s*(\d+)/i
|
|
96
100
|
];
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
const CRITERIA_EXPLICIT_MARKER_PATTERN = /筛选条件\s*[::]/i;
|
|
102
|
+
const CRITERIA_EXPLICIT_STOP_PATTERN = /(?:^|[\n;;])\s*(?:页面选择|学校标签|院校标签|学历|学位|性别|是否过滤近14天看过|目标筛选数|目标通过人数|通过筛选后动作|最大招呼数|最大打招呼数|岗位)\s*[::]/i;
|
|
103
|
+
const CRITERIA_META_FIELD_PREFIX_PATTERNS = [
|
|
104
|
+
/^(?:页面选择|学校标签|院校标签|学历|学位|性别|是否过滤近14天看过|目标筛选数|目标通过人数|通过筛选后动作|最大招呼数|最大打招呼数|岗位)\s*(?:[::]|$)/i,
|
|
105
|
+
/^(?:近?14天(?:内)?(?:没有|没看过|未查看)|(?:不过滤|保留|过滤|排除)[^。;;\n]{0,12}14天)\s*(?:[::]|$)?/i,
|
|
106
|
+
/^(?:目标(?:处理|筛选|通过)?(?:人数|数量)?|至少(?:处理|筛选|通过)|(?:处理|筛选|通过)\s*\d+\s*(?:位|人))(?:[::\s]|$)/i,
|
|
107
|
+
/^(?:最多(?:打招呼|沟通|联系)|(?:打招呼|沟通|联系)(?:上限|最多|不超过|至多))(?:[::\s]|$)/i,
|
|
108
|
+
/^(?:(?:通过筛选后)?动作|post[_\s-]?action|max[_\s-]?greet[_\s-]?count|target[_\s-]?count)\s*(?:[::]|$)/i
|
|
105
109
|
];
|
|
106
110
|
const META_CLAUSE_PATTERNS = [
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
/^推荐页|^推荐页面|^boss推荐/i,
|
|
112
|
+
/^帮我|^请|^运行|^使用.*skill/i,
|
|
113
|
+
/^启动boss推荐任务/i,
|
|
114
|
+
/^条件如下(?:[::]|$)/i,
|
|
115
|
+
/^(?:符合标准(?:的人选)?(?:都)?(?:的)?(?:动作)?[::]?\s*)?(?:收藏|打招呼|直接沟通|什么也不做|不做任何操作|不操作|仅筛选|只筛选)(?:[::]|$)/i
|
|
109
116
|
];
|
|
110
117
|
const FEATURED_SCOPE_PATTERN = /(?:精选牛人|精选页|精选页面|精选tab|精选标签|tab[^。;;\n]{0,6}精选|精选)/i;
|
|
111
118
|
const LATEST_SCOPE_PATTERN = /(?:最新页|最新页面|最新tab|最新标签|tab[^。;;\n]{0,6}最新|最新)/i;
|
|
@@ -394,6 +401,13 @@ function extractMaxGreetCount(text) {
|
|
|
394
401
|
return null;
|
|
395
402
|
}
|
|
396
403
|
|
|
404
|
+
function extractJobSelectionHint(text) {
|
|
405
|
+
const normalized = String(text || "").replace(/\r\n/g, "\n");
|
|
406
|
+
const match = normalized.match(/(?:^|[\n;;])\s*(?:岗位|职位|job)\s*[::]\s*([^\n;;]+)/i);
|
|
407
|
+
if (!match?.[1]) return null;
|
|
408
|
+
return normalizeText(String(match[1] || "").replace(/[。;;]+$/, "").trim());
|
|
409
|
+
}
|
|
410
|
+
|
|
397
411
|
function sanitizeClause(clause) {
|
|
398
412
|
let current = normalizeText(clause);
|
|
399
413
|
for (const pattern of LEADING_NOISE_PATTERNS) {
|
|
@@ -408,25 +422,128 @@ function sanitizeClause(clause) {
|
|
|
408
422
|
return current;
|
|
409
423
|
}
|
|
410
424
|
|
|
411
|
-
function
|
|
412
|
-
const
|
|
413
|
-
if (
|
|
414
|
-
|
|
425
|
+
function isMetaClause(clause) {
|
|
426
|
+
const normalized = sanitizeClause(clause);
|
|
427
|
+
if (!normalized) return true;
|
|
428
|
+
const withoutNumbering = normalized.replace(/^\d+\s*[))]\s*/, "").trim();
|
|
429
|
+
if (!withoutNumbering) return true;
|
|
430
|
+
if (CRITERIA_META_FIELD_PREFIX_PATTERNS.some((pattern) => pattern.test(withoutNumbering))) return true;
|
|
431
|
+
if (META_CLAUSE_PATTERNS.some((pattern) => pattern.test(withoutNumbering))) return true;
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function splitRawCriteriaClauses(text) {
|
|
436
|
+
const normalized = String(text || "").replace(/\r\n/g, "\n").trim();
|
|
437
|
+
if (!normalized) return [];
|
|
438
|
+
const firstNumberedIndex = normalized.search(/\d+\s*[))]/);
|
|
439
|
+
if (firstNumberedIndex === -1) {
|
|
440
|
+
return normalized
|
|
441
|
+
.split(/[;;\n]+/)
|
|
442
|
+
.map((item) => String(item || "").trim())
|
|
443
|
+
.filter(Boolean);
|
|
415
444
|
}
|
|
416
445
|
|
|
446
|
+
const prefix = normalized
|
|
447
|
+
.slice(0, firstNumberedIndex)
|
|
448
|
+
.replace(/[;;,,。]+$/, "")
|
|
449
|
+
.trim();
|
|
450
|
+
const numberedClauses = normalized
|
|
451
|
+
.slice(firstNumberedIndex)
|
|
452
|
+
.split(/(?=\d+\s*[))])/)
|
|
453
|
+
.map((item) => String(item || "").trim())
|
|
454
|
+
.filter(Boolean);
|
|
455
|
+
|
|
456
|
+
return prefix ? [prefix, ...numberedClauses] : numberedClauses;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function normalizeRawCriteriaClauses(clauses = []) {
|
|
460
|
+
const filtered = clauses
|
|
461
|
+
.map((item) => String(item || "").replace(/^[;;,,。]+/, "").replace(/[;;,,。]+$/, "").trim())
|
|
462
|
+
.filter(Boolean)
|
|
463
|
+
.filter((item) => !isMetaClause(item));
|
|
464
|
+
const unique = uniqueList(filtered);
|
|
465
|
+
if (!unique.length) return null;
|
|
466
|
+
return unique.reduce((acc, clause) => {
|
|
467
|
+
if (!acc) return clause;
|
|
468
|
+
if (/[::]$/.test(acc) && /^\d+\s*[))]/.test(clause)) {
|
|
469
|
+
return `${acc}${clause}`;
|
|
470
|
+
}
|
|
471
|
+
return `${acc};${clause}`;
|
|
472
|
+
}, "");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function normalizeCriteriaClauses(clauses = []) {
|
|
476
|
+
const filtered = clauses
|
|
477
|
+
.map((item) => sanitizeClause(item))
|
|
478
|
+
.map((item) => item.replace(/^[;;,,。]+/, "").replace(/[;;,,。]+$/, "").trim())
|
|
479
|
+
.filter(Boolean)
|
|
480
|
+
.filter((item) => !isMetaClause(item));
|
|
481
|
+
const unique = uniqueList(filtered.map((item) => normalizeText(item)));
|
|
482
|
+
if (!unique.length) return null;
|
|
483
|
+
return unique.reduce((acc, clause) => {
|
|
484
|
+
if (!acc) return clause;
|
|
485
|
+
if (/[::]$/.test(acc) && /^\d+\s*[))]/.test(clause)) {
|
|
486
|
+
return `${acc}${clause}`;
|
|
487
|
+
}
|
|
488
|
+
return `${acc};${clause}`;
|
|
489
|
+
}, "");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function extractExplicitCriteriaBlock(text) {
|
|
493
|
+
const normalizedText = String(text || "").replace(/\r\n/g, "\n");
|
|
494
|
+
const markerMatch = normalizedText.match(CRITERIA_EXPLICIT_MARKER_PATTERN);
|
|
495
|
+
if (!markerMatch) return {
|
|
496
|
+
raw: null,
|
|
497
|
+
normalized: null
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
let block = normalizedText.slice(markerMatch.index + markerMatch[0].length);
|
|
501
|
+
const stopMatch = block.match(CRITERIA_EXPLICIT_STOP_PATTERN);
|
|
502
|
+
if (stopMatch && stopMatch.index > 0) {
|
|
503
|
+
block = block.slice(0, stopMatch.index);
|
|
504
|
+
}
|
|
505
|
+
const rawClauses = splitRawCriteriaClauses(block);
|
|
506
|
+
return {
|
|
507
|
+
raw: normalizeRawCriteriaClauses(rawClauses),
|
|
508
|
+
normalized: normalizeCriteriaClauses(rawClauses)
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function buildFallbackCriteria(text) {
|
|
417
513
|
const clauses = sanitizeInstruction(text)
|
|
418
514
|
.split(/[,,。;;\n]/)
|
|
419
|
-
.map((item) =>
|
|
515
|
+
.map((item) => String(item || "").trim())
|
|
420
516
|
.filter(Boolean);
|
|
517
|
+
return {
|
|
518
|
+
raw: normalizeRawCriteriaClauses(clauses),
|
|
519
|
+
normalized: normalizeCriteriaClauses(clauses)
|
|
520
|
+
};
|
|
521
|
+
}
|
|
421
522
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
523
|
+
function buildCriteria({ instruction, rawInstruction, overrideCriteria }) {
|
|
524
|
+
const rawOverride = String(overrideCriteria || "").trim();
|
|
525
|
+
const normalizedOverride = normalizeText(rawOverride);
|
|
526
|
+
if (normalizedOverride) {
|
|
527
|
+
return {
|
|
528
|
+
raw: rawOverride || normalizedOverride,
|
|
529
|
+
normalized: normalizedOverride,
|
|
530
|
+
source: "override"
|
|
531
|
+
};
|
|
532
|
+
}
|
|
427
533
|
|
|
428
|
-
const
|
|
429
|
-
|
|
534
|
+
const explicitCriteria = extractExplicitCriteriaBlock(rawInstruction || instruction);
|
|
535
|
+
if (explicitCriteria.raw) {
|
|
536
|
+
return {
|
|
537
|
+
...explicitCriteria,
|
|
538
|
+
source: "explicit"
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const fallbackCriteria = buildFallbackCriteria(rawInstruction || instruction);
|
|
543
|
+
return {
|
|
544
|
+
...fallbackCriteria,
|
|
545
|
+
source: fallbackCriteria.raw ? "fallback" : null
|
|
546
|
+
};
|
|
430
547
|
}
|
|
431
548
|
|
|
432
549
|
function resolvePostAction({ instruction, confirmation, overrides }) {
|
|
@@ -550,7 +667,8 @@ function collectSuspiciousFields({ invalidOverrideSchoolTags, maxGreetCountResol
|
|
|
550
667
|
}
|
|
551
668
|
|
|
552
669
|
export function parseRecommendInstruction({ instruction, confirmation, overrides }) {
|
|
553
|
-
const
|
|
670
|
+
const rawInstruction = String(instruction || "");
|
|
671
|
+
const text = normalizeText(rawInstruction);
|
|
554
672
|
const detectedSchoolTags = extractSchoolTags(text);
|
|
555
673
|
const detectedDegrees = extractDegrees(text);
|
|
556
674
|
const schoolTagAudit = auditSchoolTagSelections(overrides?.school_tag);
|
|
@@ -563,7 +681,17 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
|
|
|
563
681
|
const overrideRecentNotView = normalizeRecentNotView(overrides?.recent_not_view);
|
|
564
682
|
const confirmationRecentNotView = normalizeRecentNotView(confirmation?.recent_not_view_value);
|
|
565
683
|
const overrideCriteria = overrides?.criteria;
|
|
566
|
-
const
|
|
684
|
+
const criteriaResolution = buildCriteria({
|
|
685
|
+
instruction: text,
|
|
686
|
+
rawInstruction,
|
|
687
|
+
overrideCriteria
|
|
688
|
+
});
|
|
689
|
+
const jobSelectionHint = normalizeText(
|
|
690
|
+
overrides?.job
|
|
691
|
+
|| confirmation?.job_value
|
|
692
|
+
|| extractJobSelectionHint(rawInstruction)
|
|
693
|
+
|| ""
|
|
694
|
+
);
|
|
567
695
|
const pageScopeResolution = resolvePageScope({ instruction: text, confirmation, overrides });
|
|
568
696
|
|
|
569
697
|
const inferredSchoolTag = detectedSchoolTags.length > 0
|
|
@@ -584,7 +712,7 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
|
|
|
584
712
|
recent_not_view: overrideRecentNotView || confirmationRecentNotView || extractRecentNotView(text) || "不限"
|
|
585
713
|
};
|
|
586
714
|
const screenParams = {
|
|
587
|
-
criteria:
|
|
715
|
+
criteria: criteriaResolution.raw || criteriaResolution.normalized || null,
|
|
588
716
|
target_count: null,
|
|
589
717
|
post_action: null,
|
|
590
718
|
max_greet_count: null
|
|
@@ -765,6 +893,7 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
|
|
|
765
893
|
needs_post_action_confirmation,
|
|
766
894
|
needs_max_greet_count_confirmation,
|
|
767
895
|
needs_page_confirmation,
|
|
896
|
+
criteria_normalized: criteriaResolution.normalized,
|
|
768
897
|
proposed_target_count: targetCountResolution.proposed_target_count,
|
|
769
898
|
proposed_post_action: postActionResolution.proposed_post_action,
|
|
770
899
|
proposed_max_greet_count: maxGreetCountResolution.proposed_max_greet_count,
|
|
@@ -777,13 +906,17 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
|
|
|
777
906
|
extracted_search_params: searchParams,
|
|
778
907
|
extracted_screen_params: {
|
|
779
908
|
criteria: screenParams.criteria,
|
|
909
|
+
criteria_normalized: criteriaResolution.normalized,
|
|
780
910
|
target_count: targetCountResolution.proposed_target_count,
|
|
781
911
|
post_action: postActionResolution.proposed_post_action,
|
|
782
912
|
max_greet_count: maxGreetCountResolution.proposed_max_greet_count
|
|
783
913
|
},
|
|
784
914
|
current_page_scope: pageScopeResolution.page_scope,
|
|
785
915
|
current_search_params: searchParams,
|
|
786
|
-
current_screen_params:
|
|
916
|
+
current_screen_params: {
|
|
917
|
+
...screenParams,
|
|
918
|
+
criteria_normalized: criteriaResolution.normalized
|
|
919
|
+
},
|
|
787
920
|
missing_fields,
|
|
788
921
|
suspicious_fields,
|
|
789
922
|
pending_questions
|
package/src/test-parser.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { parseRecommendInstruction } from "./parser.js";
|
|
3
3
|
|
|
4
|
+
const REPRODUCTION_INSTRUCTION = `启动boss推荐任务。条件如下:
|
|
5
|
+
|
|
6
|
+
页面选择:推荐;
|
|
7
|
+
学校标签:985/211/国内外名校;
|
|
8
|
+
学历:本科及以上;
|
|
9
|
+
性别:不限;
|
|
10
|
+
是否过滤近14天看过:近14天没有;
|
|
11
|
+
目标筛选数:152;
|
|
12
|
+
通过筛选后动作:打招呼;
|
|
13
|
+
最大招呼数:152;
|
|
14
|
+
岗位:研发工程师(AI应用方向)-2026届校招 _ 杭州;
|
|
15
|
+
筛选条件:需同时满足全部条件:1)如果有本科学历,本科学历必须为 211 及以上或 QS 前 500 海外院校;2)至少一段学历为 985、QS 前 100 海外院校或中科院;3)具备大模型 / AI / 图形学 / 计算机视觉 / 3D相关的算法或工程经验(实习、项目、科研均可)。学校是否是985、211、qs排名等判断如果简历内没有明确标明,需要通过学校名称来判断;4)必须是25年应届生或者26年应届生或者27年应届生,除了标签以外需要通过人选最高学历的求学年份判断(比如:本科简历里写了2021 - 2025,应该理解为25年毕业,属于25年应届生);5)年龄必须35岁以内。`;
|
|
16
|
+
|
|
4
17
|
function testNeedConfirmationIncludesPostAction() {
|
|
5
18
|
const result = parseRecommendInstruction({
|
|
6
19
|
instruction: "推荐页上筛选985男生,近14天没有,有大模型平台经验,符合标准的收藏",
|
|
@@ -606,6 +619,85 @@ function testPageScopeOverrideShouldNotBypassConfirmation() {
|
|
|
606
619
|
assert.equal(result.pending_questions.some((item) => item.field === "page_scope"), true);
|
|
607
620
|
}
|
|
608
621
|
|
|
622
|
+
function testExplicitCriteriaBlockShouldKeepAllCoreRulesAndExcludeMetaFields() {
|
|
623
|
+
const result = parseRecommendInstruction({
|
|
624
|
+
instruction: REPRODUCTION_INSTRUCTION,
|
|
625
|
+
confirmation: null,
|
|
626
|
+
overrides: null
|
|
627
|
+
});
|
|
628
|
+
const criteria = result.screenParams.criteria || "";
|
|
629
|
+
|
|
630
|
+
assert.equal(criteria.includes("需同时满足全部条件"), true);
|
|
631
|
+
assert.equal(criteria.includes("1)如果有本科学历,本科学历必须为 211 及以上或 QS 前 500 海外院校"), true);
|
|
632
|
+
assert.equal(criteria.includes("2)至少一段学历为 985、QS 前 100 海外院校或中科院"), true);
|
|
633
|
+
assert.equal(criteria.includes("3)具备大模型 / AI / 图形学 / 计算机视觉 / 3D相关的算法或工程经验"), true);
|
|
634
|
+
assert.equal(criteria.includes("4)必须是25年应届生或者26年应届生或者27年应届生"), true);
|
|
635
|
+
assert.equal(criteria.includes("5)年龄必须35岁以内"), true);
|
|
636
|
+
|
|
637
|
+
assert.equal(criteria.includes("页面选择"), false);
|
|
638
|
+
assert.equal(criteria.includes("目标筛选数"), false);
|
|
639
|
+
assert.equal(criteria.includes("通过筛选后动作"), false);
|
|
640
|
+
assert.equal(criteria.includes("最大招呼数"), false);
|
|
641
|
+
assert.equal(criteria.includes("岗位"), false);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function testFallbackCriteriaShouldStillWorkWithoutExplicitMarker() {
|
|
645
|
+
const result = parseRecommendInstruction({
|
|
646
|
+
instruction: "页面选择:推荐;学校标签:985/211;岗位:算法工程师;候选人需满足至少两段 AI 项目经验;最大招呼数:20;",
|
|
647
|
+
confirmation: null,
|
|
648
|
+
overrides: null
|
|
649
|
+
});
|
|
650
|
+
const criteria = result.screenParams.criteria || "";
|
|
651
|
+
|
|
652
|
+
assert.equal(criteria.includes("至少两段 AI 项目经验"), true);
|
|
653
|
+
assert.equal(criteria.includes("页面选择"), false);
|
|
654
|
+
assert.equal(criteria.includes("岗位"), false);
|
|
655
|
+
assert.equal(criteria.includes("最大招呼数"), false);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function testOverrideCriteriaShouldHaveHighestPriorityOverExplicitCriteriaBlock() {
|
|
659
|
+
const result = parseRecommendInstruction({
|
|
660
|
+
instruction: REPRODUCTION_INSTRUCTION,
|
|
661
|
+
confirmation: null,
|
|
662
|
+
overrides: {
|
|
663
|
+
criteria: "只看有开源 Agent 项目经验的人选"
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
assert.equal(result.screenParams.criteria, "只看有开源 Agent 项目经验的人选");
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function testFallbackCriteriaShouldNotDropReal985211QsRules() {
|
|
671
|
+
const result = parseRecommendInstruction({
|
|
672
|
+
instruction: "页面选择:推荐;目标筛选数:10;至少一段学历为985或QS前100海外院校;如果有本科学历,本科必须211或QS前500;岗位:算法工程师;",
|
|
673
|
+
confirmation: null,
|
|
674
|
+
overrides: null
|
|
675
|
+
});
|
|
676
|
+
const criteria = result.screenParams.criteria || "";
|
|
677
|
+
|
|
678
|
+
assert.equal(criteria.includes("985"), true);
|
|
679
|
+
assert.equal(criteria.includes("QS前100"), true);
|
|
680
|
+
assert.equal(criteria.includes("211"), true);
|
|
681
|
+
assert.equal(criteria.includes("QS前500"), true);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function testMetaHintsShouldBeProposedFromInstruction() {
|
|
685
|
+
const result = parseRecommendInstruction({
|
|
686
|
+
instruction: `页面选择:推荐;目标筛选数:5;通过筛选后动作:打招呼;最大招呼数:3;岗位:研发工程师(AI应用方向)-2026届校招 _ 杭州;筛选条件:需同时满足全部条件:1)具备AI经验;`,
|
|
687
|
+
confirmation: null,
|
|
688
|
+
overrides: null
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
assert.equal(result.proposed_page_scope, "recommend");
|
|
692
|
+
assert.equal(result.proposed_target_count, 5);
|
|
693
|
+
assert.equal(result.proposed_post_action, "greet");
|
|
694
|
+
assert.equal(result.proposed_max_greet_count, 3);
|
|
695
|
+
assert.equal(result.job_selection_hint, "研发工程师(AI应用方向)-2026届校招 _ 杭州");
|
|
696
|
+
assert.equal(result.screenParams.target_count, null);
|
|
697
|
+
assert.equal(result.screenParams.post_action, null);
|
|
698
|
+
assert.equal(result.screenParams.max_greet_count, null);
|
|
699
|
+
}
|
|
700
|
+
|
|
609
701
|
function main() {
|
|
610
702
|
testNeedConfirmationIncludesPostAction();
|
|
611
703
|
testConfirmedPostActionAndOverrides();
|
|
@@ -639,6 +731,11 @@ function main() {
|
|
|
639
731
|
testLatestKeywordShouldProposeLatestPageScope();
|
|
640
732
|
testConfirmedPageScopeShouldBeResolved();
|
|
641
733
|
testPageScopeOverrideShouldNotBypassConfirmation();
|
|
734
|
+
testExplicitCriteriaBlockShouldKeepAllCoreRulesAndExcludeMetaFields();
|
|
735
|
+
testFallbackCriteriaShouldStillWorkWithoutExplicitMarker();
|
|
736
|
+
testOverrideCriteriaShouldHaveHighestPriorityOverExplicitCriteriaBlock();
|
|
737
|
+
testFallbackCriteriaShouldNotDropReal985211QsRules();
|
|
738
|
+
testMetaHintsShouldBeProposedFromInstruction();
|
|
642
739
|
console.log("parser tests passed");
|
|
643
740
|
}
|
|
644
741
|
|
|
@@ -3305,6 +3305,60 @@ class RecommendScreenCli {
|
|
|
3305
3305
|
return info;
|
|
3306
3306
|
}
|
|
3307
3307
|
|
|
3308
|
+
async resolveDomResumeFallback(candidate, cardProfile) {
|
|
3309
|
+
let domCandidateInfo = await this.extractResumeTextFromDom(candidate);
|
|
3310
|
+
let networkCandidateInfo = null;
|
|
3311
|
+
if (domCandidateInfo && !isDomProfileConsistentWithCard(cardProfile, domCandidateInfo)) {
|
|
3312
|
+
this.recordResumeNetworkDiagnostic({
|
|
3313
|
+
kind: "dom_profile_mismatch",
|
|
3314
|
+
candidate_key: normalizeText(candidate?.key || candidate?.geek_id || ""),
|
|
3315
|
+
card_name: normalizeText(cardProfile?.name || ""),
|
|
3316
|
+
dom_name: normalizeText(domCandidateInfo?.name || ""),
|
|
3317
|
+
card_school: normalizeText(cardProfile?.school || ""),
|
|
3318
|
+
dom_school: normalizeText(domCandidateInfo?.school || "")
|
|
3319
|
+
});
|
|
3320
|
+
log(
|
|
3321
|
+
`[DOM简历疑似错位] candidate=${candidate?.key || candidate?.geek_id || "unknown"} ` +
|
|
3322
|
+
`card=${normalizeText(cardProfile?.name || "-")} dom=${normalizeText(domCandidateInfo?.name || "-")},尝试重试一次点击+监听。`
|
|
3323
|
+
);
|
|
3324
|
+
try {
|
|
3325
|
+
const retryCaptureStartedAt = Date.now();
|
|
3326
|
+
await this.clickCandidate(candidate);
|
|
3327
|
+
const retryDetailOpen = await this.ensureDetailOpen();
|
|
3328
|
+
if (retryDetailOpen) {
|
|
3329
|
+
networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(
|
|
3330
|
+
candidate,
|
|
3331
|
+
NETWORK_RESUME_RETRY_WAIT_MS,
|
|
3332
|
+
{ minTs: retryCaptureStartedAt }
|
|
3333
|
+
);
|
|
3334
|
+
if (!normalizeText(networkCandidateInfo?.resumeText)) {
|
|
3335
|
+
const retryDomCandidateInfo = await this.extractResumeTextFromDom(candidate);
|
|
3336
|
+
if (retryDomCandidateInfo && isDomProfileConsistentWithCard(cardProfile, retryDomCandidateInfo)) {
|
|
3337
|
+
domCandidateInfo = retryDomCandidateInfo;
|
|
3338
|
+
} else {
|
|
3339
|
+
domCandidateInfo = null;
|
|
3340
|
+
}
|
|
3341
|
+
} else {
|
|
3342
|
+
domCandidateInfo = null;
|
|
3343
|
+
}
|
|
3344
|
+
} else {
|
|
3345
|
+
domCandidateInfo = null;
|
|
3346
|
+
}
|
|
3347
|
+
} catch (retryError) {
|
|
3348
|
+
domCandidateInfo = null;
|
|
3349
|
+
this.recordResumeNetworkDiagnostic({
|
|
3350
|
+
kind: "dom_profile_mismatch_retry_failed",
|
|
3351
|
+
candidate_key: normalizeText(candidate?.key || candidate?.geek_id || ""),
|
|
3352
|
+
error: normalizeText(retryError?.message || retryError)
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
return {
|
|
3357
|
+
domCandidateInfo,
|
|
3358
|
+
networkCandidateInfo
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3308
3362
|
handleNetworkRequestWillBeSent(params) {
|
|
3309
3363
|
const url = normalizeText(params?.request?.url || "");
|
|
3310
3364
|
const postData = params?.request?.postData || "";
|
|
@@ -5222,82 +5276,69 @@ class RecommendScreenCli {
|
|
|
5222
5276
|
}
|
|
5223
5277
|
);
|
|
5224
5278
|
}
|
|
5225
|
-
if (!normalizeText(networkCandidateInfo?.resumeText)) {
|
|
5226
|
-
domCandidateInfo = await this.extractResumeTextFromDom(nextCandidate);
|
|
5227
|
-
if (domCandidateInfo && !isDomProfileConsistentWithCard(cardProfile, domCandidateInfo)) {
|
|
5228
|
-
this.recordResumeNetworkDiagnostic({
|
|
5229
|
-
kind: "dom_profile_mismatch",
|
|
5230
|
-
candidate_key: normalizeText(nextCandidate?.key || nextCandidate?.geek_id || ""),
|
|
5231
|
-
card_name: normalizeText(cardProfile?.name || ""),
|
|
5232
|
-
dom_name: normalizeText(domCandidateInfo?.name || ""),
|
|
5233
|
-
card_school: normalizeText(cardProfile?.school || ""),
|
|
5234
|
-
dom_school: normalizeText(domCandidateInfo?.school || "")
|
|
5235
|
-
});
|
|
5236
|
-
log(
|
|
5237
|
-
`[DOM简历疑似错位] candidate=${nextCandidate?.key || nextCandidate?.geek_id || "unknown"} ` +
|
|
5238
|
-
`card=${normalizeText(cardProfile?.name || "-")} dom=${normalizeText(domCandidateInfo?.name || "-")},尝试重试一次点击+监听。`
|
|
5239
|
-
);
|
|
5240
|
-
try {
|
|
5241
|
-
const retryCaptureStartedAt = Date.now();
|
|
5242
|
-
await this.clickCandidate(nextCandidate);
|
|
5243
|
-
const retryDetailOpen = await this.ensureDetailOpen();
|
|
5244
|
-
if (retryDetailOpen) {
|
|
5245
|
-
networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(
|
|
5246
|
-
nextCandidate,
|
|
5247
|
-
NETWORK_RESUME_RETRY_WAIT_MS,
|
|
5248
|
-
{ minTs: retryCaptureStartedAt }
|
|
5249
|
-
);
|
|
5250
|
-
if (!normalizeText(networkCandidateInfo?.resumeText)) {
|
|
5251
|
-
const retryDomCandidateInfo = await this.extractResumeTextFromDom(nextCandidate);
|
|
5252
|
-
if (retryDomCandidateInfo && isDomProfileConsistentWithCard(cardProfile, retryDomCandidateInfo)) {
|
|
5253
|
-
domCandidateInfo = retryDomCandidateInfo;
|
|
5254
|
-
} else {
|
|
5255
|
-
domCandidateInfo = null;
|
|
5256
|
-
}
|
|
5257
|
-
} else {
|
|
5258
|
-
domCandidateInfo = null;
|
|
5259
|
-
}
|
|
5260
|
-
} else {
|
|
5261
|
-
domCandidateInfo = null;
|
|
5262
|
-
}
|
|
5263
|
-
} catch (retryError) {
|
|
5264
|
-
domCandidateInfo = null;
|
|
5265
|
-
this.recordResumeNetworkDiagnostic({
|
|
5266
|
-
kind: "dom_profile_mismatch_retry_failed",
|
|
5267
|
-
candidate_key: normalizeText(nextCandidate?.key || nextCandidate?.geek_id || ""),
|
|
5268
|
-
error: normalizeText(retryError?.message || retryError)
|
|
5269
|
-
});
|
|
5270
|
-
}
|
|
5271
|
-
}
|
|
5272
|
-
}
|
|
5273
|
-
const resumeCandidateInfo = networkCandidateInfo?.resumeText ? networkCandidateInfo : domCandidateInfo;
|
|
5274
|
-
candidateProfile = mergeCandidateProfiles(
|
|
5275
|
-
resumeCandidateInfo || null,
|
|
5276
|
-
cardProfile || null,
|
|
5277
|
-
{
|
|
5278
|
-
name: nextCandidate.name || "",
|
|
5279
|
-
school: nextCandidate.school || "",
|
|
5280
|
-
major: nextCandidate.major || "",
|
|
5281
|
-
company: nextCandidate.last_company || "",
|
|
5282
|
-
position: nextCandidate.last_position || ""
|
|
5283
|
-
}
|
|
5284
|
-
);
|
|
5285
5279
|
|
|
5286
5280
|
if (networkCandidateInfo?.resumeText) {
|
|
5287
5281
|
screening = await this.callTextModel(networkCandidateInfo.resumeText);
|
|
5288
5282
|
resumeSource = "network";
|
|
5289
5283
|
resumeTextLength = normalizeText(networkCandidateInfo.resumeText).length;
|
|
5290
5284
|
this.resumeSourceStats.network += 1;
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5285
|
+
candidateProfile = mergeCandidateProfiles(
|
|
5286
|
+
networkCandidateInfo || null,
|
|
5287
|
+
cardProfile || null,
|
|
5288
|
+
{
|
|
5289
|
+
name: nextCandidate.name || "",
|
|
5290
|
+
school: nextCandidate.school || "",
|
|
5291
|
+
major: nextCandidate.major || "",
|
|
5292
|
+
company: nextCandidate.last_company || "",
|
|
5293
|
+
position: nextCandidate.last_position || ""
|
|
5294
|
+
}
|
|
5295
|
+
);
|
|
5296
5296
|
} else {
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5297
|
+
try {
|
|
5298
|
+
resumeSource = "image_fallback";
|
|
5299
|
+
capture = await this.captureResumeImage(nextCandidate);
|
|
5300
|
+
screening = await this.callVisionModel(capture.stitchedImage);
|
|
5301
|
+
this.resumeSourceStats.image_fallback += 1;
|
|
5302
|
+
} catch (imageFallbackError) {
|
|
5303
|
+
const domFallback = await this.resolveDomResumeFallback(nextCandidate, cardProfile || null);
|
|
5304
|
+
if (domFallback?.networkCandidateInfo?.resumeText) {
|
|
5305
|
+
networkCandidateInfo = domFallback.networkCandidateInfo;
|
|
5306
|
+
screening = await this.callTextModel(networkCandidateInfo.resumeText);
|
|
5307
|
+
resumeSource = "network";
|
|
5308
|
+
resumeTextLength = normalizeText(networkCandidateInfo.resumeText).length;
|
|
5309
|
+
this.resumeSourceStats.network += 1;
|
|
5310
|
+
candidateProfile = mergeCandidateProfiles(
|
|
5311
|
+
networkCandidateInfo || null,
|
|
5312
|
+
cardProfile || null,
|
|
5313
|
+
{
|
|
5314
|
+
name: nextCandidate.name || "",
|
|
5315
|
+
school: nextCandidate.school || "",
|
|
5316
|
+
major: nextCandidate.major || "",
|
|
5317
|
+
company: nextCandidate.last_company || "",
|
|
5318
|
+
position: nextCandidate.last_position || ""
|
|
5319
|
+
}
|
|
5320
|
+
);
|
|
5321
|
+
} else if (domFallback?.domCandidateInfo?.resumeText) {
|
|
5322
|
+
domCandidateInfo = domFallback.domCandidateInfo;
|
|
5323
|
+
screening = await this.callTextModel(domCandidateInfo.resumeText);
|
|
5324
|
+
resumeSource = "dom_fallback";
|
|
5325
|
+
resumeTextLength = normalizeText(domCandidateInfo.resumeText).length;
|
|
5326
|
+
this.resumeSourceStats.dom_fallback += 1;
|
|
5327
|
+
candidateProfile = mergeCandidateProfiles(
|
|
5328
|
+
domCandidateInfo || null,
|
|
5329
|
+
cardProfile || null,
|
|
5330
|
+
{
|
|
5331
|
+
name: nextCandidate.name || "",
|
|
5332
|
+
school: nextCandidate.school || "",
|
|
5333
|
+
major: nextCandidate.major || "",
|
|
5334
|
+
company: nextCandidate.last_company || "",
|
|
5335
|
+
position: nextCandidate.last_position || ""
|
|
5336
|
+
}
|
|
5337
|
+
);
|
|
5338
|
+
} else {
|
|
5339
|
+
throw imageFallbackError;
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5301
5342
|
}
|
|
5302
5343
|
this.resetResumeCaptureFailureStreak();
|
|
5303
5344
|
log(`筛选结果: ${screening.passed ? "通过" : "不通过"}`);
|
|
@@ -467,7 +467,7 @@ async function testRecommendShouldPreferNetworkResumeWhenAvailable() {
|
|
|
467
467
|
assert.equal(result.result.resume_source, "network");
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
-
async function
|
|
470
|
+
async function testNetworkMissShouldFallbackToImageThenDom() {
|
|
471
471
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-network-dom-fallback-"));
|
|
472
472
|
const candidate = { key: "dom-1", geek_id: "dom-1", name: "dom candidate" };
|
|
473
473
|
const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
|
|
@@ -485,13 +485,15 @@ async function testNetworkMissShouldFallbackToDomBeforeImageCapture() {
|
|
|
485
485
|
});
|
|
486
486
|
|
|
487
487
|
cli.waitForNetworkResumeCandidateInfo = async () => null;
|
|
488
|
+
let captureAttempted = false;
|
|
488
489
|
cli.callTextModel = async (resumeText) => ({
|
|
489
490
|
passed: true,
|
|
490
491
|
reason: resumeText.includes("华中科技大学") ? "dom fallback used" : "unexpected",
|
|
491
492
|
summary: "dom fallback used"
|
|
492
493
|
});
|
|
493
494
|
cli.captureResumeImage = async () => {
|
|
494
|
-
|
|
495
|
+
captureAttempted = true;
|
|
496
|
+
throw new Error("capture failed, should fallback to dom");
|
|
495
497
|
};
|
|
496
498
|
|
|
497
499
|
const result = await cli.run();
|
|
@@ -501,6 +503,7 @@ async function testNetworkMissShouldFallbackToDomBeforeImageCapture() {
|
|
|
501
503
|
assert.equal(cli.passedCandidates.length, 1);
|
|
502
504
|
assert.equal(cli.passedCandidates[0].school, "华中科技大学");
|
|
503
505
|
assert.equal(cli.passedCandidates[0].resumeSource, "dom_fallback");
|
|
506
|
+
assert.equal(captureAttempted, true);
|
|
504
507
|
}
|
|
505
508
|
|
|
506
509
|
async function testNetworkMissShouldFallbackToImageCapture() {
|
|
@@ -600,7 +603,7 @@ function testLatestPayloadShouldRemainAvailableWhenCandidateKeyMissing() {
|
|
|
600
603
|
key: "",
|
|
601
604
|
geek_id: ""
|
|
602
605
|
});
|
|
603
|
-
assert.equal(extracted?.resumeText, "recent resume payload");
|
|
606
|
+
assert.equal(extracted?.candidateInfo?.resumeText, "recent resume payload");
|
|
604
607
|
}
|
|
605
608
|
|
|
606
609
|
async function testVisionModelFailureShouldSkipCandidateAndContinue() {
|
|
@@ -638,25 +641,44 @@ async function testVisionModelFailureShouldSkipCandidateAndContinue() {
|
|
|
638
641
|
assert.equal(result.result.skipped_count, 1);
|
|
639
642
|
}
|
|
640
643
|
|
|
641
|
-
async function
|
|
644
|
+
async function testFeaturedNetworkMissShouldFallbackToDomAfterImageFailure() {
|
|
642
645
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-network-only-"));
|
|
643
646
|
const args = createArgs(tempDir);
|
|
644
647
|
args.pageScope = "featured";
|
|
645
648
|
const candidate = { key: "featured-no-network", geek_id: "featured-no-network", name: "featured no network" };
|
|
646
649
|
const cli = new FakeRecommendScreenCli(args, {
|
|
647
|
-
candidates: [candidate]
|
|
650
|
+
candidates: [candidate],
|
|
651
|
+
domResumeByKey: new Map([
|
|
652
|
+
["featured-no-network", {
|
|
653
|
+
name: "featured no network",
|
|
654
|
+
school: "华中科技大学",
|
|
655
|
+
major: "软件工程",
|
|
656
|
+
company: "测试公司",
|
|
657
|
+
position: "后端工程师",
|
|
658
|
+
resumeText: "featured network miss 后应在截图失败后走 DOM 兜底。"
|
|
659
|
+
}]
|
|
660
|
+
])
|
|
648
661
|
});
|
|
649
662
|
cli.waitForNetworkResumeCandidateInfo = async () => null;
|
|
663
|
+
let captureAttempted = false;
|
|
664
|
+
cli.callTextModel = async () => ({
|
|
665
|
+
passed: true,
|
|
666
|
+
reason: "dom fallback used",
|
|
667
|
+
summary: "dom fallback used"
|
|
668
|
+
});
|
|
650
669
|
cli.captureResumeImage = async () => {
|
|
651
|
-
|
|
670
|
+
captureAttempted = true;
|
|
671
|
+
throw new Error("capture failed for featured scope");
|
|
652
672
|
};
|
|
653
673
|
|
|
654
674
|
const result = await cli.run();
|
|
655
675
|
assert.equal(result.status, "COMPLETED");
|
|
656
676
|
assert.equal(result.result.processed_count, 1);
|
|
657
|
-
assert.equal(result.result.passed_count,
|
|
658
|
-
assert.equal(result.result.skipped_count,
|
|
659
|
-
assert.equal(result.result.resume_source, "
|
|
677
|
+
assert.equal(result.result.passed_count, 1);
|
|
678
|
+
assert.equal(result.result.skipped_count, 0);
|
|
679
|
+
assert.equal(result.result.resume_source, "dom_fallback");
|
|
680
|
+
assert.equal(captureAttempted, true);
|
|
681
|
+
assert.equal(cli.passedCandidates[0].resumeSource, "dom_fallback");
|
|
660
682
|
}
|
|
661
683
|
|
|
662
684
|
async function testFeaturedFavoriteShouldNotUseDomFallback() {
|
|
@@ -1461,14 +1483,14 @@ async function main() {
|
|
|
1461
1483
|
await testTargetCountShouldNotTreatProcessedCountAsReached();
|
|
1462
1484
|
await testFeaturedShouldUseNetworkResumeOnly();
|
|
1463
1485
|
await testRecommendShouldPreferNetworkResumeWhenAvailable();
|
|
1464
|
-
await
|
|
1486
|
+
await testNetworkMissShouldFallbackToImageThenDom();
|
|
1465
1487
|
await testNetworkMissShouldFallbackToImageCapture();
|
|
1466
1488
|
await testLatestShouldPreferNetworkResumeWhenAvailable();
|
|
1467
1489
|
await testLatestNetworkMissShouldFallbackToImageCapture();
|
|
1468
1490
|
testLatestPayloadShouldNotLeakAcrossCandidates();
|
|
1469
1491
|
testLatestPayloadShouldRemainAvailableWhenCandidateKeyMissing();
|
|
1470
1492
|
await testVisionModelFailureShouldSkipCandidateAndContinue();
|
|
1471
|
-
await
|
|
1493
|
+
await testFeaturedNetworkMissShouldFallbackToDomAfterImageFailure();
|
|
1472
1494
|
await testFeaturedFavoriteShouldNotUseDomFallback();
|
|
1473
1495
|
await testFeaturedFavoriteShouldSkipClickWhenAlreadyInterested();
|
|
1474
1496
|
await testFeaturedFavoriteShouldRecognizeAlreadyFavoritedByDelThenAdd();
|