@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
|
@@ -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
|
|
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
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
.find((
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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.
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
|
1354
|
-
|
|
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:
|
|
1369
|
-
degree:
|
|
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,
|