@reconcrap/boss-recommend-mcp 0.1.2 → 0.1.3

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -361,11 +361,10 @@ class RecommendSearchCli {
361
361
  const rect = el.getBoundingClientRect();
362
362
  return rect.width > 2 && rect.height > 2;
363
363
  };
364
- const school = doc.querySelector('.check-box.school');
365
- const degree = doc.querySelector('.check-box.degree');
366
- const gender = doc.querySelector('.check-box.gender');
367
- const recent = doc.querySelector('.check-box.recentNotView');
368
- return Boolean((school && degree && gender && recent) || isVisible(panel));
364
+ const groups = Array.from(doc.querySelectorAll('.check-box'));
365
+ const visibleGroups = groups.filter((group) => isVisible(group));
366
+ if (visibleGroups.length >= 2) return true;
367
+ return Boolean(isVisible(panel) && visibleGroups.length >= 1);
369
368
  })()`);
370
369
  return result === true;
371
370
  }
@@ -522,11 +521,46 @@ class RecommendSearchCli {
522
521
  return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
523
522
  }
524
523
  const doc = frame.contentDocument;
525
- const group = doc.querySelector('.check-box.' + groupClass);
524
+ const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
525
+ const groupCandidates = Array.from(doc.querySelectorAll('.check-box'));
526
+ const getOptionSet = (group) => new Set(
527
+ Array.from(group.querySelectorAll('.default.option, .options .option, .option'))
528
+ .map((item) => normalize(item.textContent))
529
+ .filter(Boolean)
530
+ );
531
+ const findGroup = () => {
532
+ const direct = doc.querySelector('.check-box.' + groupClass);
533
+ if (direct) return direct;
534
+ if (groupClass === 'school') {
535
+ return groupCandidates.find((group) => {
536
+ const set = getOptionSet(group);
537
+ return set.has('985') || set.has('211') || set.has('双一流院校');
538
+ }) || null;
539
+ }
540
+ if (groupClass === 'degree') {
541
+ return groupCandidates.find((group) => {
542
+ const set = getOptionSet(group);
543
+ return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
544
+ }) || null;
545
+ }
546
+ if (groupClass === 'gender') {
547
+ return groupCandidates.find((group) => {
548
+ const set = getOptionSet(group);
549
+ return set.has('男') || set.has('女');
550
+ }) || null;
551
+ }
552
+ if (groupClass === 'recentNotView') {
553
+ return groupCandidates.find((group) => {
554
+ const set = getOptionSet(group);
555
+ return set.has('近14天没有');
556
+ }) || null;
557
+ }
558
+ return null;
559
+ };
560
+ const group = findGroup();
526
561
  if (!group) {
527
562
  return { ok: false, error: 'GROUP_NOT_FOUND' };
528
563
  }
529
- const normalize = (value) => String(value || '').replace(/\s+/g, '').trim();
530
564
  const frameRect = frame.getBoundingClientRect();
531
565
  const getPoint = (el) => {
532
566
  const rect = el.getBoundingClientRect();
@@ -555,8 +589,98 @@ class RecommendSearchCli {
555
589
  })(${JSON.stringify(groupClass)}, ${JSON.stringify(label)})`);
556
590
  }
557
591
 
