@reconcrap/boss-recommend-mcp 1.0.2 → 1.0.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": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -85,6 +85,16 @@ function selectionEquals(left, right) {
85
85
  if (left.length !== right.length) return false;
86
86
  return left.every((value, index) => value === right[index]);
87
87
  }
88
+
89
+ function uniqueNormalizedLabels(values) {
90
+ return Array.from(
91
+ new Set(
92
+ (values || [])
93
+ .map((item) => normalizeText(item))
94
+ .filter(Boolean)
95
+ )
96
+ );
97
+ }
88
98
 
89
99
  function expandDegreeAtOrAbove(value) {
90
100
  const normalized = normalizeDegree(value);
@@ -1083,43 +1093,99 @@ class RecommendSearchCli {
1083
1093
  return false;
1084
1094
  }
1085
1095
 
1086
- async getSchoolFilterState() {
1087
- return this.evaluate(`(() => {
1088
- const frame = document.querySelector('iframe[name="recommendFrame"]')
1089
- || document.querySelector('iframe[src*="/web/frame/recommend/"]')
1090
- || document.querySelector('iframe');
1091
- if (!frame || !frame.contentDocument) {
1092
- return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
1093
- }
1094
- const doc = frame.contentDocument;
1095
- const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
1096
- const groups = Array.from(doc.querySelectorAll('.check-box'));
1097
- const group = doc.querySelector('.check-box.school')
1098
- || groups.find((item) => {
1099
- const set = new Set(
1100
- Array.from(item.querySelectorAll('.default.option, .options .option, .option'))
1101
- .map((node) => normalize(node.textContent))
1102
- .filter(Boolean)
1103
- );
1104
- return set.has('985') || set.has('211') || set.has('双一流院校');
1105
- });
1106
- if (!group) {
1107
- return { ok: false, error: 'GROUP_NOT_FOUND' };
1108
- }
1109
- const labels = ${JSON.stringify(SCHOOL_TAG_OPTIONS)};
1110
- const activeLabels = labels.filter((label) => {
1111
- const node = Array.from(group.querySelectorAll('.options .option, .option'))
1112
- .find((item) => normalize(item.textContent) === normalize(label));
1113
- return Boolean(node && node.classList.contains('active'));
1114
- });
1115
- const defaultOption = group.querySelector('.default.option');
1116
- return {
1117
- ok: true,
1118
- defaultActive: Boolean(defaultOption && defaultOption.classList.contains('active')),
1119
- activeLabels
1120
- };
1121
- })()`);
1122
- }
1096
+ async getFilterGroupState(groupClass) {
1097
+ return this.evaluate(`((groupClass) => {
1098
+ const frame = document.querySelector('iframe[name="recommendFrame"]')
1099
+ || document.querySelector('iframe[src*="/web/frame/recommend/"]')
1100
+ || document.querySelector('iframe');
1101
+ if (!frame || !frame.contentDocument) {
1102
+ return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
1103
+ }
1104
+ const doc = frame.contentDocument;
1105
+ const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
1106
+ const groupCandidates = Array.from(doc.querySelectorAll('.check-box'));
1107
+ const getOptionSet = (group) => new Set(
1108
+ Array.from(group.querySelectorAll('.default.option, .options .option, .option'))
1109
+ .map((item) => normalize(item.textContent))
1110
+ .filter(Boolean)
1111
+ );
1112
+ const findGroup = () => {
1113
+ const direct = doc.querySelector('.check-box.' + groupClass);
1114
+ if (direct) return direct;
1115
+ if (groupClass === 'school') {
1116
+ return groupCandidates.find((group) => {
1117
+ const set = getOptionSet(group);
1118
+ return set.has('985') || set.has('211') || set.has('双一流院校');
1119
+ }) || null;
1120
+ }
1121
+ if (groupClass === 'degree') {
1122
+ return groupCandidates.find((group) => {
1123
+ const set = getOptionSet(group);
1124
+ return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
1125
+ }) || null;
1126
+ }
1127
+ if (groupClass === 'gender') {
1128
+ return groupCandidates.find((group) => {
1129
+ const set = getOptionSet(group);
1130
+ return set.has('男') || set.has('女');
1131
+ }) || null;
1132
+ }
1133
+ if (groupClass === 'recentNotView') {
1134
+ return groupCandidates.find((group) => {
1135
+ const set = getOptionSet(group);
1136
+ return set.has('近14天没有');
1137
+ }) || null;
1138
+ }
1139
+ return null;
1140
+ };
1141
+
1142
+ const group = findGroup();
1143
+ if (!group) {
1144
+ return { ok: false, error: 'GROUP_NOT_FOUND' };
1145
+ }
1146
+
1147
+ const defaultOption = group.querySelector('.default.option');
1148
+ const options = Array.from(group.querySelectorAll('.default.option, .options .option, .option'));
1149
+ const byLabel = new Map();
1150
+ for (const node of options) {
1151
+ const label = normalize(node.textContent);
1152
+ if (!label) continue;
1153
+ const className = String(node.className || '').trim();
1154
+ const active = node.classList.contains('active');
1155
+ const existing = byLabel.get(label);
1156
+ if (existing) {
1157
+ existing.active = existing.active || active;
1158
+ if (className && !existing.classNames.includes(className)) {
1159
+ existing.classNames.push(className);
1160
+ }
1161
+ } else {
1162
+ byLabel.set(label, {
1163
+ label,
1164
+ active,
1165
+ classNames: className ? [className] : []
1166
+ });
1167
+ }
1168
+ }
1169
+
1170
+ const normalizedOptions = Array.from(byLabel.values()).map((item) => ({
1171
+ label: item.label,
1172
+ active: item.active,
1173
+ class_name: item.classNames.join(' | ')
1174
+ }));
1175
+ return {
1176
+ ok: true,
1177
+ group_class: groupClass,
1178
+ defaultActive: Boolean(defaultOption && defaultOption.classList.contains('active')),
1179
+ defaultClassName: defaultOption ? String(defaultOption.className || '').trim() : '',
1180
+ options: normalizedOptions,
1181
+ activeLabels: normalizedOptions.filter((item) => item.active).map((item) => item.label)
1182
+ };
1183
+ })(${JSON.stringify(groupClass)})`);
1184
+ }
1185
+
1186
+ async getSchoolFilterState() {
1187
+ return this.getFilterGroupState("school");
1188
+ }
1123
1189
 
1124
1190
  async selectSchoolFilter(labels) {
1125
1191
  const ensure = await this.ensureGroupReady("school");
@@ -1169,43 +1235,17 @@ class RecommendSearchCli {
1169
1235
  throw new Error(`SCHOOL_FILTER_VERIFY_FAILED:${JSON.stringify(lastState || {})}`);
1170
1236
  }
1171
1237
 
1172
- async getDegreeFilterState() {
1173
- return this.evaluate(`(() => {
1174
- const frame = document.querySelector('iframe[name="recommendFrame"]')
1175
- || document.querySelector('iframe[src*="/web/frame/recommend/"]')
1176
- || document.querySelector('iframe');
1177
- if (!frame || !frame.contentDocument) {
1178
- return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
1179
- }
1180
- const doc = frame.contentDocument;
1181
- const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
1182
- const groups = Array.from(doc.querySelectorAll('.check-box'));
1183
- const group = doc.querySelector('.check-box.degree')
1184
- || groups.find((item) => {
1185
- const set = new Set(
1186
- Array.from(item.querySelectorAll('.default.option, .options .option, .option'))
1187
- .map((node) => normalize(node.textContent))
1188
- .filter(Boolean)
1189
- );
1190
- return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
1191
- });
1192
- if (!group) {
1193
- return { ok: false, error: 'GROUP_NOT_FOUND' };
1194
- }
1195
- const labels = ${JSON.stringify(DEGREE_OPTIONS)};
1196
- const activeLabels = labels.filter((label) => {
1197
- const node = Array.from(group.querySelectorAll('.options .option'))
1198
- .find((item) => normalize(item.textContent) === normalize(label));
1199
- return Boolean(node && node.classList.contains('active'));
1200
- });
1201
- const defaultOption = group.querySelector('.default.option');
1202
- return {
1203
- ok: true,
1204
- defaultActive: Boolean(defaultOption && defaultOption.classList.contains('active')),
1205
- activeLabels
1206
- };
1207
- })()`);
1208
- }
1238
+ async getDegreeFilterState() {
1239
+ return this.getFilterGroupState("degree");
1240
+ }
1241
+
1242
+ async getGenderFilterState() {
1243
+ return this.getFilterGroupState("gender");
1244
+ }
1245
+
1246
+ async getRecentNotViewFilterState() {
1247
+ return this.getFilterGroupState("recentNotView");
1248
+ }
1209
1249
 
1210
1250
  async selectDegreeFilter(labels) {
1211
1251
  const ensure = await this.ensureGroupReady("degree");
@@ -1254,6 +1294,112 @@ class RecommendSearchCli {
1254
1294
 
1255
1295
  throw new Error(`DEGREE_FILTER_VERIFY_FAILED:${JSON.stringify(lastState || {})}`);
1256
1296
  }
1297
+
1298
+ buildGroupClassVerification(groupName, state, expectedLabels, availableOptions, sortFn) {
1299
+ if (!state?.ok) {
1300
+ return {
1301
+ group: groupName,
1302
+ ok: false,
1303
+ reason: state?.error || "GROUP_STATE_UNAVAILABLE",
1304
+ expected_labels: expectedLabels,
1305
+ state: state || null
1306
+ };
1307
+ }
1308
+
1309
+ const expectedSorted = sortFn(uniqueNormalizedLabels(expectedLabels));
1310
+ const expectedSet = new Set(expectedSorted);
1311
+ const allowedSet = new Set(uniqueNormalizedLabels(availableOptions));
1312
+ const optionMap = new Map();
1313
+ for (const option of state.options || []) {
1314
+ optionMap.set(normalizeText(option.label), option);
1315
+ }
1316
+
1317
+ const selectedNotActive = [];
1318
+ const unselectedButActive = [];
1319
+ for (const label of expectedSorted) {
1320
+ const option = optionMap.get(label);
1321
+ if (!option || option.active !== true) {
1322
+ selectedNotActive.push(label);
1323
+ }
1324
+ }
1325
+ for (const label of allowedSet) {
1326
+ if (expectedSet.has(label)) continue;
1327
+ const option = optionMap.get(label);
1328
+ if (option?.active === true) {
1329
+ unselectedButActive.push(label);
1330
+ }
1331
+ }
1332
+
1333
+ const expectDefault = expectedSet.has("不限");
1334
+ const defaultMismatch = expectDefault ? !state.defaultActive : Boolean(state.defaultActive);
1335
+ const ok = (
1336
+ selectedNotActive.length === 0
1337
+ && unselectedButActive.length === 0
1338
+ && !defaultMismatch
1339
+ );
1340
+
1341
+ return {
1342
+ group: groupName,
1343
+ ok,
1344
+ expected_labels: expectedSorted,
1345
+ actual_active_labels: sortFn(uniqueNormalizedLabels(state.activeLabels || [])),
1346
+ default_active: Boolean(state.defaultActive),
1347
+ selected_not_active: selectedNotActive,
1348
+ unselected_but_active: unselectedButActive,
1349
+ default_mismatch: defaultMismatch,
1350
+ options: state.options || []
1351
+ };
1352
+ }
1353
+
1354
+ async verifyFilterDomClassStates(expected) {
1355
+ const schoolState = await this.getSchoolFilterState();
1356
+ const degreeState = await this.getDegreeFilterState();
1357
+ const genderState = await this.getGenderFilterState();
1358
+ const recentState = await this.getRecentNotViewFilterState();
1359
+
1360
+ const checks = [
1361
+ this.buildGroupClassVerification(
1362
+ "school",
1363
+ schoolState,
1364
+ Array.isArray(expected?.schoolTag) && expected.schoolTag.length > 0 ? expected.schoolTag : ["不限"],
1365
+ SCHOOL_TAG_OPTIONS,
1366
+ sortSchoolSelection
1367
+ ),
1368
+ this.buildGroupClassVerification(
1369
+ "degree",
1370
+ degreeState,
1371
+ Array.isArray(expected?.degree) && expected.degree.length > 0 ? expected.degree : ["不限"],
1372
+ DEGREE_OPTIONS,
1373
+ sortDegreeSelection
1374
+ ),
1375
+ this.buildGroupClassVerification(
1376
+ "gender",
1377
+ genderState,
1378
+ [normalizeText(expected?.gender || "不限")],
1379
+ GENDER_OPTIONS,
1380
+ uniqueNormalizedLabels
1381
+ ),
1382
+ this.buildGroupClassVerification(
1383
+ "recent_not_view",
1384
+ recentState,
1385
+ [normalizeText(expected?.recentNotView || "不限")],
1386
+ RECENT_NOT_VIEW_OPTIONS,
1387
+ uniqueNormalizedLabels
1388
+ )
1389
+ ];
1390
+ const failures = checks.filter((item) => item.ok === false);
1391
+ return {
1392
+ ok: failures.length === 0,
1393
+ checks,
1394
+ failures,
1395
+ states: {
1396
+ school: schoolState,
1397
+ degree: degreeState,
1398
+ gender: genderState,
1399
+ recent_not_view: recentState
1400
+ }
1401
+ };
1402
+ }
1257
1403
 
1258
1404
  async countCandidates() {
1259
1405
  return this.evaluate(`(() => {
@@ -1350,8 +1496,15 @@ class RecommendSearchCli {
1350
1496
  await this.selectOption("gender", this.args.gender);
1351
1497
  await this.selectOption("recentNotView", this.args.recentNotView);
1352
1498
  await this.selectDegreeFilter(this.args.degree);
1353
- const verifiedSchoolState = await this.getSchoolFilterState();
1354
- const verifiedDegreeState = await this.getDegreeFilterState();
1499
+ const domClassVerification = await this.verifyFilterDomClassStates({
1500
+ schoolTag: this.args.schoolTag,
1501
+ degree: this.args.degree,
1502
+ gender: this.args.gender,
1503
+ recentNotView: this.args.recentNotView
1504
+ });
1505
+ if (!domClassVerification.ok) {
1506
+ throw new Error(`FILTER_DOM_CLASS_VERIFY_FAILED:${JSON.stringify(domClassVerification.failures)}`);
1507
+ }
1355
1508
  await this.closeFilterPanel();
1356
1509
  const candidateInfo = await this.waitForCandidateCountStable();
1357
1510
 
@@ -1365,8 +1518,14 @@ class RecommendSearchCli {
1365
1518
  recent_not_view: this.args.recentNotView
1366
1519
  },
1367
1520
  verified_filters: {
1368
- school: verifiedSchoolState,
1369
- degree: verifiedDegreeState
1521
+ school: domClassVerification.states.school,
1522
+ degree: domClassVerification.states.degree,
1523
+ gender: domClassVerification.states.gender,
1524
+ recent_not_view: domClassVerification.states.recent_not_view,
1525
+ dom_class_check: {
1526
+ ok: domClassVerification.ok,
1527
+ checks: domClassVerification.checks
1528
+ }
1370
1529
  },
1371
1530
  selected_job: selectedJob,
1372
1531
  candidate_count: candidateInfo?.candidateCount ?? null,