@reconcrap/boss-recommend-mcp 1.0.1 → 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.1",
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",
@@ -76,9 +76,25 @@ function normalizeDegree(value) {
76
76
  return DEGREE_OPTIONS.includes(normalized) ? normalized : null;
77
77
  }
78
78
 
79
- function sortDegreeSelection(values) {
80
- return Array.from(new Set(values.filter(Boolean))).sort((left, right) => DEGREE_ORDER.indexOf(left) - DEGREE_ORDER.indexOf(right));
81
- }
79
+ function sortDegreeSelection(values) {
80
+ return Array.from(new Set(values.filter(Boolean))).sort((left, right) => DEGREE_ORDER.indexOf(left) - DEGREE_ORDER.indexOf(right));
81
+ }
82
+
83
+ function selectionEquals(left, right) {
84
+ if (!Array.isArray(left) || !Array.isArray(right)) return false;
85
+ if (left.length !== right.length) return false;
86
+ return left.every((value, index) => value === right[index]);
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
+ }
82
98
 
83
99
  function expandDegreeAtOrAbove(value) {
84
100
  const normalized = normalizeDegree(value);
@@ -1077,141 +1093,313 @@ class RecommendSearchCli {
1077
1093
  return false;
1078
1094
  }
1079
1095
 
1080
- async getSchoolFilterState() {
1081
- return this.evaluate(`(() => {
1082
- const frame = document.querySelector('iframe[name="recommendFrame"]')
1083
- || document.querySelector('iframe[src*="/web/frame/recommend/"]')
1084
- || document.querySelector('iframe');
1085
- if (!frame || !frame.contentDocument) {
1086
- return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
1087
- }
1088
- const doc = frame.contentDocument;
1089
- const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
1090
- const groups = Array.from(doc.querySelectorAll('.check-box'));
1091
- const group = doc.querySelector('.check-box.school')
1092
- || groups.find((item) => {
1093
- const set = new Set(
1094
- Array.from(item.querySelectorAll('.default.option, .options .option, .option'))
1095
- .map((node) => normalize(node.textContent))
1096
- .filter(Boolean)
1097
- );
1098
- return set.has('985') || set.has('211') || set.has('双一流院校');
1099
- });
1100
- if (!group) {
1101
- return { ok: false, error: 'GROUP_NOT_FOUND' };
1102
- }
1103
- const labels = ${JSON.stringify(SCHOOL_TAG_OPTIONS)};
1104
- const activeLabels = labels.filter((label) => {
1105
- const node = Array.from(group.querySelectorAll('.options .option, .option'))
1106
- .find((item) => normalize(item.textContent) === normalize(label));
1107
- return Boolean(node && node.classList.contains('active'));
1108
- });
1109
- const defaultOption = group.querySelector('.default.option');
1110
- return {
1111
- ok: true,
1112
- defaultActive: Boolean(defaultOption && defaultOption.classList.contains('active')),
1113
- activeLabels
1114
- };
1115
- })()`);
1116
- }
1117
-
1118
- async selectSchoolFilter(labels) {
1119
- const ensure = await this.ensureGroupReady("school");
1120
- if (!ensure?.ok) {
1121
- throw new Error(ensure?.error || "GROUP_NOT_FOUND");
1122
- }
1123
-
1124
- const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
1125
- if (targetLabels.includes("不限")) {
1126
- await this.selectOption("school", "不限");
1127
- return;
1128
- }
1129
-
1130
- const currentState = await this.getSchoolFilterState();
1131
- if (!currentState?.ok) {
1132
- throw new Error(currentState?.error || "SCHOOL_FILTER_STATE_FAILED");
1133
- }
1134
- const current = sortSchoolSelection(currentState.activeLabels || []);
1135
- const desired = sortSchoolSelection(targetLabels);
1136
- const same =
1137
- !currentState.defaultActive
1138
- && current.length === desired.length
1139
- && current.every((value, index) => value === desired[index]);
1140
- if (same) return;
1141
-
1142
- await this.selectOption("school", "不限");
1143
- for (const label of desired) {
1144
- await this.selectOption("school", label);
1145
- }
1146
- }
1147
-
1148
- async getDegreeFilterState() {
1149
- return this.evaluate(`(() => {
1150
- const frame = document.querySelector('iframe[name="recommendFrame"]')
1151
- || document.querySelector('iframe[src*="/web/frame/recommend/"]')
1152
- || document.querySelector('iframe');
1153
- if (!frame || !frame.contentDocument) {
1154
- return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
1155
- }
1156
- const doc = frame.contentDocument;
1157
- const normalize = (value) => String(value || '').replace(/\\s+/g, '').trim();
1158
- const groups = Array.from(doc.querySelectorAll('.check-box'));
1159
- const group = doc.querySelector('.check-box.degree')
1160
- || groups.find((item) => {
1161
- const set = new Set(
1162
- Array.from(item.querySelectorAll('.default.option, .options .option, .option'))
1163
- .map((node) => normalize(node.textContent))
1164
- .filter(Boolean)
1165
- );
1166
- return set.has('大专') || set.has('本科') || set.has('硕士') || set.has('博士');
1167
- });
1168
- if (!group) {
1169
- return { ok: false, error: 'GROUP_NOT_FOUND' };
1170
- }
1171
- const labels = ${JSON.stringify(DEGREE_OPTIONS)};
1172
- const activeLabels = labels.filter((label) => {
1173
- const node = Array.from(group.querySelectorAll('.options .option'))
1174
- .find((item) => normalize(item.textContent) === normalize(label));
1175
- return Boolean(node && node.classList.contains('active'));
1176
- });
1177
- const defaultOption = group.querySelector('.default.option');
1178
- return {
1179
- ok: true,
1180
- defaultActive: Boolean(defaultOption && defaultOption.classList.contains('active')),
1181
- activeLabels
1182
- };
1183
- })()`);
1184
- }
1185
-
1186
- async selectDegreeFilter(labels) {
1187
- const ensure = await this.ensureGroupReady("degree");
1188
- if (!ensure?.ok) {
1189
- throw new Error(ensure?.error || "GROUP_NOT_FOUND");
1190
- }
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
+ }
1191
1189
 
1192
- const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
1193
- if (targetLabels.includes("不限")) {
1194
- await this.selectOption("degree", "不限");
1195
- return;
1196
- }
1190
+ async selectSchoolFilter(labels) {
1191
+ const ensure = await this.ensureGroupReady("school");
1192
+ if (!ensure?.ok) {
1193
+ throw new Error(ensure?.error || "GROUP_NOT_FOUND");
1194
+ }
1195
+
1196
+ const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
1197
+ const desired = sortSchoolSelection(targetLabels);
1198
+ const expectDefaultOnly = desired.includes("不限");
1199
+ let lastState = null;
1200
+
1201
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1202
+ const state = await this.getSchoolFilterState();
1203
+ if (!state?.ok) {
1204
+ throw new Error(state?.error || "SCHOOL_FILTER_STATE_FAILED");
1205
+ }
1206
+ lastState = state;
1207
+ const current = sortSchoolSelection(state.activeLabels || []);
1208
+ const matched = expectDefaultOnly
1209
+ ? Boolean(state.defaultActive)
1210
+ : (!state.defaultActive && selectionEquals(current, desired));
1211
+ if (matched) {
1212
+ return;
1213
+ }
1214
+
1215
+ if (expectDefaultOnly) {
1216
+ await this.selectOption("school", "不限");
1217
+ await sleep(humanDelay(180, 50));
1218
+ continue;
1219
+ }
1220
+
1221
+ if (state.defaultActive) {
1222
+ const clearDefault = await this.clickOptionBySelector("school", "不限");
1223
+ if (!clearDefault?.ok) {
1224
+ throw new Error(clearDefault?.error || "SCHOOL_DEFAULT_CLEAR_FAILED");
1225
+ }
1226
+ await sleep(humanDelay(180, 50));
1227
+ }
1228
+ for (const label of desired) {
1229
+ await this.selectOption("school", label);
1230
+ await sleep(humanDelay(120, 40));
1231
+ }
1232
+ await sleep(humanDelay(180, 50));
1233
+ }
1234
+
1235
+ throw new Error(`SCHOOL_FILTER_VERIFY_FAILED:${JSON.stringify(lastState || {})}`);
1236
+ }
1197
1237
 