592
+ async ensureGroupReady(groupClass) {
593
+ return this.evaluate(`((groupClass) => {
594
+ const frame = document.querySelector('iframe[name="recommendFrame"]')
595
+ || document.querySelector('iframe[src*="/web/frame/recommend/"]')
596
+ || document.querySelector('iframe');
597
+ if (!frame || !frame.contentDocument) {
598
+ return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
599
+ }
600
+ const doc = frame.contentDocument;
601
+ const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
602
+ const groupCandidates = Array.from(doc.querySelectorAll('.check-box'));
603
+ const getOptionSet = (group) => new Set(
604
+ Array.from(group.querySelectorAll('.default.option, .options .option, .option'))
605
+ .map((item) => normalize(item.textContent))
606
+ .filter(Boolean)
607
+ );
608
+ const findGroup = () => {
609
+ const direct = doc.querySelector('.check-box.' + groupClass);
610
+ if (direct) return direct;
611
+ if (groupClass === 'school') {
612
+ return groupCandidates.find((group) => {
613
+ const set = getOptionSet(group);
614
+ return set.has('985') || set.has('211') || set.has('双一流院校');
615
+ }) || null;
616
+ }
617
+ if (groupClass === 'degree') {
618
+ return groupCandidates.find((group) => {
619
+ const set = getOptionSet(group);
620
+ return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
621
+ }) || null;
622
+ }
623
+ if (groupClass === 'gender') {
624
+ return groupCandidates.find((group) => {
625
+ const set = getOptionSet(group);
626
+ return set.has('男') || set.has('女');
627
+ }) || null;
628
+ }
629
+ if (groupClass === 'recentNotView') {
630
+ return groupCandidates.find((group) => {
631
+ const set = getOptionSet(group);
632
+ return set.has('近14天没有');
633
+ }) || null;
634
+ }
635
+ return null;
636
+ };
637
+
638
+ const scrollGroupIntoView = (group) => {
639
+ try {
640
+ group.scrollIntoView({ behavior: 'instant', block: 'center' });
641
+ } catch {
642
+ try { group.scrollIntoView({ block: 'center' }); } catch {}
643
+ }
644
+ };
645
+
646
+ let group = findGroup();
647
+ if (group) {
648
+ scrollGroupIntoView(group);
649
+ return { ok: true, found: true, scrolled: false };
650
+ }
651
+
652
+ const topScroller = doc.querySelector('.recommend-filter.op-filter .filter-panel .top')
653
+ || doc.querySelector('.recommend-filter.op-filter .top')
654
+ || doc.querySelector('.recommend-filter.op-filter .filter-panel');
655
+ if (!topScroller) {
656
+ return { ok: false, error: 'FILTER_SCROLL_CONTAINER_NOT_FOUND' };
657
+ }
658
+ const maxScrollTop = Math.max(0, topScroller.scrollHeight - topScroller.clientHeight);
659
+ const steps = 14;
660
+ for (let index = 0; index <= steps; index += 1) {
661
+ const nextTop = maxScrollTop <= 0 ? 0 : Math.round((maxScrollTop * index) / steps);
662
+ topScroller.scrollTop = nextTop;
663
+ group = findGroup();
664
+ if (group) {
665
+ scrollGroupIntoView(group);
666
+ return { ok: true, found: true, scrolled: true, step: index };
667
+ }
668
+ }
669
+ return { ok: false, error: 'GROUP_NOT_FOUND' };
670
+ })(${JSON.stringify(groupClass)})`);
671
+ }
672
+
558
673
  async selectOption(groupClass, label) {
559
- const option = await this.getOptionInfo(groupClass, label);
674
+ let option = await this.getOptionInfo(groupClass, label);
675
+ if (!option?.ok && option?.error === "GROUP_NOT_FOUND") {
676
+ await this.openFilterPanel();
677
+ const ensure = await this.ensureGroupReady(groupClass);
678
+ if (!ensure?.ok) {
679
+ throw new Error(ensure?.error || "GROUP_NOT_FOUND");
680
+ }
681
+ await sleep(humanDelay(180, 60));
682
+ option = await this.getOptionInfo(groupClass, label);
683
+ }
560
684
  if (!option?.ok) {
561
685
  throw new Error(option?.error || 'OPTION_NOT_FOUND');
562
686
  }
@@ -565,6 +689,82 @@ class RecommendSearchCli {
565
689
  }
566
690
  await this.simulateHumanClick(option.x, option.y);
567
691
  await sleep(humanDelay(300, 80));
692
+
693
+ let afterClick = await this.getOptionInfo(groupClass, label);
694
+ if (afterClick?.ok && afterClick.alreadySelected) {
695
+ return;
696
+ }
697
+
698
+ const fallback = await this.clickOptionBySelector(groupClass, label);
699
+ if (!fallback?.ok) {
700
+ throw new Error(fallback?.error || "OPTION_FALLBACK_CLICK_FAILED");
701
+ }
702
+ await sleep(humanDelay(220, 60));
703
+ afterClick = await this.getOptionInfo(groupClass, label);
704
+ if (!(afterClick?.ok && afterClick.alreadySelected)) {
705
+ throw new Error("OPTION_SELECTION_NOT_APPLIED");
706
+ }
707
+ }
708
+
709
+ async clickOptionBySelector(groupClass, label) {
710
+ return this.evaluate(`((groupClass, label) => {
711
+ const frame = document.querySelector('iframe[name="recommendFrame"]')
712
+ || document.querySelector('iframe[src*="/web/frame/recommend/"]')
713
+ || document.querySelector('iframe');
714
+ if (!frame || !frame.contentDocument) {
715
+ return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
716
+ }
717
+ const doc = frame.contentDocument;
718
+ const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
719
+ const groupCandidates = Array.from(doc.querySelectorAll('.check-box'));
720
+ const getOptionSet = (group) => new Set(
721
+ Array.from(group.querySelectorAll('.default.option, .options .option, .option'))
722
+ .map((item) => normalize(item.textContent))
723
+ .filter(Boolean)
724
+ );
725
+ const findGroup = () => {
726
+ const direct = doc.querySelector('.check-box.' + groupClass);
727
+ if (direct) return direct;
728
+ if (groupClass === 'school') {
729
+ return groupCandidates.find((group) => {
730
+ const set = getOptionSet(group);
731
+ return set.has('985') || set.has('211') || set.has('双一流院校');
732
+ }) || null;
733
+ }
734
+ if (groupClass === 'degree') {
735
+ return groupCandidates.find((group) => {
736
+ const set = getOptionSet(group);
737
+ return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
738
+ }) || null;
739
+ }
740
+ if (groupClass === 'gender') {
741
+ return groupCandidates.find((group) => {
742
+ const set = getOptionSet(group);
743
+ return set.has('男') || set.has('女');
744
+ }) || null;
745
+ }
746
+ if (groupClass === 'recentNotView') {
747
+ return groupCandidates.find((group) => {
748
+ const set = getOptionSet(group);
749
+ return set.has('近14天没有');
750
+ }) || null;
751
+ }
752
+ return null;
753
+ };
754
+ const group = findGroup();
755
+ if (!group) {
756
+ return { ok: false, error: 'GROUP_NOT_FOUND' };
757
+ }
758
+ const options = Array.from(group.querySelectorAll('.options .option, .option'));
759
+ const target = label === '不限'
760
+ ? (group.querySelector('.default.option') || options.find((item) => normalize(item.textContent) === '不限'))
761
+ : options.find((item) => normalize(item.textContent) === normalize(label));
762
+ if (!target) {
763
+ return { ok: false, error: 'OPTION_NOT_FOUND' };
764
+ }
765
+ target.click();
766
+ return { ok: true };
767
+ })(${JSON.stringify(groupClass)}, ${JSON.stringify(label)})`);
568
768
  }
