@reconcrap/boss-recommend-mcp 2.1.14 → 2.1.16
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 +34 -5
- package/package.json +8 -7
- 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 +127 -88
- package/src/core/greet-quota/index.js +17 -0
- package/src/core/reporting/legacy-csv.js +5 -1
- package/src/domains/chat/detail.js +79 -47
- package/src/domains/chat/run-service.js +400 -158
- package/src/domains/recommend/colleague-contact.js +333 -0
- package/src/domains/recommend/index.js +1 -0
- package/src/domains/recommend/run-service.js +166 -77
- package/src/domains/recruit/constants.js +69 -0
- package/src/domains/recruit/instruction-parser.js +403 -86
- package/src/domains/recruit/run-service.js +320 -11
- package/src/domains/recruit/search.js +2118 -306
- package/src/index.js +38 -23
- package/src/parser.js +45 -2
- package/src/recommend-mcp.js +92 -18
- package/src/recruit-mcp.js +236 -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,47 +328,433 @@ 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
|
|
149
|
-
: null
|
|
702
|
+
: null,
|
|
703
|
+
skip_recent_colleague_contacted: searchParams.skip_recent_colleague_contacted !== false
|
|
150
704
|
};
|
|
151
705
|
const job = normalizeText(searchParams.job || searchParams.job_title || searchParams.selected_job);
|
|
152
706
|
if (job) normalized.job = job;
|
|
707
|
+
if (experience) normalized.experience = experience;
|
|
708
|
+
if (gender) normalized.gender = gender;
|
|
709
|
+
if (age) normalized.age = age;
|
|
153
710
|
return normalized;
|
|
154
711
|
}
|
|
155
712
|
|
|
713
|
+
export function buildRecruitSearchApplicationStepNames(searchParams = {}) {
|
|
714
|
+
const normalized = normalizeRecruitSearchParams(searchParams);
|
|
715
|
+
const steps = [];
|
|
716
|
+
// Recruit search applies job first because job changes can reset other filters.
|
|
717
|
+
if (normalized.job) steps.push("job_title");
|
|
718
|
+
if (normalized.city) steps.push("city");
|
|
719
|
+
steps.push("degree", "schools");
|
|
720
|
+
if (normalized.experience) steps.push("experience");
|
|
721
|
+
if (normalized.gender) steps.push("gender");
|
|
722
|
+
if (normalized.age) steps.push("age");
|
|
723
|
+
if (typeof normalized.filter_recent_viewed === "boolean") steps.push("recent_viewed");
|
|
724
|
+
if (typeof normalized.skip_recent_colleague_contacted === "boolean") steps.push("exchange_resume");
|
|
725
|
+
// Keyword is the final filter before executing the search.
|
|
726
|
+
steps.push("keyword", "search");
|
|
727
|
+
return steps;
|
|
728
|
+
}
|
|
729
|
+
|
|
156
730
|
export function hasRecruitSearchParams(searchParams = {}) {
|
|
157
731
|
const degrees = normalizeRecruitDegreeLabels(searchParams.degrees || searchParams.degree || "不限");
|
|
158
732
|
const job = normalizeText(searchParams.job || searchParams.job_title || searchParams.selected_job);
|
|
733
|
+
const experience = normalizeRecruitExperienceFilter(pickRecruitExperienceSource(searchParams));
|
|
734
|
+
const gender = normalizeRecruitGenderFilter(searchParams.gender);
|
|
735
|
+
const age = normalizeRecruitAgeFilter(pickRecruitAgeSource(searchParams));
|
|
159
736
|
const normalized = {
|
|
160
737
|
city: normalizeText(searchParams.city) || null,
|
|
161
738
|
degree: degrees[0] || "不限",
|
|
162
739
|
degrees,
|
|
163
|
-
schools:
|
|
164
|
-
? searchParams.schools.map(normalizeText).filter(Boolean)
|
|
165
|
-
: [],
|
|
740
|
+
schools: normalizeRecruitSchoolList(searchParams.schools),
|
|
166
741
|
keyword: normalizeText(searchParams.keyword),
|
|
167
742
|
filter_recent_viewed: typeof searchParams.filter_recent_viewed === "boolean"
|
|
168
743
|
? searchParams.filter_recent_viewed
|
|
169
|
-
: null
|
|
744
|
+
: null,
|
|
745
|
+
skip_recent_colleague_contacted: searchParams.skip_recent_colleague_contacted !== false
|
|
170
746
|
};
|
|
171
747
|
return Boolean(
|
|
172
748
|
job
|
|
173
749
|
|| normalized.city
|
|
174
750
|
|| normalized.degrees.some((degree) => degree && degree !== "不限")
|
|
175
751
|
|| normalized.schools.length
|
|
752
|
+
|| experience
|
|
753
|
+
|| gender
|
|
754
|
+
|| age
|
|
176
755
|
|| normalized.keyword
|
|
177
756
|
|| typeof normalized.filter_recent_viewed === "boolean"
|
|
757
|
+
|| typeof normalized.skip_recent_colleague_contacted === "boolean"
|
|
178
758
|
);
|
|
179
759
|
}
|
|
180
760
|
|
|
@@ -186,14 +766,28 @@ function candidateIsActive(attributes = {}, outerHTML = "") {
|
|
|
186
766
|
|| /\bchecked(?:=["']?checked)?\b/i.test(openingTag);
|
|
187
767
|
}
|
|
188
768
|
|
|
769
|
+
function isVisibleBox(box) {
|
|
770
|
+
return Boolean(box && box.rect.width > 4 && box.rect.height > 4);
|
|
771
|
+
}
|
|
772
|
+
|
|
189
773
|
async function readTextCandidate(client, nodeId, {
|
|
190
774
|
selector = "",
|
|
191
|
-
index = 0
|
|
775
|
+
index = 0,
|
|
776
|
+
includeBox = false
|
|
192
777
|
} = {}) {
|
|
193
778
|
const [attributes, outerHTML] = await Promise.all([
|
|
194
779
|
getAttributesMap(client, nodeId),
|
|
195
780
|
getOuterHTML(client, nodeId)
|
|
196
781
|
]);
|
|
782
|
+
let box = null;
|
|
783
|
+
let boxError = "";
|
|
784
|
+
if (includeBox) {
|
|
785
|
+
try {
|
|
786
|
+
box = await getNodeBox(client, nodeId);
|
|
787
|
+
} catch (error) {
|
|
788
|
+
boxError = error?.message || String(error);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
197
791
|
const text = normalizeText(htmlToText(outerHTML));
|
|
198
792
|
return {
|
|
199
793
|
node_id: nodeId,
|
|
@@ -202,12 +796,16 @@ async function readTextCandidate(client, nodeId, {
|
|
|
202
796
|
label: normalizeRecruitSearchLabel(text),
|
|
203
797
|
text,
|
|
204
798
|
active: candidateIsActive(attributes, outerHTML),
|
|
799
|
+
visible: includeBox ? isVisibleBox(box) : undefined,
|
|
800
|
+
center: box?.center || null,
|
|
801
|
+
rect: box?.rect || null,
|
|
802
|
+
box_error: boxError || undefined,
|
|
205
803
|
class_name: attributes.class || "",
|
|
206
804
|
attributes
|
|
207
805
|
};
|
|
208
806
|
}
|
|
209
807
|
|
|
210
|
-
async function listTextCandidates(client, rootNodeId, selectors = []) {
|
|
808
|
+
async function listTextCandidates(client, rootNodeId, selectors = [], options = {}) {
|
|
211
809
|
const candidates = [];
|
|
212
810
|
const seen = new Set();
|
|
213
811
|
for (const selector of selectors) {
|
|
@@ -216,7 +814,7 @@ async function listTextCandidates(client, rootNodeId, selectors = []) {
|
|
|
216
814
|
const nodeId = nodeIds[index];
|
|
217
815
|
if (seen.has(nodeId)) continue;
|
|
218
816
|
seen.add(nodeId);
|
|
219
|
-
candidates.push(await readTextCandidate(client, nodeId, { selector, index }));
|
|
817
|
+
candidates.push(await readTextCandidate(client, nodeId, { selector, index, ...options }));
|
|
220
818
|
}
|
|
221
819
|
}
|
|
222
820
|
return candidates;
|
|
@@ -240,6 +838,12 @@ export function chooseRecruitTextCandidate(candidates = [], {
|
|
|
240
838
|
return candidates.find((candidate) => candidate.label.includes(target) || target.includes(candidate.label)) || null;
|
|
241
839
|
}
|
|
242
840
|
|
|
841
|
+
function chooseRecruitSchoolCandidate(candidates = [], school) {
|
|
842
|
+
const targetKeys = new Set(buildRecruitSchoolSearchLabels(school).map(normalizeRecruitSchoolCompareKey));
|
|
843
|
+
if (!targetKeys.size) return null;
|
|
844
|
+
return candidates.find((candidate) => targetKeys.has(normalizeRecruitSchoolCompareKey(candidate.text || candidate.label))) || null;
|
|
845
|
+
}
|
|
846
|
+
|
|
243
847
|
async function findTextCandidate(client, rootNodeId, selectors, label, options = {}) {
|
|
244
848
|
const candidates = await listTextCandidates(client, rootNodeId, selectors);
|
|
245
849
|
return {
|
|
@@ -310,46 +914,82 @@ async function waitForRecruitJobTitleCandidate(client, rootNodeId, selectors, jo
|
|
|
310
914
|
};
|
|
311
915
|
}
|
|
312
916
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
917
|
+
function compactRecruitTextCandidate(candidate = {}) {
|
|
918
|
+
return {
|
|
919
|
+
label: candidate.text || "",
|
|
920
|
+
normalized_label: candidate.label || "",
|
|
921
|
+
active: Boolean(candidate.active),
|
|
922
|
+
visible: Boolean(candidate.visible),
|
|
923
|
+
class_name: candidate.class_name || "",
|
|
924
|
+
node_id: candidate.node_id,
|
|
925
|
+
selector: candidate.selector,
|
|
926
|
+
center: candidate.center || null,
|
|
927
|
+
rect: candidate.rect || null,
|
|
928
|
+
box_error: candidate.box_error || null
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async function listRecruitJobTitleOptions(client, frameNodeId) {
|
|
933
|
+
const candidates = await listTextCandidates(
|
|
934
|
+
client,
|
|
935
|
+
frameNodeId,
|
|
936
|
+
RECRUIT_SEARCH_SELECTORS.jobTitleOption,
|
|
937
|
+
{ includeBox: true }
|
|
938
|
+
);
|
|
939
|
+
return candidates.filter((candidate) => candidate.text && candidate.text.length <= 160);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function findVisibleRecruitJobTitleTrigger(client, frameNodeId) {
|
|
943
|
+
const candidates = [];
|
|
944
|
+
const seen = new Set();
|
|
945
|
+
for (const selector of RECRUIT_SEARCH_SELECTORS.jobTitleTrigger) {
|
|
946
|
+
const nodeIds = uniqueNodeIds(await querySelectorAll(client, frameNodeId, selector));
|
|
947
|
+
for (const nodeId of nodeIds) {
|
|
948
|
+
if (seen.has(nodeId)) continue;
|
|
949
|
+
seen.add(nodeId);
|
|
950
|
+
let box = null;
|
|
951
|
+
try {
|
|
952
|
+
box = await getNodeBox(client, nodeId);
|
|
953
|
+
} catch {}
|
|
954
|
+
if (!isVisibleBox(box)) continue;
|
|
955
|
+
candidates.push({
|
|
956
|
+
selector,
|
|
957
|
+
node_id: nodeId,
|
|
958
|
+
center: box.center,
|
|
959
|
+
rect: box.rect
|
|
960
|
+
});
|
|
961
|
+
}
|
|
321
962
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (optional) {
|
|
963
|
+
return candidates[0] || null;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async function waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
967
|
+
timeoutMs = 4000,
|
|
968
|
+
intervalMs = 200
|
|
969
|
+
} = {}) {
|
|
970
|
+
const started = Date.now();
|
|
971
|
+
let options = [];
|
|
972
|
+
while (Date.now() - started <= timeoutMs) {
|
|
973
|
+
options = await listRecruitJobTitleOptions(client, frameNodeId);
|
|
974
|
+
const visibleOptions = options.filter((option) => option.visible);
|
|
975
|
+
if (visibleOptions.length) {
|
|
336
976
|
return {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
selector: found.selector,
|
|
340
|
-
node_id: found.nodeId,
|
|
341
|
-
error: error?.message || String(error)
|
|
977
|
+
options,
|
|
978
|
+
visible_options: visibleOptions
|
|
342
979
|
};
|
|
343
980
|
}
|
|
344
|
-
|
|
981
|
+
await sleep(intervalMs);
|
|
345
982
|
}
|
|
983
|
+
return {
|
|
984
|
+
options,
|
|
985
|
+
visible_options: []
|
|
986
|
+
};
|
|
346
987
|
}
|
|
347
988
|
|
|
348
|
-
async function
|
|
989
|
+
async function closeRecruitJobTitleDropdown(client, settleMs = 300) {
|
|
349
990
|
if (typeof client?.Input?.dispatchKeyEvent !== "function") {
|
|
350
991
|
return {
|
|
351
|
-
|
|
352
|
-
skipped: true,
|
|
992
|
+
ok: false,
|
|
353
993
|
reason: "dispatch_key_unavailable"
|
|
354
994
|
};
|
|
355
995
|
}
|
|
@@ -360,67 +1000,352 @@ async function dismissRecruitSearchOverlays(client, settleMs = 250) {
|
|
|
360
1000
|
});
|
|
361
1001
|
if (settleMs > 0) await sleep(settleMs);
|
|
362
1002
|
return {
|
|
363
|
-
|
|
364
|
-
|
|
1003
|
+
ok: true,
|
|
1004
|
+
reason: "escape"
|
|
365
1005
|
};
|
|
366
1006
|
}
|
|
367
1007
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
candidate_card: RECRUIT_CARD_SELECTOR,
|
|
376
|
-
no_data: RECRUIT_NO_DATA_SELECTORS.join(", ")
|
|
1008
|
+
async function openRecruitJobTitleDropdown(client, frameNodeId, {
|
|
1009
|
+
timeoutMs = 4000,
|
|
1010
|
+
maxAttempts = 3
|
|
1011
|
+
} = {}) {
|
|
1012
|
+
const alreadyOpen = await waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
1013
|
+
timeoutMs: 300,
|
|
1014
|
+
intervalMs: 100
|
|
377
1015
|
});
|
|
1016
|
+
if (alreadyOpen.visible_options.length) {
|
|
1017
|
+
return {
|
|
1018
|
+
opened: true,
|
|
1019
|
+
already_open: true,
|
|
1020
|
+
options: alreadyOpen.options,
|
|
1021
|
+
visible_options: alreadyOpen.visible_options
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
await closeRecruitJobTitleDropdown(client);
|
|
1026
|
+
const attempts = [];
|
|
1027
|
+
for (let attempt = 1; attempt <= Math.max(1, maxAttempts); attempt += 1) {
|
|
1028
|
+
const trigger = await findVisibleRecruitJobTitleTrigger(client, frameNodeId);
|
|
1029
|
+
if (!trigger) {
|
|
1030
|
+
throw new Error("Recruit job trigger was not found");
|
|
1031
|
+
}
|
|
1032
|
+
if (attempt > 1) await closeRecruitJobTitleDropdown(client);
|
|
1033
|
+
const clickBox = await clickNodeCenter(client, trigger.node_id, DETERMINISTIC_CLICK_OPTIONS);
|
|
1034
|
+
const opened = await waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
1035
|
+
timeoutMs,
|
|
1036
|
+
intervalMs: 200
|
|
1037
|
+
});
|
|
1038
|
+
attempts.push({
|
|
1039
|
+
attempt,
|
|
1040
|
+
trigger,
|
|
1041
|
+
click_box: {
|
|
1042
|
+
center: clickBox.center,
|
|
1043
|
+
rect: clickBox.rect
|
|
1044
|
+
},
|
|
1045
|
+
option_count: opened.options.length,
|
|
1046
|
+
visible_option_count: opened.visible_options.length
|
|
1047
|
+
});
|
|
1048
|
+
if (opened.visible_options.length) {
|
|
1049
|
+
return {
|
|
1050
|
+
opened: true,
|
|
1051
|
+
already_open: false,
|
|
1052
|
+
trigger,
|
|
1053
|
+
options: opened.options,
|
|
1054
|
+
visible_options: opened.visible_options,
|
|
1055
|
+
attempts
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const error = new Error("Recruit job dropdown did not expose visible options after trigger click");
|
|
1061
|
+
error.job_dropdown_attempts = attempts;
|
|
1062
|
+
throw error;
|
|
378
1063
|
}
|
|
379
1064
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
1065
|
+
async function closeRecruitJobTitleDropdownFully(client, frameNodeId, {
|
|
1066
|
+
settleMs = 300,
|
|
1067
|
+
timeoutMs = 1500
|
|
383
1068
|
} = {}) {
|
|
1069
|
+
const before = await waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
1070
|
+
timeoutMs: 200,
|
|
1071
|
+
intervalMs: 100
|
|
1072
|
+
});
|
|
1073
|
+
const attempts = [];
|
|
1074
|
+
if (!before.visible_options.length) {
|
|
1075
|
+
return {
|
|
1076
|
+
ok: true,
|
|
1077
|
+
closed: false,
|
|
1078
|
+
reason: "already_closed",
|
|
1079
|
+
visible_before_count: 0,
|
|
1080
|
+
visible_after_count: 0,
|
|
1081
|
+
attempts
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
384
1085
|
const started = Date.now();
|
|
385
|
-
let
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
1086
|
+
for (let attempt = 1; attempt <= 2 && Date.now() - started <= timeoutMs; attempt += 1) {
|
|
1087
|
+
const close = await closeRecruitJobTitleDropdown(client, settleMs);
|
|
1088
|
+
const after = await waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
1089
|
+
timeoutMs: 250,
|
|
1090
|
+
intervalMs: 100
|
|
1091
|
+
});
|
|
1092
|
+
attempts.push({
|
|
1093
|
+
method: "escape",
|
|
1094
|
+
attempt,
|
|
1095
|
+
close,
|
|
1096
|
+
visible_after_count: after.visible_options.length
|
|
1097
|
+
});
|
|
1098
|
+
if (!after.visible_options.length) {
|
|
1099
|
+
return {
|
|
1100
|
+
ok: true,
|
|
1101
|
+
closed: true,
|
|
1102
|
+
reason: "escape",
|
|
1103
|
+
visible_before_count: before.visible_options.length,
|
|
1104
|
+
visible_after_count: 0,
|
|
1105
|
+
attempts
|
|
397
1106
|
};
|
|
398
|
-
if (lastState.ok) return lastState;
|
|
399
1107
|
}
|
|
400
|
-
await sleep(intervalMs);
|
|
401
1108
|
}
|
|
1109
|
+
|
|
1110
|
+
const trigger = await findVisibleRecruitJobTitleTrigger(client, frameNodeId).catch(() => null);
|
|
1111
|
+
if (trigger?.node_id) {
|
|
1112
|
+
const click = await clickNodeCenter(client, trigger.node_id, DETERMINISTIC_CLICK_OPTIONS).catch((error) => ({
|
|
1113
|
+
error: error?.message || String(error || "")
|
|
1114
|
+
}));
|
|
1115
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
1116
|
+
const afterToggle = await waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
1117
|
+
timeoutMs: 250,
|
|
1118
|
+
intervalMs: 100
|
|
1119
|
+
});
|
|
1120
|
+
attempts.push({
|
|
1121
|
+
method: "trigger_toggle",
|
|
1122
|
+
click,
|
|
1123
|
+
visible_after_count: afterToggle.visible_options.length
|
|
1124
|
+
});
|
|
1125
|
+
if (!afterToggle.visible_options.length) {
|
|
1126
|
+
return {
|
|
1127
|
+
ok: true,
|
|
1128
|
+
closed: true,
|
|
1129
|
+
reason: "trigger_toggle",
|
|
1130
|
+
visible_before_count: before.visible_options.length,
|
|
1131
|
+
visible_after_count: 0,
|
|
1132
|
+
attempts
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const outside = await clickPoint(client, 12, 12, DETERMINISTIC_CLICK_OPTIONS).catch((error) => ({
|
|
1138
|
+
error: error?.message || String(error || "")
|
|
1139
|
+
}));
|
|
1140
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
1141
|
+
const afterOutside = await waitForVisibleRecruitJobTitleOptions(client, frameNodeId, {
|
|
1142
|
+
timeoutMs: 250,
|
|
1143
|
+
intervalMs: 100
|
|
1144
|
+
});
|
|
1145
|
+
attempts.push({
|
|
1146
|
+
method: "outside_click",
|
|
1147
|
+
click: outside,
|
|
1148
|
+
visible_after_count: afterOutside.visible_options.length
|
|
1149
|
+
});
|
|
1150
|
+
if (!afterOutside.visible_options.length) {
|
|
1151
|
+
return {
|
|
1152
|
+
ok: true,
|
|
1153
|
+
closed: true,
|
|
1154
|
+
reason: "outside_click",
|
|
1155
|
+
visible_before_count: before.visible_options.length,
|
|
1156
|
+
visible_after_count: 0,
|
|
1157
|
+
attempts
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
402
1161
|
return {
|
|
403
1162
|
ok: false,
|
|
404
|
-
|
|
405
|
-
|
|
1163
|
+
closed: false,
|
|
1164
|
+
reason: "still_visible_after_close_attempts",
|
|
1165
|
+
visible_before_count: before.visible_options.length,
|
|
1166
|
+
visible_after_count: afterOutside.visible_options.length,
|
|
1167
|
+
attempts
|
|
406
1168
|
};
|
|
407
1169
|
}
|
|
408
1170
|
|
|
409
|
-
async function
|
|
410
|
-
|
|
411
|
-
|
|
1171
|
+
async function verifyRecruitJobTitleSelection(client, frameNodeId, {
|
|
1172
|
+
jobTitle = "",
|
|
1173
|
+
delayMs = 1200,
|
|
1174
|
+
dropdownTimeoutMs = 4000
|
|
412
1175
|
} = {}) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
1176
|
+
const requested = normalizeText(jobTitle);
|
|
1177
|
+
if (delayMs > 0) await sleep(delayMs);
|
|
1178
|
+
let options = [];
|
|
1179
|
+
let openError = null;
|
|
1180
|
+
try {
|
|
1181
|
+
const opened = await openRecruitJobTitleDropdown(client, frameNodeId, {
|
|
1182
|
+
timeoutMs: dropdownTimeoutMs
|
|
1183
|
+
});
|
|
1184
|
+
options = opened.options || [];
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
openError = error;
|
|
1187
|
+
options = await listRecruitJobTitleOptions(client, frameNodeId).catch(() => []);
|
|
1188
|
+
}
|
|
1189
|
+
const current = options.find((option) => option.active) || null;
|
|
1190
|
+
const searchTerms = buildRecruitJobTitleSearchTerms(requested);
|
|
1191
|
+
const verified = Boolean(
|
|
1192
|
+
current
|
|
1193
|
+
&& searchTerms.some((term) => chooseRecruitTextCandidate([current], {
|
|
1194
|
+
label: term,
|
|
1195
|
+
match: "contains"
|
|
1196
|
+
}))
|
|
1197
|
+
);
|
|
1198
|
+
const menuClose = await closeRecruitJobTitleDropdownFully(client, frameNodeId).catch((error) => ({
|
|
1199
|
+
ok: false,
|
|
1200
|
+
closed: false,
|
|
1201
|
+
reason: "close_failed",
|
|
1202
|
+
error: error?.message || String(error)
|
|
1203
|
+
}));
|
|
1204
|
+
return {
|
|
1205
|
+
verified,
|
|
1206
|
+
requested,
|
|
1207
|
+
search_terms: searchTerms,
|
|
1208
|
+
current_label: current?.text || "",
|
|
1209
|
+
current_option: current ? compactRecruitTextCandidate(current) : null,
|
|
1210
|
+
option_count: options.length,
|
|
1211
|
+
visible_option_count: options.filter((option) => option.visible).length,
|
|
1212
|
+
options: options.map(compactRecruitTextCandidate),
|
|
1213
|
+
open_error: openError ? (openError?.message || String(openError)) : null,
|
|
1214
|
+
menu_close: menuClose
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
async function clickFirstNodeBySelectors(client, rootNodeId, selectors, {
|
|
1219
|
+
optional = false,
|
|
1220
|
+
scrollIntoView = true
|
|
1221
|
+
} = {}) {
|
|
1222
|
+
const errors = [];
|
|
1223
|
+
const seen = new Set();
|
|
1224
|
+
let matched = false;
|
|
1225
|
+
for (const selector of selectors) {
|
|
1226
|
+
const nodeIds = uniqueNodeIds(await querySelectorAll(client, rootNodeId, selector));
|
|
1227
|
+
for (const nodeId of nodeIds) {
|
|
1228
|
+
if (seen.has(nodeId)) continue;
|
|
1229
|
+
seen.add(nodeId);
|
|
1230
|
+
matched = true;
|
|
1231
|
+
try {
|
|
1232
|
+
const box = await clickNodeCenter(client, nodeId, {
|
|
1233
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
1234
|
+
scrollIntoView
|
|
1235
|
+
});
|
|
1236
|
+
await sleep(250);
|
|
1237
|
+
return {
|
|
1238
|
+
clicked: true,
|
|
1239
|
+
selector,
|
|
1240
|
+
node_id: nodeId,
|
|
1241
|
+
box,
|
|
1242
|
+
skipped_errors: errors
|
|
1243
|
+
};
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
errors.push({
|
|
1246
|
+
selector,
|
|
1247
|
+
node_id: nodeId,
|
|
1248
|
+
error: error?.message || String(error)
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (!matched) {
|
|
1254
|
+
if (optional) return { clicked: false, reason: "not_found" };
|
|
1255
|
+
throw new Error(`Recruit search node was not found for selectors: ${selectors.join(", ")}`);
|
|
1256
|
+
}
|
|
1257
|
+
if (optional) {
|
|
1258
|
+
return {
|
|
1259
|
+
clicked: false,
|
|
1260
|
+
reason: "not_clickable",
|
|
1261
|
+
errors
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
const detail = errors.map((item) => `${item.selector}#${item.node_id}: ${item.error}`).join("; ");
|
|
1265
|
+
throw new Error(`Recruit search nodes were found but none were clickable for selectors: ${selectors.join(", ")}; ${detail}`);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
async function dismissRecruitSearchOverlays(client, settleMs = 250) {
|
|
1269
|
+
if (typeof client?.Input?.dispatchKeyEvent !== "function") {
|
|
1270
|
+
return {
|
|
1271
|
+
method: "Escape",
|
|
1272
|
+
skipped: true,
|
|
1273
|
+
reason: "dispatch_key_unavailable"
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
await pressKey(client, "Escape", {
|
|
1277
|
+
code: "Escape",
|
|
1278
|
+
windowsVirtualKeyCode: 27,
|
|
1279
|
+
nativeVirtualKeyCode: 27
|
|
1280
|
+
});
|
|
1281
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
1282
|
+
return {
|
|
1283
|
+
method: "Escape",
|
|
1284
|
+
settle_ms: settleMs
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
export async function getRecruitSearchCounts(client, frameNodeId) {
|
|
1289
|
+
return countSelectors(client, frameNodeId, {
|
|
1290
|
+
keyword_input: RECRUIT_SEARCH_SELECTORS.keywordInput.join(", "),
|
|
1291
|
+
search_button: RECRUIT_SEARCH_SELECTORS.searchButton.join(", "),
|
|
1292
|
+
degree_option: RECRUIT_SEARCH_SELECTORS.degreeOption.join(", "),
|
|
1293
|
+
school_item: RECRUIT_SEARCH_SELECTORS.schoolItem.join(", "),
|
|
1294
|
+
experience_option: RECRUIT_SEARCH_SELECTORS.experienceOption.join(", "),
|
|
1295
|
+
experience_custom: RECRUIT_SEARCH_SELECTORS.experienceCustom.join(", "),
|
|
1296
|
+
gender_dropdown: RECRUIT_SEARCH_SELECTORS.genderDropdown.join(", "),
|
|
1297
|
+
age_option: RECRUIT_SEARCH_SELECTORS.ageOption.join(", "),
|
|
1298
|
+
age_custom: RECRUIT_SEARCH_SELECTORS.ageCustom.join(", "),
|
|
1299
|
+
recent_viewed_label: RECRUIT_SEARCH_SELECTORS.recentViewedLabel.join(", "),
|
|
1300
|
+
candidate_card: RECRUIT_CARD_SELECTOR,
|
|
1301
|
+
no_data: RECRUIT_NO_DATA_SELECTORS.join(", ")
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
export async function waitForRecruitSearchControls(client, {
|
|
1306
|
+
timeoutMs = DEFAULT_RECRUIT_SEARCH_TIMEOUT_MS,
|
|
1307
|
+
intervalMs = 300
|
|
1308
|
+
} = {}) {
|
|
1309
|
+
const started = Date.now();
|
|
1310
|
+
let lastState = null;
|
|
1311
|
+
while (Date.now() - started <= timeoutMs) {
|
|
1312
|
+
const roots = await getRecruitRoots(client, { requireFrame: false });
|
|
1313
|
+
const frameNodeId = roots.iframe?.documentNodeId;
|
|
1314
|
+
if (frameNodeId) {
|
|
1315
|
+
const counts = await getRecruitSearchCounts(client, frameNodeId);
|
|
1316
|
+
lastState = {
|
|
1317
|
+
ok: counts.keyword_input > 0 && counts.search_button > 0,
|
|
1318
|
+
elapsed_ms: Date.now() - started,
|
|
1319
|
+
iframe_selector: roots.iframe.selector,
|
|
1320
|
+
iframe_document_node_id: frameNodeId,
|
|
1321
|
+
counts
|
|
1322
|
+
};
|
|
1323
|
+
if (lastState.ok) return lastState;
|
|
1324
|
+
}
|
|
1325
|
+
await sleep(intervalMs);
|
|
1326
|
+
}
|
|
1327
|
+
return {
|
|
1328
|
+
ok: false,
|
|
1329
|
+
elapsed_ms: Date.now() - started,
|
|
1330
|
+
...(lastState || {})
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
async function settleRecruitSearchAfterReset(client, {
|
|
1335
|
+
timeoutMs = DEFAULT_RECRUIT_RESET_TIMEOUT_MS,
|
|
1336
|
+
settleMs = 5000
|
|
1337
|
+
} = {}) {
|
|
1338
|
+
return waitForMiniFreshStartSettle(client, {
|
|
1339
|
+
domain: "search",
|
|
1340
|
+
timeoutMs,
|
|
1341
|
+
intervalMs: 500,
|
|
1342
|
+
settleMs: Math.max(0, Math.min(settleMs || 0, 5000)),
|
|
1343
|
+
readinessLabel: "search_controls_ready",
|
|
1344
|
+
checkReady: ({ remainingMs }) => waitForRecruitSearchControls(client, {
|
|
1345
|
+
timeoutMs: Math.min(Math.max(1, remainingMs), 1500),
|
|
1346
|
+
intervalMs: 300
|
|
1347
|
+
})
|
|
1348
|
+
});
|
|
424
1349
|
}
|
|
425
1350
|
|
|
426
1351
|
export async function resetRecruitSearchPage(client, {
|
|
@@ -534,15 +1459,102 @@ export async function setRecruitKeyword(client, frameNodeId, keyword) {
|
|
|
534
1459
|
if (!normalizedKeyword) {
|
|
535
1460
|
return { applied: false, reason: "empty_keyword" };
|
|
536
1461
|
}
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1462
|
+
const attempts = [];
|
|
1463
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
1464
|
+
const input = await clickFirstNodeBySelectors(client, frameNodeId, RECRUIT_SEARCH_SELECTORS.keywordInput);
|
|
1465
|
+
await clearFocusedInput(client);
|
|
1466
|
+
await sleep(120);
|
|
1467
|
+
const textEntry = await insertText(client, normalizedKeyword);
|
|
1468
|
+
await sleep(350);
|
|
1469
|
+
const verification = await verifyRecruitKeywordInputValue(client, frameNodeId, normalizedKeyword, {
|
|
1470
|
+
settleMs: 350
|
|
1471
|
+
});
|
|
1472
|
+
attempts.push({
|
|
1473
|
+
attempt,
|
|
1474
|
+
input,
|
|
1475
|
+
text_entry: textEntry,
|
|
1476
|
+
verification
|
|
1477
|
+
});
|
|
1478
|
+
if (verification.verified !== false) {
|
|
1479
|
+
return {
|
|
1480
|
+
applied: true,
|
|
1481
|
+
keyword: normalizedKeyword,
|
|
1482
|
+
input,
|
|
1483
|
+
text_entry: textEntry,
|
|
1484
|
+
verification,
|
|
1485
|
+
attempts
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
const last = attempts[attempts.length - 1]?.verification || {};
|
|
1491
|
+
throw new Error(`Recruit keyword input did not hold requested value: expected=${normalizedKeyword}; actual=${last.actual || "unknown"}`);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
export async function readRecruitKeywordInputValue(client, frameNodeId) {
|
|
1495
|
+
if (typeof client?.Accessibility?.getPartialAXTree !== "function") {
|
|
1496
|
+
return {
|
|
1497
|
+
available: false,
|
|
1498
|
+
reason: "accessibility_unavailable",
|
|
1499
|
+
value: null,
|
|
1500
|
+
normalized_value: ""
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
for (const selector of RECRUIT_SEARCH_SELECTORS.keywordInput) {
|
|
1504
|
+
const nodeIds = uniqueNodeIds(await querySelectorAll(client, frameNodeId, selector));
|
|
1505
|
+
for (const nodeId of nodeIds) {
|
|
1506
|
+
const ax = await client.Accessibility.getPartialAXTree({
|
|
1507
|
+
nodeId,
|
|
1508
|
+
fetchRelatives: false
|
|
1509
|
+
});
|
|
1510
|
+
const node = ax?.nodes?.[0] || null;
|
|
1511
|
+
if (!node) continue;
|
|
1512
|
+
const value = typeof node?.value?.value === "string" ? node.value.value : "";
|
|
1513
|
+
return {
|
|
1514
|
+
available: true,
|
|
1515
|
+
selector,
|
|
1516
|
+
node_id: nodeId,
|
|
1517
|
+
value,
|
|
1518
|
+
normalized_value: normalizeText(value),
|
|
1519
|
+
role: node?.role?.value || ""
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
542
1523
|
return {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
1524
|
+
available: false,
|
|
1525
|
+
reason: "keyword_input_not_found",
|
|
1526
|
+
value: null,
|
|
1527
|
+
normalized_value: ""
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
export async function verifyRecruitKeywordInputValue(client, frameNodeId, expectedKeyword, {
|
|
1532
|
+
settleMs = 0
|
|
1533
|
+
} = {}) {
|
|
1534
|
+
const expected = normalizeText(expectedKeyword);
|
|
1535
|
+
const before = await readRecruitKeywordInputValue(client, frameNodeId);
|
|
1536
|
+
if (!before.available || !expected) {
|
|
1537
|
+
return {
|
|
1538
|
+
verified: null,
|
|
1539
|
+
reason: before.reason || "empty_expected_keyword",
|
|
1540
|
+
expected,
|
|
1541
|
+
actual: before.normalized_value || "",
|
|
1542
|
+
before,
|
|
1543
|
+
after: before
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
let after = before;
|
|
1547
|
+
if (settleMs > 0) {
|
|
1548
|
+
await sleep(settleMs);
|
|
1549
|
+
after = await readRecruitKeywordInputValue(client, frameNodeId);
|
|
1550
|
+
}
|
|
1551
|
+
const actual = normalizeText(after.normalized_value || after.value);
|
|
1552
|
+
return {
|
|
1553
|
+
verified: actual === expected,
|
|
1554
|
+
expected,
|
|
1555
|
+
actual,
|
|
1556
|
+
before,
|
|
1557
|
+
after
|
|
546
1558
|
};
|
|
547
1559
|
}
|
|
548
1560
|
|
|
@@ -559,214 +1571,821 @@ export async function setRecruitJobTitle(client, frameNodeId, jobTitle, {
|
|
|
559
1571
|
if (!normalizedJobTitle) {
|
|
560
1572
|
return { applied: false, reason: "empty_job_title" };
|
|
561
1573
|
}
|
|
562
|
-
const
|
|
1574
|
+
const opened = await openRecruitJobTitleDropdown(client, frameNodeId, {
|
|
1575
|
+
timeoutMs: Math.min(optionTimeoutMs, 30000)
|
|
1576
|
+
});
|
|
1577
|
+
const options = opened.options.length
|
|
1578
|
+
? opened.options
|
|
1579
|
+
: await listRecruitJobTitleOptions(client, frameNodeId);
|
|
1580
|
+
const terms = buildRecruitJobTitleSearchTerms(normalizedJobTitle);
|
|
1581
|
+
let match = null;
|
|
1582
|
+
let matchedTerm = "";
|
|
1583
|
+
const visibleOptions = options.filter((option) => option.visible);
|
|
1584
|
+
const hiddenMatches = [];
|
|
1585
|
+
for (const term of terms) {
|
|
1586
|
+
match = chooseRecruitTextCandidate(visibleOptions, { label: term, match: "contains" });
|
|
1587
|
+
if (match) {
|
|
1588
|
+
matchedTerm = term;
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
const hiddenMatch = chooseRecruitTextCandidate(
|
|
1592
|
+
options.filter((option) => !option.visible),
|
|
1593
|
+
{ label: term, match: "contains" }
|
|
1594
|
+
);
|
|
1595
|
+
if (hiddenMatch) hiddenMatches.push(hiddenMatch);
|
|
1596
|
+
}
|
|
1597
|
+
if (!match) {
|
|
1598
|
+
await closeRecruitJobTitleDropdown(client);
|
|
1599
|
+
if (hiddenMatches.length) {
|
|
1600
|
+
const error = new Error(`Matched recruit job has no visible clickable option: ${hiddenMatches[0].text}`);
|
|
1601
|
+
error.hidden_job_matches = hiddenMatches.map(compactRecruitTextCandidate);
|
|
1602
|
+
throw error;
|
|
1603
|
+
}
|
|
1604
|
+
throw new Error(`Recruit job title option was not found: ${normalizedJobTitle}`);
|
|
1605
|
+
}
|
|
1606
|
+
let box = null;
|
|
1607
|
+
if (!match.active) {
|
|
1608
|
+
if (!match.center) {
|
|
1609
|
+
await closeRecruitJobTitleDropdown(client);
|
|
1610
|
+
throw new Error(`Matched recruit job has no clickable center: ${match.text}`);
|
|
1611
|
+
}
|
|
1612
|
+
box = await clickNodeCenter(client, match.node_id, {
|
|
1613
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
1614
|
+
scrollIntoView: true
|
|
1615
|
+
});
|
|
1616
|
+
await sleep(500);
|
|
1617
|
+
}
|
|
1618
|
+
const stickyVerification = await verifyRecruitJobTitleSelection(client, frameNodeId, {
|
|
1619
|
+
jobTitle: normalizedJobTitle,
|
|
1620
|
+
delayMs: 1200,
|
|
1621
|
+
dropdownTimeoutMs: Math.min(optionTimeoutMs, 5000)
|
|
1622
|
+
});
|
|
1623
|
+
if (!stickyVerification.verified) {
|
|
1624
|
+
throw new Error(`Recruit job selection was not sticky after 1.2s: requested=${normalizedJobTitle}; current=${stickyVerification.current_label || "unknown"}`);
|
|
1625
|
+
}
|
|
1626
|
+
if (stickyVerification.menu_close && stickyVerification.menu_close.ok === false) {
|
|
1627
|
+
throw new Error(`Recruit job dropdown remained open after sticky verification: ${stickyVerification.menu_close.reason || "unknown"}`);
|
|
1628
|
+
}
|
|
1629
|
+
return {
|
|
1630
|
+
applied: true,
|
|
1631
|
+
requested_job: normalizedJobTitle,
|
|
1632
|
+
selected_label: match.text,
|
|
1633
|
+
matched_term: matchedTerm,
|
|
1634
|
+
search_terms: terms,
|
|
1635
|
+
selected_node_id: match.node_id,
|
|
1636
|
+
was_active: match.active,
|
|
1637
|
+
clicked: !match.active,
|
|
1638
|
+
box,
|
|
1639
|
+
opened_dropdown: {
|
|
1640
|
+
already_open: Boolean(opened.already_open),
|
|
1641
|
+
visible_option_count: visibleOptions.length,
|
|
1642
|
+
attempts: opened.attempts || []
|
|
1643
|
+
},
|
|
1644
|
+
sticky_verification: stickyVerification,
|
|
1645
|
+
discovered_options: options.map(compactRecruitTextCandidate).slice(0, 30)
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
export async function setRecruitDegree(client, frameNodeId, degree) {
|
|
1650
|
+
const degreeLabel = resolveRecruitDegreeLabel(degree);
|
|
1651
|
+
if (!degreeLabel || degreeLabel === "不限") {
|
|
1652
|
+
return { applied: false, reason: "unlimited_degree", degree: degreeLabel || "不限" };
|
|
1653
|
+
}
|
|
1654
|
+
const { candidate, candidates } = await findTextCandidate(
|
|
563
1655
|
client,
|
|
564
1656
|
frameNodeId,
|
|
565
|
-
RECRUIT_SEARCH_SELECTORS.
|
|
566
|
-
|
|
567
|
-
{
|
|
1657
|
+
RECRUIT_SEARCH_SELECTORS.degreeOption,
|
|
1658
|
+
degreeLabel,
|
|
1659
|
+
{ match: "prefix" }
|
|
568
1660
|
);
|
|
569
|
-
if (!
|
|
570
|
-
throw new Error(`Recruit
|
|
1661
|
+
if (!candidate) {
|
|
1662
|
+
throw new Error(`Recruit degree option was not found: ${degreeLabel}`);
|
|
571
1663
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
1664
|
+
const box = await clickNodeCenter(client, candidate.node_id, {
|
|
1665
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
1666
|
+
scrollIntoView: true
|
|
1667
|
+
});
|
|
1668
|
+
await sleep(350);
|
|
1669
|
+
return {
|
|
1670
|
+
applied: true,
|
|
1671
|
+
requested_degree: degree,
|
|
1672
|
+
selected_label: candidate.text,
|
|
1673
|
+
selected_node_id: candidate.node_id,
|
|
1674
|
+
was_active: candidate.active,
|
|
1675
|
+
box,
|
|
1676
|
+
discovered_options: candidates.map((item) => ({
|
|
1677
|
+
label: item.text,
|
|
1678
|
+
active: item.active,
|
|
1679
|
+
node_id: item.node_id,
|
|
1680
|
+
selector: item.selector
|
|
1681
|
+
}))
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
export async function setRecruitDegrees(client, frameNodeId, degrees = []) {
|
|
1686
|
+
const labels = normalizeRecruitDegreeLabels(degrees).filter((label) => label && label !== "不限");
|
|
1687
|
+
if (!labels.length) {
|
|
1688
|
+
return { applied: false, reason: "unlimited_degree", degrees: ["不限"], selected: [] };
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
const selected = [];
|
|
1692
|
+
let discoveredOptions = [];
|
|
1693
|
+
for (const label of labels) {
|
|
1694
|
+
const { candidate, candidates } = await findTextCandidate(
|
|
1695
|
+
client,
|
|
1696
|
+
frameNodeId,
|
|
1697
|
+
RECRUIT_SEARCH_SELECTORS.degreeOption,
|
|
1698
|
+
label,
|
|
1699
|
+
{ match: "prefix" }
|
|
1700
|
+
);
|
|
1701
|
+
discoveredOptions = candidates.map((item) => ({
|
|
1702
|
+
label: item.text,
|
|
1703
|
+
active: item.active,
|
|
1704
|
+
node_id: item.node_id,
|
|
1705
|
+
selector: item.selector
|
|
1706
|
+
}));
|
|
1707
|
+
if (!candidate) {
|
|
1708
|
+
throw new Error(`Recruit degree option was not found: ${label}`);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
let box = null;
|
|
1712
|
+
if (!candidate.active) {
|
|
1713
|
+
box = await clickNodeCenter(client, candidate.node_id, {
|
|
1714
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
1715
|
+
scrollIntoView: true
|
|
1716
|
+
});
|
|
1717
|
+
await sleep(350);
|
|
1718
|
+
}
|
|
1719
|
+
selected.push({
|
|
1720
|
+
requested_degree: label,
|
|
1721
|
+
selected_label: candidate.text,
|
|
1722
|
+
selected_node_id: candidate.node_id,
|
|
1723
|
+
was_active: candidate.active,
|
|
1724
|
+
clicked: !candidate.active,
|
|
1725
|
+
box
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
return {
|
|
1730
|
+
applied: true,
|
|
1731
|
+
requested_degrees: labels,
|
|
1732
|
+
selected,
|
|
1733
|
+
discovered_options: discoveredOptions
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
async function findClickableDescendant(client, nodeId, selectors) {
|
|
1738
|
+
for (const selector of selectors) {
|
|
1739
|
+
const childNodeId = await querySelector(client, nodeId, selector);
|
|
1740
|
+
if (childNodeId) return { node_id: childNodeId, selector };
|
|
1741
|
+
}
|
|
1742
|
+
return { node_id: nodeId, selector: null };
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
export async function setRecruitSchools(client, frameNodeId, schools = []) {
|
|
1746
|
+
const targets = normalizeRecruitSchoolList(schools);
|
|
1747
|
+
const applied = [];
|
|
1748
|
+
const missing = [];
|
|
1749
|
+
if (!targets.length) {
|
|
1750
|
+
return { applied: false, schools: [], selected: [], missing: [] };
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
for (const school of targets) {
|
|
1754
|
+
const candidates = await listTextCandidates(client, frameNodeId, RECRUIT_SEARCH_SELECTORS.schoolItem);
|
|
1755
|
+
const candidate = chooseRecruitSchoolCandidate(candidates, school);
|
|
1756
|
+
if (!candidate) {
|
|
1757
|
+
missing.push({
|
|
1758
|
+
school,
|
|
1759
|
+
exact_labels: buildRecruitSchoolSearchLabels(school),
|
|
1760
|
+
discovered: candidates.map((item) => item.text).slice(0, 20)
|
|
1761
|
+
});
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
const clickable = await findClickableDescendant(client, candidate.node_id, RECRUIT_SEARCH_SELECTORS.schoolClickable);
|
|
1766
|
+
let clickableActive = candidate.active;
|
|
1767
|
+
if (clickable.node_id !== candidate.node_id) {
|
|
1768
|
+
const clickableCandidate = await readTextCandidate(client, clickable.node_id, {
|
|
1769
|
+
selector: clickable.selector || "",
|
|
1770
|
+
index: 0
|
|
1771
|
+
});
|
|
1772
|
+
clickableActive = clickableActive || clickableCandidate.active;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
let box = null;
|
|
1776
|
+
if (!clickableActive) {
|
|
1777
|
+
box = await clickNodeCenter(client, clickable.node_id, {
|
|
1778
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
1779
|
+
scrollIntoView: true
|
|
1780
|
+
});
|
|
1781
|
+
await sleep(350);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
applied.push({
|
|
1785
|
+
school,
|
|
1786
|
+
exact_labels: buildRecruitSchoolSearchLabels(school),
|
|
1787
|
+
selected_label: candidate.text,
|
|
1788
|
+
selected_node_id: candidate.node_id,
|
|
1789
|
+
clickable_node_id: clickable.node_id,
|
|
1790
|
+
clickable_selector: clickable.selector,
|
|
1791
|
+
was_active: clickableActive,
|
|
1792
|
+
clicked: !clickableActive,
|
|
1793
|
+
box
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
if (missing.length) {
|
|
1798
|
+
throw new Error(`Recruit school options were not found: ${missing.map((item) => item.school).join(", ")}`);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
return {
|
|
1802
|
+
applied: true,
|
|
1803
|
+
schools: targets,
|
|
1804
|
+
selected: applied,
|
|
1805
|
+
missing
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
async function findFirstRecruitSearchNode(client, rootNodeId, selectors = []) {
|
|
1810
|
+
const errors = [];
|
|
1811
|
+
for (const selector of selectors) {
|
|
1812
|
+
const nodeIds = uniqueNodeIds(await querySelectorAll(client, rootNodeId, selector));
|
|
1813
|
+
for (const nodeId of nodeIds) {
|
|
1814
|
+
try {
|
|
1815
|
+
const box = await getNodeBox(client, nodeId);
|
|
1816
|
+
if (!isVisibleBox(box)) continue;
|
|
1817
|
+
return { node_id: nodeId, selector, box };
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
errors.push({
|
|
1820
|
+
selector,
|
|
1821
|
+
node_id: nodeId,
|
|
1822
|
+
error: error?.message || String(error)
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return { node_id: 0, selector: "", box: null, errors };
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function parseRecruitExperienceHiddenValue(rawValue) {
|
|
1831
|
+
const [startRaw, endRaw] = String(rawValue || "").split(",");
|
|
1832
|
+
const startValue = Number.parseInt(startRaw, 10);
|
|
1833
|
+
const endValue = Number.parseInt(endRaw, 10);
|
|
1834
|
+
return {
|
|
1835
|
+
raw_value: String(rawValue || ""),
|
|
1836
|
+
start_value: Number.isFinite(startValue) ? startValue : null,
|
|
1837
|
+
end_value: Number.isFinite(endValue) ? endValue : null
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
async function readRecruitExperienceCustomState(client, frameNodeId) {
|
|
1842
|
+
let hidden = null;
|
|
1843
|
+
for (const selector of RECRUIT_SEARCH_SELECTORS.experienceCustomHiddenInput) {
|
|
1844
|
+
const nodeId = await querySelector(client, frameNodeId, selector);
|
|
1845
|
+
if (!nodeId) continue;
|
|
1846
|
+
const attributes = await getAttributesMap(client, nodeId);
|
|
1847
|
+
hidden = {
|
|
1848
|
+
node_id: nodeId,
|
|
1849
|
+
selector,
|
|
1850
|
+
value: attributes.value || ""
|
|
1851
|
+
};
|
|
1852
|
+
break;
|
|
1853
|
+
}
|
|
1854
|
+
const parsedHidden = parseRecruitExperienceHiddenValue(hidden?.value || "");
|
|
1855
|
+
const handleNodes = [];
|
|
1856
|
+
for (const selector of RECRUIT_SEARCH_SELECTORS.experienceCustomSliderHandle) {
|
|
1857
|
+
const nodeIds = uniqueNodeIds(await querySelectorAll(client, frameNodeId, selector));
|
|
1858
|
+
for (const nodeId of nodeIds) {
|
|
1859
|
+
if (handleNodes.some((item) => item.node_id === nodeId)) continue;
|
|
1860
|
+
try {
|
|
1861
|
+
const box = await getNodeBox(client, nodeId);
|
|
1862
|
+
if (!isVisibleBox(box)) continue;
|
|
1863
|
+
handleNodes.push({ node_id: nodeId, selector, box });
|
|
1864
|
+
} catch {
|
|
1865
|
+
// Ignore invisible handles; missing handles are reported by the caller.
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
handleNodes.sort((left, right) => left.box.center.x - right.box.center.x);
|
|
1870
|
+
return {
|
|
1871
|
+
hidden,
|
|
1872
|
+
...parsedHidden,
|
|
1873
|
+
handles: handleNodes.map((item, index) => ({
|
|
1874
|
+
index,
|
|
1875
|
+
node_id: item.node_id,
|
|
1876
|
+
selector: item.selector,
|
|
1877
|
+
center: item.box.center,
|
|
1878
|
+
rect: item.box.rect
|
|
1879
|
+
}))
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
async function readRecruitExperienceFixedOptionState(client, frameNodeId) {
|
|
1884
|
+
const candidates = await listTextCandidates(client, frameNodeId, RECRUIT_SEARCH_SELECTORS.experienceOption);
|
|
1885
|
+
return {
|
|
1886
|
+
active_labels: candidates.filter((item) => item.active).map((item) => item.text),
|
|
1887
|
+
options: summarizeTextCandidates(candidates, 20)
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
async function dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
1892
|
+
handleIndex,
|
|
1893
|
+
targetValue
|
|
1894
|
+
}) {
|
|
1895
|
+
const state = await readRecruitExperienceCustomState(client, frameNodeId);
|
|
1896
|
+
const handle = state.handles[handleIndex];
|
|
1897
|
+
if (!handle) {
|
|
1898
|
+
throw new Error(`Recruit experience custom slider handle was not found: index=${handleIndex}`);
|
|
1899
|
+
}
|
|
1900
|
+
const slider = await findFirstRecruitSearchNode(
|
|
1901
|
+
client,
|
|
1902
|
+
frameNodeId,
|
|
1903
|
+
RECRUIT_SEARCH_SELECTORS.experienceCustomSlider
|
|
1904
|
+
);
|
|
1905
|
+
let trackRect = slider.box?.rect || null;
|
|
1906
|
+
if (!trackRect && state.handles.length >= 2 && state.start_value !== null && state.end_value !== null && state.end_value !== state.start_value) {
|
|
1907
|
+
const leftHandle = state.handles[0];
|
|
1908
|
+
const rightHandle = state.handles[state.handles.length - 1];
|
|
1909
|
+
const valueSpan = state.end_value - state.start_value;
|
|
1910
|
+
const fullValueSpan = EXPERIENCE_CUSTOM_MAX_VALUE - EXPERIENCE_CUSTOM_MIN_VALUE;
|
|
1911
|
+
const inferredWidth = Math.abs(rightHandle.center.x - leftHandle.center.x) * (fullValueSpan / valueSpan);
|
|
1912
|
+
const inferredX = leftHandle.center.x - inferredWidth * ((state.start_value - EXPERIENCE_CUSTOM_MIN_VALUE) / fullValueSpan);
|
|
1913
|
+
trackRect = {
|
|
1914
|
+
x: inferredX,
|
|
1915
|
+
y: Math.min(leftHandle.rect.y, rightHandle.rect.y),
|
|
1916
|
+
width: inferredWidth,
|
|
1917
|
+
height: Math.max(leftHandle.rect.height, rightHandle.rect.height)
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
if (!trackRect) {
|
|
1921
|
+
throw new Error("Recruit experience custom slider was not found");
|
|
1922
|
+
}
|
|
1923
|
+
const percent = (targetValue - EXPERIENCE_CUSTOM_MIN_VALUE)
|
|
1924
|
+
/ (EXPERIENCE_CUSTOM_MAX_VALUE - EXPERIENCE_CUSTOM_MIN_VALUE);
|
|
1925
|
+
let targetX = trackRect.x + trackRect.width * Math.min(1, Math.max(0, percent));
|
|
1926
|
+
const endpointOvershootPx = Math.min(24, Math.max(8, trackRect.width * 0.04));
|
|
1927
|
+
const valueStepPx = trackRect.width / (EXPERIENCE_CUSTOM_MAX_VALUE - EXPERIENCE_CUSTOM_MIN_VALUE);
|
|
1928
|
+
if (targetValue > EXPERIENCE_CUSTOM_MIN_VALUE && targetValue < EXPERIENCE_CUSTOM_MAX_VALUE) {
|
|
1929
|
+
targetX += valueStepPx * 0.45;
|
|
1930
|
+
}
|
|
1931
|
+
if (targetValue === EXPERIENCE_CUSTOM_MIN_VALUE) targetX -= endpointOvershootPx;
|
|
1932
|
+
if (targetValue === EXPERIENCE_CUSTOM_MAX_VALUE) targetX += endpointOvershootPx;
|
|
1933
|
+
const targetY = handle.center.y || (trackRect.y + trackRect.height / 2);
|
|
1934
|
+
const startX = handle.center.x;
|
|
1935
|
+
const startY = handle.center.y;
|
|
1936
|
+
const steps = 8;
|
|
1937
|
+
await client.Input.dispatchMouseEvent({ type: "mouseMoved", x: startX, y: startY, button: "none" });
|
|
1938
|
+
await client.Input.dispatchMouseEvent({ type: "mousePressed", x: startX, y: startY, button: "left", clickCount: 1 });
|
|
1939
|
+
for (let step = 1; step <= steps; step += 1) {
|
|
1940
|
+
const ratio = step / steps;
|
|
1941
|
+
await client.Input.dispatchMouseEvent({
|
|
1942
|
+
type: "mouseMoved",
|
|
1943
|
+
x: startX + (targetX - startX) * ratio,
|
|
1944
|
+
y: startY + (targetY - startY) * ratio,
|
|
1945
|
+
button: "left"
|
|
1946
|
+
});
|
|
1947
|
+
await sleep(30);
|
|
1948
|
+
}
|
|
1949
|
+
await client.Input.dispatchMouseEvent({ type: "mouseReleased", x: targetX, y: targetY, button: "left", clickCount: 1 });
|
|
1950
|
+
await sleep(350);
|
|
1951
|
+
return {
|
|
1952
|
+
handle_index: handleIndex,
|
|
1953
|
+
target_value: targetValue,
|
|
1954
|
+
target_label: EXPERIENCE_CUSTOM_LABELS_BY_VALUE.get(targetValue) || String(targetValue),
|
|
1955
|
+
slider_node_id: slider.node_id || null,
|
|
1956
|
+
handle_node_id: handle.node_id,
|
|
1957
|
+
start: { x: startX, y: startY },
|
|
1958
|
+
target: { x: targetX, y: targetY },
|
|
1959
|
+
track: trackRect,
|
|
1960
|
+
inferred_track: !slider.box
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
async function nudgeRecruitExperienceCustomSelection(client, frameNodeId, filter) {
|
|
1965
|
+
if (filter.end_value > filter.start_value) {
|
|
1966
|
+
const intermediate = filter.end_value === EXPERIENCE_CUSTOM_MAX_VALUE
|
|
1967
|
+
? filter.end_value - 1
|
|
1968
|
+
: filter.end_value + 1;
|
|
1969
|
+
return [
|
|
1970
|
+
await dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
1971
|
+
handleIndex: 1,
|
|
1972
|
+
targetValue: intermediate
|
|
1973
|
+
}),
|
|
1974
|
+
await dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
1975
|
+
handleIndex: 1,
|
|
1976
|
+
targetValue: filter.end_value
|
|
1977
|
+
})
|
|
1978
|
+
];
|
|
1979
|
+
}
|
|
1980
|
+
const intermediate = filter.start_value === EXPERIENCE_CUSTOM_MIN_VALUE
|
|
1981
|
+
? filter.start_value + 1
|
|
1982
|
+
: filter.start_value - 1;
|
|
1983
|
+
return [
|
|
1984
|
+
await dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
1985
|
+
handleIndex: 0,
|
|
1986
|
+
targetValue: intermediate
|
|
1987
|
+
}),
|
|
1988
|
+
await dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
1989
|
+
handleIndex: 0,
|
|
1990
|
+
targetValue: filter.start_value
|
|
1991
|
+
})
|
|
1992
|
+
];
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
export async function setRecruitExperience(client, frameNodeId, experience) {
|
|
1996
|
+
const filter = normalizeRecruitExperienceFilter(experience);
|
|
1997
|
+
if (!filter) {
|
|
1998
|
+
return { applied: false, reason: "not_requested" };
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
if (filter.mode === "option") {
|
|
2002
|
+
const { candidate, candidates } = await findTextCandidate(
|
|
2003
|
+
client,
|
|
2004
|
+
frameNodeId,
|
|
2005
|
+
RECRUIT_SEARCH_SELECTORS.experienceOption,
|
|
2006
|
+
filter.label,
|
|
2007
|
+
{ match: "exact" }
|
|
2008
|
+
);
|
|
2009
|
+
if (!candidate) {
|
|
2010
|
+
throw new Error(`Recruit experience option was not found: ${filter.label}`);
|
|
2011
|
+
}
|
|
2012
|
+
let box = null;
|
|
2013
|
+
if (!candidate.active) {
|
|
2014
|
+
box = await clickNodeCenter(client, candidate.node_id, {
|
|
2015
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
2016
|
+
scrollIntoView: true
|
|
2017
|
+
});
|
|
2018
|
+
await sleep(500);
|
|
2019
|
+
}
|
|
2020
|
+
return {
|
|
2021
|
+
applied: true,
|
|
2022
|
+
mode: "option",
|
|
2023
|
+
requested_experience: experience,
|
|
2024
|
+
selected_label: candidate.text,
|
|
2025
|
+
selected_node_id: candidate.node_id,
|
|
2026
|
+
was_active: candidate.active,
|
|
2027
|
+
clicked: !candidate.active,
|
|
2028
|
+
box,
|
|
2029
|
+
discovered_options: summarizeTextCandidates(candidates, 20)
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
const customClick = await clickFirstNodeBySelectors(
|
|
2034
|
+
client,
|
|
2035
|
+
frameNodeId,
|
|
2036
|
+
RECRUIT_SEARCH_SELECTORS.experienceCustom,
|
|
2037
|
+
{ optional: false, scrollIntoView: true }
|
|
2038
|
+
);
|
|
2039
|
+
const before = await readRecruitExperienceCustomState(client, frameNodeId);
|
|
2040
|
+
const fixedOptionsBefore = await readRecruitExperienceFixedOptionState(client, frameNodeId);
|
|
2041
|
+
const drags = [];
|
|
2042
|
+
if (before.start_value !== filter.start_value) {
|
|
2043
|
+
drags.push(await dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
2044
|
+
handleIndex: 0,
|
|
2045
|
+
targetValue: filter.start_value
|
|
2046
|
+
}));
|
|
2047
|
+
}
|
|
2048
|
+
const afterStart = await readRecruitExperienceCustomState(client, frameNodeId);
|
|
2049
|
+
if (afterStart.end_value !== filter.end_value) {
|
|
2050
|
+
drags.push(await dragRecruitExperienceSliderHandle(client, frameNodeId, {
|
|
2051
|
+
handleIndex: 1,
|
|
2052
|
+
targetValue: filter.end_value
|
|
2053
|
+
}));
|
|
2054
|
+
}
|
|
2055
|
+
if (!drags.length && fixedOptionsBefore.active_labels.length) {
|
|
2056
|
+
drags.push(...await nudgeRecruitExperienceCustomSelection(client, frameNodeId, filter));
|
|
2057
|
+
}
|
|
2058
|
+
const after = await readRecruitExperienceCustomState(client, frameNodeId);
|
|
2059
|
+
const fixedOptionsAfter = await readRecruitExperienceFixedOptionState(client, frameNodeId);
|
|
2060
|
+
const verified = after.start_value === filter.start_value && after.end_value === filter.end_value;
|
|
2061
|
+
if (!verified) {
|
|
2062
|
+
throw new Error(
|
|
2063
|
+
`Recruit experience custom range did not stick: requested=${filter.start_value},${filter.end_value}; actual=${after.raw_value || "unknown"}`
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
if (fixedOptionsAfter.active_labels.length) {
|
|
2067
|
+
throw new Error(
|
|
2068
|
+
`Recruit experience custom range still has fixed option active: ${fixedOptionsAfter.active_labels.join(", ")}`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
return {
|
|
2072
|
+
applied: true,
|
|
2073
|
+
mode: "custom",
|
|
2074
|
+
requested_experience: experience,
|
|
2075
|
+
requested_range: {
|
|
2076
|
+
start_label: filter.start_label,
|
|
2077
|
+
end_label: filter.end_label,
|
|
2078
|
+
start_value: filter.start_value,
|
|
2079
|
+
end_value: filter.end_value
|
|
2080
|
+
},
|
|
2081
|
+
custom_click: customClick,
|
|
2082
|
+
fixed_options_before: fixedOptionsBefore,
|
|
2083
|
+
before,
|
|
2084
|
+
after,
|
|
2085
|
+
fixed_options_after: fixedOptionsAfter,
|
|
2086
|
+
drags,
|
|
2087
|
+
verification: {
|
|
2088
|
+
verified,
|
|
2089
|
+
fixed_option_cleared: fixedOptionsAfter.active_labels.length === 0,
|
|
2090
|
+
expected: `${filter.start_value},${filter.end_value}`,
|
|
2091
|
+
actual: after.raw_value
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
async function findRecruitGenderDropdown(client, frameNodeId) {
|
|
2097
|
+
const candidates = await listTextCandidates(client, frameNodeId, RECRUIT_SEARCH_SELECTORS.genderDropdown, {
|
|
2098
|
+
includeBox: true
|
|
2099
|
+
});
|
|
2100
|
+
const visible = candidates
|
|
2101
|
+
.filter((item) => item.visible && item.rect)
|
|
2102
|
+
.sort((left, right) => left.rect.x - right.rect.x);
|
|
2103
|
+
return {
|
|
2104
|
+
candidate: visible[0] || null,
|
|
2105
|
+
candidates
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
async function readRecruitGenderState(client, frameNodeId) {
|
|
2110
|
+
const { candidate, candidates } = await findRecruitGenderDropdown(client, frameNodeId);
|
|
2111
|
+
if (!candidate) {
|
|
2112
|
+
return {
|
|
2113
|
+
available: false,
|
|
2114
|
+
discovered: summarizeTextCandidates(candidates, 10)
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
const hiddenNodeId = await querySelector(client, candidate.node_id, "input[type='hidden']");
|
|
2118
|
+
const hidden = hiddenNodeId ? await getAttributesMap(client, hiddenNodeId) : {};
|
|
2119
|
+
const hiddenSelectedLabel = hidden.value === "-1" ? "不限" : "";
|
|
2120
|
+
return {
|
|
2121
|
+
available: true,
|
|
2122
|
+
selected_label: hiddenSelectedLabel || normalizeText(candidate.text),
|
|
2123
|
+
selected_node_id: candidate.node_id,
|
|
2124
|
+
hidden_value: hidden.value || "",
|
|
2125
|
+
discovered: summarizeTextCandidates(candidates, 10)
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
export async function setRecruitGender(client, frameNodeId, gender) {
|
|
2130
|
+
const filter = normalizeRecruitGenderFilter(gender);
|
|
2131
|
+
if (!filter) {
|
|
2132
|
+
return { applied: false, reason: "not_requested" };
|
|
2133
|
+
}
|
|
2134
|
+
const beforeRoot = await findRecruitGenderDropdown(client, frameNodeId);
|
|
2135
|
+
if (!beforeRoot.candidate) {
|
|
2136
|
+
throw new Error("Recruit gender dropdown was not found");
|
|
2137
|
+
}
|
|
2138
|
+
const openBox = await clickNodeCenter(client, beforeRoot.candidate.node_id, {
|
|
2139
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
2140
|
+
scrollIntoView: true
|
|
2141
|
+
});
|
|
2142
|
+
await sleep(350);
|
|
2143
|
+
const rootAfterOpen = await findRecruitGenderDropdown(client, frameNodeId);
|
|
2144
|
+
const rootNodeId = rootAfterOpen.candidate?.node_id || beforeRoot.candidate.node_id;
|
|
2145
|
+
const options = await listTextCandidates(client, rootNodeId, ["li"], { includeBox: true });
|
|
2146
|
+
const option = chooseRecruitTextCandidate(options, { label: filter.label, match: "exact" });
|
|
2147
|
+
if (!option) {
|
|
2148
|
+
throw new Error(`Recruit gender option was not found: ${filter.label}`);
|
|
2149
|
+
}
|
|
2150
|
+
let selectBox = null;
|
|
2151
|
+
if (!option.active) {
|
|
2152
|
+
selectBox = await clickNodeCenter(client, option.node_id, {
|
|
575
2153
|
...DETERMINISTIC_CLICK_OPTIONS,
|
|
576
2154
|
scrollIntoView: true
|
|
577
2155
|
});
|
|
578
|
-
await sleep(
|
|
2156
|
+
await sleep(600);
|
|
2157
|
+
} else {
|
|
2158
|
+
await pressKey(client, "Escape", {
|
|
2159
|
+
code: "Escape",
|
|
2160
|
+
windowsVirtualKeyCode: 27,
|
|
2161
|
+
nativeVirtualKeyCode: 27
|
|
2162
|
+
});
|
|
2163
|
+
await sleep(250);
|
|
2164
|
+
}
|
|
2165
|
+
const after = await readRecruitGenderState(client, frameNodeId);
|
|
2166
|
+
const verified = after.selected_label === filter.label
|
|
2167
|
+
|| (filter.label === "不限" && /^(?:性别|不限)$/.test(after.selected_label));
|
|
2168
|
+
if (!verified) {
|
|
2169
|
+
throw new Error(`Recruit gender selection did not stick: requested=${filter.label}; actual=${after.selected_label || "unknown"}`);
|
|
2170
|
+
}
|
|
2171
|
+
return {
|
|
2172
|
+
applied: true,
|
|
2173
|
+
requested_gender: gender,
|
|
2174
|
+
selected_label: filter.label,
|
|
2175
|
+
opened_dropdown: {
|
|
2176
|
+
node_id: beforeRoot.candidate.node_id,
|
|
2177
|
+
box: openBox
|
|
2178
|
+
},
|
|
2179
|
+
selected_node_id: option.node_id,
|
|
2180
|
+
option_was_active: option.active,
|
|
2181
|
+
clicked: !option.active,
|
|
2182
|
+
box: selectBox,
|
|
2183
|
+
verification: {
|
|
2184
|
+
verified,
|
|
2185
|
+
selected_label: after.selected_label,
|
|
2186
|
+
hidden_value: after.hidden_value
|
|
2187
|
+
},
|
|
2188
|
+
discovered_options: summarizeTextCandidates(options, 10)
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
async function readRecruitAgeCustomState(client, frameNodeId) {
|
|
2193
|
+
const inputNodeIds = uniqueNodeIds(await querySelectorAll(
|
|
2194
|
+
client,
|
|
2195
|
+
frameNodeId,
|
|
2196
|
+
RECRUIT_SEARCH_SELECTORS.ageCustomInput.join(", ")
|
|
2197
|
+
));
|
|
2198
|
+
const inputs = [];
|
|
2199
|
+
for (const nodeId of inputNodeIds) {
|
|
2200
|
+
const attributes = await getAttributesMap(client, nodeId);
|
|
2201
|
+
let box = null;
|
|
2202
|
+
try {
|
|
2203
|
+
box = await getNodeBox(client, nodeId);
|
|
2204
|
+
} catch {}
|
|
2205
|
+
inputs.push({
|
|
2206
|
+
node_id: nodeId,
|
|
2207
|
+
type: attributes.type || "",
|
|
2208
|
+
value: attributes.value || "",
|
|
2209
|
+
placeholder: attributes.placeholder || "",
|
|
2210
|
+
visible: isVisibleBox(box),
|
|
2211
|
+
rect: box?.rect || null
|
|
2212
|
+
});
|
|
579
2213
|
}
|
|
2214
|
+
const hidden = inputs.filter((item) => item.type === "hidden");
|
|
580
2215
|
return {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
search_terms: lookup.search_terms,
|
|
586
|
-
selected_node_id: lookup.candidate.node_id,
|
|
587
|
-
was_active: lookup.candidate.active,
|
|
588
|
-
clicked: !lookup.candidate.active,
|
|
589
|
-
box,
|
|
590
|
-
discovered_options: summarizeTextCandidates(lookup.candidates, 30)
|
|
2216
|
+
inputs,
|
|
2217
|
+
min: parseRecruitAgeCustomHiddenValue(hidden[0]?.value),
|
|
2218
|
+
max: parseRecruitAgeCustomHiddenValue(hidden[1]?.value),
|
|
2219
|
+
raw_values: hidden.map((item) => item.value)
|
|
591
2220
|
};
|
|
592
2221
|
}
|
|
593
2222
|
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
2223
|
+
async function readRecruitAgeFixedOptionState(client, frameNodeId) {
|
|
2224
|
+
const candidates = await listTextCandidates(client, frameNodeId, RECRUIT_SEARCH_SELECTORS.ageOption);
|
|
2225
|
+
return {
|
|
2226
|
+
active_labels: candidates.filter((item) => item.active).map((item) => item.text),
|
|
2227
|
+
options: summarizeTextCandidates(candidates, 20)
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
function ageCustomOptionLabel(value) {
|
|
2232
|
+
if (value === null || value === undefined) return "不限";
|
|
2233
|
+
return `${value}岁`;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
function parseRecruitAgeCustomHiddenValue(value) {
|
|
2237
|
+
const text = normalizeText(value);
|
|
2238
|
+
if (!text || text === "0" || text === "-1") return null;
|
|
2239
|
+
return parseAgeNumber(text, null);
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
async function selectRecruitAgeCustomDropdownValue(client, frameNodeId, {
|
|
2243
|
+
dropdownIndex,
|
|
2244
|
+
value
|
|
2245
|
+
}) {
|
|
2246
|
+
const dropdownNodeIds = uniqueNodeIds(await querySelectorAll(
|
|
600
2247
|
client,
|
|
601
2248
|
frameNodeId,
|
|
602
|
-
RECRUIT_SEARCH_SELECTORS.
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
)
|
|
606
|
-
|
|
607
|
-
throw new Error(`Recruit degree option was not found: ${degreeLabel}`);
|
|
2249
|
+
RECRUIT_SEARCH_SELECTORS.ageCustomDropdown.join(", ")
|
|
2250
|
+
));
|
|
2251
|
+
const dropdownNodeId = dropdownNodeIds[dropdownIndex];
|
|
2252
|
+
if (!dropdownNodeId) {
|
|
2253
|
+
throw new Error(`Recruit age custom dropdown was not found: index=${dropdownIndex}`);
|
|
608
2254
|
}
|
|
609
|
-
const
|
|
2255
|
+
const openBox = await clickNodeCenter(client, dropdownNodeId, {
|
|
610
2256
|
...DETERMINISTIC_CLICK_OPTIONS,
|
|
611
2257
|
scrollIntoView: true
|
|
612
2258
|
});
|
|
613
2259
|
await sleep(350);
|
|
2260
|
+
const label = ageCustomOptionLabel(value);
|
|
2261
|
+
const options = await listTextCandidates(client, frameNodeId, RECRUIT_SEARCH_SELECTORS.ageCustomOption, {
|
|
2262
|
+
includeBox: true
|
|
2263
|
+
});
|
|
2264
|
+
const option = chooseRecruitTextCandidate(options, { label, match: "exact" });
|
|
2265
|
+
if (!option) {
|
|
2266
|
+
throw new Error(`Recruit age custom option was not found: ${label}`);
|
|
2267
|
+
}
|
|
2268
|
+
const box = await clickNodeCenter(client, option.node_id, {
|
|
2269
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
2270
|
+
scrollIntoView: true
|
|
2271
|
+
});
|
|
2272
|
+
await sleep(600);
|
|
614
2273
|
return {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
selected_label:
|
|
618
|
-
|
|
619
|
-
|
|
2274
|
+
dropdown_index: dropdownIndex,
|
|
2275
|
+
requested_value: value,
|
|
2276
|
+
selected_label: option.text,
|
|
2277
|
+
dropdown_node_id: dropdownNodeId,
|
|
2278
|
+
option_node_id: option.node_id,
|
|
2279
|
+
open_box: openBox,
|
|
620
2280
|
box,
|
|
621
|
-
discovered_options:
|
|
622
|
-
label: item.text,
|
|
623
|
-
active: item.active,
|
|
624
|
-
node_id: item.node_id,
|
|
625
|
-
selector: item.selector
|
|
626
|
-
}))
|
|
2281
|
+
discovered_options: summarizeTextCandidates(options, 40)
|
|
627
2282
|
};
|
|
628
2283
|
}
|
|
629
2284
|
|
|
630
|
-
export async function
|
|
631
|
-
const
|
|
632
|
-
if (!
|
|
633
|
-
return { applied: false, reason: "
|
|
2285
|
+
export async function setRecruitAge(client, frameNodeId, age) {
|
|
2286
|
+
const filter = normalizeRecruitAgeFilter(age);
|
|
2287
|
+
if (!filter) {
|
|
2288
|
+
return { applied: false, reason: "not_requested" };
|
|
634
2289
|
}
|
|
635
2290
|
|
|
636
|
-
|
|
637
|
-
let discoveredOptions = [];
|
|
638
|
-
for (const label of labels) {
|
|
2291
|
+
if (filter.mode === "option") {
|
|
639
2292
|
const { candidate, candidates } = await findTextCandidate(
|
|
640
2293
|
client,
|
|
641
2294
|
frameNodeId,
|
|
642
|
-
RECRUIT_SEARCH_SELECTORS.
|
|
643
|
-
label,
|
|
644
|
-
{ match: "
|
|
2295
|
+
RECRUIT_SEARCH_SELECTORS.ageOption,
|
|
2296
|
+
filter.label,
|
|
2297
|
+
{ match: "exact" }
|
|
645
2298
|
);
|
|
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
2299
|
if (!candidate) {
|
|
653
|
-
throw new Error(`Recruit
|
|
2300
|
+
throw new Error(`Recruit age option was not found: ${filter.label}`);
|
|
654
2301
|
}
|
|
655
|
-
|
|
656
2302
|
let box = null;
|
|
657
2303
|
if (!candidate.active) {
|
|
658
2304
|
box = await clickNodeCenter(client, candidate.node_id, {
|
|
659
2305
|
...DETERMINISTIC_CLICK_OPTIONS,
|
|
660
2306
|
scrollIntoView: true
|
|
661
2307
|
});
|
|
662
|
-
await sleep(
|
|
2308
|
+
await sleep(500);
|
|
663
2309
|
}
|
|
664
|
-
|
|
665
|
-
|
|
2310
|
+
return {
|
|
2311
|
+
applied: true,
|
|
2312
|
+
mode: "option",
|
|
2313
|
+
requested_age: age,
|
|
666
2314
|
selected_label: candidate.text,
|
|
667
2315
|
selected_node_id: candidate.node_id,
|
|
668
2316
|
was_active: candidate.active,
|
|
669
2317
|
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: [] };
|
|
2318
|
+
box,
|
|
2319
|
+
discovered_options: summarizeTextCandidates(candidates, 20)
|
|
2320
|
+
};
|
|
696
2321
|
}
|
|
697
2322
|
|
|
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
|
-
});
|
|
2323
|
+
const customClick = await clickFirstNodeBySelectors(
|
|
2324
|
+
client,
|
|
2325
|
+
frameNodeId,
|
|
2326
|
+
RECRUIT_SEARCH_SELECTORS.ageCustom,
|
|
2327
|
+
{ optional: false, scrollIntoView: true }
|
|
2328
|
+
);
|
|
2329
|
+
const before = await readRecruitAgeCustomState(client, frameNodeId);
|
|
2330
|
+
const fixedBefore = await readRecruitAgeFixedOptionState(client, frameNodeId);
|
|
2331
|
+
const selected = [];
|
|
2332
|
+
selected.push(await selectRecruitAgeCustomDropdownValue(client, frameNodeId, {
|
|
2333
|
+
dropdownIndex: 0,
|
|
2334
|
+
value: filter.min
|
|
2335
|
+
}));
|
|
2336
|
+
selected.push(await selectRecruitAgeCustomDropdownValue(client, frameNodeId, {
|
|
2337
|
+
dropdownIndex: 1,
|
|
2338
|
+
value: filter.max
|
|
2339
|
+
}));
|
|
2340
|
+
const after = await readRecruitAgeCustomState(client, frameNodeId);
|
|
2341
|
+
const fixedAfter = await readRecruitAgeFixedOptionState(client, frameNodeId);
|
|
2342
|
+
const verified = after.min === filter.min && after.max === filter.max;
|
|
2343
|
+
if (!verified) {
|
|
2344
|
+
throw new Error(`Recruit age custom values did not stick: requested=${filter.min},${filter.max}; actual=${after.raw_values.join(",")}`);
|
|
743
2345
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
throw new Error(`Recruit school options were not found: ${missing.map((item) => item.school).join(", ")}`);
|
|
2346
|
+
if (fixedAfter.active_labels.length) {
|
|
2347
|
+
throw new Error(`Recruit age custom still has fixed option active: ${fixedAfter.active_labels.join(", ")}`);
|
|
747
2348
|
}
|
|
748
|
-
|
|
749
2349
|
return {
|
|
750
2350
|
applied: true,
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
2351
|
+
mode: "custom",
|
|
2352
|
+
requested_age: age,
|
|
2353
|
+
requested_range: {
|
|
2354
|
+
min: filter.min,
|
|
2355
|
+
max: filter.max
|
|
2356
|
+
},
|
|
2357
|
+
custom_click: customClick,
|
|
2358
|
+
before,
|
|
2359
|
+
after,
|
|
2360
|
+
fixed_options_before: fixedBefore,
|
|
2361
|
+
fixed_options_after: fixedAfter,
|
|
2362
|
+
selected,
|
|
2363
|
+
verification: {
|
|
2364
|
+
verified,
|
|
2365
|
+
fixed_option_cleared: fixedAfter.active_labels.length === 0,
|
|
2366
|
+
expected: [filter.min, filter.max],
|
|
2367
|
+
actual: [after.min, after.max]
|
|
2368
|
+
}
|
|
754
2369
|
};
|
|
755
2370
|
}
|
|
756
2371
|
|
|
757
|
-
|
|
2372
|
+
async function setRecruitCheckboxFilter(client, frameNodeId, enabled, {
|
|
2373
|
+
selectors,
|
|
2374
|
+
label,
|
|
2375
|
+
errorLabel
|
|
2376
|
+
} = {}) {
|
|
758
2377
|
if (typeof enabled !== "boolean") {
|
|
759
2378
|
return { applied: false, reason: "not_requested" };
|
|
760
2379
|
}
|
|
761
2380
|
const { candidate, candidates } = await findTextCandidate(
|
|
762
2381
|
client,
|
|
763
2382
|
frameNodeId,
|
|
764
|
-
|
|
765
|
-
|
|
2383
|
+
selectors,
|
|
2384
|
+
label,
|
|
766
2385
|
{ match: "contains" }
|
|
767
2386
|
);
|
|
768
2387
|
if (!candidate) {
|
|
769
|
-
throw new Error("Recruit
|
|
2388
|
+
throw new Error(`${errorLabel || "Recruit checkbox filter"} was not found`);
|
|
770
2389
|
}
|
|
771
2390
|
|
|
772
2391
|
let box = null;
|
|
@@ -795,12 +2414,66 @@ export async function setRecruitRecentViewedFilter(client, frameNodeId, enabled)
|
|
|
795
2414
|
};
|
|
796
2415
|
}
|
|
797
2416
|
|
|
2417
|
+
export async function setRecruitRecentViewedFilter(client, frameNodeId, enabled) {
|
|
2418
|
+
return setRecruitCheckboxFilter(client, frameNodeId, enabled, {
|
|
2419
|
+
selectors: RECRUIT_SEARCH_SELECTORS.recentViewedLabel,
|
|
2420
|
+
label: "过滤近14天查看",
|
|
2421
|
+
errorLabel: "Recruit recent-viewed filter"
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
export async function setRecruitExchangeResumeFilter(client, frameNodeId, enabled) {
|
|
2426
|
+
return setRecruitCheckboxFilter(client, frameNodeId, enabled, {
|
|
2427
|
+
selectors: RECRUIT_SEARCH_SELECTORS.exchangeResumeLabel,
|
|
2428
|
+
label: "近30天未和同事交换简历",
|
|
2429
|
+
errorLabel: "Recruit exchange-resume filter"
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
async function openRecruitCityPicker(client, frameNodeId, {
|
|
2434
|
+
settleMs = 350
|
|
2435
|
+
} = {}) {
|
|
2436
|
+
const alreadyOpenInput = await clickFirstNodeBySelectors(
|
|
2437
|
+
client,
|
|
2438
|
+
frameNodeId,
|
|
2439
|
+
RECRUIT_SEARCH_SELECTORS.cityInput,
|
|
2440
|
+
{ optional: true }
|
|
2441
|
+
);
|
|
2442
|
+
if (alreadyOpenInput.clicked) {
|
|
2443
|
+
return {
|
|
2444
|
+
opened: true,
|
|
2445
|
+
already_open: true,
|
|
2446
|
+
input: alreadyOpenInput,
|
|
2447
|
+
trigger: null
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
const trigger = await clickFirstNodeBySelectors(
|
|
2452
|
+
client,
|
|
2453
|
+
frameNodeId,
|
|
2454
|
+
RECRUIT_SEARCH_SELECTORS.cityTrigger,
|
|
2455
|
+
{ scrollIntoView: false }
|
|
2456
|
+
);
|
|
2457
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
2458
|
+
const input = await clickFirstNodeBySelectors(
|
|
2459
|
+
client,
|
|
2460
|
+
frameNodeId,
|
|
2461
|
+
RECRUIT_SEARCH_SELECTORS.cityInput
|
|
2462
|
+
);
|
|
2463
|
+
return {
|
|
2464
|
+
opened: true,
|
|
2465
|
+
already_open: false,
|
|
2466
|
+
trigger,
|
|
2467
|
+
input
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
|
|
798
2471
|
async function selectRecruitNationalCityThroughPicker(client, frameNodeId, {
|
|
799
2472
|
requestedCity = "全国",
|
|
800
2473
|
reason = "national_city_requested",
|
|
801
2474
|
optionTimeoutMs = DEFAULT_RECRUIT_CITY_OPTION_TIMEOUT_MS
|
|
802
2475
|
} = {}) {
|
|
803
|
-
const
|
|
2476
|
+
const picker = await openRecruitCityPicker(client, frameNodeId);
|
|
804
2477
|
await clearFocusedInput(client);
|
|
805
2478
|
await sleep(500);
|
|
806
2479
|
|
|
@@ -854,7 +2527,8 @@ async function selectRecruitNationalCityThroughPicker(client, frameNodeId, {
|
|
|
854
2527
|
applied: false,
|
|
855
2528
|
reason: "national_city_popular_not_found",
|
|
856
2529
|
requested_city: requestedCity,
|
|
857
|
-
input,
|
|
2530
|
+
input: picker.input,
|
|
2531
|
+
picker,
|
|
858
2532
|
path,
|
|
859
2533
|
discovered_options: summarizeTextCandidates(popularLookup.candidates)
|
|
860
2534
|
};
|
|
@@ -892,7 +2566,8 @@ async function selectRecruitNationalCityThroughPicker(client, frameNodeId, {
|
|
|
892
2566
|
applied: false,
|
|
893
2567
|
reason: "national_city_option_not_found",
|
|
894
2568
|
requested_city: requestedCity,
|
|
895
|
-
input,
|
|
2569
|
+
input: picker.input,
|
|
2570
|
+
picker,
|
|
896
2571
|
path,
|
|
897
2572
|
discovered_options: summarizeTextCandidates(nationalLookup.candidates)
|
|
898
2573
|
};
|
|
@@ -917,7 +2592,8 @@ async function selectRecruitNationalCityThroughPicker(client, frameNodeId, {
|
|
|
917
2592
|
requested_city: requestedCity,
|
|
918
2593
|
selected_label: nationalLookup.candidate.text,
|
|
919
2594
|
selected_node_id: nationalLookup.candidate.node_id,
|
|
920
|
-
input,
|
|
2595
|
+
input: picker.input,
|
|
2596
|
+
picker,
|
|
921
2597
|
path,
|
|
922
2598
|
box: nationalBox,
|
|
923
2599
|
selection_mode: "city_picker",
|
|
@@ -972,7 +2648,7 @@ export async function setRecruitCity(client, frameNodeId, city, {
|
|
|
972
2648
|
});
|
|
973
2649
|
}
|
|
974
2650
|
|
|
975
|
-
const
|
|
2651
|
+
const picker = await openRecruitCityPicker(client, frameNodeId);
|
|
976
2652
|
await clearFocusedInput(client);
|
|
977
2653
|
await sleep(120);
|
|
978
2654
|
await insertText(client, normalizedCity);
|
|
@@ -1016,7 +2692,8 @@ export async function setRecruitCity(client, frameNodeId, city, {
|
|
|
1016
2692
|
requested_city: normalizedCity,
|
|
1017
2693
|
requested_city_not_found: true,
|
|
1018
2694
|
fallback_to_national: true,
|
|
1019
|
-
original_input: input,
|
|
2695
|
+
original_input: picker.input,
|
|
2696
|
+
picker,
|
|
1020
2697
|
elapsed_ms: Date.now() - started,
|
|
1021
2698
|
discovered_options_before_fallback: candidates.map((item) => item.text).slice(0, 20)
|
|
1022
2699
|
};
|
|
@@ -1034,7 +2711,8 @@ export async function setRecruitCity(client, frameNodeId, city, {
|
|
|
1034
2711
|
requested_city: normalizedCity,
|
|
1035
2712
|
requested_city_not_found: true,
|
|
1036
2713
|
fallback_to_national: true,
|
|
1037
|
-
original_input: input,
|
|
2714
|
+
original_input: picker.input,
|
|
2715
|
+
picker,
|
|
1038
2716
|
picker_fallback: nationalFallback,
|
|
1039
2717
|
elapsed_ms: Date.now() - started,
|
|
1040
2718
|
discovered_options_before_fallback: candidates.map((item) => item.text).slice(0, 20)
|
|
@@ -1045,7 +2723,8 @@ export async function setRecruitCity(client, frameNodeId, city, {
|
|
|
1045
2723
|
applied: false,
|
|
1046
2724
|
reason: "city_result_not_found",
|
|
1047
2725
|
city: normalizedCity,
|
|
1048
|
-
input,
|
|
2726
|
+
input: picker.input,
|
|
2727
|
+
picker,
|
|
1049
2728
|
elapsed_ms: Date.now() - started,
|
|
1050
2729
|
discovered_options: candidates.map((item) => item.text).slice(0, 20),
|
|
1051
2730
|
national_fallback: nationalFallback,
|
|
@@ -1063,7 +2742,8 @@ export async function setRecruitCity(client, frameNodeId, city, {
|
|
|
1063
2742
|
city: normalizedCity,
|
|
1064
2743
|
selected_label: candidate.text,
|
|
1065
2744
|
selected_node_id: candidate.node_id,
|
|
1066
|
-
input,
|
|
2745
|
+
input: picker.input,
|
|
2746
|
+
picker,
|
|
1067
2747
|
elapsed_ms: Date.now() - started,
|
|
1068
2748
|
box
|
|
1069
2749
|
};
|
|
@@ -1095,6 +2775,86 @@ export async function clickRecruitSearch(client, frameNodeId) {
|
|
|
1095
2775
|
};
|
|
1096
2776
|
}
|
|
1097
2777
|
|
|
2778
|
+
export async function clickRecruitSearchWithKeywordGuard(client, frameNodeId, keyword, {
|
|
2779
|
+
maxAttempts = 2,
|
|
2780
|
+
postSearchSettleMs = 2200
|
|
2781
|
+
} = {}) {
|
|
2782
|
+
const normalizedKeyword = normalizeText(keyword);
|
|
2783
|
+
if (!normalizedKeyword) {
|
|
2784
|
+
return clickRecruitSearch(client, frameNodeId);
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
const attempts = [];
|
|
2788
|
+
let currentFrameNodeId = frameNodeId;
|
|
2789
|
+
for (let attempt = 1; attempt <= Math.max(1, maxAttempts); attempt += 1) {
|
|
2790
|
+
let rootsBeforeAttempt = null;
|
|
2791
|
+
try {
|
|
2792
|
+
rootsBeforeAttempt = await getRecruitRoots(client, { requireFrame: false });
|
|
2793
|
+
if (rootsBeforeAttempt?.iframe?.documentNodeId) {
|
|
2794
|
+
currentFrameNodeId = rootsBeforeAttempt.iframe.documentNodeId;
|
|
2795
|
+
}
|
|
2796
|
+
} catch {}
|
|
2797
|
+
const before = await verifyRecruitKeywordInputValue(client, currentFrameNodeId, normalizedKeyword);
|
|
2798
|
+
let reapply = null;
|
|
2799
|
+
if (before.verified === false) {
|
|
2800
|
+
reapply = await setRecruitKeyword(client, currentFrameNodeId, normalizedKeyword);
|
|
2801
|
+
}
|
|
2802
|
+
const search = await clickRecruitSearch(client, currentFrameNodeId);
|
|
2803
|
+
let rootsAfterSearch = null;
|
|
2804
|
+
try {
|
|
2805
|
+
rootsAfterSearch = await getRecruitRoots(client, { requireFrame: false });
|
|
2806
|
+
if (rootsAfterSearch?.iframe?.documentNodeId) {
|
|
2807
|
+
currentFrameNodeId = rootsAfterSearch.iframe.documentNodeId;
|
|
2808
|
+
}
|
|
2809
|
+
} catch {}
|
|
2810
|
+
const after = await verifyRecruitKeywordInputValue(client, currentFrameNodeId, normalizedKeyword, {
|
|
2811
|
+
settleMs: postSearchSettleMs
|
|
2812
|
+
});
|
|
2813
|
+
attempts.push({
|
|
2814
|
+
attempt,
|
|
2815
|
+
before,
|
|
2816
|
+
reapply,
|
|
2817
|
+
search,
|
|
2818
|
+
after,
|
|
2819
|
+
frame_reacquired_before_attempt: rootsBeforeAttempt?.iframe?.documentNodeId
|
|
2820
|
+
? {
|
|
2821
|
+
selector: rootsBeforeAttempt.iframe.selector,
|
|
2822
|
+
document_node_id: rootsBeforeAttempt.iframe.documentNodeId
|
|
2823
|
+
}
|
|
2824
|
+
: null,
|
|
2825
|
+
frame_reacquired: rootsAfterSearch?.iframe?.documentNodeId
|
|
2826
|
+
? {
|
|
2827
|
+
selector: rootsAfterSearch.iframe.selector,
|
|
2828
|
+
document_node_id: rootsAfterSearch.iframe.documentNodeId
|
|
2829
|
+
}
|
|
2830
|
+
: null
|
|
2831
|
+
});
|
|
2832
|
+
if (after.verified !== false) {
|
|
2833
|
+
return {
|
|
2834
|
+
searched: true,
|
|
2835
|
+
mode: search.mode,
|
|
2836
|
+
search,
|
|
2837
|
+
keyword_guard: {
|
|
2838
|
+
verified: after.verified,
|
|
2839
|
+
expected: after.expected,
|
|
2840
|
+
actual: after.actual,
|
|
2841
|
+
attempts
|
|
2842
|
+
}
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
const last = attempts[attempts.length - 1]?.after || {};
|
|
2848
|
+
const error = new Error(`Recruit keyword was not preserved after search: expected=${normalizedKeyword}; actual=${last.actual || "unknown"}`);
|
|
2849
|
+
error.keyword_guard = {
|
|
2850
|
+
verified: false,
|
|
2851
|
+
expected: normalizedKeyword,
|
|
2852
|
+
actual: last.actual || "",
|
|
2853
|
+
attempts
|
|
2854
|
+
};
|
|
2855
|
+
throw error;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
1098
2858
|
export async function waitForRecruitSearchResultState(client, {
|
|
1099
2859
|
timeoutMs = DEFAULT_RECRUIT_SEARCH_TIMEOUT_MS,
|
|
1100
2860
|
intervalMs = 500
|
|
@@ -1166,70 +2926,121 @@ export async function applyRecruitSearchParams(client, {
|
|
|
1166
2926
|
const initialFrameNodeId = frameNodeId;
|
|
1167
2927
|
const beforeCounts = await getRecruitSearchCounts(client, frameNodeId);
|
|
1168
2928
|
const steps = [];
|
|
2929
|
+
const applicationStepNames = buildRecruitSearchApplicationStepNames(normalizedSearchParams);
|
|
1169
2930
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
if (
|
|
1188
|
-
const
|
|
1189
|
-
|
|
2931
|
+
for (const stepName of applicationStepNames) {
|
|
2932
|
+
if (stepName === "job_title") {
|
|
2933
|
+
steps.push({
|
|
2934
|
+
step: "job_title",
|
|
2935
|
+
result: await setRecruitJobTitle(client, frameNodeId, normalizedSearchParams.job, {
|
|
2936
|
+
optionTimeoutMs: searchTimeoutMs
|
|
2937
|
+
})
|
|
2938
|
+
});
|
|
2939
|
+
const rootsAfterJob = await getRecruitRoots(client);
|
|
2940
|
+
frameNodeId = rootsAfterJob.iframe.documentNodeId;
|
|
2941
|
+
steps.push({
|
|
2942
|
+
step: "reacquire_after_job",
|
|
2943
|
+
result: {
|
|
2944
|
+
selector: rootsAfterJob.iframe.selector,
|
|
2945
|
+
document_node_id: frameNodeId
|
|
2946
|
+
}
|
|
2947
|
+
});
|
|
2948
|
+
} else if (stepName === "city") {
|
|
2949
|
+
const cityResult = await setRecruitCity(client, frameNodeId, normalizedSearchParams.city, {
|
|
2950
|
+
optionTimeoutMs: cityOptionTimeoutMs
|
|
2951
|
+
});
|
|
2952
|
+
steps.push({
|
|
2953
|
+
step: "city",
|
|
2954
|
+
result: cityResult
|
|
2955
|
+
});
|
|
2956
|
+
if (cityResult?.reacquire_frame) {
|
|
2957
|
+
const rootsAfterCity = await getRecruitRoots(client);
|
|
2958
|
+
frameNodeId = rootsAfterCity.iframe.documentNodeId;
|
|
2959
|
+
steps.push({
|
|
2960
|
+
step: "reacquire_after_city",
|
|
2961
|
+
result: {
|
|
2962
|
+
selector: rootsAfterCity.iframe.selector,
|
|
2963
|
+
document_node_id: frameNodeId,
|
|
2964
|
+
reason: cityResult.reason
|
|
2965
|
+
}
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
} else if (stepName === "degree") {
|
|
2969
|
+
steps.push({
|
|
2970
|
+
step: "degree",
|
|
2971
|
+
result: await setRecruitDegrees(client, frameNodeId, normalizedSearchParams.degrees)
|
|
2972
|
+
});
|
|
2973
|
+
} else if (stepName === "schools") {
|
|
2974
|
+
steps.push({
|
|
2975
|
+
step: "schools",
|
|
2976
|
+
result: await setRecruitSchools(client, frameNodeId, normalizedSearchParams.schools)
|
|
2977
|
+
});
|
|
2978
|
+
} else if (stepName === "experience") {
|
|
2979
|
+
steps.push({
|
|
2980
|
+
step: "experience",
|
|
2981
|
+
result: await setRecruitExperience(client, frameNodeId, normalizedSearchParams.experience)
|
|
2982
|
+
});
|
|
2983
|
+
} else if (stepName === "gender") {
|
|
2984
|
+
steps.push({
|
|
2985
|
+
step: "gender",
|
|
2986
|
+
result: await setRecruitGender(client, frameNodeId, normalizedSearchParams.gender)
|
|
2987
|
+
});
|
|
2988
|
+
} else if (stepName === "age") {
|
|
2989
|
+
steps.push({
|
|
2990
|
+
step: "age",
|
|
2991
|
+
result: await setRecruitAge(client, frameNodeId, normalizedSearchParams.age)
|
|
2992
|
+
});
|
|
2993
|
+
} else if (stepName === "keyword") {
|
|
2994
|
+
const rootsBeforeKeyword = await getRecruitRoots(client);
|
|
2995
|
+
frameNodeId = rootsBeforeKeyword.iframe.documentNodeId;
|
|
2996
|
+
steps.push({
|
|
2997
|
+
step: "reacquire_before_keyword",
|
|
2998
|
+
result: {
|
|
2999
|
+
selector: rootsBeforeKeyword.iframe.selector,
|
|
3000
|
+
document_node_id: frameNodeId
|
|
3001
|
+
}
|
|
3002
|
+
});
|
|
3003
|
+
steps.push({
|
|
3004
|
+
step: "keyword",
|
|
3005
|
+
result: await setRecruitKeyword(client, frameNodeId, normalizedSearchParams.keyword)
|
|
3006
|
+
});
|
|
3007
|
+
} else if (stepName === "search") {
|
|
3008
|
+
const rootsBeforeSearch = await getRecruitRoots(client);
|
|
3009
|
+
frameNodeId = rootsBeforeSearch.iframe.documentNodeId;
|
|
1190
3010
|
steps.push({
|
|
1191
|
-
step: "
|
|
3011
|
+
step: "reacquire_before_search",
|
|
1192
3012
|
result: {
|
|
1193
|
-
selector:
|
|
1194
|
-
document_node_id: frameNodeId
|
|
1195
|
-
reason: cityResult.reason
|
|
3013
|
+
selector: rootsBeforeSearch.iframe.selector,
|
|
3014
|
+
document_node_id: frameNodeId
|
|
1196
3015
|
}
|
|
1197
3016
|
});
|
|
3017
|
+
steps.push({
|
|
3018
|
+
step: "search",
|
|
3019
|
+
result: await clickRecruitSearchWithKeywordGuard(client, frameNodeId, normalizedSearchParams.keyword)
|
|
3020
|
+
});
|
|
3021
|
+
} else if (stepName === "recent_viewed") {
|
|
3022
|
+
const recentFilterRoots = await getRecruitRoots(client);
|
|
3023
|
+
steps.push({
|
|
3024
|
+
step: "recent_viewed",
|
|
3025
|
+
result: await setRecruitRecentViewedFilter(
|
|
3026
|
+
client,
|
|
3027
|
+
recentFilterRoots.iframe.documentNodeId,
|
|
3028
|
+
normalizedSearchParams.filter_recent_viewed
|
|
3029
|
+
)
|
|
3030
|
+
});
|
|
3031
|
+
} else if (stepName === "exchange_resume") {
|
|
3032
|
+
const exchangeFilterRoots = await getRecruitRoots(client);
|
|
3033
|
+
steps.push({
|
|
3034
|
+
step: "exchange_resume",
|
|
3035
|
+
result: await setRecruitExchangeResumeFilter(
|
|
3036
|
+
client,
|
|
3037
|
+
exchangeFilterRoots.iframe.documentNodeId,
|
|
3038
|
+
normalizedSearchParams.skip_recent_colleague_contacted
|
|
3039
|
+
)
|
|
3040
|
+
});
|
|
1198
3041
|
}
|
|
1199
3042
|
}
|
|
1200
3043
|
|
|
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
3044
|
const postSearchState = await waitForRecruitSearchResultState(client, {
|
|
1234
3045
|
timeoutMs: searchTimeoutMs
|
|
1235
3046
|
});
|
|
@@ -1243,6 +3054,7 @@ export async function applyRecruitSearchParams(client, {
|
|
|
1243
3054
|
reset,
|
|
1244
3055
|
overlay_dismissal: overlayDismissal,
|
|
1245
3056
|
controls,
|
|
3057
|
+
application_step_names: applicationStepNames,
|
|
1246
3058
|
initial_iframe: {
|
|
1247
3059
|
selector: initialRoots.iframe.selector,
|
|
1248
3060
|
document_node_id: initialFrameNodeId
|