1198
- const currentState = await this.getDegreeFilterState();
1199
- if (!currentState?.ok) {
1200
- throw new Error(currentState?.error || "DEGREE_FILTER_STATE_FAILED");
1201
- }
1202
- const current = sortDegreeSelection(currentState.activeLabels || []);
1203
- const desired = sortDegreeSelection(targetLabels);
1204
- const same =
1205
- !currentState.defaultActive
1206
- && current.length === desired.length
1207
- && current.every((value, index) => value === desired[index]);
1208
- if (same) return;
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
- await this.selectOption("degree", "不限");
1211
- for (const label of desired) {
1212
- await this.selectOption("degree", label);
1213
- }
1214
- }
1250
+ async selectDegreeFilter(labels) {
1251
+ const ensure = await this.ensureGroupReady("degree");
1252
+ if (!ensure?.ok) {
1253
+ throw new Error(ensure?.error || "GROUP_NOT_FOUND");
1254
+ }
1255
+
1256
+ const targetLabels = Array.isArray(labels) && labels.length > 0 ? labels : ["不限"];
1257
+ const desired = sortDegreeSelection(targetLabels);
1258
+ const expectDefaultOnly = desired.includes("不限");
1259
+ let lastState = null;
1260
+
1261
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1262
+ const state = await this.getDegreeFilterState();
1263
+ if (!state?.ok) {
1264
+ throw new Error(state?.error || "DEGREE_FILTER_STATE_FAILED");
1265
+ }
1266
+ lastState = state;
1267
+ const current = sortDegreeSelection(state.activeLabels || []);
1268
+ const matched = expectDefaultOnly
1269
+ ? Boolean(state.defaultActive)
1270
+ : (!state.defaultActive && selectionEquals(current, desired));
1271
+ if (matched) {
1272
+ return;
1273
+ }
1274
+
1275
+ if (expectDefaultOnly) {
1276
+ await this.selectOption("degree", "不限");
1277
+ await sleep(humanDelay(180, 50));
1278
+ continue;
1279
+ }
1280
+
1281
+ if (state.defaultActive) {
1282
+ const clearDefault = await this.clickOptionBySelector("degree", "不限");
1283
+ if (!clearDefault?.ok) {
1284
+ throw new Error(clearDefault?.error || "DEGREE_DEFAULT_CLEAR_FAILED");
1285
+ }
1286
+ await sleep(humanDelay(180, 50));
1287
+ }
1288
+ for (const label of desired) {
1289
+ await this.selectOption("degree", label);
1290
+ await sleep(humanDelay(120, 40));
1291
+ }
1292
+ await sleep(humanDelay(180, 50));
1293
+ }
1294
+
1295
+ throw new Error(`DEGREE_FILTER_VERIFY_FAILED:${JSON.stringify(lastState || {})}`);
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
+ }
1215
1403
 
