@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23

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.
Files changed (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +2 -1
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2416 -2385
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1315 -1306
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +44 -27
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +655 -646
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +575 -351
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1616 -878
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +121 -4
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +1174 -914
  66. package/src/recommend-scheduler.js +535 -469
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,620 +1,620 @@
1
- import {
2
- clickNodeCenter,
3
- clickPoint,
4
- DETERMINISTIC_CLICK_OPTIONS,
5
- getAttributesMap,
6
- getNodeBox,
7
- getOuterHTML,
8
- pressKey,
9
- querySelector,
10
- querySelectorAll,
11
- sleep
12
- } from "../../core/browser/index.js";
13
- import {
14
- htmlToText,
15
- normalizeText
16
- } from "../../core/screening/index.js";
17
- import {
18
- CHAT_JOB_FALLBACK_SELECTORS,
19
- CHAT_JOB_LABEL_SELECTORS,
20
- CHAT_JOB_OPTION_SELECTORS,
21
- CHAT_JOB_TRIGGER_SELECTORS
22
- } from "./constants.js";
23
- import { getChatRoots } from "./roots.js";
24
-
25
- function isActiveClass(className = "") {
26
- return /\b(active|selected|current)\b/i.test(String(className || ""));
27
- }
28
-
29
- function normalizeJobText(value) {
30
- return normalizeText(value).replace(/\s+_/g, " _").replace(/_\s+/g, "_ ");
31
- }
32
-
33
- async function freshTopRootNodeId(client, fallbackNodeId) {
34
- try {
35
- const rootState = await getChatRoots(client);
36
- return rootState.rootNodes.top || fallbackNodeId;
37
- } catch {
38
- return fallbackNodeId;
39
- }
40
- }
41
-
42
- async function safeQuerySelector(client, rootNodeId, selector) {
43
- try {
44
- return await querySelector(client, rootNodeId, selector);
45
- } catch {
46
- return 0;
47
- }
48
- }
49
-
50
- async function safeQuerySelectorAll(client, rootNodeId, selector) {
51
- try {
52
- return await querySelectorAll(client, rootNodeId, selector);
53
- } catch {
54
- return [];
55
- }
56
- }
57
-
58
- async function readNodeText(client, nodeId) {
59
- const outerHTML = await getOuterHTML(client, nodeId);
60
- return {
61
- outerHTML,
62
- text: normalizeJobText(htmlToText(outerHTML))
63
- };
64
- }
65
-
66
- async function readSelectedJobLabel(client, rootNodeId) {
67
- for (const selector of CHAT_JOB_LABEL_SELECTORS) {
68
- const nodeId = await safeQuerySelector(client, rootNodeId, selector);
69
- if (!nodeId) continue;
70
- try {
71
- const { text } = await readNodeText(client, nodeId);
72
- if (text) return { selector, label: text };
73
- } catch {
74
- continue;
75
- }
76
- }
77
- return { selector: "", label: "" };
78
- }
79
-
80
- async function readOptionNode(client, nodeId, index, { selector, source }) {
81
- const [attributes, textResult] = await Promise.all([
82
- getAttributesMap(client, nodeId),
83
- readNodeText(client, nodeId)
84
- ]);
85
- const label = normalizeJobText(attributes.title || textResult.text);
86
- if (!label) return null;
87
- const rawValue = normalizeText(attributes.value || attributes["data-value"] || attributes["data-id"] || "");
88
- return {
89
- node_id: nodeId,
90
- index,
91
- label,
92
- title: label,
93
- value: rawValue || label,
94
- active: isActiveClass(attributes.class),
95
- is_all: rawValue === "-1" || /^(全部职位|全部岗位|全部)$/u.test(label),
96
- source,
97
- selector
98
- };
99
- }
100
-
101
- async function readClickableOptionNode(client, nodeId, index, { selector, source }) {
102
- const option = await readOptionNode(client, nodeId, index, { selector, source });
103
- if (!option) return null;
104
- try {
105
- const box = await getNodeBox(client, nodeId);
106
- option.center = box.center;
107
- option.rect = box.rect;
108
- option.visible = box.rect.width > 2 && box.rect.height > 2;
109
- } catch {
110
- option.center = null;
111
- option.rect = null;
112
- option.visible = false;
113
- }
114
- return option;
115
- }
116
-
117
- async function readOptionsForSelector(client, rootNodeId, selector, { source }) {
118
- const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
119
- const options = [];
120
- for (const nodeId of nodeIds) {
121
- let option = null;
122
- try {
123
- option = await readClickableOptionNode(client, nodeId, options.length + 1, {
124
- selector,
125
- source
126
- });
127
- } catch {
128
- option = null;
129
- }
130
- if (option) options.push(option);
131
- }
132
- return options;
133
- }
134
-
135
- function dedupeJobOptions(options = []) {
136
- const seen = new Set();
137
- const deduped = [];
138
- for (const option of options) {
139
- const key = `${normalizeText(option.value).toLowerCase()}|${normalizeText(option.label).toLowerCase()}`;
140
- if (seen.has(key)) continue;
141
- seen.add(key);
142
- deduped.push({
143
- ...option,
144
- index: deduped.length + 1
145
- });
146
- }
147
- return deduped;
148
- }
149
-
150
- export async function readChatJobOptions(client, rootNodeId, {
151
- timeoutMs = 12000,
152
- intervalMs = 300
153
- } = {}) {
154
- const started = Date.now();
155
- let selected = { selector: "", label: "" };
156
- let lastPrimary = {
157
- selector: "",
158
- source: "chat-job-list",
159
- options: []
160
- };
161
-
162
- while (Date.now() - started <= timeoutMs) {
163
- selected = await readSelectedJobLabel(client, rootNodeId);
164
- for (const selector of CHAT_JOB_OPTION_SELECTORS) {
165
- const options = await readOptionsForSelector(client, rootNodeId, selector, {
166
- source: "chat-job-list"
167
- });
168
- if (options.length) {
169
- lastPrimary = {
170
- selector,
171
- source: "chat-job-list",
172
- options: dedupeJobOptions(options)
173
- };
174
- return {
175
- selector,
176
- source: "chat-job-list",
177
- selected_label: selected.label || "",
178
- selected_selector: selected.selector || "",
179
- job_options: lastPrimary.options
180
- };
181
- }
182
- }
183
- await sleep(intervalMs);
184
- }
185
-
186
- const fallbackOptions = [];
187
- for (const selector of CHAT_JOB_FALLBACK_SELECTORS) {
188
- const options = await readOptionsForSelector(client, rootNodeId, selector, {
189
- source: "conversation-source-job"
190
- });
191
- fallbackOptions.push(...options);
192
- }
193
-
194
- const dedupedFallback = dedupeJobOptions(fallbackOptions);
195
- if (dedupedFallback.length) {
196
- return {
197
- selector: CHAT_JOB_FALLBACK_SELECTORS.join(", "),
198
- source: "conversation-source-job",
199
- selected_label: selected.label || "",
200
- selected_selector: selected.selector || "",
201
- job_options: dedupedFallback
202
- };
203
- }
204
-
205
- return {
206
- selector: lastPrimary.selector,
207
- source: lastPrimary.source,
208
- selected_label: selected.label || "",
209
- selected_selector: selected.selector || "",
210
- job_options: []
211
- };
212
- }
213
-
214
- function matchJobOption(option, jobLabel = "") {
215
- const requested = normalizeJobText(jobLabel).toLowerCase();
216
- if (!requested) return false;
217
- return [
218
- option.value,
219
- option.label,
220
- option.title
221
- ].map((value) => normalizeJobText(value).toLowerCase()).some((value) => (
222
- value === requested
223
- || value.includes(requested)
224
- || requested.includes(value)
225
- ));
226
- }
227
-
228
- function activeMatchingJobOption(options = [], jobLabel = "") {
229
- return (options || []).find((option) => option.active && matchJobOption(option, jobLabel)) || null;
230
- }
231
-
232
- function selectedLabelMatches(label = "", jobLabel = "") {
233
- const normalized = normalizeJobText(label);
234
- return Boolean(normalized && matchJobOption({ label: normalized, value: normalized, title: normalized }, jobLabel));
235
- }
236
-
237
- async function clickFirstVisible(client, rootNodeId, selectors = []) {
238
- for (const selector of selectors) {
239
- const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
240
- for (const nodeId of nodeIds) {
241
- try {
242
- const box = await getNodeBox(client, nodeId);
243
- if (box.rect.width <= 2 || box.rect.height <= 2) continue;
244
- await clickPoint(client, box.center.x, box.center.y, DETERMINISTIC_CLICK_OPTIONS);
245
- return {
246
- clicked: true,
247
- selector,
248
- node_id: nodeId,
249
- center: box.center
250
- };
251
- } catch {}
252
- }
253
- }
254
- return {
255
- clicked: false,
256
- selector: "",
257
- node_id: 0
258
- };
259
- }
260
-
261
- async function openChatJobDropdown(client, rootNodeId, {
262
- timeoutMs = 12000,
263
- intervalMs = 300,
264
- settleMs = 800
265
- } = {}) {
266
- const started = Date.now();
267
- const triedPoints = new Set();
268
- const attempts = [];
269
- const initialClose = await closeChatJobDropdownQuietly(client, rootNodeId, Math.min(settleMs, 300));
270
- for (const selector of CHAT_JOB_TRIGGER_SELECTORS) {
271
- const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
272
- const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
273
- for (const nodeId of nodeIds) {
274
- try {
275
- const box = await getNodeBox(client, nodeId);
276
- if (box.rect.width <= 2 || box.rect.height <= 2) continue;
277
- const y = box.center.y;
278
- const xCandidates = [
279
- ["center", box.center.x],
280
- ["right_12", box.rect.x + box.rect.width - 12],
281
- ["right_44", box.rect.x + box.rect.width - 44],
282
- ["right_64", box.rect.x + box.rect.width - 64]
283
- ].filter(([, x]) => x > box.rect.x + 4 && x < box.rect.x + box.rect.width - 4);
284
- for (const [pointName, x] of xCandidates) {
285
- const pointKey = `${nodeId}:${Math.round(x)}:${Math.round(y)}`;
286
- if (triedPoints.has(pointKey)) continue;
287
- triedPoints.add(pointKey);
288
- await clickPoint(client, x, y, DETERMINISTIC_CLICK_OPTIONS);
289
- if (settleMs > 0) await sleep(Math.min(settleMs, 800));
290
- const remaining = Math.max(300, timeoutMs - (Date.now() - started));
291
- const optionsResult = await waitForChatJobOptions(client, currentRootNodeId, {
292
- timeoutMs: Math.min(remaining, 1800),
293
- intervalMs,
294
- requireVisible: true
295
- });
296
- const visibleCount = (optionsResult.job_options || []).filter((option) => option.visible).length;
297
- const attempt = {
298
- clicked: true,
299
- selector,
300
- node_id: nodeId,
301
- point: pointName,
302
- center: { x, y },
303
- visible_option_count: visibleCount,
304
- initial_close: initialClose
305
- };
306
- attempts.push(attempt);
307
- if (visibleCount > 0) {
308
- return {
309
- ...attempt,
310
- attempts,
311
- options_result: optionsResult
312
- };
313
- }
314
- if (Date.now() - started > timeoutMs) break;
315
- }
316
- } catch (error) {
317
- attempts.push({
318
- clicked: false,
319
- selector,
320
- node_id: nodeId,
321
- error: error?.message || String(error)
322
- });
323
- }
324
- if (Date.now() - started > timeoutMs) break;
325
- }
326
- if (Date.now() - started > timeoutMs) break;
327
- }
328
- return {
329
- clicked: attempts.some((attempt) => attempt.clicked),
330
- selector: attempts.find((attempt) => attempt.clicked)?.selector || "",
331
- node_id: attempts.find((attempt) => attempt.clicked)?.node_id || 0,
332
- attempts,
333
- options_result: null
334
- };
335
- }
336
-
337
- async function waitForChatJobOptions(client, rootNodeId, {
338
- timeoutMs = 12000,
339
- intervalMs = 300,
340
- requireVisible = false
341
- } = {}) {
342
- const started = Date.now();
343
- let latest = null;
344
- while (Date.now() - started <= timeoutMs) {
345
- const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
346
- latest = await readChatJobOptions(client, currentRootNodeId, {
347
- timeoutMs: Math.min(intervalMs, 300),
348
- intervalMs
349
- });
350
- const options = latest.job_options || [];
351
- if (options.length && (!requireVisible || options.some((option) => option.visible))) {
352
- return latest;
353
- }
354
- await sleep(intervalMs);
355
- }
356
- return latest || {
357
- selector: "",
358
- source: "chat-job-list",
359
- selected_label: "",
360
- job_options: []
361
- };
362
- }
363
-
364
- async function waitForSelectedChatJob(client, rootNodeId, jobLabel = "", {
365
- timeoutMs = 5000,
366
- intervalMs = 300
367
- } = {}) {
368
- const started = Date.now();
369
- let latest = null;
370
- while (Date.now() - started <= timeoutMs) {
371
- const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
372
- latest = await readChatJobOptions(client, currentRootNodeId, {
373
- timeoutMs: Math.min(intervalMs, 300),
374
- intervalMs
375
- });
376
- if (
377
- selectedLabelMatches(latest.selected_label, jobLabel)
378
- || activeMatchingJobOption(latest.job_options || [], jobLabel)
379
- ) {
380
- return {
381
- verified: true,
382
- result: latest
383
- };
384
- }
385
- await sleep(intervalMs);
386
- }
387
- return {
388
- verified: false,
389
- result: latest
390
- };
391
- }
392
-
393
- async function visibleChatJobOptions(client, rootNodeId) {
394
- const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
395
- const visible = [];
396
- for (const selector of CHAT_JOB_OPTION_SELECTORS) {
397
- const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
398
- for (const nodeId of nodeIds) {
399
- try {
400
- const box = await getNodeBox(client, nodeId);
401
- if (box.rect.width > 2 && box.rect.height > 2) {
402
- visible.push({
403
- selector,
404
- node_id: nodeId,
405
- center: box.center,
406
- rect: box.rect
407
- });
408
- }
409
- } catch {
410
- // Hidden job options are normal when the dropdown is closed.
411
- }
412
- }
413
- }
414
- return visible;
415
- }
416
-
417
- export async function closeChatJobDropdown(client, rootNodeId, {
418
- settleMs = 180
419
- } = {}) {
420
- const before = await visibleChatJobOptions(client, rootNodeId);
421
- if (!before.length) {
422
- return {
423
- ok: true,
424
- closed: false,
425
- reason: "already_closed",
426
- visible_before_count: 0,
427
- visible_after_count: 0
428
- };
429
- }
430
- if (typeof client?.Input?.dispatchKeyEvent !== "function") {
431
- return {
432
- ok: false,
433
- closed: false,
434
- reason: "dispatch_key_unavailable",
435
- visible_before_count: before.length,
436
- visible_after_count: before.length
437
- };
438
- }
439
- await pressKey(client, "Escape", {
440
- code: "Escape",
441
- windowsVirtualKeyCode: 27,
442
- nativeVirtualKeyCode: 27
443
- });
444
- if (settleMs > 0) await sleep(settleMs);
445
- const after = await visibleChatJobOptions(client, rootNodeId);
446
- if (after.length) {
447
- const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
448
- for (const selector of CHAT_JOB_TRIGGER_SELECTORS) {
449
- const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
450
- for (const nodeId of nodeIds) {
451
- try {
452
- const box = await getNodeBox(client, nodeId);
453
- if (box.rect.width <= 2 || box.rect.height <= 2) continue;
454
- await clickPoint(client, box.center.x, box.center.y, DETERMINISTIC_CLICK_OPTIONS);
455
- if (settleMs > 0) await sleep(settleMs);
456
- const afterToggle = await visibleChatJobOptions(client, currentRootNodeId);
457
- if (!afterToggle.length) {
458
- return {
459
- ok: true,
460
- closed: true,
461
- reason: "trigger_toggle",
462
- visible_before_count: before.length,
463
- visible_after_count: 0,
464
- first_visible_before: before[0] || null,
465
- first_visible_after: null
466
- };
467
- }
468
- } catch {
469
- continue;
470
- }
471
- }
472
- }
473
- }
474
- return {
475
- ok: after.length === 0,
476
- closed: after.length === 0,
477
- reason: after.length ? "still_visible_after_escape" : "escape",
478
- visible_before_count: before.length,
479
- visible_after_count: after.length,
480
- first_visible_before: before[0] || null,
481
- first_visible_after: after[0] || null
482
- };
483
- }
484
-
485
- async function closeChatJobDropdownQuietly(client, rootNodeId, settleMs = 180) {
486
- try {
487
- return await closeChatJobDropdown(client, rootNodeId, { settleMs });
488
- } catch (error) {
489
- return {
490
- ok: false,
491
- closed: false,
492
- reason: "close_failed",
493
- error: error?.message || String(error)
494
- };
495
- }
496
- }
497
-
498
- export async function selectChatJob(client, rootNodeId, {
499
- jobLabel = "",
500
- timeoutMs = 12000,
501
- intervalMs = 300,
502
- settleMs = 800
503
- } = {}) {
504
- const requested = normalizeJobText(jobLabel);
505
- if (!requested) {
506
- return {
507
- selected: false,
508
- reason: "missing_job_label"
509
- };
510
- }
511
-
512
- let currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
513
- let optionsResult = await readChatJobOptions(client, currentRootNodeId, {
514
- timeoutMs: Math.min(timeoutMs, 1500),
515
- intervalMs
516
- });
517
- let matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
518
- if (
519
- matched
520
- && (
521
- matched.active
522
- || selectedLabelMatches(optionsResult.selected_label, matched.label)
523
- || selectedLabelMatches(optionsResult.selected_label, requested)
524
- )
525
- ) {
526
- const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
527
- return {
528
- selected: true,
529
- verified: true,
530
- already_current: true,
531
- requested,
532
- selected_option: matched,
533
- options: optionsResult.job_options || [],
534
- selected_label: optionsResult.selected_label || matched.label,
535
- menu_close: menuClose
536
- };
537
- }
538
-
539
- if (!matched || !matched.visible) {
540
- const triggerRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
541
- const trigger = await openChatJobDropdown(client, triggerRootNodeId, {
542
- timeoutMs,
543
- intervalMs,
544
- settleMs
545
- });
546
- currentRootNodeId = await freshTopRootNodeId(client, triggerRootNodeId);
547
- optionsResult = trigger.options_result || await waitForChatJobOptions(client, currentRootNodeId, {
548
- timeoutMs,
549
- intervalMs,
550
- requireVisible: true
551
- });
552
- matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
553
- if (!matched || !matched.visible) {
554
- const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
555
- return {
556
- selected: false,
557
- reason: matched ? "job_option_not_visible" : "job_option_not_found",
558
- requested,
559
- trigger,
560
- options: optionsResult.job_options || [],
561
- selected_label_before: optionsResult.selected_label || "",
562
- menu_close: menuClose
563
- };
564
- }
565
- }
566
-
567
- if (matched.active || normalizeJobText(optionsResult.selected_label).toLowerCase() === normalizeJobText(matched.label).toLowerCase()) {
568
- const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
569
- return {
570
- selected: true,
571
- verified: true,
572
- already_current: true,
573
- requested,
574
- selected_option: matched,
575
- options: optionsResult.job_options || [],
576
- selected_label: optionsResult.selected_label || matched.label,
577
- menu_close: menuClose
578
- };
579
- }
580
-
581
- if (matched.center) {
582
- await clickPoint(client, matched.center.x, matched.center.y, DETERMINISTIC_CLICK_OPTIONS);
583
- } else {
584
- await clickNodeCenter(client, matched.node_id, {
585
- ...DETERMINISTIC_CLICK_OPTIONS,
586
- scrollIntoView: true
587
- });
588
- }
589
- if (settleMs > 0) await sleep(settleMs);
590
-
591
- const afterRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
592
- const verification = await waitForSelectedChatJob(client, afterRootNodeId, matched.label, {
593
- timeoutMs: Math.min(timeoutMs, 5000),
594
- intervalMs
595
- });
596
- const after = verification.result || {
597
- selected_label: "",
598
- job_options: []
599
- };
600
- const afterMatch = (after.job_options || []).find((option) => matchJobOption(option, matched.label)) || matched;
601
- const selectedLabel = normalizeJobText(after.selected_label || "");
602
- const activeMatch = activeMatchingJobOption(after.job_options || [], matched.label);
603
- const verified = Boolean(verification.verified || selectedLabelMatches(selectedLabel, matched.label) || activeMatch);
604
- const menuClose = await closeChatJobDropdownQuietly(client, afterRootNodeId, Math.min(settleMs, 300));
605
-
606
- return {
607
- selected: verified,
608
- verified,
609
- already_current: false,
610
- reason: verified ? "verified" : "job_selection_not_verified",
611
- requested,
612
- selected_option: afterMatch,
613
- active_option: activeMatch,
614
- options: after.job_options || optionsResult.job_options || [],
615
- selected_label: selectedLabel,
616
- before: optionsResult,
617
- after,
618
- menu_close: menuClose
619
- };
620
- }
1
+ import {
2
+ clickNodeCenter,
3
+ clickPoint,
4
+ DETERMINISTIC_CLICK_OPTIONS,
5
+ getAttributesMap,
6
+ getNodeBox,
7
+ getOuterHTML,
8
+ pressKey,
9
+ querySelector,
10
+ querySelectorAll,
11
+ sleep
12
+ } from "../../core/browser/index.js";
13
+ import {
14
+ htmlToText,
15
+ normalizeText
16
+ } from "../../core/screening/index.js";
17
+ import {
18
+ CHAT_JOB_FALLBACK_SELECTORS,
19
+ CHAT_JOB_LABEL_SELECTORS,
20
+ CHAT_JOB_OPTION_SELECTORS,
21
+ CHAT_JOB_TRIGGER_SELECTORS
22
+ } from "./constants.js";
23
+ import { getChatRoots } from "./roots.js";
24
+
25
+ function isActiveClass(className = "") {
26
+ return /\b(active|selected|current)\b/i.test(String(className || ""));
27
+ }
28
+
29
+ function normalizeJobText(value) {
30
+ return normalizeText(value).replace(/\s+_/g, " _").replace(/_\s+/g, "_ ");
31
+ }
32
+
33
+ async function freshTopRootNodeId(client, fallbackNodeId) {
34
+ try {
35
+ const rootState = await getChatRoots(client);
36
+ return rootState.rootNodes.top || fallbackNodeId;
37
+ } catch {
38
+ return fallbackNodeId;
39
+ }
40
+ }
41
+
42
+ async function safeQuerySelector(client, rootNodeId, selector) {
43
+ try {
44
+ return await querySelector(client, rootNodeId, selector);
45
+ } catch {
46
+ return 0;
47
+ }
48
+ }
49
+
50
+ async function safeQuerySelectorAll(client, rootNodeId, selector) {
51
+ try {
52
+ return await querySelectorAll(client, rootNodeId, selector);
53
+ } catch {
54
+ return [];
55
+ }
56
+ }
57
+
58
+ async function readNodeText(client, nodeId) {
59
+ const outerHTML = await getOuterHTML(client, nodeId);
60
+ return {
61
+ outerHTML,
62
+ text: normalizeJobText(htmlToText(outerHTML))
63
+ };
64
+ }
65
+
66
+ async function readSelectedJobLabel(client, rootNodeId) {
67
+ for (const selector of CHAT_JOB_LABEL_SELECTORS) {
68
+ const nodeId = await safeQuerySelector(client, rootNodeId, selector);
69
+ if (!nodeId) continue;
70
+ try {
71
+ const { text } = await readNodeText(client, nodeId);
72
+ if (text) return { selector, label: text };
73
+ } catch {
74
+ continue;
75
+ }
76
+ }
77
+ return { selector: "", label: "" };
78
+ }
79
+
80
+ async function readOptionNode(client, nodeId, index, { selector, source }) {
81
+ const [attributes, textResult] = await Promise.all([
82
+ getAttributesMap(client, nodeId),
83
+ readNodeText(client, nodeId)
84
+ ]);
85
+ const label = normalizeJobText(attributes.title || textResult.text);
86
+ if (!label) return null;
87
+ const rawValue = normalizeText(attributes.value || attributes["data-value"] || attributes["data-id"] || "");
88
+ return {
89
+ node_id: nodeId,
90
+ index,
91
+ label,
92
+ title: label,
93
+ value: rawValue || label,
94
+ active: isActiveClass(attributes.class),
95
+ is_all: rawValue === "-1" || /^(全部职位|全部岗位|全部)$/u.test(label),
96
+ source,
97
+ selector
98
+ };
99
+ }
100
+
101
+ async function readClickableOptionNode(client, nodeId, index, { selector, source }) {
102
+ const option = await readOptionNode(client, nodeId, index, { selector, source });
103
+ if (!option) return null;
104
+ try {
105
+ const box = await getNodeBox(client, nodeId);
106
+ option.center = box.center;
107
+ option.rect = box.rect;
108
+ option.visible = box.rect.width > 2 && box.rect.height > 2;
109
+ } catch {
110
+ option.center = null;
111
+ option.rect = null;
112
+ option.visible = false;
113
+ }
114
+ return option;
115
+ }
116
+
117
+ async function readOptionsForSelector(client, rootNodeId, selector, { source }) {
118
+ const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
119
+ const options = [];
120
+ for (const nodeId of nodeIds) {
121
+ let option = null;
122
+ try {
123
+ option = await readClickableOptionNode(client, nodeId, options.length + 1, {
124
+ selector,
125
+ source
126
+ });
127
+ } catch {
128
+ option = null;
129
+ }
130
+ if (option) options.push(option);
131
+ }
132
+ return options;
133
+ }
134
+
135
+ function dedupeJobOptions(options = []) {
136
+ const seen = new Set();
137
+ const deduped = [];
138
+ for (const option of options) {
139
+ const key = `${normalizeText(option.value).toLowerCase()}|${normalizeText(option.label).toLowerCase()}`;
140
+ if (seen.has(key)) continue;
141
+ seen.add(key);
142
+ deduped.push({
143
+ ...option,
144
+ index: deduped.length + 1
145
+ });
146
+ }
147
+ return deduped;
148
+ }
149
+
150
+ export async function readChatJobOptions(client, rootNodeId, {
151
+ timeoutMs = 12000,
152
+ intervalMs = 300
153
+ } = {}) {
154
+ const started = Date.now();
155
+ let selected = { selector: "", label: "" };
156
+ let lastPrimary = {
157
+ selector: "",
158
+ source: "chat-job-list",
159
+ options: []
160
+ };
161
+
162
+ while (Date.now() - started <= timeoutMs) {
163
+ selected = await readSelectedJobLabel(client, rootNodeId);
164
+ for (const selector of CHAT_JOB_OPTION_SELECTORS) {
165
+ const options = await readOptionsForSelector(client, rootNodeId, selector, {
166
+ source: "chat-job-list"
167
+ });
168
+ if (options.length) {
169
+ lastPrimary = {
170
+ selector,
171
+ source: "chat-job-list",
172
+ options: dedupeJobOptions(options)
173
+ };
174
+ return {
175
+ selector,
176
+ source: "chat-job-list",
177
+ selected_label: selected.label || "",
178
+ selected_selector: selected.selector || "",
179
+ job_options: lastPrimary.options
180
+ };
181
+ }
182
+ }
183
+ await sleep(intervalMs);
184
+ }
185
+
186
+ const fallbackOptions = [];
187
+ for (const selector of CHAT_JOB_FALLBACK_SELECTORS) {
188
+ const options = await readOptionsForSelector(client, rootNodeId, selector, {
189
+ source: "conversation-source-job"
190
+ });
191
+ fallbackOptions.push(...options);
192
+ }
193
+
194
+ const dedupedFallback = dedupeJobOptions(fallbackOptions);
195
+ if (dedupedFallback.length) {
196
+ return {
197
+ selector: CHAT_JOB_FALLBACK_SELECTORS.join(", "),
198
+ source: "conversation-source-job",
199
+ selected_label: selected.label || "",
200
+ selected_selector: selected.selector || "",
201
+ job_options: dedupedFallback
202
+ };
203
+ }
204
+
205
+ return {
206
+ selector: lastPrimary.selector,
207
+ source: lastPrimary.source,
208
+ selected_label: selected.label || "",
209
+ selected_selector: selected.selector || "",
210
+ job_options: []
211
+ };
212
+ }
213
+
214
+ function matchJobOption(option, jobLabel = "") {
215
+ const requested = normalizeJobText(jobLabel).toLowerCase();
216
+ if (!requested) return false;
217
+ return [
218
+ option.value,
219
+ option.label,
220
+ option.title
221
+ ].map((value) => normalizeJobText(value).toLowerCase()).some((value) => (
222
+ value === requested
223
+ || value.includes(requested)
224
+ || requested.includes(value)
225
+ ));
226
+ }
227
+
228
+ function activeMatchingJobOption(options = [], jobLabel = "") {
229
+ return (options || []).find((option) => option.active && matchJobOption(option, jobLabel)) || null;
230
+ }
231
+
232
+ function selectedLabelMatches(label = "", jobLabel = "") {
233
+ const normalized = normalizeJobText(label);
234
+ return Boolean(normalized && matchJobOption({ label: normalized, value: normalized, title: normalized }, jobLabel));
235
+ }
236
+
237
+ async function clickFirstVisible(client, rootNodeId, selectors = []) {
238
+ for (const selector of selectors) {
239
+ const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
240
+ for (const nodeId of nodeIds) {
241
+ try {
242
+ const box = await getNodeBox(client, nodeId);
243
+ if (box.rect.width <= 2 || box.rect.height <= 2) continue;
244
+ await clickPoint(client, box.center.x, box.center.y, DETERMINISTIC_CLICK_OPTIONS);
245
+ return {
246
+ clicked: true,
247
+ selector,
248
+ node_id: nodeId,
249
+ center: box.center
250
+ };
251
+ } catch {}
252
+ }
253
+ }
254
+ return {
255
+ clicked: false,
256
+ selector: "",
257
+ node_id: 0
258
+ };
259
+ }
260
+
261
+ async function openChatJobDropdown(client, rootNodeId, {
262
+ timeoutMs = 12000,
263
+ intervalMs = 300,
264
+ settleMs = 800
265
+ } = {}) {
266
+ const started = Date.now();
267
+ const triedPoints = new Set();
268
+ const attempts = [];
269
+ const initialClose = await closeChatJobDropdownQuietly(client, rootNodeId, Math.min(settleMs, 300));
270
+ for (const selector of CHAT_JOB_TRIGGER_SELECTORS) {
271
+ const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
272
+ const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
273
+ for (const nodeId of nodeIds) {
274
+ try {
275
+ const box = await getNodeBox(client, nodeId);
276
+ if (box.rect.width <= 2 || box.rect.height <= 2) continue;
277
+ const y = box.center.y;
278
+ const xCandidates = [
279
+ ["center", box.center.x],
280
+ ["right_12", box.rect.x + box.rect.width - 12],
281
+ ["right_44", box.rect.x + box.rect.width - 44],
282
+ ["right_64", box.rect.x + box.rect.width - 64]
283
+ ].filter(([, x]) => x > box.rect.x + 4 && x < box.rect.x + box.rect.width - 4);
284
+ for (const [pointName, x] of xCandidates) {
285
+ const pointKey = `${nodeId}:${Math.round(x)}:${Math.round(y)}`;
286
+ if (triedPoints.has(pointKey)) continue;
287
+ triedPoints.add(pointKey);
288
+ await clickPoint(client, x, y, DETERMINISTIC_CLICK_OPTIONS);
289
+ if (settleMs > 0) await sleep(Math.min(settleMs, 800));
290
+ const remaining = Math.max(300, timeoutMs - (Date.now() - started));
291
+ const optionsResult = await waitForChatJobOptions(client, currentRootNodeId, {
292
+ timeoutMs: Math.min(remaining, 1800),
293
+ intervalMs,
294
+ requireVisible: true
295
+ });
296
+ const visibleCount = (optionsResult.job_options || []).filter((option) => option.visible).length;
297
+ const attempt = {
298
+ clicked: true,
299
+ selector,
300
+ node_id: nodeId,
301
+ point: pointName,
302
+ center: { x, y },
303
+ visible_option_count: visibleCount,
304
+ initial_close: initialClose
305
+ };
306
+ attempts.push(attempt);
307
+ if (visibleCount > 0) {
308
+ return {
309
+ ...attempt,
310
+ attempts,
311
+ options_result: optionsResult
312
+ };
313
+ }
314
+ if (Date.now() - started > timeoutMs) break;
315
+ }
316
+ } catch (error) {
317
+ attempts.push({
318
+ clicked: false,
319
+ selector,
320
+ node_id: nodeId,
321
+ error: error?.message || String(error)
322
+ });
323
+ }
324
+ if (Date.now() - started > timeoutMs) break;
325
+ }
326
+ if (Date.now() - started > timeoutMs) break;
327
+ }
328
+ return {
329
+ clicked: attempts.some((attempt) => attempt.clicked),
330
+ selector: attempts.find((attempt) => attempt.clicked)?.selector || "",
331
+ node_id: attempts.find((attempt) => attempt.clicked)?.node_id || 0,
332
+ attempts,
333
+ options_result: null
334
+ };
335
+ }
336
+
337
+ async function waitForChatJobOptions(client, rootNodeId, {
338
+ timeoutMs = 12000,
339
+ intervalMs = 300,
340
+ requireVisible = false
341
+ } = {}) {
342
+ const started = Date.now();
343
+ let latest = null;
344
+ while (Date.now() - started <= timeoutMs) {
345
+ const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
346
+ latest = await readChatJobOptions(client, currentRootNodeId, {
347
+ timeoutMs: Math.min(intervalMs, 300),
348
+ intervalMs
349
+ });
350
+ const options = latest.job_options || [];
351
+ if (options.length && (!requireVisible || options.some((option) => option.visible))) {
352
+ return latest;
353
+ }
354
+ await sleep(intervalMs);
355
+ }
356
+ return latest || {
357
+ selector: "",
358
+ source: "chat-job-list",
359
+ selected_label: "",
360
+ job_options: []
361
+ };
362
+ }
363
+
364
+ async function waitForSelectedChatJob(client, rootNodeId, jobLabel = "", {
365
+ timeoutMs = 5000,
366
+ intervalMs = 300
367
+ } = {}) {
368
+ const started = Date.now();
369
+ let latest = null;
370
+ while (Date.now() - started <= timeoutMs) {
371
+ const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
372
+ latest = await readChatJobOptions(client, currentRootNodeId, {
373
+ timeoutMs: Math.min(intervalMs, 300),
374
+ intervalMs
375
+ });
376
+ if (
377
+ selectedLabelMatches(latest.selected_label, jobLabel)
378
+ || activeMatchingJobOption(latest.job_options || [], jobLabel)
379
+ ) {
380
+ return {
381
+ verified: true,
382
+ result: latest
383
+ };
384
+ }
385
+ await sleep(intervalMs);
386
+ }
387
+ return {
388
+ verified: false,
389
+ result: latest
390
+ };
391
+ }
392
+
393
+ async function visibleChatJobOptions(client, rootNodeId) {
394
+ const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
395
+ const visible = [];
396
+ for (const selector of CHAT_JOB_OPTION_SELECTORS) {
397
+ const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
398
+ for (const nodeId of nodeIds) {
399
+ try {
400
+ const box = await getNodeBox(client, nodeId);
401
+ if (box.rect.width > 2 && box.rect.height > 2) {
402
+ visible.push({
403
+ selector,
404
+ node_id: nodeId,
405
+ center: box.center,
406
+ rect: box.rect
407
+ });
408
+ }
409
+ } catch {
410
+ // Hidden job options are normal when the dropdown is closed.
411
+ }
412
+ }
413
+ }
414
+ return visible;
415
+ }
416
+
417
+ export async function closeChatJobDropdown(client, rootNodeId, {
418
+ settleMs = 180
419
+ } = {}) {
420
+ const before = await visibleChatJobOptions(client, rootNodeId);
421
+ if (!before.length) {
422
+ return {
423
+ ok: true,
424
+ closed: false,
425
+ reason: "already_closed",
426
+ visible_before_count: 0,
427
+ visible_after_count: 0
428
+ };
429
+ }
430
+ if (typeof client?.Input?.dispatchKeyEvent !== "function") {
431
+ return {
432
+ ok: false,
433
+ closed: false,
434
+ reason: "dispatch_key_unavailable",
435
+ visible_before_count: before.length,
436
+ visible_after_count: before.length
437
+ };
438
+ }
439
+ await pressKey(client, "Escape", {
440
+ code: "Escape",
441
+ windowsVirtualKeyCode: 27,
442
+ nativeVirtualKeyCode: 27
443
+ });
444
+ if (settleMs > 0) await sleep(settleMs);
445
+ const after = await visibleChatJobOptions(client, rootNodeId);
446
+ if (after.length) {
447
+ const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
448
+ for (const selector of CHAT_JOB_TRIGGER_SELECTORS) {
449
+ const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
450
+ for (const nodeId of nodeIds) {
451
+ try {
452
+ const box = await getNodeBox(client, nodeId);
453
+ if (box.rect.width <= 2 || box.rect.height <= 2) continue;
454
+ await clickPoint(client, box.center.x, box.center.y, DETERMINISTIC_CLICK_OPTIONS);
455
+ if (settleMs > 0) await sleep(settleMs);
456
+ const afterToggle = await visibleChatJobOptions(client, currentRootNodeId);
457
+ if (!afterToggle.length) {
458
+ return {
459
+ ok: true,
460
+ closed: true,
461
+ reason: "trigger_toggle",
462
+ visible_before_count: before.length,
463
+ visible_after_count: 0,
464
+ first_visible_before: before[0] || null,
465
+ first_visible_after: null
466
+ };
467
+ }
468
+ } catch {
469
+ continue;
470
+ }
471
+ }
472
+ }
473
+ }
474
+ return {
475
+ ok: after.length === 0,
476
+ closed: after.length === 0,
477
+ reason: after.length ? "still_visible_after_escape" : "escape",
478
+ visible_before_count: before.length,
479
+ visible_after_count: after.length,
480
+ first_visible_before: before[0] || null,
481
+ first_visible_after: after[0] || null
482
+ };
483
+ }
484
+
485
+ async function closeChatJobDropdownQuietly(client, rootNodeId, settleMs = 180) {
486
+ try {
487
+ return await closeChatJobDropdown(client, rootNodeId, { settleMs });
488
+ } catch (error) {
489
+ return {
490
+ ok: false,
491
+ closed: false,
492
+ reason: "close_failed",
493
+ error: error?.message || String(error)
494
+ };
495
+ }
496
+ }
497
+
498
+ export async function selectChatJob(client, rootNodeId, {
499
+ jobLabel = "",
500
+ timeoutMs = 12000,
501
+ intervalMs = 300,
502
+ settleMs = 800
503
+ } = {}) {
504
+ const requested = normalizeJobText(jobLabel);
505
+ if (!requested) {
506
+ return {
507
+ selected: false,
508
+ reason: "missing_job_label"
509
+ };
510
+ }
511
+
512
+ let currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
513
+ let optionsResult = await readChatJobOptions(client, currentRootNodeId, {
514
+ timeoutMs: Math.min(timeoutMs, 1500),
515
+ intervalMs
516
+ });
517
+ let matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
518
+ if (
519
+ matched
520
+ && (
521
+ matched.active
522
+ || selectedLabelMatches(optionsResult.selected_label, matched.label)
523
+ || selectedLabelMatches(optionsResult.selected_label, requested)
524
+ )
525
+ ) {
526
+ const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
527
+ return {
528
+ selected: true,
529
+ verified: true,
530
+ already_current: true,
531
+ requested,
532
+ selected_option: matched,
533
+ options: optionsResult.job_options || [],
534
+ selected_label: optionsResult.selected_label || matched.label,
535
+ menu_close: menuClose
536
+ };
537
+ }
538
+
539
+ if (!matched || !matched.visible) {
540
+ const triggerRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
541
+ const trigger = await openChatJobDropdown(client, triggerRootNodeId, {
542
+ timeoutMs,
543
+ intervalMs,
544
+ settleMs
545
+ });
546
+ currentRootNodeId = await freshTopRootNodeId(client, triggerRootNodeId);
547
+ optionsResult = trigger.options_result || await waitForChatJobOptions(client, currentRootNodeId, {
548
+ timeoutMs,
549
+ intervalMs,
550
+ requireVisible: true
551
+ });
552
+ matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
553
+ if (!matched || !matched.visible) {
554
+ const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
555
+ return {
556
+ selected: false,
557
+ reason: matched ? "job_option_not_visible" : "job_option_not_found",
558
+ requested,
559
+ trigger,
560
+ options: optionsResult.job_options || [],
561
+ selected_label_before: optionsResult.selected_label || "",
562
+ menu_close: menuClose
563
+ };
564
+ }
565
+ }
566
+
567
+ if (matched.active || normalizeJobText(optionsResult.selected_label).toLowerCase() === normalizeJobText(matched.label).toLowerCase()) {
568
+ const menuClose = await closeChatJobDropdownQuietly(client, currentRootNodeId, Math.min(settleMs, 300));
569
+ return {
570
+ selected: true,
571
+ verified: true,
572
+ already_current: true,
573
+ requested,
574
+ selected_option: matched,
575
+ options: optionsResult.job_options || [],
576
+ selected_label: optionsResult.selected_label || matched.label,
577
+ menu_close: menuClose
578
+ };
579
+ }
580
+
581
+ if (matched.center) {
582
+ await clickPoint(client, matched.center.x, matched.center.y, DETERMINISTIC_CLICK_OPTIONS);
583
+ } else {
584
+ await clickNodeCenter(client, matched.node_id, {
585
+ ...DETERMINISTIC_CLICK_OPTIONS,
586
+ scrollIntoView: true
587
+ });
588
+ }
589
+ if (settleMs > 0) await sleep(settleMs);
590
+
591
+ const afterRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
592
+ const verification = await waitForSelectedChatJob(client, afterRootNodeId, matched.label, {
593
+ timeoutMs: Math.min(timeoutMs, 5000),
594
+ intervalMs
595
+ });
596
+ const after = verification.result || {
597
+ selected_label: "",
598
+ job_options: []
599
+ };
600
+ const afterMatch = (after.job_options || []).find((option) => matchJobOption(option, matched.label)) || matched;
601
+ const selectedLabel = normalizeJobText(after.selected_label || "");
602
+ const activeMatch = activeMatchingJobOption(after.job_options || [], matched.label);
603
+ const verified = Boolean(verification.verified || selectedLabelMatches(selectedLabel, matched.label) || activeMatch);
604
+ const menuClose = await closeChatJobDropdownQuietly(client, afterRootNodeId, Math.min(settleMs, 300));
605
+
606
+ return {
607
+ selected: verified,
608
+ verified,
609
+ already_current: false,
610
+ reason: verified ? "verified" : "job_selection_not_verified",
611
+ requested,
612
+ selected_option: afterMatch,
613
+ active_option: activeMatch,
614
+ options: after.job_options || optionsResult.job_options || [],
615
+ selected_label: selectedLabel,
616
+ before: optionsResult,
617
+ after,
618
+ menu_close: menuClose
619
+ };
620
+ }