@reconcrap/boss-recommend-mcp 2.1.13 → 2.1.15
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/README.md +17 -6
- package/config/screening-config.example.json +5 -0
- package/package.json +1 -1
- package/skills/boss-chat/README.md +2 -2
- package/skills/boss-chat/SKILL.md +7 -7
- package/skills/boss-recruit-pipeline/SKILL.md +23 -1
- package/src/chat-mcp.js +70 -73
- package/src/chat-runtime-config.js +26 -0
- package/src/core/reporting/legacy-csv.js +9 -1
- package/src/core/screening/index.js +351 -20
- package/src/domains/chat/detail.js +79 -47
- package/src/domains/chat/run-service.js +456 -185
- package/src/domains/recommend/run-service.js +11 -3
- package/src/domains/recruit/constants.js +65 -0
- package/src/domains/recruit/instruction-parser.js +362 -86
- package/src/domains/recruit/run-service.js +289 -10
- package/src/domains/recruit/search.js +2076 -298
- package/src/index.js +18 -12
- package/src/recommend-mcp.js +77 -8
- package/src/recruit-mcp.js +228 -3
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
clearFocusedInput,
|
|
3
3
|
clickNodeCenter,
|
|
4
|
+
clickPoint,
|
|
4
5
|
countSelectors,
|
|
5
6
|
DETERMINISTIC_CLICK_OPTIONS,
|
|
6
7
|
describeNode,
|
|
7
8
|
findFirstNode,
|
|
8
9
|
getAttributesMap,
|
|
10
|
+
getNodeBox,
|
|
9
11
|
getOuterHTML,
|
|
10
12
|
insertText,
|
|
11
13
|
pressKey,
|
|
@@ -48,6 +50,20 @@ const DEGREE_LABEL_MAP = new Map([
|
|
|
48
50
|
["博士", "博士"]
|
|
49
51
|
]);
|
|
50
52
|
|
|
53
|
+
const SCHOOL_LABEL_ALIAS_MAP = new Map([
|
|
54
|
+
["统招", ["统招本科"]],
|
|
55
|
+
["统招本科", ["统招本科"]],
|
|
56
|
+
["双一流", ["双一流院校"]],
|
|
57
|
+
["双一流院校", ["双一流院校"]],
|
|
58
|
+
["211", ["211院校"]],
|
|
59
|
+
["211院校", ["211院校"]],
|
|
60
|
+
["985", ["985院校"]],
|
|
61
|
+
["985院校", ["985院校"]],
|
|
62
|
+
["留学生", ["留学生"]],
|
|
63
|
+
["qs100", ["QS 100", "QS100"]],
|
|
64
|
+
["qs500", ["QS 500", "QS500"]]
|
|
65
|
+
]);
|
|
66
|
+
|
|
51
67
|
const NATIONAL_CITY_LABELS = new Set([
|
|
52
68
|
"全国",
|
|
53
69
|
"不限",
|
|
@@ -63,6 +79,156 @@ const CITY_NO_RESULT_LABELS = new Set([
|
|
|
63
79
|
"暂无数据",
|
|
64
80
|
"无结果"
|
|
65
81
|
]);
|
|
82
|
+
const EXPERIENCE_CUSTOM_MIN_VALUE = 1;
|
|
83
|
+
const EXPERIENCE_CUSTOM_MAX_VALUE = 12;
|
|
84
|
+
const EXPERIENCE_FIXED_LABEL_ALIASES = new Map([
|
|
85
|
+
["不限", "不限"],
|
|
86
|
+
["不限制", "不限"],
|
|
87
|
+
["无限制", "不限"],
|
|
88
|
+
["全部", "不限"],
|
|
89
|
+
["所有", "不限"],
|
|
90
|
+
["无", "不限"],
|
|
91
|
+
["none", "不限"],
|
|
92
|
+
["all", "不限"],
|
|
93
|
+
["在校/应届", "在校/应届"],
|
|
94
|
+
["在校应届", "在校/应届"],
|
|
95
|
+
["在校", "在校/应届"],
|
|
96
|
+
["应届", "在校/应届"],
|
|
97
|
+
["应届生", "在校/应届"],
|
|
98
|
+
["25年毕业", "25年毕业"],
|
|
99
|
+
["2025年毕业", "25年毕业"],
|
|
100
|
+
["25届", "25年毕业"],
|
|
101
|
+
["2025届", "25年毕业"],
|
|
102
|
+
["26年毕业", "26年毕业"],
|
|
103
|
+
["2026年毕业", "26年毕业"],
|
|
104
|
+
["26届", "26年毕业"],
|
|
105
|
+
["2026届", "26年毕业"],
|
|
106
|
+
["26年后毕业", "26年后毕业"],
|
|
107
|
+
["2026年后毕业", "26年后毕业"],
|
|
108
|
+
["26届后", "26年后毕业"],
|
|
109
|
+
["1-3年", "1-3年"],
|
|
110
|
+
["1到3年", "1-3年"],
|
|
111
|
+
["1至3年", "1-3年"],
|
|
112
|
+
["1~3年", "1-3年"],
|
|
113
|
+
["3-5年", "3-5年"],
|
|
114
|
+
["3到5年", "3-5年"],
|
|
115
|
+
["3至5年", "3-5年"],
|
|
116
|
+
["3~5年", "3-5年"],
|
|
117
|
+
["5-10年", "5-10年"],
|
|
118
|
+
["5到10年", "5-10年"],
|
|
119
|
+
["5至10年", "5-10年"],
|
|
120
|
+
["5~10年", "5-10年"]
|
|
121
|
+
]);
|
|
122
|
+
const EXPERIENCE_CUSTOM_ENDPOINTS = new Map([
|
|
123
|
+
["在校/应届", { value: 1, label: "在校/应届" }],
|
|
124
|
+
["在校应届", { value: 1, label: "在校/应届" }],
|
|
125
|
+
["在校", { value: 1, label: "在校/应届" }],
|
|
126
|
+
["应届", { value: 1, label: "在校/应届" }],
|
|
127
|
+
["应届生", { value: 1, label: "在校/应届" }],
|
|
128
|
+
["1年以内", { value: 2, label: "1年以内" }],
|
|
129
|
+
["一年以内", { value: 2, label: "1年以内" }],
|
|
130
|
+
["1年", { value: 2, label: "1年" }],
|
|
131
|
+
["一年", { value: 2, label: "1年" }],
|
|
132
|
+
["2年", { value: 3, label: "2年" }],
|
|
133
|
+
["二年", { value: 3, label: "2年" }],
|
|
134
|
+
["两年", { value: 3, label: "2年" }],
|
|
135
|
+
["3年", { value: 4, label: "3年" }],
|
|
136
|
+
["三年", { value: 4, label: "3年" }],
|
|
137
|
+
["4年", { value: 5, label: "4年" }],
|
|
138
|
+
["四年", { value: 5, label: "4年" }],
|
|
139
|
+
["5年", { value: 6, label: "5年" }],
|
|
140
|
+
["五年", { value: 6, label: "5年" }],
|
|
141
|
+
["6年", { value: 7, label: "6年" }],
|
|
142
|
+
["六年", { value: 7, label: "6年" }],
|
|
143
|
+
["7年", { value: 8, label: "7年" }],
|
|
144
|
+
["七年", { value: 8, label: "7年" }],
|
|
145
|
+
["8年", { value: 9, label: "8年" }],
|
|
146
|
+
["八年", { value: 9, label: "8年" }],
|
|
147
|
+
["9年", { value: 10, label: "9年" }],
|
|
148
|
+
["九年", { value: 10, label: "9年" }],
|
|
149
|
+
["10年", { value: 11, label: "10年" }],
|
|
150
|
+
["十年", { value: 11, label: "10年" }],
|
|
151
|
+
["10年以上", { value: 12, label: "10年以上" }],
|
|
152
|
+
["十年以上", { value: 12, label: "10年以上" }],
|
|
153
|
+
["10年+", { value: 12, label: "10年以上" }],
|
|
154
|
+
["10年以上经验", { value: 12, label: "10年以上" }],
|
|
155
|
+
["10+", { value: 12, label: "10年以上" }]
|
|
156
|
+
]);
|
|
157
|
+
const EXPERIENCE_CUSTOM_LABELS_BY_VALUE = new Map([
|
|
158
|
+
[1, "在校/应届"],
|
|
159
|
+
[2, "1年以内"],
|
|
160
|
+
[3, "2年"],
|
|
161
|
+
[4, "3年"],
|
|
162
|
+
[5, "4年"],
|
|
163
|
+
[6, "5年"],
|
|
164
|
+
[7, "6年"],
|
|
165
|
+
[8, "7年"],
|
|
166
|
+
[9, "8年"],
|
|
167
|
+
[10, "9年"],
|
|
168
|
+
[11, "10年"],
|
|
169
|
+
[12, "10年以上"]
|
|
170
|
+
]);
|
|
171
|
+
const GENDER_LABEL_ALIASES = new Map([
|
|
172
|
+
["不限", "不限"],
|
|
173
|
+
["不限制", "不限"],
|
|
174
|
+
["无限制", "不限"],
|
|
175
|
+
["全部", "不限"],
|
|
176
|
+
["所有", "不限"],
|
|
177
|
+
["无", "不限"],
|
|
178
|
+
["none", "不限"],
|
|
179
|
+
["all", "不限"],
|
|
180
|
+
["男", "男"],
|
|
181
|
+
["男性", "男"],
|
|
182
|
+
["男生", "男"],
|
|
183
|
+
["male", "男"],
|
|
184
|
+
["m", "男"],
|
|
185
|
+
["女", "女"],
|
|
186
|
+
["女性", "女"],
|
|
187
|
+
["女生", "女"],
|
|
188
|
+
["female", "女"],
|
|
189
|
+
["f", "女"]
|
|
190
|
+
]);
|
|
191
|
+
const AGE_FIXED_LABEL_ALIASES = new Map([
|
|
192
|
+
["不限", "不限"],
|
|
193
|
+
["不限制", "不限"],
|
|
194
|
+
["无限制", "不限"],
|
|
195
|
+
["全部", "不限"],
|
|
196
|
+
["所有", "不限"],
|
|
197
|
+
["无", "不限"],
|
|
198
|
+
["none", "不限"],
|
|
199
|
+
["all", "不限"],
|
|
200
|
+
["20-25", "20-25"],
|
|
201
|
+
["20到25", "20-25"],
|
|
202
|
+
["20至25", "20-25"],
|
|
203
|
+
["20~25", "20-25"],
|
|
204
|
+
["20-25岁", "20-25"],
|
|
205
|
+
["25-30", "25-30"],
|
|
206
|
+
["25到30", "25-30"],
|
|
207
|
+
["25至30", "25-30"],
|
|
208
|
+
["25~30", "25-30"],
|
|
209
|
+
["25-30岁", "25-30"],
|
|
210
|
+
["30-35", "30-35"],
|
|
211
|
+
["30到35", "30-35"],
|
|
212
|
+
["30至35", "30-35"],
|
|
213
|
+
["30~35", "30-35"],
|
|
214
|
+
["30-35岁", "30-35"],
|
|
215
|
+
["35-40", "35-40"],
|
|
216
|
+
["35到40", "35-40"],
|
|
217
|
+
["35至40", "35-40"],
|
|
218
|
+
["35~40", "35-40"],
|
|
219
|
+
["35-40岁", "35-40"],
|
|
220
|
+
["40-50", "40-50"],
|
|
221
|
+
["40到50", "40-50"],
|
|
222
|
+
["40至50", "40-50"],
|
|
223
|
+
["40~50", "40-50"],
|
|
224
|
+
["40-50岁", "40-50"],
|
|
225
|
+
["50以上", "50以上"],
|
|
226
|
+
["50岁以上", "50以上"],
|
|
227
|
+
["50+", "50以上"],
|
|
228
|
+
["50岁+", "50以上"]
|
|
229
|
+
]);
|
|
230
|
+
const AGE_CUSTOM_MIN = 16;
|
|
231
|
+
const AGE_CUSTOM_MAX = 46;
|
|
66
232
|
|
|
67
233
|
function uniqueNodeIds(nodeIds = []) {
|
|
68
234
|
return Array.from(new Set(nodeIds.filter(Boolean)));
|
|
@@ -96,6 +262,34 @@ export function normalizeRecruitSearchLabel(label) {
|
|
|
96
262
|
return normalizeText(label).replace(/\s+/g, "");
|
|
97
263
|
}
|
|
98
264
|
|
|
265
|
+
function normalizeRecruitSchoolCompareKey(label) {
|
|
266
|
+
return normalizeRecruitSearchLabel(label).toLowerCase();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function resolveRecruitQsSchoolBucket(label) {
|
|
270
|
+
const compareKey = normalizeRecruitSchoolCompareKey(label);
|
|
271
|
+
const match = compareKey.match(/qs(?:世界大学排名)?(?:top)?(\d+)/i);
|
|
272
|
+
if (!match) return [];
|
|
273
|
+
const rank = Number(match[1]);
|
|
274
|
+
if (!Number.isFinite(rank)) return [];
|
|
275
|
+
return rank <= 100 ? ["QS 100", "QS100"] : ["QS 500", "QS500"];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function buildRecruitSchoolSearchLabels(school) {
|
|
279
|
+
const raw = normalizeText(school);
|
|
280
|
+
if (!raw) return [];
|
|
281
|
+
const normalized = normalizeRecruitSearchLabel(raw);
|
|
282
|
+
const compareKey = normalizeRecruitSchoolCompareKey(raw);
|
|
283
|
+
const labels = new Set([raw, normalized]);
|
|
284
|
+
for (const alias of resolveRecruitQsSchoolBucket(raw)) {
|
|
285
|
+
labels.add(alias);
|
|
286
|
+
}
|
|
287
|
+
for (const alias of SCHOOL_LABEL_ALIAS_MAP.get(compareKey) || []) {
|
|
288
|
+
labels.add(alias);
|
|
289
|
+
}
|
|
290
|
+
return Array.from(labels).map(normalizeText).filter(Boolean);
|
|
291
|
+
}
|
|
292
|
+
|
|
99
293
|
export function buildRecruitJobTitleSearchTerms(jobTitle) {
|
|
100
294
|
const normalized = normalizeText(jobTitle);
|
|
101
295
|
if (!normalized) return [];
|
|
@@ -134,15 +328,374 @@ export function normalizeRecruitDegreeLabels(value) {
|
|
|
134
328
|
return uniqueLabels.length ? uniqueLabels : ["不限"];
|
|
135
329
|
}
|
|
136
330
|
|
|
331
|
+
function normalizeRecruitSchoolList(value) {
|
|
332
|
+
const rawItems = Array.isArray(value)
|
|
333
|
+
? value.flatMap((item) => (
|
|
334
|
+
typeof item === "string"
|
|
335
|
+
? item.split(/[,,、|/]/)
|
|
336
|
+
: [item]
|
|
337
|
+
))
|
|
338
|
+
: typeof value === "string"
|
|
339
|
+
? value.split(/[,,、|/]/)
|
|
340
|
+
: [];
|
|
341
|
+
return uniqueNodeIds(rawItems.map(normalizeText).filter(Boolean));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function normalizeExperienceCompareKey(value) {
|
|
345
|
+
return normalizeRecruitSearchLabel(value).toLowerCase();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function resolveRecruitExperienceFixedLabel(value) {
|
|
349
|
+
const normalized = normalizeText(value);
|
|
350
|
+
if (!normalized) return null;
|
|
351
|
+
const direct = EXPERIENCE_FIXED_LABEL_ALIASES.get(normalized)
|
|
352
|
+
|| EXPERIENCE_FIXED_LABEL_ALIASES.get(normalizeExperienceCompareKey(normalized));
|
|
353
|
+
if (direct) return direct;
|
|
354
|
+
const compactRange = normalized
|
|
355
|
+
.replace(/\s+/g, "")
|
|
356
|
+
.replace(/[-—–]/g, "-")
|
|
357
|
+
.replace(/(?:到|至)/g, "-")
|
|
358
|
+
.replace(/[~~]/g, "-");
|
|
359
|
+
return EXPERIENCE_FIXED_LABEL_ALIASES.get(compactRange) || null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function normalizeRecruitExperienceCustomEndpoint(value, fallbackValue) {
|
|
363
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
364
|
+
const number = Math.min(
|
|
365
|
+
EXPERIENCE_CUSTOM_MAX_VALUE,
|
|
366
|
+
Math.max(EXPERIENCE_CUSTOM_MIN_VALUE, Math.round(value))
|
|
367
|
+
);
|
|
368
|
+
return {
|
|
369
|
+
value: number,
|
|
370
|
+
label: EXPERIENCE_CUSTOM_LABELS_BY_VALUE.get(number) || String(number)
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const normalized = normalizeText(value);
|
|
374
|
+
if (!normalized) {
|
|
375
|
+
const number = fallbackValue;
|
|
376
|
+
return {
|
|
377
|
+
value: number,
|
|
378
|
+
label: EXPERIENCE_CUSTOM_LABELS_BY_VALUE.get(number) || String(number)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const direct = EXPERIENCE_CUSTOM_ENDPOINTS.get(normalized)
|
|
382
|
+
|| EXPERIENCE_CUSTOM_ENDPOINTS.get(normalizeExperienceCompareKey(normalized));
|
|
383
|
+
if (direct) return { ...direct };
|
|
384
|
+
const numeric = normalized.match(/^(\d+)\s*(?:年)?$/);
|
|
385
|
+
if (numeric) {
|
|
386
|
+
const years = Number.parseInt(numeric[1], 10);
|
|
387
|
+
const valueByYears = new Map([
|
|
388
|
+
[0, 1],
|
|
389
|
+
[1, 2],
|
|
390
|
+
[2, 3],
|
|
391
|
+
[3, 4],
|
|
392
|
+
[4, 5],
|
|
393
|
+
[5, 6],
|
|
394
|
+
[6, 7],
|
|
395
|
+
[7, 8],
|
|
396
|
+
[8, 9],
|
|
397
|
+
[9, 10],
|
|
398
|
+
[10, 11]
|
|
399
|
+
]);
|
|
400
|
+
const number = valueByYears.get(years);
|
|
401
|
+
if (number) {
|
|
402
|
+
return {
|
|
403
|
+
value: number,
|
|
404
|
+
label: EXPERIENCE_CUSTOM_LABELS_BY_VALUE.get(number) || `${years}年`
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
throw new Error(`Unsupported recruit experience custom endpoint: ${normalized}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function parseRecruitExperienceCustomRangeText(value) {
|
|
412
|
+
let text = normalizeText(value);
|
|
413
|
+
if (!text) return null;
|
|
414
|
+
text = text.replace(/^(?:自定义|custom)\s*[::]?\s*/i, "").trim();
|
|
415
|
+
if (!text) return {
|
|
416
|
+
start: "在校/应届",
|
|
417
|
+
end: "10年以上"
|
|
418
|
+
};
|
|
419
|
+
const parts = text
|
|
420
|
+
.replace(/[-—–]/g, "-")
|
|
421
|
+
.split(/\s*(?:到|至|~|~|-)\s*/)
|
|
422
|
+
.map(normalizeText)
|
|
423
|
+
.filter(Boolean);
|
|
424
|
+
if (parts.length >= 2) {
|
|
425
|
+
return {
|
|
426
|
+
start: parts[0],
|
|
427
|
+
end: parts[parts.length - 1]
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function normalizeRecruitExperienceFilter(value) {
|
|
434
|
+
if (value === null || value === undefined) return null;
|
|
435
|
+
if (Array.isArray(value)) {
|
|
436
|
+
const first = value.find((item) => normalizeText(item));
|
|
437
|
+
return first === undefined ? null : normalizeRecruitExperienceFilter(first);
|
|
438
|
+
}
|
|
439
|
+
if (typeof value === "object") {
|
|
440
|
+
const mode = normalizeText(value.mode).toLowerCase();
|
|
441
|
+
const label = normalizeText(value.label || value.option || value.value);
|
|
442
|
+
const hasCustomEndpoints = Object.prototype.hasOwnProperty.call(value, "start")
|
|
443
|
+
|| Object.prototype.hasOwnProperty.call(value, "end")
|
|
444
|
+
|| Object.prototype.hasOwnProperty.call(value, "min")
|
|
445
|
+
|| Object.prototype.hasOwnProperty.call(value, "max")
|
|
446
|
+
|| Object.prototype.hasOwnProperty.call(value, "from")
|
|
447
|
+
|| Object.prototype.hasOwnProperty.call(value, "to")
|
|
448
|
+
|| Object.prototype.hasOwnProperty.call(value, "start_value")
|
|
449
|
+
|| Object.prototype.hasOwnProperty.call(value, "end_value");
|
|
450
|
+
if (mode === "custom" || hasCustomEndpoints) {
|
|
451
|
+
const startSource = value.start ?? value.min ?? value.from ?? value.start_label ?? value.start_value;
|
|
452
|
+
const endSource = value.end ?? value.max ?? value.to ?? value.end_label ?? value.end_value;
|
|
453
|
+
const start = normalizeRecruitExperienceCustomEndpoint(startSource, EXPERIENCE_CUSTOM_MIN_VALUE);
|
|
454
|
+
const end = normalizeRecruitExperienceCustomEndpoint(endSource, EXPERIENCE_CUSTOM_MAX_VALUE);
|
|
455
|
+
if (start.value > end.value) {
|
|
456
|
+
throw new Error(`Recruit experience custom start must be <= end: ${start.label} > ${end.label}`);
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
mode: "custom",
|
|
460
|
+
start_label: start.label,
|
|
461
|
+
end_label: end.label,
|
|
462
|
+
start_value: start.value,
|
|
463
|
+
end_value: end.value,
|
|
464
|
+
label: `${start.label}-${end.label}`
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return normalizeRecruitExperienceFilter(label);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const text = normalizeText(value);
|
|
471
|
+
if (!text) return null;
|
|
472
|
+
const fixedLabel = resolveRecruitExperienceFixedLabel(text);
|
|
473
|
+
if (fixedLabel) {
|
|
474
|
+
return {
|
|
475
|
+
mode: "option",
|
|
476
|
+
label: fixedLabel,
|
|
477
|
+
unlimited: fixedLabel === "不限"
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const customRange = parseRecruitExperienceCustomRangeText(text);
|
|
481
|
+
if (customRange || /^(?:自定义|custom)$/i.test(text)) {
|
|
482
|
+
return normalizeRecruitExperienceFilter({
|
|
483
|
+
mode: "custom",
|
|
484
|
+
start: customRange?.start || "在校/应届",
|
|
485
|
+
end: customRange?.end || "10年以上"
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
throw new Error(`Unsupported recruit experience filter: ${text}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function normalizeGenderCompareKey(value) {
|
|
492
|
+
return normalizeRecruitSearchLabel(value).toLowerCase();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function normalizeRecruitGenderFilter(value) {
|
|
496
|
+
if (value === null || value === undefined) return null;
|
|
497
|
+
if (Array.isArray(value)) {
|
|
498
|
+
const first = value.find((item) => normalizeText(item));
|
|
499
|
+
return first === undefined ? null : normalizeRecruitGenderFilter(first);
|
|
500
|
+
}
|
|
501
|
+
if (typeof value === "object") {
|
|
502
|
+
return normalizeRecruitGenderFilter(value.label || value.value || value.gender);
|
|
503
|
+
}
|
|
504
|
+
const text = normalizeText(value);
|
|
505
|
+
if (!text) return null;
|
|
506
|
+
const label = GENDER_LABEL_ALIASES.get(text) || GENDER_LABEL_ALIASES.get(normalizeGenderCompareKey(text));
|
|
507
|
+
if (!label) throw new Error(`Unsupported recruit gender filter: ${text}`);
|
|
508
|
+
return {
|
|
509
|
+
label,
|
|
510
|
+
unlimited: label === "不限"
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function parseAgeNumber(value, fallback = null) {
|
|
515
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.round(value);
|
|
516
|
+
const text = normalizeText(value);
|
|
517
|
+
if (!text) return fallback;
|
|
518
|
+
if (/^(?:不限|不限制|无限制|全部|所有|无|none|all)$/i.test(text)) return fallback;
|
|
519
|
+
const match = text.match(/(\d+)/);
|
|
520
|
+
if (!match) return fallback;
|
|
521
|
+
return Number.parseInt(match[1], 10);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function normalizeAgeFixedLabel(value) {
|
|
525
|
+
const text = normalizeText(value);
|
|
526
|
+
if (!text) return null;
|
|
527
|
+
const compact = text
|
|
528
|
+
.replace(/\s+/g, "")
|
|
529
|
+
.replace(/[-—–]/g, "-")
|
|
530
|
+
.replace(/(?:到|至)/g, "-")
|
|
531
|
+
.replace(/[~~]/g, "-");
|
|
532
|
+
return AGE_FIXED_LABEL_ALIASES.get(text)
|
|
533
|
+
|| AGE_FIXED_LABEL_ALIASES.get(compact)
|
|
534
|
+
|| AGE_FIXED_LABEL_ALIASES.get(normalizeRecruitSearchLabel(compact));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function parseAgeCustomRangeText(value) {
|
|
538
|
+
let text = normalizeText(value);
|
|
539
|
+
if (!text) return null;
|
|
540
|
+
text = text.replace(/^(?:自定义|custom)\s*[::]?\s*/i, "").trim();
|
|
541
|
+
if (!text) return { min: null, max: null };
|
|
542
|
+
const rangeMatch = text
|
|
543
|
+
.replace(/[-—–]/g, "-")
|
|
544
|
+
.match(/(\d+)\s*(?:岁)?\s*(?:-|到|至|~|~)\s*(\d+)\s*(?:岁)?/);
|
|
545
|
+
if (rangeMatch) {
|
|
546
|
+
return {
|
|
547
|
+
min: Number.parseInt(rangeMatch[1], 10),
|
|
548
|
+
max: Number.parseInt(rangeMatch[2], 10)
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function parseAgeCustomComparisonText(value) {
|
|
555
|
+
const text = normalizeText(value).replace(/\s+/g, "");
|
|
556
|
+
if (!text) return null;
|
|
557
|
+
const strictUpper = text.match(/^(?:低于|小于|少于|未满|不满|<)(\d+)(?:岁)?$/);
|
|
558
|
+
if (strictUpper) {
|
|
559
|
+
return {
|
|
560
|
+
min: null,
|
|
561
|
+
max: Number.parseInt(strictUpper[1], 10) - 1
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
const inclusiveUpper = text.match(/^(?:不超过|不大于|至多|最多|<=|≤)(\d+)(?:岁)?$/)
|
|
565
|
+
|| text.match(/^(\d+)(?:岁)?(?:及以下|以下|以内)$/);
|
|
566
|
+
if (inclusiveUpper) {
|
|
567
|
+
return {
|
|
568
|
+
min: null,
|
|
569
|
+
max: Number.parseInt(inclusiveUpper[1], 10)
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const strictLower = text.match(/^(?:高于|大于|超过|>)(\d+)(?:岁)?$/);
|
|
573
|
+
if (strictLower) {
|
|
574
|
+
return {
|
|
575
|
+
min: Number.parseInt(strictLower[1], 10) + 1,
|
|
576
|
+
max: null
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const inclusiveLower = text.match(/^(?:不低于|不小于|至少|最低|>=|≥)(\d+)(?:岁)?$/)
|
|
580
|
+
|| text.match(/^(\d+)(?:岁)?(?:及以上|以上)$/);
|
|
581
|
+
if (inclusiveLower) {
|
|
582
|
+
return {
|
|
583
|
+
min: Number.parseInt(inclusiveLower[1], 10),
|
|
584
|
+
max: null
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export function normalizeRecruitAgeFilter(value) {
|
|
591
|
+
if (value === null || value === undefined) return null;
|
|
592
|
+
if (Array.isArray(value)) {
|
|
593
|
+
const first = value.find((item) => normalizeText(item));
|
|
594
|
+
return first === undefined ? null : normalizeRecruitAgeFilter(first);
|
|
595
|
+
}
|
|
596
|
+
if (typeof value === "object") {
|
|
597
|
+
const mode = normalizeText(value.mode).toLowerCase();
|
|
598
|
+
const label = normalizeText(value.label || value.option || value.value);
|
|
599
|
+
const hasCustom = Object.prototype.hasOwnProperty.call(value, "min")
|
|
600
|
+
|| Object.prototype.hasOwnProperty.call(value, "max")
|
|
601
|
+
|| Object.prototype.hasOwnProperty.call(value, "start")
|
|
602
|
+
|| Object.prototype.hasOwnProperty.call(value, "end")
|
|
603
|
+
|| Object.prototype.hasOwnProperty.call(value, "from")
|
|
604
|
+
|| Object.prototype.hasOwnProperty.call(value, "to");
|
|
605
|
+
if (mode === "custom" || hasCustom) {
|
|
606
|
+
const min = parseAgeNumber(value.min ?? value.start ?? value.from, null);
|
|
607
|
+
const max = parseAgeNumber(value.max ?? value.end ?? value.to, null);
|
|
608
|
+
if (min !== null && (min < AGE_CUSTOM_MIN || min > AGE_CUSTOM_MAX)) {
|
|
609
|
+
throw new Error(`Recruit age custom min must be between ${AGE_CUSTOM_MIN} and ${AGE_CUSTOM_MAX}: ${min}`);
|
|
610
|
+
}
|
|
611
|
+
if (max !== null && (max < AGE_CUSTOM_MIN || max > AGE_CUSTOM_MAX)) {
|
|
612
|
+
throw new Error(`Recruit age custom max must be between ${AGE_CUSTOM_MIN} and ${AGE_CUSTOM_MAX}: ${max}`);
|
|
613
|
+
}
|
|
614
|
+
if (min !== null && max !== null && min > max) {
|
|
615
|
+
throw new Error(`Recruit age custom min must be <= max: ${min} > ${max}`);
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
mode: "custom",
|
|
619
|
+
min,
|
|
620
|
+
max,
|
|
621
|
+
label: `${min ?? "不限"}-${max ?? "不限"}`
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
return normalizeRecruitAgeFilter(label);
|
|
625
|
+
}
|
|
626
|
+
const text = normalizeText(value);
|
|
627
|
+
if (!text) return null;
|
|
628
|
+
const fixedLabel = normalizeAgeFixedLabel(text);
|
|
629
|
+
if (fixedLabel) {
|
|
630
|
+
return {
|
|
631
|
+
mode: "option",
|
|
632
|
+
label: fixedLabel,
|
|
633
|
+
unlimited: fixedLabel === "不限"
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
const customRange = parseAgeCustomRangeText(text);
|
|
637
|
+
if (customRange || /^(?:自定义|custom)$/i.test(text)) {
|
|
638
|
+
return normalizeRecruitAgeFilter({
|
|
639
|
+
mode: "custom",
|
|
640
|
+
min: customRange?.min ?? null,
|
|
641
|
+
max: customRange?.max ?? null
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
const customComparison = parseAgeCustomComparisonText(text);
|
|
645
|
+
if (customComparison) {
|
|
646
|
+
return normalizeRecruitAgeFilter({
|
|
647
|
+
mode: "custom",
|
|
648
|
+
min: customComparison.min,
|
|
649
|
+
max: customComparison.max
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
throw new Error(`Unsupported recruit age filter: ${text}`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function pickRecruitExperienceSource(searchParams = {}) {
|
|
656
|
+
if (Object.prototype.hasOwnProperty.call(searchParams, "experience")) return searchParams.experience;
|
|
657
|
+
if (Object.prototype.hasOwnProperty.call(searchParams, "experiences")) return searchParams.experiences;
|
|
658
|
+
if (Object.prototype.hasOwnProperty.call(searchParams, "experience_range")) return searchParams.experience_range;
|
|
659
|
+
if (
|
|
660
|
+
Object.prototype.hasOwnProperty.call(searchParams, "experience_start")
|
|
661
|
+
|| Object.prototype.hasOwnProperty.call(searchParams, "experience_end")
|
|
662
|
+
) {
|
|
663
|
+
return {
|
|
664
|
+
start: searchParams.experience_start,
|
|
665
|
+
end: searchParams.experience_end
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function pickRecruitAgeSource(searchParams = {}) {
|
|
672
|
+
if (Object.prototype.hasOwnProperty.call(searchParams, "age")) return searchParams.age;
|
|
673
|
+
if (Object.prototype.hasOwnProperty.call(searchParams, "ages")) return searchParams.ages;
|
|
674
|
+
if (Object.prototype.hasOwnProperty.call(searchParams, "age_range")) return searchParams.age_range;
|
|
675
|
+
if (
|
|
676
|
+
Object.prototype.hasOwnProperty.call(searchParams, "age_min")
|
|
677
|
+
|| Object.prototype.hasOwnProperty.call(searchParams, "age_max")
|
|
678
|
+
|| Object.prototype.hasOwnProperty.call(searchParams, "min_age")
|
|
679
|
+
|| Object.prototype.hasOwnProperty.call(searchParams, "max_age")
|
|
680
|
+
) {
|
|
681
|
+
return {
|
|
682
|
+
min: searchParams.age_min ?? searchParams.min_age,
|
|
683
|
+
max: searchParams.age_max ?? searchParams.max_age
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
137
689
|
export function normalizeRecruitSearchParams(searchParams = {}) {
|
|
138
690
|
const degrees = normalizeRecruitDegreeLabels(searchParams.degrees || searchParams.degree || "不限");
|
|
691
|
+
const experience = normalizeRecruitExperienceFilter(pickRecruitExperienceSource(searchParams));
|
|
692
|
+
const gender = normalizeRecruitGenderFilter(searchParams.gender);
|
|
693
|
+
const age = normalizeRecruitAgeFilter(pickRecruitAgeSource(searchParams));
|
|
139
694
|
const normalized = {
|
|
140
695
|
city: normalizeText(searchParams.city) || null,
|
|
141
696
|
degree: degrees[0] || "不限",
|
|
142
697
|
degrees,
|
|
143
|
-
schools:
|
|
144
|
-
? searchParams.schools.map(normalizeText).filter(Boolean)
|
|
145
|
-
: [],
|
|
698
|
+
schools: normalizeRecruitSchoolList(searchParams.schools),
|
|
146
699
|
keyword: normalizeText(searchParams.keyword) || DEFAULT_RECRUIT_KEYWORD,
|
|
147
700
|
filter_recent_viewed: typeof searchParams.filter_recent_viewed === "boolean"
|
|
148
701
|
? searchParams.filter_recent_viewed
|
|
@@ -150,19 +703,39 @@ export function normalizeRecruitSearchParams(searchParams = {}) {
|
|
|
150
703
|
};
|
|
151
704
|
const job = normalizeText(searchParams.job || searchParams.job_title || searchParams.selected_job);
|
|
152
705
|
if (job) normalized.job = job;
|
|
706
|
+
if (experience) normalized.experience = experience;
|
|
707
|
+
if (gender) normalized.gender = gender;
|
|
708
|
+
if (age) normalized.age = age;
|
|
153
709
|
return normalized;
|
|
154
710
|
}
|
|
155
711
|
|
|
712
|
+
export function buildRecruitSearchApplicationStepNames(searchParams = {}) {
|
|
713
|
+
const normalized = normalizeRecruitSearchParams(searchParams);
|
|
714
|
+
const steps = [];
|
|
715
|
+
// Recruit search applies job first because job changes can reset other filters.
|
|
716
|
+
if (normalized.job) steps.push("job_title");
|
|
717
|
+
if (normalized.city) steps.push("city");
|
|
718
|
+
steps.push("degree", "schools");
|
|
719
|
+
if (normalized.experience) steps.push("experience");
|
|
720
|
+
if (normalized.gender) steps.push("gender");
|
|
721
|
+
if (normalized.age) steps.push("age");
|
|
722
|
+
if (typeof normalized.filter_recent_viewed === "boolean") steps.push("recent_viewed");
|
|
723
|
+
// Keyword is the final filter before executing the search.
|
|
724
|
+
steps.push("keyword", "search");
|
|
725
|
+
return steps;
|
|
726
|
+
}
|
|
727
|
+
|
|
156
728
|
export function hasRecruitSearchParams(searchParams = {}) {
|
|
157
729
|
const degrees = normalizeRecruitDegreeLabels(searchParams.degrees || searchParams.degree || "不限");
|
|
158
730
|
const job = normalizeText(searchParams.job || searchParams.job_title || searchParams.selected_job);
|
|
731
|
+
const experience = normalizeRecruitExperienceFilter(pickRecruitExperienceSource(searchParams));
|
|
732
|
+
const gender = normalizeRecruitGenderFilter(searchParams.gender);
|
|
733
|
+
const age = normalizeRecruitAgeFilter(pickRecruitAgeSource(searchParams));
|
|
159
734
|
const normalized = {
|
|
160
735
|
city: normalizeText(searchParams.city) || null,
|
|
161
736
|
degree: degrees[0] || "不限",
|
|
162
737
|
degrees,
|
|
163
|
-
schools:
|
|
164
|
-
? searchParams.schools.map(normalizeText).filter(Boolean)
|
|
165
|
-
: [],
|
|
738
|
+
schools: normalizeRecruitSchoolList(searchParams.schools),
|
|
166
739
|
keyword: normalizeText(searchParams.keyword),
|
|
167
740
|
filter_recent_viewed: typeof searchParams.filter_recent_viewed === "boolean"
|
|
168
741
|
? searchParams.filter_recent_viewed
|
|
@@ -173,6 +746,9 @@ export function hasRecruitSearchParams(searchParams = {}) {
|
|
|
173
746
|
|| normalized.city
|
|
174
747
|
|| normalized.degrees.some((degree) => degree && degree !== "不限")
|
|
175
748
|
|| normalized.schools.length
|
|
749
|
+
|| experience
|
|
750
|
+
|| gender
|
|
751
|
+
|| age
|
|
176
752
|
|| normalized.keyword
|
|
177
753
|
|| typeof normalized.filter_recent_viewed === "boolean"
|
|
178
754
|
);
|
|
@@ -186,14 +762,28 @@ function candidateIsActive(attributes = {}, outerHTML = "") {
|
|
|
186
762
|
|| /\bchecked(?:=["']?checked)?\b/i.test(openingTag);
|
|
187
763
|
}
|
|
188
764
|
|
|
765
|
+
function isVisibleBox(box) {
|
|
766
|
+
return Boolean(box && box.rect.width > 4 && box.rect.height > 4);
|
|
767
|
+
}
|
|
768
|
+
|
|
189
769
|
async function readTextCandidate(client, nodeId, {
|
|
190
770
|
selector = "",
|
|
191
|
-
index = 0
|
|
771
|
+
index = 0,
|
|
772
|
+
includeBox = false
|
|
192
773
|
} = {}) {
|
|
193
774
|
const [attributes, outerHTML] = await Promise.all([
|
|
194
775
|
getAttributesMap(client, nodeId),
|
|
195
776
|
getOuterHTML(client, nodeId)
|
|
196
777
|
]);
|
|
778
|
+
let box = null;
|
|
779
|
+
let boxError = "";
|
|
780
|
+
if (includeBox) {
|
|
781
|
+
try {
|
|
782
|
+
box = await getNodeBox(client, nodeId);
|
|
783
|
+
} catch (error) {
|
|
784
|
+
boxError = error?.message || String(error);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
197
787
|
const text = normalizeText(htmlToText(outerHTML));
|
|
198
788
|
return {
|
|
199
789
|
node_id: nodeId,
|
|
@@ -202,12 +792,16 @@ async function readTextCandidate(client, nodeId, {
|
|
|
202
792
|
label: normalizeRecruitSearchLabel(text),
|
|
203
793
|
text,
|
|
204
794
|
active: candidateIsActive(attributes, outerHTML),
|
|
795
|
+
visible: includeBox ? isVisibleBox(box) : undefined,
|
|
796
|
+
center: box?.center || null,
|
|
797
|
+
rect: box?.rect || null,
|
|
798
|
+
box_error: boxError || undefined,
|
|
205
799
|
class_name: attributes.class || "",
|
|
206
800
|
attributes
|
|
207
801
|
};
|
|
208
802
|
}
|
|
209
803
|
|
|
210
|
-
async function listTextCandidates(client, rootNodeId, selectors = []) {
|
|
804
|
+
async function listTextCandidates(client, rootNodeId, selectors = [], options = {}) {
|
|
211
805
|
const candidates = [];
|
|
212
806
|
const seen = new Set();
|
|
213
807
|
for (const selector of selectors) {
|
|
@@ -216,7 +810,7 @@ async function listTextCandidates(client, rootNodeId, selectors = []) {
|
|
|
216
810
|
const nodeId = nodeIds[index];
|
|
217
811
|
if (seen.has(nodeId)) continue;
|
|
218
812
|
seen.add(nodeId);
|
|
219
|
-
candidates.push(await readTextCandidate(client, nodeId, { selector, index }));
|
|
813
|
+
candidates.push(await readTextCandidate(client, nodeId, { selector, index, ...options }));
|
|
220
814
|
}
|
|
221
815
|
}
|
|
222
816
|
return candidates;
|
|
@@ -240,6 +834,12 @@ export function chooseRecruitTextCandidate(candidates = [], {
|
|
|
240
834
|
return candidates.find((candidate) => candidate.label.includes(target) || target.includes(candidate.label)) || null;
|
|
241
835
|
}
|
|
242
836
|
|
|
837
|
+
function chooseRecruitSchoolCandidate(candidates = [], school) {
|
|
838
|
+
const targetKeys = new Set(buildRecruitSchoolSearchLabels(school).map(normalizeRecruitSchoolCompareKey));
|
|
839
|
+
if (!targetKeys.size) return null;
|
|
840
|
+
return candidates.find((candidate) => targetKeys.has(normalizeRecruitSchoolCompareKey(candidate.text || candidate.label))) || null;
|
|
841
|
+
}
|
|
842
|
+
|
|
243
843
|
async function findTextCandidate(client, rootNodeId, selectors, label, options = {}) {
|
|
244
844
|
const candidates = await listTextCandidates(client, rootNodeId, selectors);
|
|
245
845
|
return {
|
|
@@ -310,46 +910,82 @@ async function waitForRecruitJobTitleCandidate(client, rootNodeId, selectors, jo
|
|
|
310
910
|
};
|
|
311
911
|
}
|
|
312
912
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
913
|
+
function compactRecruitTextCandidate(candidate = {}) {
|
|
914
|
+
return {
|
|
915
|
+
label: candidate.text || "",
|
|
916
|
+
normalized_label: candidate.label || "",
|
|
917
|
+
active: Boolean(candidate.active),
|
|
918
|
+
visible: Boolean(candidate.visible),
|
|
919
|
+
class_name: candidate.class_name || "",
|
|
920
|
+
node_id: candidate.node_id,
|
|
921
|
+
selector: candidate.selector,
|
|
922
|
+
center: candidate.center || null,
|
|
923
|
+
rect: candidate.rect || null,
|
|
924
|
+
box_error: candidate.box_error || null
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async function listRecruitJobTitleOptions(client, frameNodeId) {
|
|
929
|
+
const candidates = await listTextCandidates(
|
|
930
|
+
client,
|
|
931
|
+
frameNodeId,
|
|
932
|
+
RECRUIT_SEARCH_SELECTORS.jobTitleOption,
|
|
933
|
+
{ includeBox: true }
|
|
934
|
+
);
|
|
935
|
+
return candidates.filter((candidate) => candidate.text && candidate.text.length <= 160);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function findVisibleRecruitJobTitleTrigger(client, frameNodeId) {
|
|
939
|
+
const candidates = [];
|
|
940
|
+
const seen = new Set();
|
|
941
|
+
for (const selector of RECRUIT_SEARCH_SELECTORS.jobTitleTrigger) {
|
|
942
|
+
const nodeIds = uniqueNodeIds(await querySelectorAll(client, frameNodeId, selector));
|
|
943
|
+
for (const nodeId of nodeIds) {
|
|
944
|
+
if (seen.has(nodeId)) continue;
|
|
945
|
+
seen.add(nodeId);
|
|
946
|
+
let box = null;
|
|
947
|
+
try {
|
|
948
|
+
box = await getNodeBox(client, nodeId);
|
|
949
|
+
} catch {}
|
|
950
|
+
if (!isVisibleBox(box)) continue;
|
|
951
|
+
candidates.push({
|
|
952
|
+
selector,
|
|
953
|
+
node_id: nodeId,
|
|
954
|
+
center: box.center,
|
|
955
|
+
rect: box.rect
|
|
956
|
+
});
|
|
957
|
+
}
|
|
321
958
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (optional) {
|
|
959
|
+
return candidates[0] || null;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
async function waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
963
|
+
timeoutMs = 4000,
|
|
964
|
+
intervalMs = 200
|
|
965
|
+
} = {}) {
|
|
966
|
+
const started = Date.now();
|
|
967
|
+
let options = [];
|
|
968
|
+
while (Date.now() - started <= timeoutMs) {
|
|
969
|
+
options = await listRecruitJobTitleOptions(client, frameNodeId);
|
|
970
|
+
const visibleOptions = options.filter((option) => option.visible);
|
|
971
|
+
if (visibleOptions.length) {
|
|
336
972
|
return {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
selector: found.selector,
|
|
340
|
-
node_id: found.nodeId,
|
|
341
|
-
error: error?.message || String(error)
|
|
973
|
+
options,
|
|
974
|
+
visible_options: visibleOptions
|
|
342
975
|
};
|
|
343
976
|
}
|
|
344
|
-
|
|
977
|
+
await sleep(intervalMs);
|
|
345
978
|
}
|
|
979
|
+
return {
|
|
980
|
+
options,
|
|
981
|
+
visible_options: []
|
|
982
|
+
};
|
|
346
983
|
}
|
|
347
984
|
|
|
348
|
-
async function
|
|
985
|
+
async function closeRecruitJobTitleDropdown(client, settleMs = 300) {
|
|
349
986
|
if (typeof client?.Input?.dispatchKeyEvent !== "function") {
|
|
350
987
|
return {
|
|
351
|
-
|
|
352
|
-
skipped: true,
|
|
988
|
+
ok: false,
|
|
353
989
|
reason: "dispatch_key_unavailable"
|
|
354
990
|
};
|
|
355
991
|
}
|
|
@@ -360,67 +996,352 @@ async function dismissRecruitSearchOverlays(client, settleMs = 250) {
|
|
|
360
996
|
});
|
|
361
997
|
if (settleMs > 0) await sleep(settleMs);
|
|
362
998
|
return {
|
|
363
|
-
|
|
364
|
-
|
|
999
|
+
ok: true,
|
|
1000
|
+
reason: "escape"
|
|
365
1001
|
};
|
|
366
1002
|
}
|
|
367
1003
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
candidate_card: RECRUIT_CARD_SELECTOR,
|
|
376
|
-
no_data: RECRUIT_NO_DATA_SELECTORS.join(", ")
|
|
1004
|
+
async function openRecruitJobTitleDropdown(client, frameNodeId, {
|
|
1005
|
+
timeoutMs = 4000,
|
|
1006
|
+
maxAttempts = 3
|
|
1007
|
+
} = {}) {
|
|
1008
|
+
const alreadyOpen = await waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
1009
|
+
timeoutMs: 300,
|
|
1010
|
+
intervalMs: 100
|
|
377
1011
|
});
|
|
1012
|
+
if (alreadyOpen.visible_options.length) {
|
|
1013
|
+
return {
|
|
1014
|
+
opened: true,
|
|
1015
|
+
already_open: true,
|
|
1016
|
+
options: alreadyOpen.options,
|
|
1017
|
+
visible_options: alreadyOpen.visible_options
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
await closeRecruitJobTitleDropdown(client);
|
|
1022
|
+
const attempts = [];
|
|
1023
|
+
for (let attempt = 1; attempt <= Math.max(1, maxAttempts); attempt += 1) {
|
|
1024
|
+
const trigger = await findVisibleRecruitJobTitleTrigger(client, frameNodeId);
|
|
1025
|
+
if (!trigger) {
|
|
1026
|
+
throw new Error("Recruit job trigger was not found");
|
|
1027
|
+
}
|
|
1028
|
+
if (attempt > 1) await closeRecruitJobTitleDropdown(client);
|
|
1029
|
+
const clickBox = await clickNodeCenter(client, trigger.node_id, DETERMINISTIC_CLICK_OPTIONS);
|
|
1030
|
+
const opened = await waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
1031
|
+
timeoutMs,
|
|
1032
|
+
intervalMs: 200
|
|
1033
|
+
});
|
|
1034
|
+
attempts.push({
|
|
1035
|
+
attempt,
|
|
1036
|
+
trigger,
|
|
1037
|
+
click_box: {
|
|
1038
|
+
center: clickBox.center,
|
|
1039
|
+
rect: clickBox.rect
|
|
1040
|
+
},
|
|
1041
|
+
option_count: opened.options.length,
|
|
1042
|
+
visible_option_count: opened.visible_options.length
|
|
1043
|
+
});
|
|
1044
|
+
if (opened.visible_options.length) {
|
|
1045
|
+
return {
|
|
1046
|
+
opened: true,
|
|
1047
|
+
already_open: false,
|
|
1048
|
+
trigger,
|
|
1049
|
+
options: opened.options,
|
|
1050
|
+
visible_options: opened.visible_options,
|
|
1051
|
+
attempts
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const error = new Error("Recruit job dropdown did not expose visible options after trigger click");
|
|
1057
|
+
error.job_dropdown_attempts = attempts;
|
|
1058
|
+
throw error;
|
|
378
1059
|
}
|
|
379
1060
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
1061
|
+
async function closeRecruitJobTitleDropdownFully(client, frameNodeId, {
|
|
1062
|
+
settleMs = 300,
|
|
1063
|
+
timeoutMs = 1500
|
|
383
1064
|
} = {}) {
|
|
1065
|
+
const before = await waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
1066
|
+
timeoutMs: 200,
|
|
1067
|
+
intervalMs: 100
|
|
1068
|
+
});
|
|
1069
|
+
const attempts = [];
|
|
1070
|
+
if (!before.visible_options.length) {
|
|
1071
|
+
return {
|
|
1072
|
+
ok: true,
|
|
1073
|
+
closed: false,
|
|
1074
|
+
reason: "already_closed",
|
|
1075
|
+
visible_before_count: 0,
|
|
1076
|
+
visible_after_count: 0,
|
|
1077
|
+
attempts
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
384
1081
|
const started = Date.now();
|
|
385
|
-
let
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
1082
|
+
for (let attempt = 1; attempt <= 2 && Date.now() - started <= timeoutMs; attempt += 1) {
|
|
1083
|
+
const close = await closeRecruitJobTitleDropdown(client, settleMs);
|
|
1084
|
+
const after = await waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
1085
|
+
timeoutMs: 250,
|
|
1086
|
+
intervalMs: 100
|
|
1087
|
+
});
|
|
1088
|
+
attempts.push({
|
|
1089
|
+
method: "escape",
|
|
1090
|
+
attempt,
|
|
1091
|
+
close,
|
|
1092
|
+
visible_after_count: after.visible_options.length
|
|
1093
|
+
});
|
|
1094
|
+
if (!after.visible_options.length) {
|
|
1095
|
+
return {
|
|
1096
|
+
ok: true,
|
|
1097
|
+
closed: true,
|
|
1098
|
+
reason: "escape",
|
|
1099
|
+
visible_before_count: before.visible_options.length,
|
|
1100
|
+
visible_after_count: 0,
|
|
1101
|
+
attempts
|
|
397
1102
|
};
|
|
398
|
-
if (lastState.ok) return lastState;
|
|
399
1103
|
}
|
|
400
|
-
await sleep(intervalMs);
|
|
401
1104
|
}
|
|
1105
|
+
|
|
1106
|
+
const trigger = await findVisibleRecruitJobTitleTrigger(client, frameNodeId).catch(() => null);
|
|
1107
|
+
if (trigger?.node_id) {
|
|
1108
|
+
const click = await clickNodeCenter(client, trigger.node_id, DETERMINISTIC_CLICK_OPTIONS).catch((error) => ({
|
|
1109
|
+
error: error?.message || String(error || "")
|
|
1110
|
+
}));
|
|
1111
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
1112
|
+
const afterToggle = await waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
1113
|
+
timeoutMs: 250,
|
|
1114
|
+
intervalMs: 100
|
|
1115
|
+
});
|
|
1116
|
+
attempts.push({
|
|
1117
|
+
method: "trigger_toggle",
|
|
1118
|
+
click,
|
|
1119
|
+
visible_after_count: afterToggle.visible_options.length
|
|
1120
|
+
});
|
|
1121
|
+
if (!afterToggle.visible_options.length) {
|
|
1122
|
+
return {
|
|
1123
|
+
ok: true,
|
|
1124
|
+
closed: true,
|
|
1125
|
+
reason: "trigger_toggle",
|
|
1126
|
+
visible_before_count: before.visible_options.length,
|
|
1127
|
+
visible_after_count: 0,
|
|
1128
|
+
attempts
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const outside = await clickPoint(client, 12, 12, DETERMINISTIC_CLICK_OPTIONS).catch((error) => ({
|
|
1134
|
+
error: error?.message || String(error || "")
|
|
1135
|
+
}));
|
|
1136
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
1137
|
+
const afterOutside = await waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
1138
|
+
timeoutMs: 250,
|
|
1139
|
+
intervalMs: 100
|
|
1140
|
+
});
|
|
1141
|
+
attempts.push({
|
|
1142
|
+
method: "outside_click",
|
|
1143
|
+
click: outside,
|
|
1144
|
+
visible_after_count: afterOutside.visible_options.length
|
|
1145
|
+
});
|
|
1146
|
+
if (!afterOutside.visible_options.length) {
|
|
1147
|
+
return {
|
|
1148
|
+
ok: true,
|
|
1149
|
+
closed: true,
|
|
1150
|
+
reason: "outside_click",
|
|
1151
|
+
visible_before_count: before.visible_options.length,
|
|
1152
|
+
visible_after_count: 0,
|
|
1153
|
+
attempts
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
402
1157
|
return {
|
|
403
1158
|
ok: false,
|
|
404
|
-
|
|
405
|
-
|
|
1159
|
+
closed: false,
|
|
1160
|
+
reason: "still_visible_after_close_attempts",
|
|
1161
|
+
visible_before_count: before.visible_options.length,
|
|
1162
|
+
visible_after_count: afterOutside.visible_options.length,
|
|
1163
|
+
attempts
|
|
406
1164
|
};
|
|
407
1165
|
}
|
|
408
1166
|
|
|
409
|
-
async function
|
|
410
|
-
|
|
411
|
-
|
|
1167
|
+
async function verifyRecruitJobTitleSelection(client, frameNodeId, {
|
|
1168
|
+
jobTitle = "",
|
|
1169
|
+
delayMs = 1200,
|
|
1170
|
+
dropdownTimeoutMs = 4000
|
|
412
1171
|
} = {}) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
1172
|
+
const requested = normalizeText(jobTitle);
|
|
1173
|
+
if (delayMs > 0) await sleep(delayMs);
|
|
1174
|
+
let options = [];
|
|
1175
|
+
let openError = null;
|
|
1176
|
+
try {
|
|
1177
|
+
const opened = await openRecruitJobTitleDropdown(client, frameNodeId, {
|
|
1178
|
+
timeoutMs: dropdownTimeoutMs
|
|
1179
|
+
});
|
|
1180
|
+
options = opened.options || [];
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
openError = error;
|
|
1183
|
+
options = await listRecruitJobTitleOptions(client, frameNodeId).catch(() => []);
|
|
1184
|
+
}
|
|
1185
|
+
const current = options.find((option) => option.active) || null;
|
|
1186
|
+
const searchTerms = buildRecruitJobTitleSearchTerms(requested);
|
|
1187
|
+
const verified = Boolean(
|
|
1188
|
+
current
|
|
1189
|
+
&& searchTerms.some((term) => chooseRecruitTextCandidate([current], {
|
|
1190
|
+
label: term,
|
|
1191
|
+
match: "contains"
|
|
1192
|
+
}))
|
|
1193
|
+
);
|
|
1194
|
+
const menuClose = await closeRecruitJobTitleDropdownFully(client, frameNodeId).catch((error) => ({
|
|
1195
|
+
ok: false,
|
|
1196
|
+
closed: false,
|
|
1197
|
+
reason: "close_failed",
|
|
1198
|
+
error: error?.message || String(error)
|
|
1199
|
+
}));
|
|
1200
|
+
return {
|
|
1201
|
+
verified,
|
|
1202
|
+
requested,
|
|
1203
|
+
search_terms: searchTerms,
|
|
1204
|
+
current_label: current?.text || "",
|
|
1205
|
+
current_option: current ? compactRecruitTextCandidate(current) : null,
|
|
1206
|
+
option_count: options.length,
|
|
1207
|
+
visible_option_count: options.filter((option) => option.visible).length,
|
|
1208
|
+
options: options.map(compactRecruitTextCandidate),
|
|
1209
|
+
open_error: openError ? (openError?.message || String(openError)) : null,
|
|
1210
|
+
menu_close: menuClose
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
async function clickFirstNodeBySelectors(client, rootNodeId, selectors, {
|
|
1215
|
+
optional = false,
|
|
1216
|
+
scrollIntoView = true
|
|
1217
|
+
} = {}) {
|
|
1218
|
+
const errors = [];
|
|
1219
|
+
const seen = new Set();
|
|
1220
|
+
let matched = false;
|
|
1221
|
+
for (const selector of selectors) {
|
|
1222
|
+
const nodeIds = uniqueNodeIds(await querySelectorAll(client, rootNodeId, selector));
|
|
1223
|
+
for (const nodeId of nodeIds) {
|
|
1224
|
+
if (seen.has(nodeId)) continue;
|
|
1225
|
+
seen.add(nodeId);
|
|
1226
|
+
matched = true;
|
|
1227
|
+
try {
|
|
1228
|
+
const box = await clickNodeCenter(client, nodeId, {
|
|
1229
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
1230
|
+
scrollIntoView
|
|
1231
|
+
});
|
|
1232
|
+
await sleep(250);
|
|
1233
|
+
return {
|
|
1234
|
+
clicked: true,
|
|
1235
|
+
selector,
|
|
1236
|
+
node_id: nodeId,
|
|
1237
|
+
box,
|
|
1238
|
+
skipped_errors: errors
|
|
1239
|
+
};
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
errors.push({
|
|
1242
|
+
selector,
|
|
1243
|
+
node_id: nodeId,
|
|
1244
|
+
error: error?.message || String(error)
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (!matched) {
|
|
1250
|
+
if (optional) return { clicked: false, reason: "not_found" };
|
|
1251
|
+
throw new Error(`Recruit search node was not found for selectors: ${selectors.join(", ")}`);
|
|
1252
|
+
}
|
|
1253
|
+
if (optional) {
|
|
1254
|
+
return {
|
|
1255
|
+
clicked: false,
|
|
1256
|
+
reason: "not_clickable",
|
|
1257
|
+
errors
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
const detail = errors.map((item) => `${item.selector}#${item.node_id}: ${item.error}`).join("; ");
|
|
1261
|
+
throw new Error(`Recruit search nodes were found but none were clickable for selectors: ${selectors.join(", ")}; ${detail}`);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
async function dismissRecruitSearchOverlays(client, settleMs = 250) {
|
|
1265
|
+
if (typeof client?.Input?.dispatchKeyEvent !== "function") {
|
|
1266
|
+
return {
|
|
1267
|
+
method: "Escape",
|
|
1268
|
+
skipped: true,
|
|
1269
|
+
reason: "dispatch_key_unavailable"
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
await pressKey(client, "Escape", {
|
|
1273
|
+
code: "Escape",
|
|
1274
|
+
windowsVirtualKeyCode: 27,
|
|
1275
|
+
nativeVirtualKeyCode: 27
|
|
1276
|
+
});
|
|
1277
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
1278
|
+
return {
|
|
1279
|
+
method: "Escape",
|
|
1280
|
+
settle_ms: settleMs
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
export async function getRecruitSearchCounts(client, frameNodeId) {
|
|
1285
|
+
return countSelectors(client, frameNodeId, {
|
|
1286
|
+
keyword_input: RECRUIT_SEARCH_SELECTORS.keywordInput.join(", "),
|
|
1287
|
+
search_button: RECRUIT_SEARCH_SELECTORS.searchButton.join(", "),
|
|
1288
|
+
degree_option: RECRUIT_SEARCH_SELECTORS.degreeOption.join(", "),
|
|
1289
|
+
school_item: RECRUIT_SEARCH_SELECTORS.schoolItem.join(", "),
|
|
1290
|
+
experience_option: RECRUIT_SEARCH_SELECTORS.experienceOption.join(", "),
|
|
1291
|
+
experience_custom: RECRUIT_SEARCH_SELECTORS.experienceCustom.join(", "),
|
|
1292
|
+
gender_dropdown: RECRUIT_SEARCH_SELECTORS.genderDropdown.join(", "),
|
|
1293
|
+
age_option: RECRUIT_SEARCH_SELECTORS.ageOption.join(", "),
|
|
1294
|
+
age_custom: RECRUIT_SEARCH_SELECTORS.ageCustom.join(", "),
|
|
1295
|
+
recent_viewed_label: RECRUIT_SEARCH_SELECTORS.recentViewedLabel.join(", "),
|
|
1296
|
+
candidate_card: RECRUIT_CARD_SELECTOR,
|
|
1297
|
+
no_data: RECRUIT_NO_DATA_SELECTORS.join(", ")
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
export async function waitForRecruitSearchControls(client, {
|
|
1302
|
+
timeoutMs = DEFAULT_RECRUIT_SEARCH_TIMEOUT_MS,
|
|
1303
|
+
intervalMs = 300
|
|
1304
|
+
} = {}) {
|
|
1305
|
+
const started = Date.now();
|
|
1306
|
+
let lastState = null;
|
|
1307
|
+
while (Date.now() - started <= timeoutMs) {
|
|
1308
|
+
const roots = await getRecruitRoots(client, { requireFrame: false });
|
|
1309
|
+
const frameNodeId = roots.iframe?.documentNodeId;
|
|
1310
|
+
if (frameNodeId) {
|
|
1311
|
+
const counts = await getRecruitSearchCounts(client, frameNodeId);
|
|
1312
|
+
lastState = {
|
|
1313
|
+
ok: counts.keyword_input > 0 && counts.search_button > 0,
|
|
1314
|
+
elapsed_ms: Date.now() - started,
|
|
1315
|
+
iframe_selector: roots.iframe.selector,
|
|
1316
|
+
iframe_document_node_id: frameNodeId,
|
|
1317
|
+
counts
|
|
1318
|
+
};
|
|
1319
|
+
if (lastState.ok) return lastState;
|
|
1320
|
+
}
|
|
1321
|
+
await sleep(intervalMs);
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
ok: false,
|
|
1325
|
+
elapsed_ms: Date.now() - started,
|
|
1326
|
+
...(lastState || {})
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
async function settleRecruitSearchAfterReset(client, {
|
|
1331
|
+
timeoutMs = DEFAULT_RECRUIT_RESET_TIMEOUT_MS,
|
|
1332
|
+
settleMs = 5000
|
|
1333
|
+
} = {}) {
|
|
1334
|
+
return waitForMiniFreshStartSettle(client, {
|
|
1335
|
+
domain: "search",
|
|
1336
|
+
timeoutMs,
|
|
1337
|
+
intervalMs: 500,
|
|
1338
|
+
settleMs: Math.max(0, Math.min(settleMs || 0, 5000)),
|
|
1339
|
+
readinessLabel: "search_controls_ready",
|
|
1340
|
+
checkReady: ({ remainingMs }) => waitForRecruitSearchControls(client, {
|
|
1341
|
+
timeoutMs: Math.min(Math.max(1, remainingMs), 1500),
|
|
1342
|
+
intervalMs: 300
|
|
1343
|
+
})
|
|
1344
|
+
});
|
|
424
1345
|
}
|
|
425
1346
|
|
|
426
1347
|
export async function resetRecruitSearchPage(client, {
|
|
@@ -534,15 +1455,102 @@ export async function setRecruitKeyword(client, frameNodeId, keyword) {
|
|
|
534
1455
|
if (!normalizedKeyword) {
|
|
535
1456
|
return { applied: false, reason: "empty_keyword" };
|
|
536
1457
|
}
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1458
|
+
const attempts = [];
|
|
1459
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
1460
|
+
const input = await clickFirstNodeBySelectors(client, frameNodeId, RECRUIT_SEARCH_SELECTORS.keywordInput);
|
|
1461
|
+
await clearFocusedInput(client);
|
|
1462
|
+
await sleep(120);
|
|
1463
|
+
const textEntry = await insertText(client, normalizedKeyword);
|
|
1464
|
+
await sleep(350);
|
|
1465
|
+
const verification = await verifyRecruitKeywordInputValue(client, frameNodeId, normalizedKeyword, {
|
|
1466
|
+
settleMs: 350
|
|
1467
|
+
});
|
|
1468
|
+
attempts.push({
|
|
1469
|
+
attempt,
|
|
1470
|
+
input,
|
|
1471
|
+
text_entry: textEntry,
|
|
1472
|
+
verification
|
|
1473
|
+
});
|
|
1474
|
+
if (verification.verified !== false) {
|
|
1475
|
+
return {
|
|
1476
|
+
applied: true,
|
|
1477
|
+
keyword: normalizedKeyword,
|
|
1478
|
+
input,
|
|
1479
|
+
text_entry: textEntry,
|
|
1480
|
+
verification,
|
|
1481
|
+
attempts
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const last = attempts[attempts.length - 1]?.verification || {};
|
|
1487
|
+
throw new Error(`Recruit keyword input did not hold requested value: expected=${normalizedKeyword}; actual=${last.actual || "unknown"}`);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
export async function readRecruitKeywordInputValue(client, frameNodeId) {
|
|
1491
|
+
if (typeof client?.Accessibility?.getPartialAXTree !== "function") {
|
|
1492
|
+
return {
|
|
1493
|
+
available: false,
|
|
1494
|
+
reason: "accessibility_unavailable",
|
|
1495
|
+
value: null,
|
|
1496
|
+
normalized_value: ""
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
for (const selector of RECRUIT_SEARCH_SELECTORS.keywordInput) {
|
|
1500
|
+
const nodeIds = uniqueNodeIds(await querySelectorAll(client, frameNodeId, selector));
|
|
1501
|
+
for (const nodeId of nodeIds) {
|
|
1502
|
+
const ax = await client.Accessibility.getPartialAXTree({
|
|
1503
|
+
nodeId,
|
|
1504
|
+
fetchRelatives: false
|
|
1505
|
+
});
|
|
1506
|
+
const node = ax?.nodes?.[0] || null;
|
|
1507
|
+
if (!node) continue;
|
|
1508
|
+
const value = typeof node?.value?.value === "string" ? node.value.value : "";
|
|
1509
|
+
return {
|
|
1510
|
+
available: true,
|
|
1511
|
+
selector,
|
|
1512
|
+
node_id: nodeId,
|
|
1513
|
+
value,
|
|
1514
|
+
normalized_value: normalizeText(value),
|
|
1515
|
+
role: node?.role?.value || ""
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
542
1519
|
return {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
1520
|
+
available: false,
|
|
1521
|
+
reason: "keyword_input_not_found",
|
|
1522
|
+
value: null,
|
|
1523
|
+
normalized_value: ""
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
export async function verifyRecruitKeywordInputValue(client, frameNodeId, expectedKeyword, {
|
|
1528
|
+
settleMs = 0
|
|
1529
|
+
} = {}) {
|
|
1530
|
+
const expected = normalizeText(expectedKeyword);
|
|
1531
|
+
const before = await readRecruitKeywordInputValue(client, frameNodeId);
|
|
1532
|
+
if (!before.available || !expected) {
|
|
1533
|
+
return {
|
|
1534
|
+
verified: null,
|
|
1535
|
+
reason: before.reason || "empty_expected_keyword",
|
|
1536
|
+
expected,
|
|
1537
|
+
actual: before.normalized_value || "",
|
|
1538
|
+
before,
|
|
1539
|
+
after: before
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
let after = before;
|
|
1543
|
+
if (settleMs > 0) {
|
|
1544
|
+
await sleep(settleMs);
|
|
1545
|
+
after = await readRecruitKeywordInputValue(client, frameNodeId);
|
|
1546
|
+
}
|
|
1547
|
+
const actual = normalizeText(after.normalized_value || after.value);
|
|
1548
|
+
return {
|
|
1549
|
+
verified: actual === expected,
|
|
1550
|
+
expected,
|
|
1551
|
+
actual,
|
|
1552
|
+
before,
|
|
1553
|
+
after
|
|
546
1554
|
};
|
|
547
1555
|
}
|
|
548
1556
|
|
|
@@ -559,198 +1567,801 @@ export async function setRecruitJobTitle(client, frameNodeId, jobTitle, {
|
|
|
559
1567
|
if (!normalizedJobTitle) {
|
|
560
1568
|
return { applied: false, reason: "empty_job_title" };
|
|
561
1569
|
}
|
|
562
|
-
const
|
|
1570
|
+
const opened = await openRecruitJobTitleDropdown(client, frameNodeId, {
|
|
1571
|
+
timeoutMs: Math.min(optionTimeoutMs, 30000)
|
|
1572
|
+
});
|
|
1573
|
+
const options = opened.options.length
|
|
1574
|
+
? opened.options
|
|
1575
|
+
: await listRecruitJobTitleOptions(client, frameNodeId);
|
|
1576
|
+
const terms = buildRecruitJobTitleSearchTerms(normalizedJobTitle);
|
|
1577
|
+
let match = null;
|
|
1578
|
+
let matchedTerm = "";
|
|
1579
|
+
const visibleOptions = options.filter((option) => option.visible);
|
|
1580
|
+
const hiddenMatches = [];
|
|
1581
|
+
for (const term of terms) {
|
|
1582
|
+
match = chooseRecruitTextCandidate(visibleOptions, { label: term, match: "contains" });
|
|
1583
|
+
if (match) {
|
|
1584
|
+
matchedTerm = term;
|
|
1585
|
+
break;
|
|
1586
|
+
}
|
|
1587
|
+
const hiddenMatch = chooseRecruitTextCandidate(
|
|
1588
|
+
options.filter((option) => !option.visible),
|
|
1589
|
+
{ label: term, match: "contains" }
|
|
1590
|
+
);
|
|
1591
|
+
if (hiddenMatch) hiddenMatches.push(hiddenMatch);
|
|
1592
|
+
}
|
|
1593
|
+
if (!match) {
|
|
1594
|
+
await closeRecruitJobTitleDropdown(client);
|
|
1595
|
+
if (hiddenMatches.length) {
|
|
1596
|
+
const error = new Error(`Matched recruit job has no visible clickable option: ${hiddenMatches[0].text}`);
|
|
1597
|
+
error.hidden_job_matches = hiddenMatches.map(compactRecruitTextCandidate);
|
|
1598
|
+
throw error;
|
|
1599
|
+
}
|
|
1600
|
+
throw new Error(`Recruit job title option was not found: ${normalizedJobTitle}`);
|
|
1601
|
+
}
|
|
1602
|
+
let box = null;
|
|
1603
|
+
if (!match.active) {
|
|
1604
|
+
if (!match.center) {
|
|
1605
|
+
await closeRecruitJobTitleDropdown(client);
|
|
1606
|
+
throw new Error(`Matched recruit job has no clickable center: ${match.text}`);
|
|
1607
|
+
}
|
|
1608
|
+
box = await clickNodeCenter(client, match.node_id, {
|
|
1609
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
1610
|
+
scrollIntoView: true
|
|
1611
|
+
});
|
|
1612
|
+
await sleep(500);
|
|
1613
|
+
}
|
|
1614
|
+
const stickyVerification = await verifyRecruitJobTitleSelection(client, frameNodeId, {
|
|
1615
|
+
jobTitle: normalizedJobTitle,
|
|
1616
|
+
delayMs: 1200,
|
|
1617
|
+
dropdownTimeoutMs: Math.min(optionTimeoutMs, 5000)
|
|
1618
|
+
});
|
|
1619
|
+
if (!stickyVerification.verified) {
|
|
1620
|
+
throw new Error(`Recruit job selection was not sticky after 1.2s: requested=${normalizedJobTitle}; current=${stickyVerification.current_label || "unknown"}`);
|
|
1621
|
+
}
|
|
1622
|
+
if (stickyVerification.menu_close && stickyVerification.menu_close.ok === false) {
|
|
1623
|
+
throw new Error(`Recruit job dropdown remained open after sticky verification: ${stickyVerification.menu_close.reason || "unknown"}`);
|
|
1624
|
+
}
|
|
1625
|
+
return {
|
|
1626
|
+
applied: true,
|
|
1627
|
+
requested_job: normalizedJobTitle,
|
|
1628
|
+
selected_label: match.text,
|
|
1629
|
+
matched_term: matchedTerm,
|
|
1630
|
+
search_terms: terms,
|
|
1631
|
+
selected_node_id: match.node_id,
|
|
1632
|
+
was_active: match.active,
|
|
1633
|
+
clicked: !match.active,
|
|
1634
|
+
box,
|
|
1635
|
+
opened_dropdown: {
|
|
1636
|
+
already_open: Boolean(opened.already_open),
|
|
1637
|
+
visible_option_count: visibleOptions.length,
|
|
1638
|
+
attempts: opened.attempts || []
|
|
1639
|
+
},
|
|
1640
|
+
sticky_verification: stickyVerification,
|
|
1641
|
+
discovered_options: options.map(compactRecruitTextCandidate).slice(0, 30)
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
export async function setRecruitDegree(client, frameNodeId, degree) {
|
|
1646
|
+
const degreeLabel = resolveRecruitDegreeLabel(degree);
|
|
1647
|
+
if (!degreeLabel || degreeLabel === "不限") {
|
|
1648
|
+
return { applied: false, reason: "unlimited_degree", degree: degreeLabel || "不限" };
|
|
1649
|
+
}
|
|
1650
|
+
const { candidate, candidates } = await findTextCandidate(
|
|
563
1651
|
client,
|
|
564
1652
|
frameNodeId,
|
|
565
|
-
RECRUIT_SEARCH_SELECTORS.
|
|
566
|
-
|
|
567
|
-
{
|
|
1653
|
+
RECRUIT_SEARCH_SELECTORS.degreeOption,
|
|
1654
|
+
degreeLabel,
|
|
1655
|
+
{ match: "prefix" }
|
|
568
1656
|
);
|
|
569
|
-
if (!
|
|
570
|
-
throw new Error(`Recruit
|
|
1657
|
+
if (!candidate) {
|
|
1658
|
+
throw new Error(`Recruit degree option was not found: ${degreeLabel}`);
|
|
571
1659
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
1660
|
+
const box = await clickNodeCenter(client, candidate.node_id, {
|
|
1661
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
1662
|
+
scrollIntoView: true
|
|
1663
|
+
});
|
|
1664
|
+
await sleep(350);
|
|
1665
|
+
return {
|
|
1666
|
+
applied: true,
|
|
1667
|
+
requested_degree: degree,
|
|
1668
|
+
selected_label: candidate.text,
|
|
1669
|
+
selected_node_id: candidate.node_id,
|
|
1670
|
+
was_active: candidate.active,
|
|
1671
|
+
box,
|
|
1672
|
+
discovered_options: candidates.map((item) => ({
|
|
1673
|
+
label: item.text,
|
|
1674
|
+
active: item.active,
|
|
1675
|
+
node_id: item.node_id,
|
|
1676
|
+
selector: item.selector
|
|
1677
|
+
}))
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
export async function setRecruitDegrees(client, frameNodeId, degrees = []) {
|
|
1682
|
+
const labels = normalizeRecruitDegreeLabels(degrees).filter((label) => label && label !== "不限");
|
|
1683
|
+
if (!labels.length) {
|
|
1684
|
+
return { applied: false, reason: "unlimited_degree", degrees: ["不限"], selected: [] };
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
const selected = [];
|
|
1688
|
+
let discoveredOptions = [];
|
|
1689
|
+
for (const label of labels) {
|
|
1690
|
+
const { candidate, candidates } = await findTextCandidate(
|
|
1691
|
+
client,
|
|
1692
|
+
frameNodeId,
|
|
1693
|
+
RECRUIT_SEARCH_SELECTORS.degreeOption,
|
|
1694
|
+
label,
|
|
1695
|
+
{ match: "prefix" }
|
|
1696
|
+
);
|
|
1697
|
+
discoveredOptions = candidates.map((item) => ({
|
|
1698
|
+
label: item.text,
|
|
1699
|
+
active: item.active,
|
|
1700
|
+
node_id: item.node_id,
|
|
1701
|
+
selector: item.selector
|
|
1702
|
+
}));
|
|
1703
|
+
if (!candidate) {
|
|
1704
|
+
throw new Error(`Recruit degree option was not found: ${label}`);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
let box = null;
|
|
1708
|
+
if (!candidate.active) {
|
|
1709
|
+
box = await clickNodeCenter(client, candidate.node_id, {
|
|
1710
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
1711
|
+
scrollIntoView: true
|
|
1712
|
+
});
|
|
1713
|
+
await sleep(350);
|
|
1714
|
+
}
|
|
1715
|
+
selected.push({
|
|
1716
|
+
requested_degree: label,
|
|
1717
|
+
selected_label: candidate.text,
|
|
1718
|
+
selected_node_id: candidate.node_id,
|
|
1719
|
+
was_active: candidate.active,
|
|
1720
|
+
clicked: !candidate.active,
|
|
1721
|
+
box
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
return {
|
|
1726
|
+
applied: true,
|
|
1727
|
+
requested_degrees: labels,
|
|
1728
|
+
selected,
|
|
1729
|
+
discovered_options: discoveredOptions
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
async function findClickableDescendant(client, nodeId, selectors) {
|
|
1734
|
+
for (const selector of selectors) {
|
|
1735
|
+
const childNodeId = await querySelector(client, nodeId, selector);
|
|
1736
|
+
if (childNodeId) return { node_id: childNodeId, selector };
|
|
1737
|
+
}
|
|
1738
|
+
return { node_id: nodeId, selector: null };
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
export async function setRecruitSchools(client, frameNodeId, schools = []) {
|
|
1742
|
+
const targets = normalizeRecruitSchoolList(schools);
|
|
1743
|
+
const applied = [];
|
|
1744
|
+
const missing = [];
|
|
1745
|
+
if (!targets.length) {
|
|
1746
|
+
return { applied: false, schools: [], selected: [], missing: [] };
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
for (const school of targets) {
|
|
1750
|
+
const candidates = await listTextCandidates(client, frameNodeId, RECRUIT_SEARCH_SELECTORS.schoolItem);
|
|
1751
|
+
const candidate = chooseRecruitSchoolCandidate(candidates, school);
|
|
1752
|
+
if (!candidate) {
|
|
1753
|
+
missing.push({
|
|
1754
|
+
school,
|
|
1755
|
+
exact_labels: buildRecruitSchoolSearchLabels(school),
|
|
1756
|
+
discovered: candidates.map((item) => item.text).slice(0, 20)
|
|
1757
|
+
});
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
const clickable = await findClickableDescendant(client, candidate.node_id, RECRUIT_SEARCH_SELECTORS.schoolClickable);
|
|
1762
|
+
let clickableActive = candidate.active;
|
|
1763
|
+
if (clickable.node_id !== candidate.node_id) {
|
|
1764
|
+
const clickableCandidate = await readTextCandidate(client, clickable.node_id, {
|
|
1765
|
+
selector: clickable.selector || "",
|
|
1766
|
+
index: 0
|
|
1767
|
+
});
|
|
1768
|
+
clickableActive = clickableActive || clickableCandidate.active;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
let box = null;
|
|
1772
|
+
if (!clickableActive) {
|
|
1773
|
+
box = await clickNodeCenter(client, clickable.node_id, {
|
|
1774
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
1775
|
+
scrollIntoView: true
|
|
1776
|
+
});
|
|
1777
|
+
await sleep(350);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
applied.push({
|
|
1781
|
+
school,
|
|
1782
|
+
exact_labels: buildRecruitSchoolSearchLabels(school),
|
|
1783
|
+
selected_label: candidate.text,
|
|
1784
|
+
selected_node_id: candidate.node_id,
|
|
1785
|
+
clickable_node_id: clickable.node_id,
|
|
1786
|
+
clickable_selector: clickable.selector,
|
|
1787
|
+
was_active: clickableActive,
|
|
1788
|
+
clicked: !clickableActive,
|
|
1789
|
+
box
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
if (missing.length) {
|
|
1794
|
+
throw new Error(`Recruit school options were not found: ${missing.map((item) => item.school).join(", ")}`);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
return {
|
|
1798
|
+
applied: true,
|
|
1799
|
+
schools: targets,
|
|
1800
|
+
selected: applied,
|
|
1801
|
+
missing
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
async function findFirstRecruitSearchNode(client, rootNodeId, selectors = []) {
|
|
1806
|
+
const errors = [];
|
|
1807
|
+
for (const selector of selectors) {
|
|
1808
|
+
const nodeIds = uniqueNodeIds(await querySelectorAll(client, rootNodeId, selector));
|
|
1809
|
+
for (const nodeId of nodeIds) {
|
|
1810
|
+
try {
|
|
1811
|
+
const box = await getNodeBox(client, nodeId);
|
|
1812
|
+
if (!isVisibleBox(box)) continue;
|
|
1813
|
+
return { node_id: nodeId, selector, box };
|
|
1814
|
+
} catch (error) {
|
|
1815
|
+
errors.push({
|
|
1816
|
+
selector,
|
|
1817
|
+
node_id: nodeId,
|
|
1818
|
+
error: error?.message || String(error)
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return { node_id: 0, selector: "", box: null, errors };
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function parseRecruitExperienceHiddenValue(rawValue) {
|
|
1827
|
+
const [startRaw, endRaw] = String(rawValue || "").split(",");
|
|
1828
|
+
const startValue = Number.parseInt(startRaw, 10);
|
|
1829
|
+
const endValue = Number.parseInt(endRaw, 10);
|
|
1830
|
+
return {
|
|
1831
|
+
raw_value: String(rawValue || ""),
|
|
1832
|
+
start_value: Number.isFinite(startValue) ? startValue : null,
|
|
1833
|
+
end_value: Number.isFinite(endValue) ? endValue : null
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
async function readRecruitExperienceCustomState(client, frameNodeId) {
|
|
1838
|
+
let hidden = null;
|
|
1839
|
+
for (const selector of RECRUIT_SEARCH_SELECTORS.experienceCustomHiddenInput) {
|
|
1840
|
+
const nodeId = await querySelector(client, frameNodeId, selector);
|
|
1841
|
+
if (!nodeId) continue;
|
|
1842
|
+
const attributes = await getAttributesMap(client, nodeId);
|
|
1843
|
+
hidden = {
|
|
1844
|
+
node_id: nodeId,
|
|
1845
|
+
selector,
|
|
1846
|
+
value: attributes.value || ""
|
|
1847
|
+
};
|
|
1848
|
+
break;
|
|
1849
|
+
}
|
|
1850
|
+
const parsedHidden = parseRecruitExperienceHiddenValue(hidden?.value || "");
|
|
1851
|
+
const handleNodes = [];
|
|
1852
|
+
for (const selector of RECRUIT_SEARCH_SELECTORS.experienceCustomSliderHandle) {
|
|
1853
|
+
const nodeIds = uniqueNodeIds(await querySelectorAll(client, frameNodeId, selector));
|
|
1854
|
+
for (const nodeId of nodeIds) {
|
|
1855
|
+
if (handleNodes.some((item) => item.node_id === nodeId)) continue;
|
|
1856
|
+
try {
|
|
1857
|
+
const box = await getNodeBox(client, nodeId);
|
|
1858
|
+
if (!isVisibleBox(box)) continue;
|
|
1859
|
+
handleNodes.push({ node_id: nodeId, selector, box });
|
|
1860
|
+
} catch {
|
|
1861
|
+
// Ignore invisible handles; missing handles are reported by the caller.
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
handleNodes.sort((left, right) => left.box.center.x - right.box.center.x);
|
|
1866
|
+
return {
|
|
1867
|
+
hidden,
|
|
1868
|
+
...parsedHidden,
|
|
1869
|
+
handles: handleNodes.map((item, index) => ({
|
|
1870
|
+
index,
|
|
1871
|
+
node_id: item.node_id,
|
|
1872
|
+
selector: item.selector,
|
|
1873
|
+
center: item.box.center,
|
|
1874
|
+
rect: item.box.rect
|
|
1875
|
+
}))
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
async function readRecruitExperienceFixedOptionState(client, frameNodeId) {
|
|
1880
|
+
const candidates = await listTextCandidates(client, frameNodeId, RECRUIT_SEARCH_SELECTORS.experienceOption);
|
|
1881
|
+
return {
|
|
1882
|
+
active_labels: candidates.filter((item) => item.active).map((item) => item.text),
|
|
1883
|
+
options: summarizeTextCandidates(candidates, 20)
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
async function dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
1888
|
+
handleIndex,
|
|
1889
|
+
targetValue
|
|
1890
|
+
}) {
|
|
1891
|
+
const state = await readRecruitExperienceCustomState(client, frameNodeId);
|
|
1892
|
+
const handle = state.handles[handleIndex];
|
|
1893
|
+
if (!handle) {
|
|
1894
|
+
throw new Error(`Recruit experience custom slider handle was not found: index=${handleIndex}`);
|
|
1895
|
+
}
|
|
1896
|
+
const slider = await findFirstRecruitSearchNode(
|
|
1897
|
+
client,
|
|
1898
|
+
frameNodeId,
|
|
1899
|
+
RECRUIT_SEARCH_SELECTORS.experienceCustomSlider
|
|
1900
|
+
);
|
|
1901
|
+
let trackRect = slider.box?.rect || null;
|
|
1902
|
+
if (!trackRect && state.handles.length >= 2 && state.start_value !== null && state.end_value !== null && state.end_value !== state.start_value) {
|
|
1903
|
+
const leftHandle = state.handles[0];
|
|
1904
|
+
const rightHandle = state.handles[state.handles.length - 1];
|
|
1905
|
+
const valueSpan = state.end_value - state.start_value;
|
|
1906
|
+
const fullValueSpan = EXPERIENCE_CUSTOM_MAX_VALUE - EXPERIENCE_CUSTOM_MIN_VALUE;
|
|
1907
|
+
const inferredWidth = Math.abs(rightHandle.center.x - leftHandle.center.x) * (fullValueSpan / valueSpan);
|
|
1908
|
+
const inferredX = leftHandle.center.x - inferredWidth * ((state.start_value - EXPERIENCE_CUSTOM_MIN_VALUE) / fullValueSpan);
|
|
1909
|
+
trackRect = {
|
|
1910
|
+
x: inferredX,
|
|
1911
|
+
y: Math.min(leftHandle.rect.y, rightHandle.rect.y),
|
|
1912
|
+
width: inferredWidth,
|
|
1913
|
+
height: Math.max(leftHandle.rect.height, rightHandle.rect.height)
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
if (!trackRect) {
|
|
1917
|
+
throw new Error("Recruit experience custom slider was not found");
|
|
1918
|
+
}
|
|
1919
|
+
const percent = (targetValue - EXPERIENCE_CUSTOM_MIN_VALUE)
|
|
1920
|
+
/ (EXPERIENCE_CUSTOM_MAX_VALUE - EXPERIENCE_CUSTOM_MIN_VALUE);
|
|
1921
|
+
let targetX = trackRect.x + trackRect.width * Math.min(1, Math.max(0, percent));
|
|
1922
|
+
const endpointOvershootPx = Math.min(24, Math.max(8, trackRect.width * 0.04));
|
|
1923
|
+
const valueStepPx = trackRect.width / (EXPERIENCE_CUSTOM_MAX_VALUE - EXPERIENCE_CUSTOM_MIN_VALUE);
|
|
1924
|
+
if (targetValue > EXPERIENCE_CUSTOM_MIN_VALUE && targetValue < EXPERIENCE_CUSTOM_MAX_VALUE) {
|
|
1925
|
+
targetX += valueStepPx * 0.45;
|
|
1926
|
+
}
|
|
1927
|
+
if (targetValue === EXPERIENCE_CUSTOM_MIN_VALUE) targetX -= endpointOvershootPx;
|
|
1928
|
+
if (targetValue === EXPERIENCE_CUSTOM_MAX_VALUE) targetX += endpointOvershootPx;
|
|
1929
|
+
const targetY = handle.center.y || (trackRect.y + trackRect.height / 2);
|
|
1930
|
+
const startX = handle.center.x;
|
|
1931
|
+
const startY = handle.center.y;
|
|
1932
|
+
const steps = 8;
|
|
1933
|
+
await client.Input.dispatchMouseEvent({ type: "mouseMoved", x: startX, y: startY, button: "none" });
|
|
1934
|
+
await client.Input.dispatchMouseEvent({ type: "mousePressed", x: startX, y: startY, button: "left", clickCount: 1 });
|
|
1935
|
+
for (let step = 1; step <= steps; step += 1) {
|
|
1936
|
+
const ratio = step / steps;
|
|
1937
|
+
await client.Input.dispatchMouseEvent({
|
|
1938
|
+
type: "mouseMoved",
|
|
1939
|
+
x: startX + (targetX - startX) * ratio,
|
|
1940
|
+
y: startY + (targetY - startY) * ratio,
|
|
1941
|
+
button: "left"
|
|
1942
|
+
});
|
|
1943
|
+
await sleep(30);
|
|
1944
|
+
}
|
|
1945
|
+
await client.Input.dispatchMouseEvent({ type: "mouseReleased", x: targetX, y: targetY, button: "left", clickCount: 1 });
|
|
1946
|
+
await sleep(350);
|
|
1947
|
+
return {
|
|
1948
|
+
handle_index: handleIndex,
|
|
1949
|
+
target_value: targetValue,
|
|
1950
|
+
target_label: EXPERIENCE_CUSTOM_LABELS_BY_VALUE.get(targetValue) || String(targetValue),
|
|
1951
|
+
slider_node_id: slider.node_id || null,
|
|
1952
|
+
handle_node_id: handle.node_id,
|
|
1953
|
+
start: { x: startX, y: startY },
|
|
1954
|
+
target: { x: targetX, y: targetY },
|
|
1955
|
+
track: trackRect,
|
|
1956
|
+
inferred_track: !slider.box
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
async function nudgeRecruitExperienceCustomSelection(client, frameNodeId, filter) {
|
|
1961
|
+
if (filter.end_value > filter.start_value) {
|
|
1962
|
+
const intermediate = filter.end_value === EXPERIENCE_CUSTOM_MAX_VALUE
|
|
1963
|
+
? filter.end_value - 1
|
|
1964
|
+
: filter.end_value + 1;
|
|
1965
|
+
return [
|
|
1966
|
+
await dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
1967
|
+
handleIndex: 1,
|
|
1968
|
+
targetValue: intermediate
|
|
1969
|
+
}),
|
|
1970
|
+
await dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
1971
|
+
handleIndex: 1,
|
|
1972
|
+
targetValue: filter.end_value
|
|
1973
|
+
})
|
|
1974
|
+
];
|
|
1975
|
+
}
|
|
1976
|
+
const intermediate = filter.start_value === EXPERIENCE_CUSTOM_MIN_VALUE
|
|
1977
|
+
? filter.start_value + 1
|
|
1978
|
+
: filter.start_value - 1;
|
|
1979
|
+
return [
|
|
1980
|
+
await dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
1981
|
+
handleIndex: 0,
|
|
1982
|
+
targetValue: intermediate
|
|
1983
|
+
}),
|
|
1984
|
+
await dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
1985
|
+
handleIndex: 0,
|
|
1986
|
+
targetValue: filter.start_value
|
|
1987
|
+
})
|
|
1988
|
+
];
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
export async function setRecruitExperience(client, frameNodeId, experience) {
|
|
1992
|
+
const filter = normalizeRecruitExperienceFilter(experience);
|
|
1993
|
+
if (!filter) {
|
|
1994
|
+
return { applied: false, reason: "not_requested" };
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
if (filter.mode === "option") {
|
|
1998
|
+
const { candidate, candidates } = await findTextCandidate(
|
|
1999
|
+
client,
|
|
2000
|
+
frameNodeId,
|
|
2001
|
+
RECRUIT_SEARCH_SELECTORS.experienceOption,
|
|
2002
|
+
filter.label,
|
|
2003
|
+
{ match: "exact" }
|
|
2004
|
+
);
|
|
2005
|
+
if (!candidate) {
|
|
2006
|
+
throw new Error(`Recruit experience option was not found: ${filter.label}`);
|
|
2007
|
+
}
|
|
2008
|
+
let box = null;
|
|
2009
|
+
if (!candidate.active) {
|
|
2010
|
+
box = await clickNodeCenter(client, candidate.node_id, {
|
|
2011
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
2012
|
+
scrollIntoView: true
|
|
2013
|
+
});
|
|
2014
|
+
await sleep(500);
|
|
2015
|
+
}
|
|
2016
|
+
return {
|
|
2017
|
+
applied: true,
|
|
2018
|
+
mode: "option",
|
|
2019
|
+
requested_experience: experience,
|
|
2020
|
+
selected_label: candidate.text,
|
|
2021
|
+
selected_node_id: candidate.node_id,
|
|
2022
|
+
was_active: candidate.active,
|
|
2023
|
+
clicked: !candidate.active,
|
|
2024
|
+
box,
|
|
2025
|
+
discovered_options: summarizeTextCandidates(candidates, 20)
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
const customClick = await clickFirstNodeBySelectors(
|
|
2030
|
+
client,
|
|
2031
|
+
frameNodeId,
|
|
2032
|
+
RECRUIT_SEARCH_SELECTORS.experienceCustom,
|
|
2033
|
+
{ optional: false, scrollIntoView: true }
|
|
2034
|
+
);
|
|
2035
|
+
const before = await readRecruitExperienceCustomState(client, frameNodeId);
|
|
2036
|
+
const fixedOptionsBefore = await readRecruitExperienceFixedOptionState(client, frameNodeId);
|
|
2037
|
+
const drags = [];
|
|
2038
|
+
if (before.start_value !== filter.start_value) {
|
|
2039
|
+
drags.push(await dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
2040
|
+
handleIndex: 0,
|
|
2041
|
+
targetValue: filter.start_value
|
|
2042
|
+
}));
|
|
2043
|
+
}
|
|
2044
|
+
const afterStart = await readRecruitExperienceCustomState(client, frameNodeId);
|
|
2045
|
+
if (afterStart.end_value !== filter.end_value) {
|
|
2046
|
+
drags.push(await dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
2047
|
+
handleIndex: 1,
|
|
2048
|
+
targetValue: filter.end_value
|
|
2049
|
+
}));
|
|
2050
|
+
}
|
|
2051
|
+
if (!drags.length && fixedOptionsBefore.active_labels.length) {
|
|
2052
|
+
drags.push(...await nudgeRecruitExperienceCustomSelection(client, frameNodeId, filter));
|
|
2053
|
+
}
|
|
2054
|
+
const after = await readRecruitExperienceCustomState(client, frameNodeId);
|
|
2055
|
+
const fixedOptionsAfter = await readRecruitExperienceFixedOptionState(client, frameNodeId);
|
|
2056
|
+
const verified = after.start_value === filter.start_value && after.end_value === filter.end_value;
|
|
2057
|
+
if (!verified) {
|
|
2058
|
+
throw new Error(
|
|
2059
|
+
`Recruit experience custom range did not stick: requested=${filter.start_value},${filter.end_value}; actual=${after.raw_value || "unknown"}`
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
if (fixedOptionsAfter.active_labels.length) {
|
|
2063
|
+
throw new Error(
|
|
2064
|
+
`Recruit experience custom range still has fixed option active: ${fixedOptionsAfter.active_labels.join(", ")}`
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
return {
|
|
2068
|
+
applied: true,
|
|
2069
|
+
mode: "custom",
|
|
2070
|
+
requested_experience: experience,
|
|
2071
|
+
requested_range: {
|
|
2072
|
+
start_label: filter.start_label,
|
|
2073
|
+
end_label: filter.end_label,
|
|
2074
|
+
start_value: filter.start_value,
|
|
2075
|
+
end_value: filter.end_value
|
|
2076
|
+
},
|
|
2077
|
+
custom_click: customClick,
|
|
2078
|
+
fixed_options_before: fixedOptionsBefore,
|
|
2079
|
+
before,
|
|
2080
|
+
after,
|
|
2081
|
+
fixed_options_after: fixedOptionsAfter,
|
|
2082
|
+
drags,
|
|
2083
|
+
verification: {
|
|
2084
|
+
verified,
|
|
2085
|
+
fixed_option_cleared: fixedOptionsAfter.active_labels.length === 0,
|
|
2086
|
+
expected: `${filter.start_value},${filter.end_value}`,
|
|
2087
|
+
actual: after.raw_value
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
async function findRecruitGenderDropdown(client, frameNodeId) {
|
|
2093
|
+
const candidates = await listTextCandidates(client, frameNodeId, RECRUIT_SEARCH_SELECTORS.genderDropdown, {
|
|
2094
|
+
includeBox: true
|
|
2095
|
+
});
|
|
2096
|
+
const visible = candidates
|
|
2097
|
+
.filter((item) => item.visible && item.rect)
|
|
2098
|
+
.sort((left, right) => left.rect.x - right.rect.x);
|
|
2099
|
+
return {
|
|
2100
|
+
candidate: visible[0] || null,
|
|
2101
|
+
candidates
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
async function readRecruitGenderState(client, frameNodeId) {
|
|
2106
|
+
const { candidate, candidates } = await findRecruitGenderDropdown(client, frameNodeId);
|
|
2107
|
+
if (!candidate) {
|
|
2108
|
+
return {
|
|
2109
|
+
available: false,
|
|
2110
|
+
discovered: summarizeTextCandidates(candidates, 10)
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
const hiddenNodeId = await querySelector(client, candidate.node_id, "input[type='hidden']");
|
|
2114
|
+
const hidden = hiddenNodeId ? await getAttributesMap(client, hiddenNodeId) : {};
|
|
2115
|
+
const hiddenSelectedLabel = hidden.value === "-1" ? "不限" : "";
|
|
2116
|
+
return {
|
|
2117
|
+
available: true,
|
|
2118
|
+
selected_label: hiddenSelectedLabel || normalizeText(candidate.text),
|
|
2119
|
+
selected_node_id: candidate.node_id,
|
|
2120
|
+
hidden_value: hidden.value || "",
|
|
2121
|
+
discovered: summarizeTextCandidates(candidates, 10)
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
export async function setRecruitGender(client, frameNodeId, gender) {
|
|
2126
|
+
const filter = normalizeRecruitGenderFilter(gender);
|
|
2127
|
+
if (!filter) {
|
|
2128
|
+
return { applied: false, reason: "not_requested" };
|
|
2129
|
+
}
|
|
2130
|
+
const beforeRoot = await findRecruitGenderDropdown(client, frameNodeId);
|
|
2131
|
+
if (!beforeRoot.candidate) {
|
|
2132
|
+
throw new Error("Recruit gender dropdown was not found");
|
|
2133
|
+
}
|
|
2134
|
+
const openBox = await clickNodeCenter(client, beforeRoot.candidate.node_id, {
|
|
2135
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
2136
|
+
scrollIntoView: true
|
|
2137
|
+
});
|
|
2138
|
+
await sleep(350);
|
|
2139
|
+
const rootAfterOpen = await findRecruitGenderDropdown(client, frameNodeId);
|
|
2140
|
+
const rootNodeId = rootAfterOpen.candidate?.node_id || beforeRoot.candidate.node_id;
|
|
2141
|
+
const options = await listTextCandidates(client, rootNodeId, ["li"], { includeBox: true });
|
|
2142
|
+
const option = chooseRecruitTextCandidate(options, { label: filter.label, match: "exact" });
|
|
2143
|
+
if (!option) {
|
|
2144
|
+
throw new Error(`Recruit gender option was not found: ${filter.label}`);
|
|
2145
|
+
}
|
|
2146
|
+
let selectBox = null;
|
|
2147
|
+
if (!option.active) {
|
|
2148
|
+
selectBox = await clickNodeCenter(client, option.node_id, {
|
|
575
2149
|
...DETERMINISTIC_CLICK_OPTIONS,
|
|
576
2150
|
scrollIntoView: true
|
|
577
2151
|
});
|
|
578
|
-
await sleep(
|
|
2152
|
+
await sleep(600);
|
|
2153
|
+
} else {
|
|
2154
|
+
await pressKey(client, "Escape", {
|
|
2155
|
+
code: "Escape",
|
|
2156
|
+
windowsVirtualKeyCode: 27,
|
|
2157
|
+
nativeVirtualKeyCode: 27
|
|
2158
|
+
});
|
|
2159
|
+
await sleep(250);
|
|
2160
|
+
}
|
|
2161
|
+
const after = await readRecruitGenderState(client, frameNodeId);
|
|
2162
|
+
const verified = after.selected_label === filter.label
|
|
2163
|
+
|| (filter.label === "不限" && /^(?:性别|不限)$/.test(after.selected_label));
|
|
2164
|
+
if (!verified) {
|
|
2165
|
+
throw new Error(`Recruit gender selection did not stick: requested=${filter.label}; actual=${after.selected_label || "unknown"}`);
|
|
579
2166
|
}
|
|
580
2167
|
return {
|
|
581
2168
|
applied: true,
|
|
582
|
-
|
|
583
|
-
selected_label:
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
2169
|
+
requested_gender: gender,
|
|
2170
|
+
selected_label: filter.label,
|
|
2171
|
+
opened_dropdown: {
|
|
2172
|
+
node_id: beforeRoot.candidate.node_id,
|
|
2173
|
+
box: openBox
|
|
2174
|
+
},
|
|
2175
|
+
selected_node_id: option.node_id,
|
|
2176
|
+
option_was_active: option.active,
|
|
2177
|
+
clicked: !option.active,
|
|
2178
|
+
box: selectBox,
|
|
2179
|
+
verification: {
|
|
2180
|
+
verified,
|
|
2181
|
+
selected_label: after.selected_label,
|
|
2182
|
+
hidden_value: after.hidden_value
|
|
2183
|
+
},
|
|
2184
|
+
discovered_options: summarizeTextCandidates(options, 10)
|
|
591
2185
|
};
|
|
592
2186
|
}
|
|
593
2187
|
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
2188
|
+
async function readRecruitAgeCustomState(client, frameNodeId) {
|
|
2189
|
+
const inputNodeIds = uniqueNodeIds(await querySelectorAll(
|
|
2190
|
+
client,
|
|
2191
|
+
frameNodeId,
|
|
2192
|
+
RECRUIT_SEARCH_SELECTORS.ageCustomInput.join(", ")
|
|
2193
|
+
));
|
|
2194
|
+
const inputs = [];
|
|
2195
|
+
for (const nodeId of inputNodeIds) {
|
|
2196
|
+
const attributes = await getAttributesMap(client, nodeId);
|
|
2197
|
+
let box = null;
|
|
2198
|
+
try {
|
|
2199
|
+
box = await getNodeBox(client, nodeId);
|
|
2200
|
+
} catch {}
|
|
2201
|
+
inputs.push({
|
|
2202
|
+
node_id: nodeId,
|
|
2203
|
+
type: attributes.type || "",
|
|
2204
|
+
value: attributes.value || "",
|
|
2205
|
+
placeholder: attributes.placeholder || "",
|
|
2206
|
+
visible: isVisibleBox(box),
|
|
2207
|
+
rect: box?.rect || null
|
|
2208
|
+
});
|
|
598
2209
|
}
|
|
599
|
-
const
|
|
2210
|
+
const hidden = inputs.filter((item) => item.type === "hidden");
|
|
2211
|
+
return {
|
|
2212
|
+
inputs,
|
|
2213
|
+
min: parseRecruitAgeCustomHiddenValue(hidden[0]?.value),
|
|
2214
|
+
max: parseRecruitAgeCustomHiddenValue(hidden[1]?.value),
|
|
2215
|
+
raw_values: hidden.map((item) => item.value)
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
async function readRecruitAgeFixedOptionState(client, frameNodeId) {
|
|
2220
|
+
const candidates = await listTextCandidates(client, frameNodeId, RECRUIT_SEARCH_SELECTORS.ageOption);
|
|
2221
|
+
return {
|
|
2222
|
+
active_labels: candidates.filter((item) => item.active).map((item) => item.text),
|
|
2223
|
+
options: summarizeTextCandidates(candidates, 20)
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
function ageCustomOptionLabel(value) {
|
|
2228
|
+
if (value === null || value === undefined) return "不限";
|
|
2229
|
+
return `${value}岁`;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
function parseRecruitAgeCustomHiddenValue(value) {
|
|
2233
|
+
const text = normalizeText(value);
|
|
2234
|
+
if (!text || text === "0" || text === "-1") return null;
|
|
2235
|
+
return parseAgeNumber(text, null);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
async function selectRecruitAgeCustomDropdownValue(client, frameNodeId, {
|
|
2239
|
+
dropdownIndex,
|
|
2240
|
+
value
|
|
2241
|
+
}) {
|
|
2242
|
+
const dropdownNodeIds = uniqueNodeIds(await querySelectorAll(
|
|
600
2243
|
client,
|
|
601
2244
|
frameNodeId,
|
|
602
|
-
RECRUIT_SEARCH_SELECTORS.
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
)
|
|
606
|
-
|
|
607
|
-
throw new Error(`Recruit degree option was not found: ${degreeLabel}`);
|
|
2245
|
+
RECRUIT_SEARCH_SELECTORS.ageCustomDropdown.join(", ")
|
|
2246
|
+
));
|
|
2247
|
+
const dropdownNodeId = dropdownNodeIds[dropdownIndex];
|
|
2248
|
+
if (!dropdownNodeId) {
|
|
2249
|
+
throw new Error(`Recruit age custom dropdown was not found: index=${dropdownIndex}`);
|
|
608
2250
|
}
|
|
609
|
-
const
|
|
2251
|
+
const openBox = await clickNodeCenter(client, dropdownNodeId, {
|
|
610
2252
|
...DETERMINISTIC_CLICK_OPTIONS,
|
|
611
2253
|
scrollIntoView: true
|
|
612
2254
|
});
|
|
613
2255
|
await sleep(350);
|
|
2256
|
+
const label = ageCustomOptionLabel(value);
|
|
2257
|
+
const options = await listTextCandidates(client, frameNodeId, RECRUIT_SEARCH_SELECTORS.ageCustomOption, {
|
|
2258
|
+
includeBox: true
|
|
2259
|
+
});
|
|
2260
|
+
const option = chooseRecruitTextCandidate(options, { label, match: "exact" });
|
|
2261
|
+
if (!option) {
|
|
2262
|
+
throw new Error(`Recruit age custom option was not found: ${label}`);
|
|
2263
|
+
}
|
|
2264
|
+
const box = await clickNodeCenter(client, option.node_id, {
|
|
2265
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
2266
|
+
scrollIntoView: true
|
|
2267
|
+
});
|
|
2268
|
+
await sleep(600);
|
|
614
2269
|
return {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
selected_label:
|
|
618
|
-
|
|
619
|
-
|
|
2270
|
+
dropdown_index: dropdownIndex,
|
|
2271
|
+
requested_value: value,
|
|
2272
|
+
selected_label: option.text,
|
|
2273
|
+
dropdown_node_id: dropdownNodeId,
|
|
2274
|
+
option_node_id: option.node_id,
|
|
2275
|
+
open_box: openBox,
|
|
620
2276
|
box,
|
|
621
|
-
discovered_options:
|
|
622
|
-
label: item.text,
|
|
623
|
-
active: item.active,
|
|
624
|
-
node_id: item.node_id,
|
|
625
|
-
selector: item.selector
|
|
626
|
-
}))
|
|
2277
|
+
discovered_options: summarizeTextCandidates(options, 40)
|
|
627
2278
|
};
|
|
628
2279
|
}
|
|
629
2280
|
|
|
630
|
-
export async function
|
|
631
|
-
const
|
|
632
|
-
if (!
|
|
633
|
-
return { applied: false, reason: "
|
|
2281
|
+
export async function setRecruitAge(client, frameNodeId, age) {
|
|
2282
|
+
const filter = normalizeRecruitAgeFilter(age);
|
|
2283
|
+
if (!filter) {
|
|
2284
|
+
return { applied: false, reason: "not_requested" };
|
|
634
2285
|
}
|
|
635
2286
|
|
|
636
|
-
|
|
637
|
-
let discoveredOptions = [];
|
|
638
|
-
for (const label of labels) {
|
|
2287
|
+
if (filter.mode === "option") {
|
|
639
2288
|
const { candidate, candidates } = await findTextCandidate(
|
|
640
2289
|
client,
|
|
641
2290
|
frameNodeId,
|
|
642
|
-
RECRUIT_SEARCH_SELECTORS.
|
|
643
|
-
label,
|
|
644
|
-
{ match: "
|
|
2291
|
+
RECRUIT_SEARCH_SELECTORS.ageOption,
|
|
2292
|
+
filter.label,
|
|
2293
|
+
{ match: "exact" }
|
|
645
2294
|
);
|
|
646
|
-
discoveredOptions = candidates.map((item) => ({
|
|
647
|
-
label: item.text,
|
|
648
|
-
active: item.active,
|
|
649
|
-
node_id: item.node_id,
|
|
650
|
-
selector: item.selector
|
|
651
|
-
}));
|
|
652
2295
|
if (!candidate) {
|
|
653
|
-
throw new Error(`Recruit
|
|
2296
|
+
throw new Error(`Recruit age option was not found: ${filter.label}`);
|
|
654
2297
|
}
|
|
655
|
-
|
|
656
2298
|
let box = null;
|
|
657
2299
|
if (!candidate.active) {
|
|
658
2300
|
box = await clickNodeCenter(client, candidate.node_id, {
|
|
659
2301
|
...DETERMINISTIC_CLICK_OPTIONS,
|
|
660
2302
|
scrollIntoView: true
|
|
661
2303
|
});
|
|
662
|
-
await sleep(
|
|
2304
|
+
await sleep(500);
|
|
663
2305
|
}
|
|
664
|
-
|
|
665
|
-
|
|
2306
|
+
return {
|
|
2307
|
+
applied: true,
|
|
2308
|
+
mode: "option",
|
|
2309
|
+
requested_age: age,
|
|
666
2310
|
selected_label: candidate.text,
|
|
667
2311
|
selected_node_id: candidate.node_id,
|
|
668
2312
|
was_active: candidate.active,
|
|
669
2313
|
clicked: !candidate.active,
|
|
670
|
-
box
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
return {
|
|
675
|
-
applied: true,
|
|
676
|
-
requested_degrees: labels,
|
|
677
|
-
selected,
|
|
678
|
-
discovered_options: discoveredOptions
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
async function findClickableDescendant(client, nodeId, selectors) {
|
|
683
|
-
for (const selector of selectors) {
|
|
684
|
-
const childNodeId = await querySelector(client, nodeId, selector);
|
|
685
|
-
if (childNodeId) return { node_id: childNodeId, selector };
|
|
686
|
-
}
|
|
687
|
-
return { node_id: nodeId, selector: null };
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
export async function setRecruitSchools(client, frameNodeId, schools = []) {
|
|
691
|
-
const targets = schools.map(normalizeText).filter(Boolean);
|
|
692
|
-
const applied = [];
|
|
693
|
-
const missing = [];
|
|
694
|
-
if (!targets.length) {
|
|
695
|
-
return { applied: false, schools: [], selected: [], missing: [] };
|
|
2314
|
+
box,
|
|
2315
|
+
discovered_options: summarizeTextCandidates(candidates, 20)
|
|
2316
|
+
};
|
|
696
2317
|
}
|
|
697
2318
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
});
|
|
721
|
-
clickableActive = clickableActive || clickableCandidate.active;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
let box = null;
|
|
725
|
-
if (!clickableActive) {
|
|
726
|
-
box = await clickNodeCenter(client, clickable.node_id, {
|
|
727
|
-
...DETERMINISTIC_CLICK_OPTIONS,
|
|
728
|
-
scrollIntoView: true
|
|
729
|
-
});
|
|
730
|
-
await sleep(350);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
applied.push({
|
|
734
|
-
school,
|
|
735
|
-
selected_label: candidate.text,
|
|
736
|
-
selected_node_id: candidate.node_id,
|
|
737
|
-
clickable_node_id: clickable.node_id,
|
|
738
|
-
clickable_selector: clickable.selector,
|
|
739
|
-
was_active: clickableActive,
|
|
740
|
-
clicked: !clickableActive,
|
|
741
|
-
box
|
|
742
|
-
});
|
|
2319
|
+
const customClick = await clickFirstNodeBySelectors(
|
|
2320
|
+
client,
|
|
2321
|
+
frameNodeId,
|
|
2322
|
+
RECRUIT_SEARCH_SELECTORS.ageCustom,
|
|
2323
|
+
{ optional: false, scrollIntoView: true }
|
|
2324
|
+
);
|
|
2325
|
+
const before = await readRecruitAgeCustomState(client, frameNodeId);
|
|
2326
|
+
const fixedBefore = await readRecruitAgeFixedOptionState(client, frameNodeId);
|
|
2327
|
+
const selected = [];
|
|
2328
|
+
selected.push(await selectRecruitAgeCustomDropdownValue(client, frameNodeId, {
|
|
2329
|
+
dropdownIndex: 0,
|
|
2330
|
+
value: filter.min
|
|
2331
|
+
}));
|
|
2332
|
+
selected.push(await selectRecruitAgeCustomDropdownValue(client, frameNodeId, {
|
|
2333
|
+
dropdownIndex: 1,
|
|
2334
|
+
value: filter.max
|
|
2335
|
+
}));
|
|
2336
|
+
const after = await readRecruitAgeCustomState(client, frameNodeId);
|
|
2337
|
+
const fixedAfter = await readRecruitAgeFixedOptionState(client, frameNodeId);
|
|
2338
|
+
const verified = after.min === filter.min && after.max === filter.max;
|
|
2339
|
+
if (!verified) {
|
|
2340
|
+
throw new Error(`Recruit age custom values did not stick: requested=${filter.min},${filter.max}; actual=${after.raw_values.join(",")}`);
|
|
743
2341
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
throw new Error(`Recruit school options were not found: ${missing.map((item) => item.school).join(", ")}`);
|
|
2342
|
+
if (fixedAfter.active_labels.length) {
|
|
2343
|
+
throw new Error(`Recruit age custom still has fixed option active: ${fixedAfter.active_labels.join(", ")}`);
|
|
747
2344
|
}
|
|
748
|
-
|
|
749
2345
|
return {
|
|
750
2346
|
applied: true,
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
2347
|
+
mode: "custom",
|
|
2348
|
+
requested_age: age,
|
|
2349
|
+
requested_range: {
|
|
2350
|
+
min: filter.min,
|
|
2351
|
+
max: filter.max
|
|
2352
|
+
},
|
|
2353
|
+
custom_click: customClick,
|
|
2354
|
+
before,
|
|
2355
|
+
after,
|
|
2356
|
+
fixed_options_before: fixedBefore,
|
|
2357
|
+
fixed_options_after: fixedAfter,
|
|
2358
|
+
selected,
|
|
2359
|
+
verification: {
|
|
2360
|
+
verified,
|
|
2361
|
+
fixed_option_cleared: fixedAfter.active_labels.length === 0,
|
|
2362
|
+
expected: [filter.min, filter.max],
|
|
2363
|
+
actual: [after.min, after.max]
|
|
2364
|
+
}
|
|
754
2365
|
};
|
|
755
2366
|
}
|
|
756
2367
|
|
|
@@ -795,12 +2406,50 @@ export async function setRecruitRecentViewedFilter(client, frameNodeId, enabled)
|
|
|
795
2406
|
};
|
|
796
2407
|
}
|
|
797
2408
|
|
|
2409
|
+
async function openRecruitCityPicker(client, frameNodeId, {
|
|
2410
|
+
settleMs = 350
|
|
2411
|
+
} = {}) {
|
|
2412
|
+
const alreadyOpenInput = await clickFirstNodeBySelectors(
|
|
2413
|
+
client,
|
|
2414
|
+
frameNodeId,
|
|
2415
|
+
RECRUIT_SEARCH_SELECTORS.cityInput,
|
|
2416
|
+
{ optional: true }
|
|
2417
|
+
);
|
|
2418
|
+
if (alreadyOpenInput.clicked) {
|
|
2419
|
+
return {
|
|
2420
|
+
opened: true,
|
|
2421
|
+
already_open: true,
|
|
2422
|
+
input: alreadyOpenInput,
|
|
2423
|
+
trigger: null
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
const trigger = await clickFirstNodeBySelectors(
|
|
2428
|
+
client,
|
|
2429
|
+
frameNodeId,
|
|
2430
|
+
RECRUIT_SEARCH_SELECTORS.cityTrigger,
|
|
2431
|
+
{ scrollIntoView: false }
|
|
2432
|
+
);
|
|
2433
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
2434
|
+
const input = await clickFirstNodeBySelectors(
|
|
2435
|
+
client,
|
|
2436
|
+
frameNodeId,
|
|
2437
|
+
RECRUIT_SEARCH_SELECTORS.cityInput
|
|
2438
|
+
);
|
|
2439
|
+
return {
|
|
2440
|
+
opened: true,
|
|
2441
|
+
already_open: false,
|
|
2442
|
+
trigger,
|
|
2443
|
+
input
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
|
|
798
2447
|
async function selectRecruitNationalCityThroughPicker(client, frameNodeId, {
|
|
799
2448
|
requestedCity = "全国",
|
|
800
2449
|
reason = "national_city_requested",
|
|
801
2450
|
optionTimeoutMs = DEFAULT_RECRUIT_CITY_OPTION_TIMEOUT_MS
|
|
802
2451
|
} = {}) {
|
|
803
|
-
const
|
|
2452
|
+
const picker = await openRecruitCityPicker(client, frameNodeId);
|
|
804
2453
|
await clearFocusedInput(client);
|
|
805
2454
|
await sleep(500);
|
|
806
2455
|
|
|
@@ -854,7 +2503,8 @@ async function selectRecruitNationalCityThroughPicker(client, frameNodeId, {
|
|
|
854
2503
|
applied: false,
|
|
855
2504
|
reason: "national_city_popular_not_found",
|
|
856
2505
|
requested_city: requestedCity,
|
|
857
|
-
input,
|
|
2506
|
+
input: picker.input,
|
|
2507
|
+
picker,
|
|
858
2508
|
path,
|
|
859
2509
|
discovered_options: summarizeTextCandidates(popularLookup.candidates)
|
|
860
2510
|
};
|
|
@@ -892,7 +2542,8 @@ async function selectRecruitNationalCityThroughPicker(client, frameNodeId, {
|
|
|
892
2542
|
applied: false,
|
|
893
2543
|
reason: "national_city_option_not_found",
|
|
894
2544
|
requested_city: requestedCity,
|
|
895
|
-
input,
|
|
2545
|
+
input: picker.input,
|
|
2546
|
+
picker,
|
|
896
2547
|
path,
|
|
897
2548
|
discovered_options: summarizeTextCandidates(nationalLookup.candidates)
|
|
898
2549
|
};
|
|
@@ -917,7 +2568,8 @@ async function selectRecruitNationalCityThroughPicker(client, frameNodeId, {
|
|
|
917
2568
|
requested_city: requestedCity,
|
|
918
2569
|
selected_label: nationalLookup.candidate.text,
|
|
919
2570
|
selected_node_id: nationalLookup.candidate.node_id,
|
|
920
|
-
input,
|
|
2571
|
+
input: picker.input,
|
|
2572
|
+
picker,
|
|
921
2573
|
path,
|
|
922
2574
|
box: nationalBox,
|
|
923
2575
|
selection_mode: "city_picker",
|
|
@@ -972,7 +2624,7 @@ export async function setRecruitCity(client, frameNodeId, city, {
|
|
|
972
2624
|
});
|
|
973
2625
|
}
|
|
974
2626
|
|
|
975
|
-
const
|
|
2627
|
+
const picker = await openRecruitCityPicker(client, frameNodeId);
|
|
976
2628
|
await clearFocusedInput(client);
|
|
977
2629
|
await sleep(120);
|
|
978
2630
|
await insertText(client, normalizedCity);
|
|
@@ -1016,7 +2668,8 @@ export async function setRecruitCity(client, frameNodeId, city, {
|
|
|
1016
2668
|
requested_city: normalizedCity,
|
|
1017
2669
|
requested_city_not_found: true,
|
|
1018
2670
|
fallback_to_national: true,
|
|
1019
|
-
original_input: input,
|
|
2671
|
+
original_input: picker.input,
|
|
2672
|
+
picker,
|
|
1020
2673
|
elapsed_ms: Date.now() - started,
|
|
1021
2674
|
discovered_options_before_fallback: candidates.map((item) => item.text).slice(0, 20)
|
|
1022
2675
|
};
|
|
@@ -1034,7 +2687,8 @@ export async function setRecruitCity(client, frameNodeId, city, {
|
|
|
1034
2687
|
requested_city: normalizedCity,
|
|
1035
2688
|
requested_city_not_found: true,
|
|
1036
2689
|
fallback_to_national: true,
|
|
1037
|
-
original_input: input,
|
|
2690
|
+
original_input: picker.input,
|
|
2691
|
+
picker,
|
|
1038
2692
|
picker_fallback: nationalFallback,
|
|
1039
2693
|
elapsed_ms: Date.now() - started,
|
|
1040
2694
|
discovered_options_before_fallback: candidates.map((item) => item.text).slice(0, 20)
|
|
@@ -1045,7 +2699,8 @@ export async function setRecruitCity(client, frameNodeId, city, {
|
|
|
1045
2699
|
applied: false,
|
|
1046
2700
|
reason: "city_result_not_found",
|
|
1047
2701
|
city: normalizedCity,
|
|
1048
|
-
input,
|
|
2702
|
+
input: picker.input,
|
|
2703
|
+
picker,
|
|
1049
2704
|
elapsed_ms: Date.now() - started,
|
|
1050
2705
|
discovered_options: candidates.map((item) => item.text).slice(0, 20),
|
|
1051
2706
|
national_fallback: nationalFallback,
|
|
@@ -1063,7 +2718,8 @@ export async function setRecruitCity(client, frameNodeId, city, {
|
|
|
1063
2718
|
city: normalizedCity,
|
|
1064
2719
|
selected_label: candidate.text,
|
|
1065
2720
|
selected_node_id: candidate.node_id,
|
|
1066
|
-
input,
|
|
2721
|
+
input: picker.input,
|
|
2722
|
+
picker,
|
|
1067
2723
|
elapsed_ms: Date.now() - started,
|
|
1068
2724
|
box
|
|
1069
2725
|
};
|
|
@@ -1095,6 +2751,86 @@ export async function clickRecruitSearch(client, frameNodeId) {
|
|
|
1095
2751
|
};
|
|
1096
2752
|
}
|
|
1097
2753
|
|
|
2754
|
+
export async function clickRecruitSearchWithKeywordGuard(client, frameNodeId, keyword, {
|
|
2755
|
+
maxAttempts = 2,
|
|
2756
|
+
postSearchSettleMs = 2200
|
|
2757
|
+
} = {}) {
|
|
2758
|
+
const normalizedKeyword = normalizeText(keyword);
|
|
2759
|
+
if (!normalizedKeyword) {
|
|
2760
|
+
return clickRecruitSearch(client, frameNodeId);
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
const attempts = [];
|
|
2764
|
+
let currentFrameNodeId = frameNodeId;
|
|
2765
|
+
for (let attempt = 1; attempt <= Math.max(1, maxAttempts); attempt += 1) {
|
|
2766
|
+
let rootsBeforeAttempt = null;
|
|
2767
|
+
try {
|
|
2768
|
+
rootsBeforeAttempt = await getRecruitRoots(client, { requireFrame: false });
|
|
2769
|
+
if (rootsBeforeAttempt?.iframe?.documentNodeId) {
|
|
2770
|
+
currentFrameNodeId = rootsBeforeAttempt.iframe.documentNodeId;
|
|
2771
|
+
}
|
|
2772
|
+
} catch {}
|
|
2773
|
+
const before = await verifyRecruitKeywordInputValue(client, currentFrameNodeId, normalizedKeyword);
|
|
2774
|
+
let reapply = null;
|
|
2775
|
+
if (before.verified === false) {
|
|
2776
|
+
reapply = await setRecruitKeyword(client, currentFrameNodeId, normalizedKeyword);
|
|
2777
|
+
}
|
|
2778
|
+
const search = await clickRecruitSearch(client, currentFrameNodeId);
|
|
2779
|
+
let rootsAfterSearch = null;
|
|
2780
|
+
try {
|
|
2781
|
+
rootsAfterSearch = await getRecruitRoots(client, { requireFrame: false });
|
|
2782
|
+
if (rootsAfterSearch?.iframe?.documentNodeId) {
|
|
2783
|
+
currentFrameNodeId = rootsAfterSearch.iframe.documentNodeId;
|
|
2784
|
+
}
|
|
2785
|
+
} catch {}
|
|
2786
|
+
const after = await verifyRecruitKeywordInputValue(client, currentFrameNodeId, normalizedKeyword, {
|
|
2787
|
+
settleMs: postSearchSettleMs
|
|
2788
|
+
});
|
|
2789
|
+
attempts.push({
|
|
2790
|
+
attempt,
|
|
2791
|
+
before,
|
|
2792
|
+
reapply,
|
|
2793
|
+
search,
|
|
2794
|
+
after,
|
|
2795
|
+
frame_reacquired_before_attempt: rootsBeforeAttempt?.iframe?.documentNodeId
|
|
2796
|
+
? {
|
|
2797
|
+
selector: rootsBeforeAttempt.iframe.selector,
|
|
2798
|
+
document_node_id: rootsBeforeAttempt.iframe.documentNodeId
|
|
2799
|
+
}
|
|
2800
|
+
: null,
|
|
2801
|
+
frame_reacquired: rootsAfterSearch?.iframe?.documentNodeId
|
|
2802
|
+
? {
|
|
2803
|
+
selector: rootsAfterSearch.iframe.selector,
|
|
2804
|
+
document_node_id: rootsAfterSearch.iframe.documentNodeId
|
|
2805
|
+
}
|
|
2806
|
+
: null
|
|
2807
|
+
});
|
|
2808
|
+
if (after.verified !== false) {
|
|
2809
|
+
return {
|
|
2810
|
+
searched: true,
|
|
2811
|
+
mode: search.mode,
|
|
2812
|
+
search,
|
|
2813
|
+
keyword_guard: {
|
|
2814
|
+
verified: after.verified,
|
|
2815
|
+
expected: after.expected,
|
|
2816
|
+
actual: after.actual,
|
|
2817
|
+
attempts
|
|
2818
|
+
}
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
const last = attempts[attempts.length - 1]?.after || {};
|
|
2824
|
+
const error = new Error(`Recruit keyword was not preserved after search: expected=${normalizedKeyword}; actual=${last.actual || "unknown"}`);
|
|
2825
|
+
error.keyword_guard = {
|
|
2826
|
+
verified: false,
|
|
2827
|
+
expected: normalizedKeyword,
|
|
2828
|
+
actual: last.actual || "",
|
|
2829
|
+
attempts
|
|
2830
|
+
};
|
|
2831
|
+
throw error;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
1098
2834
|
export async function waitForRecruitSearchResultState(client, {
|
|
1099
2835
|
timeoutMs = DEFAULT_RECRUIT_SEARCH_TIMEOUT_MS,
|
|
1100
2836
|
intervalMs = 500
|
|
@@ -1166,70 +2902,111 @@ export async function applyRecruitSearchParams(client, {
|
|
|
1166
2902
|
const initialFrameNodeId = frameNodeId;
|
|
1167
2903
|
const beforeCounts = await getRecruitSearchCounts(client, frameNodeId);
|
|
1168
2904
|
const steps = [];
|
|
2905
|
+
const applicationStepNames = buildRecruitSearchApplicationStepNames(normalizedSearchParams);
|
|
1169
2906
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
if (
|
|
1188
|
-
const
|
|
1189
|
-
|
|
2907
|
+
for (const stepName of applicationStepNames) {
|
|
2908
|
+
if (stepName === "job_title") {
|
|
2909
|
+
steps.push({
|
|
2910
|
+
step: "job_title",
|
|
2911
|
+
result: await setRecruitJobTitle(client, frameNodeId, normalizedSearchParams.job, {
|
|
2912
|
+
optionTimeoutMs: searchTimeoutMs
|
|
2913
|
+
})
|
|
2914
|
+
});
|
|
2915
|
+
const rootsAfterJob = await getRecruitRoots(client);
|
|
2916
|
+
frameNodeId = rootsAfterJob.iframe.documentNodeId;
|
|
2917
|
+
steps.push({
|
|
2918
|
+
step: "reacquire_after_job",
|
|
2919
|
+
result: {
|
|
2920
|
+
selector: rootsAfterJob.iframe.selector,
|
|
2921
|
+
document_node_id: frameNodeId
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
} else if (stepName === "city") {
|
|
2925
|
+
const cityResult = await setRecruitCity(client, frameNodeId, normalizedSearchParams.city, {
|
|
2926
|
+
optionTimeoutMs: cityOptionTimeoutMs
|
|
2927
|
+
});
|
|
2928
|
+
steps.push({
|
|
2929
|
+
step: "city",
|
|
2930
|
+
result: cityResult
|
|
2931
|
+
});
|
|
2932
|
+
if (cityResult?.reacquire_frame) {
|
|
2933
|
+
const rootsAfterCity = await getRecruitRoots(client);
|
|
2934
|
+
frameNodeId = rootsAfterCity.iframe.documentNodeId;
|
|
2935
|
+
steps.push({
|
|
2936
|
+
step: "reacquire_after_city",
|
|
2937
|
+
result: {
|
|
2938
|
+
selector: rootsAfterCity.iframe.selector,
|
|
2939
|
+
document_node_id: frameNodeId,
|
|
2940
|
+
reason: cityResult.reason
|
|
2941
|
+
}
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
} else if (stepName === "degree") {
|
|
2945
|
+
steps.push({
|
|
2946
|
+
step: "degree",
|
|
2947
|
+
result: await setRecruitDegrees(client, frameNodeId, normalizedSearchParams.degrees)
|
|
2948
|
+
});
|
|
2949
|
+
} else if (stepName === "schools") {
|
|
2950
|
+
steps.push({
|
|
2951
|
+
step: "schools",
|
|
2952
|
+
result: await setRecruitSchools(client, frameNodeId, normalizedSearchParams.schools)
|
|
2953
|
+
});
|
|
2954
|
+
} else if (stepName === "experience") {
|
|
2955
|
+
steps.push({
|
|
2956
|
+
step: "experience",
|
|
2957
|
+
result: await setRecruitExperience(client, frameNodeId, normalizedSearchParams.experience)
|
|
2958
|
+
});
|
|
2959
|
+
} else if (stepName === "gender") {
|
|
2960
|
+
steps.push({
|
|
2961
|
+
step: "gender",
|
|
2962
|
+
result: await setRecruitGender(client, frameNodeId, normalizedSearchParams.gender)
|
|
2963
|
+
});
|
|
2964
|
+
} else if (stepName === "age") {
|
|
2965
|
+
steps.push({
|
|
2966
|
+
step: "age",
|
|
2967
|
+
result: await setRecruitAge(client, frameNodeId, normalizedSearchParams.age)
|
|
2968
|
+
});
|
|
2969
|
+
} else if (stepName === "keyword") {
|
|
2970
|
+
const rootsBeforeKeyword = await getRecruitRoots(client);
|
|
2971
|
+
frameNodeId = rootsBeforeKeyword.iframe.documentNodeId;
|
|
2972
|
+
steps.push({
|
|
2973
|
+
step: "reacquire_before_keyword",
|
|
2974
|
+
result: {
|
|
2975
|
+
selector: rootsBeforeKeyword.iframe.selector,
|
|
2976
|
+
document_node_id: frameNodeId
|
|
2977
|
+
}
|
|
2978
|
+
});
|
|
2979
|
+
steps.push({
|
|
2980
|
+
step: "keyword",
|
|
2981
|
+
result: await setRecruitKeyword(client, frameNodeId, normalizedSearchParams.keyword)
|
|
2982
|
+
});
|
|
2983
|
+
} else if (stepName === "search") {
|
|
2984
|
+
const rootsBeforeSearch = await getRecruitRoots(client);
|
|
2985
|
+
frameNodeId = rootsBeforeSearch.iframe.documentNodeId;
|
|
1190
2986
|
steps.push({
|
|
1191
|
-
step: "
|
|
2987
|
+
step: "reacquire_before_search",
|
|
1192
2988
|
result: {
|
|
1193
|
-
selector:
|
|
1194
|
-
document_node_id: frameNodeId
|
|
1195
|
-
reason: cityResult.reason
|
|
2989
|
+
selector: rootsBeforeSearch.iframe.selector,
|
|
2990
|
+
document_node_id: frameNodeId
|
|
1196
2991
|
}
|
|
1197
2992
|
});
|
|
2993
|
+
steps.push({
|
|
2994
|
+
step: "search",
|
|
2995
|
+
result: await clickRecruitSearchWithKeywordGuard(client, frameNodeId, normalizedSearchParams.keyword)
|
|
2996
|
+
});
|
|
2997
|
+
} else if (stepName === "recent_viewed") {
|
|
2998
|
+
const recentFilterRoots = await getRecruitRoots(client);
|
|
2999
|
+
steps.push({
|
|
3000
|
+
step: "recent_viewed",
|
|
3001
|
+
result: await setRecruitRecentViewedFilter(
|
|
3002
|
+
client,
|
|
3003
|
+
recentFilterRoots.iframe.documentNodeId,
|
|
3004
|
+
normalizedSearchParams.filter_recent_viewed
|
|
3005
|
+
)
|
|
3006
|
+
});
|
|
1198
3007
|
}
|
|
1199
3008
|
}
|
|
1200
3009
|
|
|
1201
|
-
steps.push({
|
|
1202
|
-
step: "degree",
|
|
1203
|
-
result: await setRecruitDegrees(client, frameNodeId, normalizedSearchParams.degrees)
|
|
1204
|
-
});
|
|
1205
|
-
|
|
1206
|
-
steps.push({
|
|
1207
|
-
step: "schools",
|
|
1208
|
-
result: await setRecruitSchools(client, frameNodeId, normalizedSearchParams.schools)
|
|
1209
|
-
});
|
|
1210
|
-
|
|
1211
|
-
steps.push({
|
|
1212
|
-
step: "keyword",
|
|
1213
|
-
result: await setRecruitKeyword(client, frameNodeId, normalizedSearchParams.keyword)
|
|
1214
|
-
});
|
|
1215
|
-
|
|
1216
|
-
steps.push({
|
|
1217
|
-
step: "search",
|
|
1218
|
-
result: await clickRecruitSearch(client, frameNodeId)
|
|
1219
|
-
});
|
|
1220
|
-
|
|
1221
|
-
if (typeof normalizedSearchParams.filter_recent_viewed === "boolean") {
|
|
1222
|
-
const postSearchRoots = await getRecruitRoots(client);
|
|
1223
|
-
steps.push({
|
|
1224
|
-
step: "recent_viewed",
|
|
1225
|
-
result: await setRecruitRecentViewedFilter(
|
|
1226
|
-
client,
|
|
1227
|
-
postSearchRoots.iframe.documentNodeId,
|
|
1228
|
-
normalizedSearchParams.filter_recent_viewed
|
|
1229
|
-
)
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
3010
|
const postSearchState = await waitForRecruitSearchResultState(client, {
|
|
1234
3011
|
timeoutMs: searchTimeoutMs
|
|
1235
3012
|
});
|
|
@@ -1243,6 +3020,7 @@ export async function applyRecruitSearchParams(client, {
|
|
|
1243
3020
|
reset,
|
|
1244
3021
|
overlay_dismissal: overlayDismissal,
|
|
1245
3022
|
controls,
|
|
3023
|
+
application_step_names: applicationStepNames,
|
|
1246
3024
|
initial_iframe: {
|
|
1247
3025
|
selector: initialRoots.iframe.selector,
|
|
1248
3026
|
document_node_id: initialFrameNodeId
|