1216
1404
  async countCandidates() {
1217
1405
  return this.evaluate(`(() => {
@@ -1306,20 +1494,39 @@ class RecommendSearchCli {
1306
1494
  await this.openFilterPanel();
1307
1495
  await this.selectSchoolFilter(this.args.schoolTag);
1308
1496
  await this.selectOption("gender", this.args.gender);
1309
- await this.selectOption("recentNotView", this.args.recentNotView);
1310
- await this.selectDegreeFilter(this.args.degree);
1311
- await this.closeFilterPanel();
1312
- const candidateInfo = await this.waitForCandidateCountStable();
1313
-
1314
- console.log(JSON.stringify({
1315
- status: "COMPLETED",
1316
- result: {
1497
+ await this.selectOption("recentNotView", this.args.recentNotView);
1498
+ await this.selectDegreeFilter(this.args.degree);
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
+ }
1508
+ await this.closeFilterPanel();
1509
+ const candidateInfo = await this.waitForCandidateCountStable();
1510
+
1511
+ console.log(JSON.stringify({
1512
+ status: "COMPLETED",
1513
+ result: {
1317
1514
  applied_filters: {
1318
1515
  school_tag: this.args.schoolTag,
1319
1516
  degree: this.args.degree,
1320
1517
  gender: this.args.gender,
1321
1518
  recent_not_view: this.args.recentNotView
1322
1519
  },
1520
+ verified_filters: {
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
+ }
1529
+ },
1323
1530
  selected_job: selectedJob,
1324
1531
  candidate_count: candidateInfo?.candidateCount ?? null,
1325
1532
  page_state: {