@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.25

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 -0
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/package.json +6 -1
  4. package/scripts/install-macos.sh +280 -280
  5. package/scripts/postinstall.cjs +44 -44
  6. package/skills/boss-chat/README.md +43 -42
  7. package/skills/boss-chat/SKILL.md +107 -106
  8. package/skills/boss-recommend-pipeline/README.md +13 -13
  9. package/skills/boss-recommend-pipeline/SKILL.md +47 -47
  10. package/skills/boss-recruit-pipeline/README.md +19 -19
  11. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  12. package/src/chat-mcp.js +301 -127
  13. package/src/chat-runtime-config.js +7 -5
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +302 -145
  16. package/src/core/capture/index.js +2930 -1201
  17. package/src/core/cv-acquisition/index.js +512 -238
  18. package/src/core/cv-capture-target/index.js +513 -299
  19. package/src/core/greet-quota/index.js +71 -71
  20. package/src/core/infinite-list/index.js +31 -31
  21. package/src/core/reporting/legacy-csv.js +12 -12
  22. package/src/core/run/detached-launcher.js +305 -0
  23. package/src/core/run/index.js +109 -55
  24. package/src/core/run/timing.js +33 -33
  25. package/src/core/run/windows-detached-worker.ps1 +106 -0
  26. package/src/core/screening/index.js +2400 -2135
  27. package/src/core/self-heal/index.js +989 -973
  28. package/src/core/self-heal/viewport.js +1505 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/action-journal.js +536 -0
  31. package/src/domains/chat/cards.js +137 -137
  32. package/src/domains/chat/constants.js +9 -9
  33. package/src/domains/chat/detail.js +1684 -401
  34. package/src/domains/chat/index.js +8 -7
  35. package/src/domains/chat/jobs.js +620 -620
  36. package/src/domains/chat/page-guard.js +157 -122
  37. package/src/domains/chat/roots.js +56 -56
  38. package/src/domains/chat/run-service.js +1801 -760
  39. package/src/domains/common/account-rights-panel.js +314 -314
  40. package/src/domains/common/recovery-settle.js +159 -159
  41. package/src/domains/recommend/actions.js +1219 -472
  42. package/src/domains/recommend/cards.js +243 -243
  43. package/src/domains/recommend/colleague-contact.js +1079 -333
  44. package/src/domains/recommend/constants.js +92 -92
  45. package/src/domains/recommend/detail.js +4037 -136
  46. package/src/domains/recommend/filters.js +608 -590
  47. package/src/domains/recommend/index.js +9 -9
  48. package/src/domains/recommend/jobs.js +571 -542
  49. package/src/domains/recommend/location.js +754 -707
  50. package/src/domains/recommend/refresh.js +677 -392
  51. package/src/domains/recommend/roots.js +80 -80
  52. package/src/domains/recommend/run-service.js +4048 -1447
  53. package/src/domains/recommend/scopes.js +246 -246
  54. package/src/domains/recruit/actions.js +277 -277
  55. package/src/domains/recruit/cards.js +74 -74
  56. package/src/domains/recruit/constants.js +236 -236
  57. package/src/domains/recruit/detail.js +588 -588
  58. package/src/domains/recruit/index.js +9 -9
  59. package/src/domains/recruit/instruction-parser.js +866 -866
  60. package/src/domains/recruit/refresh.js +45 -45
  61. package/src/domains/recruit/roots.js +68 -68
  62. package/src/domains/recruit/run-service.js +1817 -1620
  63. package/src/domains/recruit/search.js +3229 -3229
  64. package/src/index.js +16 -1
  65. package/src/parser.js +1296 -1296
  66. package/src/recommend-mcp.js +1061 -450
  67. package/src/recommend-scheduler.js +75 -75
@@ -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
+ }