@reconcrap/boss-recommend-mcp 0.1.3 → 0.1.4

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.3",
3
+ "version": "0.1.4",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -20,6 +20,39 @@ function parsePositiveInteger(raw) {
20
20
  return Number.isFinite(value) && value > 0 ? value : null;
21
21
  }
22
22
 
23
+ function sortSchoolSelection(values) {
24
+ const order = new Map(SCHOOL_TAG_OPTIONS.map((label, index) => [label, index]));
25
+ const unique = Array.from(new Set((values || []).filter((item) => order.has(item))));
26
+ if (!unique.length) return [];
27
+ if (unique.includes("不限")) {
28
+ return unique.length === 1
29
+ ? ["不限"]
30
+ : unique.filter((item) => item !== "不限").sort((left, right) => order.get(left) - order.get(right));
31
+ }
32
+ return unique.sort((left, right) => order.get(left) - order.get(right));
33
+ }
34
+
35
+ function parseSchoolSelection(raw) {
36
+ const text = normalizeText(raw);
37
+ if (!text) return null;
38
+ if (text === "不限") return ["不限"];
39
+
40
+ const selected = [];
41
+ for (const chunk of text.split(/[,,、/|]/)) {
42
+ const value = normalizeText(chunk);
43
+ if (SCHOOL_TAG_OPTIONS.includes(value)) {
44
+ selected.push(value);
45
+ }
46
+ }
47
+ for (const label of SCHOOL_TAG_OPTIONS) {
48
+ if (label !== "不限" && text.includes(label)) {
49
+ selected.push(label);
50
+ }
51
+ }
52
+ const normalized = sortSchoolSelection(selected);
53
+ return normalized.length ? normalized : null;
54
+ }
55
+
23
56
  function normalizeDegree(value) {
24
57
  const normalized = normalizeText(value);
25
58
  if (!normalized) return null;
@@ -72,7 +105,7 @@ function parseDegreeSelection(raw) {
72
105
 
73
106
  function parseArgs(argv) {
74
107
  const args = {
75
- schoolTag: "不限",
108
+ schoolTag: ["不限"],
76
109
  degree: ["不限"],
77
110
  gender: "不限",
78
111
  recentNotView: "不限",
@@ -91,7 +124,7 @@ function parseArgs(argv) {
91
124
  const token = argv[index];
92
125
  const next = argv[index + 1];
93
126
  if (token === "--school-tag" && next) {
94
- args.schoolTag = next;
127
+ args.schoolTag = parseSchoolSelection(next);
95
128
  args.__provided.schoolTag = true;
96
129
  index += 1;
97
130
  } else if (token === "--degree" && next) {
@@ -134,7 +167,12 @@ async function promptValue(ask, question, validate, defaultValue) {
134
167
 
135
168
  async function enrichArgsFromPrompt(args) {
136
169
  if (!isInteractiveTTY() || args.help) return args;
137
- const askTargets = Object.values(args.__provided || {}).some((item) => item === false) || !Array.isArray(args.degree) || args.degree.length === 0;
170
+ const askTargets =
171
+ Object.values(args.__provided || {}).some((item) => item === false)
172
+ || !Array.isArray(args.schoolTag)
173
+ || args.schoolTag.length === 0
174
+ || !Array.isArray(args.degree)
175
+ || args.degree.length === 0;
138
176
  if (!askTargets) return args;
139
177
 
140
178
  const rl = readline.createInterface({
@@ -144,11 +182,12 @@ async function enrichArgsFromPrompt(args) {
144
182
  const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
145
183
  try {
146
184
  if (!args.__provided.schoolTag) {
185
+ const current = Array.isArray(args.schoolTag) && args.schoolTag.length > 0 ? args.schoolTag.join("/") : "不限";
147
186
  args.schoolTag = await promptValue(
148
187
  ask,
149
- `学校标签(${SCHOOL_TAG_OPTIONS.join("/")},默认: ${args.schoolTag}): `,
150
- (value) => SCHOOL_TAG_OPTIONS.includes(value) ? value : null,
151
- args.schoolTag
188
+ `学校标签(可多选,逗号/斜杠分隔;${SCHOOL_TAG_OPTIONS.join("/")},默认: ${current}): `,
189
+ (value) => parseSchoolSelection(value),
190
+ Array.isArray(args.schoolTag) && args.schoolTag.length > 0 ? args.schoolTag : ["不限"]
152
191
  );
153
192
  }
154
193
  if (!args.__provided.gender) {
@@ -159,6 +198,14 @@ async function enrichArgsFromPrompt(args) {
159
198
  args.gender
160
199
  );
161
200
  }
201
+ if (!args.__provided.recentNotView) {
202
+ args.recentNotView = await promptValue(
203
+ ask,
204
+ `近14天已看过滤(${RECENT_NOT_VIEW_OPTIONS.join("/")},默认: ${args.recentNotView}): `,
205
+ (value) => RECENT_NOT_VIEW_OPTIONS.includes(value) ? value : null,
206
+ args.recentNotView
207
+ );
208
+ }
162
209
  if (!args.__provided.degree || !Array.isArray(args.degree) || args.degree.length === 0) {
163
210
  const current = Array.isArray(args.degree) && args.degree.length > 0 ? args.degree.join(",") : "不限";
164
211
  args.degree = await promptValue(
@@ -168,14 +215,6 @@ async function enrichArgsFromPrompt(args) {
168
215
  Array.isArray(args.degree) && args.degree.length > 0 ? args.degree : ["不限"]
169
216
  );
170
217
  }
171
- if (!args.__provided.recentNotView) {
172
- args.recentNotView = await promptValue(
173
- ask,
174
- `近14天已看过滤(${RECENT_NOT_VIEW_OPTIONS.join("/")},默认: ${args.recentNotView}): `,
175
- (value) => RECENT_NOT_VIEW_OPTIONS.includes(value) ? value : null,
176
- args.recentNotView
177
- );
178
- }
179
218
  if (!args.__provided.port) {
180
219
  args.port = await promptValue(
181
220
  ask,
@@ -687,21 +726,16 @@ class RecommendSearchCli {
687
726
  if (option.alreadySelected) {
688
727
  return;
689
728
  }
690
- await this.simulateHumanClick(option.x, option.y);
691
- await sleep(humanDelay(300, 80));
692
-
693
- let afterClick = await this.getOptionInfo(groupClass, label);
694
- if (afterClick?.ok && afterClick.alreadySelected) {
729
+ const domClick = await this.clickOptionBySelector(groupClass, label);
730
+ if (!domClick?.ok) {
731
+ throw new Error(domClick?.error || "OPTION_DOM_CLICK_FAILED");
732
+ }
733
+ if (await this.waitOptionSelected(groupClass, label, 10)) {
695
734
  return;
696
735
  }
697
736
 
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)) {
737
+ await this.simulateHumanClick(option.x, option.y);
738
+ if (!(await this.waitOptionSelected(groupClass, label, 10))) {
705
739
  throw new Error("OPTION_SELECTION_NOT_APPLIED");
706
740
  }
707
741
  }
@@ -767,6 +801,85 @@ class RecommendSearchCli {
767
801
  })(${JSON.stringify(groupClass)}, ${JSON.stringify(label)})`);
768
802
  }
769
803
 
804
+ async waitOptionSelected(groupClass, label, rounds = 8) {
805
+ for (let index = 0; index < rounds; index += 1) {
806
+ const state = await this.getOptionInfo(groupClass, label);
807
+ if (state?.ok && state.alreadySelected) {
808
+ return true;
809
+ }
810
+ await sleep(120 + index * 40);
811
+ }
812
+ return false;
813
+ }
814
+
815
+ async getSchoolFilterState() {
816
+ return this.evaluate(`(() => {
817
+ const frame = document.querySelector('iframe[name="recommendFrame"]')
818
+ || document.querySelector('iframe[src*="/web/frame/recommend/"]')
819
+ || document.querySelector('iframe');
820
+ if (!frame || !frame.contentDocument) {
821
+ return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
822
+ }
823
+ const doc = frame.contentDocument;
824
+ const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
825
+ const groups = Array.from(doc.querySelectorAll('.check-box'));
826
+ const group = doc.querySelector('.check-box.school')
827
+ || groups.find((item) => {
828
+ const set = new Set(
829
+ Array.from(item.querySelectorAll('.default.option, .options .option, .option'))
830
+ .map((node) => normalize(node.textContent))
831
+ .filter(Boolean)
832
+ );
833
+ return set.has('985') || set.has('211') || set.has('双一流院校');
834
+ });
835
+ if (!group) {
836
+ return { ok: false, error: 'GROUP_NOT_FOUND' };
837
+ }
838
+ const labels = ${JSON.stringify(SCHOOL_TAG_OPTIONS)};
839
+ const activeLabels = labels.filter((label) => {
840
+ const node = Array.from(group.querySelectorAll('.options .option, .option'))
841
+ .find((item) => normalize(item.textContent) === normalize(label));
842
+ return Boolean(node && node.classList.contains('active'));
843
+ });
844
+ const defaultOption = group.querySelector('.default.option');
845
+ return {
846
+ ok: true,
847
+ defaultActive: Boolean(defaultOption && defaultOption.classList.contains('active')),
848
+ activeLabels
849
+ };
850
+ })()`);
851
+ }
852
+
853
+ async selectSchoolFilter(labels) {
854
+ const ensure = await this.ensureGroupReady("school");
855
+ if (!ensure?.ok) {
856
+ throw new Error(ensure?.error || "GROUP_NOT_FOUND");
857
+ }
858
+
859
+ const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
860
+ if (targetLabels.includes("不限")) {
861
+ await this.selectOption("school", "不限");
862
+ return;
863
+ }
864
+
865
+ const currentState = await this.getSchoolFilterState();
866
+ if (!currentState?.ok) {
867
+ throw new Error(currentState?.error || "SCHOOL_FILTER_STATE_FAILED");
868
+ }
869
+ const current = sortSchoolSelection(currentState.activeLabels || []);
870
+ const desired = sortSchoolSelection(targetLabels);
871
+ const same =
872
+ !currentState.defaultActive
873
+ && current.length === desired.length
874
+ && current.every((value, index) => value === desired[index]);
875
+ if (same) return;
876
+
877
+ await this.selectOption("school", "不限");
878
+ for (const label of desired) {
879
+ await this.selectOption("school", label);
880
+ }
881
+ }
882
+
770
883
  async getDegreeFilterState() {
771
884
  return this.evaluate(`(() => {
772
885
  const frame = document.querySelector('iframe[name="recommendFrame"]')
@@ -884,11 +997,14 @@ class RecommendSearchCli {
884
997
  console.log(JSON.stringify({
885
998
  status: "COMPLETED",
886
999
  result: {
887
- usage: "node src/cli.js --school-tag 985 --degree 本科及以上 --gender 男 --recent-not-view 近14天没有 --port 9222"
1000
+ usage: "node src/cli.js --school-tag 985/211 --degree 本科及以上 --gender 男 --recent-not-view 近14天没有 --port 9222"
888
1001
  }
889
1002
  }));
890
1003
  return;
891
1004
  }
1005
+ if (!Array.isArray(this.args.schoolTag) || this.args.schoolTag.length === 0) {
1006
+ throw new Error("INVALID_SCHOOL_TAG_INPUT");
1007
+ }
892
1008
  if (!Array.isArray(this.args.degree) || this.args.degree.length === 0) {
893
1009
  throw new Error("INVALID_DEGREE_INPUT");
894
1010
  }
@@ -901,10 +1017,10 @@ class RecommendSearchCli {
901
1017
  }
902
1018
 
903
1019
  await this.openFilterPanel();
904
- await this.selectOption("school", this.args.schoolTag);
905
- await this.selectDegreeFilter(this.args.degree);
1020
+ await this.selectSchoolFilter(this.args.schoolTag);
906
1021
  await this.selectOption("gender", this.args.gender);
907
1022
  await this.selectOption("recentNotView", this.args.recentNotView);
1023
+ await this.selectDegreeFilter(this.args.degree);
908
1024
  await this.closeFilterPanel();
909
1025
  const candidateInfo = await this.waitForCandidateCountStable();
910
1026