569
769
 
570
770
  async getDegreeFilterState() {
@@ -576,11 +776,20 @@ class RecommendSearchCli {
576
776
  return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
577
777
  }
578
778
  const doc = frame.contentDocument;
579
- const group = doc.querySelector('.check-box.degree');
779
+ const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
780
+ const groups = Array.from(doc.querySelectorAll('.check-box'));
781
+ const group = doc.querySelector('.check-box.degree')
782
+ || groups.find((item) => {
783
+ const set = new Set(
784
+ Array.from(item.querySelectorAll('.default.option, .options .option, .option'))
785
+ .map((node) => normalize(node.textContent))
786
+ .filter(Boolean)
787
+ );
788
+ return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
789
+ });
580
790
  if (!group) {
581
791
  return { ok: false, error: 'GROUP_NOT_FOUND' };
582
792
  }
583
- const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
584
793
  const labels = ${JSON.stringify(DEGREE_OPTIONS)};
585
794
  const activeLabels = labels.filter((label) => {
586
795
  const node = Array.from(group.querySelectorAll('.options .option'))
@@ -597,6 +806,11 @@ class RecommendSearchCli {
597
806
  }
598
807
 
599
808
  async selectDegreeFilter(labels) {
809
+ const ensure = await this.ensureGroupReady("degree");
810
+ if (!ensure?.ok) {
811
+ throw new Error(ensure?.error || "GROUP_NOT_FOUND");
812
+ }
813
+
600
814
  const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
601
815
  if (targetLabels.includes("不限")) {
602
816
  await this.selectOption("degree", "不限");