@reconcrap/boss-recommend-mcp 2.0.47 → 2.0.49

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 (55) hide show
  1. package/bin/boss-recommend-mcp.js +4 -4
  2. package/config/screening-config.example.json +27 -27
  3. package/package.json +1 -1
  4. package/scripts/postinstall.cjs +44 -44
  5. package/skills/boss-chat/README.md +39 -39
  6. package/skills/boss-chat/SKILL.md +93 -93
  7. package/skills/boss-recommend-pipeline/README.md +12 -12
  8. package/skills/boss-recommend-pipeline/SKILL.md +180 -180
  9. package/skills/boss-recruit-pipeline/README.md +17 -17
  10. package/skills/boss-recruit-pipeline/SKILL.md +58 -58
  11. package/src/chat-mcp.js +1780 -1780
  12. package/src/chat-runtime-config.js +749 -749
  13. package/src/cli.js +3054 -3054
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +1586 -1453
  16. package/src/core/capture/index.js +1201 -1201
  17. package/src/core/cv-acquisition/index.js +238 -238
  18. package/src/core/cv-capture-target/index.js +299 -299
  19. package/src/core/greet-quota/index.js +54 -54
  20. package/src/core/infinite-list/index.js +1326 -1326
  21. package/src/core/reporting/legacy-csv.js +341 -341
  22. package/src/core/run/timing.js +33 -33
  23. package/src/core/self-heal/index.js +973 -973
  24. package/src/core/self-heal/viewport.js +564 -564
  25. package/src/domains/chat/cards.js +137 -137
  26. package/src/domains/chat/constants.js +221 -221
  27. package/src/domains/chat/detail.js +1668 -1668
  28. package/src/domains/chat/index.js +7 -7
  29. package/src/domains/chat/jobs.js +592 -592
  30. package/src/domains/chat/page-guard.js +98 -98
  31. package/src/domains/chat/roots.js +56 -56
  32. package/src/domains/chat/run-service.js +1977 -1977
  33. package/src/domains/recommend/actions.js +457 -457
  34. package/src/domains/recommend/cards.js +243 -243
  35. package/src/domains/recommend/constants.js +165 -165
  36. package/src/domains/recommend/detail.js +1 -1
  37. package/src/domains/recommend/filters.js +610 -610
  38. package/src/domains/recommend/index.js +10 -10
  39. package/src/domains/recommend/jobs.js +378 -316
  40. package/src/domains/recommend/refresh.js +491 -472
  41. package/src/domains/recommend/roots.js +80 -80
  42. package/src/domains/recommend/run-service.js +50 -29
  43. package/src/domains/recommend/scopes.js +246 -246
  44. package/src/domains/recruit/actions.js +277 -277
  45. package/src/domains/recruit/cards.js +74 -74
  46. package/src/domains/recruit/constants.js +167 -167
  47. package/src/domains/recruit/detail.js +461 -461
  48. package/src/domains/recruit/index.js +9 -9
  49. package/src/domains/recruit/instruction-parser.js +451 -451
  50. package/src/domains/recruit/refresh.js +44 -44
  51. package/src/domains/recruit/roots.js +68 -68
  52. package/src/domains/recruit/run-service.js +1207 -1207
  53. package/src/domains/recruit/search.js +1202 -1202
  54. package/src/recommend-mcp.js +22 -22
  55. package/src/recruit-mcp.js +1338 -1338
@@ -1,610 +1,610 @@
1
- import {
2
- clickNodeCenter,
3
- countSelectors,
4
- DETERMINISTIC_CLICK_OPTIONS,
5
- findFirstNode,
6
- getAttributesMap,
7
- getNodeBox,
8
- getOuterHTML,
9
- pressKey,
10
- querySelectorAll,
11
- sleep,
12
- waitForSelector
13
- } from "../../core/browser/index.js";
14
- import { htmlToText, normalizeText } from "../../core/screening/index.js";
15
- import {
16
- RECOMMEND_CARD_SELECTOR,
17
- RECOMMEND_FILTER_GROUP_ORDER,
18
- RECOMMEND_FILTER_SELECTORS
19
- } from "./constants.js";
20
-
21
- const SKIP_OPTION_LABELS = new Set(["不限", "全部", "all"]);
22
-
23
- export function normalizeFilterOptionLabel(label) {
24
- return normalizeText(label).replace(/\s+/g, "");
25
- }
26
-
27
- export function isSafeFilterOptionLabel(label) {
28
- const normalized = normalizeFilterOptionLabel(label);
29
- return Boolean(normalized) && !SKIP_OPTION_LABELS.has(normalized.toLowerCase());
30
- }
31
-
32
- export function isActiveOption(attributes = {}, outerHTML = "") {
33
- const className = attributes.class || "";
34
- return /\bactive\b/.test(className) || /\bactive\b/.test(String(outerHTML || "").split(">")[0] || "");
35
- }
36
-
37
- export function chooseFirstSafeFilterOption(options = [], groupOrder = RECOMMEND_FILTER_GROUP_ORDER) {
38
- for (const group of groupOrder) {
39
- const option = options.find((item) => (
40
- item.group === group
41
- && !item.active
42
- && isSafeFilterOptionLabel(item.label)
43
- ));
44
- if (option) return option;
45
- }
46
- return null;
47
- }
48
-
49
- export function chooseFilterOptionByLabels(options = [], {
50
- group = "",
51
- labels = []
52
- } = {}) {
53
- const normalizedGroup = normalizeText(group);
54
- const normalizedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
55
- for (const label of normalizedLabels) {
56
- const option = options.find((item) => (
57
- (!normalizedGroup || item.group === normalizedGroup)
58
- && !item.active
59
- && normalizeFilterOptionLabel(item.label) === label
60
- && isSafeFilterOptionLabel(item.label)
61
- ));
62
- if (option) return option;
63
- }
64
- return null;
65
- }
66
-
67
- export function chooseFilterOptionsByLabels(options = [], {
68
- group = "",
69
- labels = []
70
- } = {}) {
71
- const normalizedGroup = normalizeText(group);
72
- const normalizedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
73
- return normalizedLabels.map((label) => {
74
- const option = options.find((item) => (
75
- (!normalizedGroup || item.group === normalizedGroup)
76
- && normalizeFilterOptionLabel(item.label) === label
77
- && isSafeFilterOptionLabel(item.label)
78
- ));
79
- return {
80
- label,
81
- option: option || null
82
- };
83
- });
84
- }
85
-
86
- export async function getFilterPanelCount(client, frameNodeId) {
87
- return (await querySelectorAll(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.panel)).length;
88
- }
89
-
90
- export async function getRecommendFilterCounts(client, frameNodeId) {
91
- return countSelectors(client, frameNodeId, {
92
- filter_trigger: RECOMMEND_FILTER_SELECTORS.trigger,
93
- filter_panel: RECOMMEND_FILTER_SELECTORS.panel,
94
- check_box: RECOMMEND_FILTER_SELECTORS.checkBox,
95
- option: `.filter-panel ${RECOMMEND_FILTER_SELECTORS.option}`,
96
- active_option: RECOMMEND_FILTER_SELECTORS.activeOption,
97
- recommend_card: RECOMMEND_CARD_SELECTOR
98
- });
99
- }
100
-
101
- export async function findFilterTrigger(client, frameNodeId) {
102
- return findFirstNode(client, frameNodeId, [
103
- RECOMMEND_FILTER_SELECTORS.trigger,
104
- ".recommend-filter.op-filter"
105
- ]);
106
- }
107
-
108
- export async function ensureFilterPanelClosed(client, frameNodeId, triggerNodeId = 0) {
109
- const attempts = [];
110
- if (await getFilterPanelCount(client, frameNodeId) === 0) return attempts;
111
-
112
- await pressKey(client, "Escape", {
113
- code: "Escape",
114
- windowsVirtualKeyCode: 27,
115
- nativeVirtualKeyCode: 27
116
- });
117
- await sleep(400);
118
- attempts.push("Escape");
119
-
120
- if (await getFilterPanelCount(client, frameNodeId) > 0 && triggerNodeId) {
121
- await clickNodeCenter(client, triggerNodeId, DETERMINISTIC_CLICK_OPTIONS);
122
- await sleep(500);
123
- attempts.push("filter-trigger-toggle");
124
- }
125
-
126
- return attempts;
127
- }
128
-
129
- async function dismissRecommendControlOverlays(client, settleMs = 250) {
130
- if (typeof client?.Input?.dispatchKeyEvent !== "function") {
131
- return ["Escape-unavailable"];
132
- }
133
- await pressKey(client, "Escape", {
134
- code: "Escape",
135
- windowsVirtualKeyCode: 27,
136
- nativeVirtualKeyCode: 27
137
- });
138
- if (settleMs > 0) await sleep(settleMs);
139
- return ["Escape"];
140
- }
141
-
142
- async function findFilterTriggerCandidates(client, frameNodeId) {
143
- const candidates = [];
144
- const seen = new Set();
145
- for (const selector of [
146
- RECOMMEND_FILTER_SELECTORS.trigger,
147
- ".recommend-filter.op-filter"
148
- ]) {
149
- const candidate = await findFirstNode(client, frameNodeId, [selector]);
150
- if (candidate && !seen.has(candidate.nodeId)) {
151
- candidates.push(candidate);
152
- seen.add(candidate.nodeId);
153
- }
154
- }
155
- return candidates;
156
- }
157
-
158
- export async function openFilterPanel(client, frameNodeId) {
159
- let triggerCandidates = await findFilterTriggerCandidates(client, frameNodeId);
160
- if (!triggerCandidates.length) {
161
- throw new Error("Recommend filter trigger was not found");
162
- }
163
-
164
- const preOpenDismissalAttempts = await dismissRecommendControlOverlays(client);
165
- const existingPanelNodeId = await waitForSelector(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.panel, {
166
- timeoutMs: 300,
167
- intervalMs: 100
168
- });
169
- if (existingPanelNodeId) {
170
- const triggerBox = await getNodeBox(client, triggerCandidates[0].nodeId);
171
- return {
172
- trigger: triggerCandidates[0],
173
- trigger_box: triggerBox,
174
- panel_node_id: existingPanelNodeId,
175
- initial_close_attempts: preOpenDismissalAttempts,
176
- already_open: true
177
- };
178
- }
179
-
180
- const closeAttempts = await ensureFilterPanelClosed(client, frameNodeId, triggerCandidates[0].nodeId);
181
-
182
- const attempts = [];
183
- for (let round = 0; round < 3; round += 1) {
184
- triggerCandidates = await findFilterTriggerCandidates(client, frameNodeId);
185
- for (const trigger of triggerCandidates) {
186
- const triggerBox = await getNodeBox(client, trigger.nodeId);
187
- const clickBox = await clickNodeCenter(client, trigger.nodeId, DETERMINISTIC_CLICK_OPTIONS);
188
- attempts.push({
189
- selector: trigger.selector,
190
- node_id: trigger.nodeId,
191
- center: triggerBox.center,
192
- click_target: clickBox.click_target,
193
- click_result: clickBox.click_result
194
- });
195
- const panelNodeId = await waitForSelector(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.panel, {
196
- timeoutMs: 2500,
197
- intervalMs: 200
198
- });
199
- if (panelNodeId) {
200
- return {
201
- trigger,
202
- trigger_box: triggerBox,
203
- panel_node_id: panelNodeId,
204
- initial_close_attempts: [
205
- ...preOpenDismissalAttempts,
206
- ...closeAttempts
207
- ],
208
- open_attempts: attempts
209
- };
210
- }
211
- }
212
- await sleep(500);
213
- }
214
-
215
- throw new Error(`Recommend filter panel did not open after ${attempts.length} trigger attempts`);
216
- }
217
-
218
- async function readOptionNode(client, group, nodeId) {
219
- const [attributes, outerHTML] = await Promise.all([
220
- getAttributesMap(client, nodeId),
221
- getOuterHTML(client, nodeId)
222
- ]);
223
- const label = normalizeFilterOptionLabel(htmlToText(outerHTML));
224
- return {
225
- group,
226
- node_id: nodeId,
227
- label,
228
- active: isActiveOption(attributes, outerHTML),
229
- attributes: {
230
- class: attributes.class || "",
231
- value: attributes.value || "",
232
- type: attributes.type || ""
233
- }
234
- };
235
- }
236
-
237
- export async function listFilterOptions(client, frameNodeId, {
238
- groupOrder = RECOMMEND_FILTER_GROUP_ORDER
239
- } = {}) {
240
- const options = [];
241
- for (const group of groupOrder) {
242
- const groupSelector = RECOMMEND_FILTER_SELECTORS.groups[group];
243
- if (!groupSelector) continue;
244
- const groupNodeIds = await querySelectorAll(client, frameNodeId, groupSelector);
245
- for (const groupNodeId of groupNodeIds) {
246
- const optionNodeIds = await querySelectorAll(client, groupNodeId, RECOMMEND_FILTER_SELECTORS.option);
247
- for (const optionNodeId of optionNodeIds) {
248
- options.push(await readOptionNode(client, group, optionNodeId));
249
- }
250
- }
251
- }
252
- return options;
253
- }
254
-
255
- async function clickFirstAvailableNode(client, nodeIds) {
256
- const errors = [];
257
- for (const nodeId of nodeIds) {
258
- try {
259
- const box = await clickNodeCenter(client, nodeId, DETERMINISTIC_CLICK_OPTIONS);
260
- return {
261
- clicked: true,
262
- node_id: nodeId,
263
- box
264
- };
265
- } catch (error) {
266
- errors.push({
267
- node_id: nodeId,
268
- message: error?.message || String(error)
269
- });
270
- }
271
- }
272
- return {
273
- clicked: false,
274
- errors
275
- };
276
- }
277
-
278
- function normalizeButtonLabel(label) {
279
- return normalizeFilterOptionLabel(label).toLowerCase();
280
- }
281
-
282
- function buttonRank(candidate) {
283
- const label = normalizeButtonLabel(candidate.label);
284
- if (/确定|确认|完成|ok|confirm/.test(label)) return 0;
285
- if (/重置|清空|取消|reset|cancel/.test(label)) return 2;
286
- return 1;
287
- }
288
-
289
- async function readButtonCandidate(client, nodeId, index) {
290
- const [attributes, outerHTML] = await Promise.all([
291
- getAttributesMap(client, nodeId),
292
- getOuterHTML(client, nodeId)
293
- ]);
294
- return {
295
- node_id: nodeId,
296
- index,
297
- label: normalizeButtonLabel(htmlToText(outerHTML)),
298
- class_name: attributes.class || ""
299
- };
300
- }
301
-
302
- async function readConfirmButtonCandidates(client, frameNodeId) {
303
- const nodeIds = await querySelectorAll(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.confirmButton);
304
- const candidates = [];
305
- for (let index = 0; index < nodeIds.length; index += 1) {
306
- candidates.push(await readButtonCandidate(client, nodeIds[index], index));
307
- }
308
- return candidates.sort((left, right) => {
309
- const rankDiff = buttonRank(left) - buttonRank(right);
310
- if (rankDiff !== 0) return rankDiff;
311
- return right.index - left.index;
312
- });
313
- }
314
-
315
- export async function selectFirstSafeFilterOption(client, frameNodeId, {
316
- groupOrder = RECOMMEND_FILTER_GROUP_ORDER
317
- } = {}) {
318
- const options = await listFilterOptions(client, frameNodeId, { groupOrder });
319
- const selected = chooseFirstSafeFilterOption(options, groupOrder);
320
- if (!selected) {
321
- throw new Error("No safe non-active recommend filter option was found");
322
- }
323
-
324
- const box = await clickNodeCenter(client, selected.node_id, {
325
- ...DETERMINISTIC_CLICK_OPTIONS,
326
- scrollIntoView: true
327
- });
328
- await sleep(300);
329
-
330
- return {
331
- selected_option: {
332
- group: selected.group,
333
- label: selected.label,
334
- node_id: selected.node_id,
335
- was_active: selected.active
336
- },
337
- option_box: box,
338
- discovered_options: options.map((option) => ({
339
- group: option.group,
340
- label: option.label,
341
- active: option.active,
342
- node_id: option.node_id
343
- }))
344
- };
345
- }
346
-
347
- export async function selectFilterOption(client, frameNodeId, {
348
- group = "",
349
- labels = [],
350
- groupOrder = RECOMMEND_FILTER_GROUP_ORDER
351
- } = {}) {
352
- const options = await listFilterOptions(client, frameNodeId, { groupOrder });
353
- const selected = labels.length
354
- ? chooseFilterOptionByLabels(options, { group, labels })
355
- : chooseFirstSafeFilterOption(options, groupOrder);
356
-
357
- if (!selected) {
358
- const target = labels.length
359
- ? `${group || "any group"} / ${labels.join(", ")}`
360
- : "first safe non-active option";
361
- throw new Error(`No matching recommend filter option was found for ${target}`);
362
- }
363
-
364
- const box = await clickNodeCenter(client, selected.node_id, {
365
- ...DETERMINISTIC_CLICK_OPTIONS,
366
- scrollIntoView: true
367
- });
368
- await sleep(300);
369
-
370
- return {
371
- selected_option: {
372
- group: selected.group,
373
- label: selected.label,
374
- node_id: selected.node_id,
375
- was_active: selected.active,
376
- requested_group: group || null,
377
- requested_labels: labels
378
- },
379
- option_box: box,
380
- discovered_options: options.map((option) => ({
381
- group: option.group,
382
- label: option.label,
383
- active: option.active,
384
- node_id: option.node_id
385
- }))
386
- };
387
- }
388
-
389
- export async function selectFilterOptions(client, frameNodeId, {
390
- group = "",
391
- labels = [],
392
- groupOrder = RECOMMEND_FILTER_GROUP_ORDER
393
- } = {}) {
394
- if (!labels.length) {
395
- return selectFilterOption(client, frameNodeId, { group, labels, groupOrder });
396
- }
397
-
398
- const selectedOptions = [];
399
- const missingLabels = [];
400
- let discoveredOptions = [];
401
-
402
- for (const label of labels) {
403
- if (await getFilterPanelCount(client, frameNodeId) === 0) {
404
- await openFilterPanel(client, frameNodeId);
405
- }
406
-
407
- const options = await listFilterOptions(client, frameNodeId, { groupOrder });
408
- discoveredOptions = options.map((option) => ({
409
- group: option.group,
410
- label: option.label,
411
- active: option.active,
412
- node_id: option.node_id
413
- }));
414
- const selected = chooseFilterOptionByLabels(options, { group, labels: [label] });
415
- const alreadyActive = options.find((option) => (
416
- (!group || option.group === group)
417
- && normalizeFilterOptionLabel(option.label) === normalizeFilterOptionLabel(label)
418
- && option.active
419
- ));
420
-
421
- if (alreadyActive) {
422
- selectedOptions.push({
423
- group: alreadyActive.group,
424
- label: alreadyActive.label,
425
- node_id: alreadyActive.node_id,
426
- was_active: true,
427
- clicked: false,
428
- requested_group: group || null
429
- });
430
- continue;
431
- }
432
-
433
- if (!selected) {
434
- missingLabels.push(label);
435
- continue;
436
- }
437
-
438
- const box = await clickNodeCenter(client, selected.node_id, {
439
- ...DETERMINISTIC_CLICK_OPTIONS,
440
- scrollIntoView: true
441
- });
442
- selectedOptions.push({
443
- group: selected.group,
444
- label: selected.label,
445
- node_id: selected.node_id,
446
- was_active: false,
447
- clicked: true,
448
- requested_group: group || null,
449
- option_box: box
450
- });
451
- await sleep(450);
452
- }
453
-
454
- if (missingLabels.length) {
455
- throw new Error(`No matching recommend filter options were found for ${group || "any group"} / ${missingLabels.join(", ")}`);
456
- }
457
-
458
- return {
459
- selected_option: selectedOptions[0] || null,
460
- selected_options: selectedOptions.map((option) => ({
461
- group: option.group,
462
- label: option.label,
463
- node_id: option.node_id,
464
- was_active: option.was_active,
465
- clicked: option.clicked,
466
- requested_group: option.requested_group,
467
- requested_labels: labels
468
- })),
469
- option_box: selectedOptions.find((option) => option.option_box)?.option_box || null,
470
- discovered_options: discoveredOptions
471
- };
472
- }
473
-
474
- export async function selectFilterGroups(client, frameNodeId, {
475
- filterGroups = [],
476
- groupOrder = RECOMMEND_FILTER_GROUP_ORDER
477
- } = {}) {
478
- const selectedOptions = [];
479
- const discoveredOptions = [];
480
- const groups = filterGroups.filter((item) => item && (item.group || item.labels?.length));
481
- if (!groups.length) {
482
- return selectFilterOption(client, frameNodeId, { groupOrder });
483
- }
484
-
485
- for (const spec of groups) {
486
- const labels = Array.isArray(spec.labels) ? spec.labels : [];
487
- const selection = spec.selectAllLabels === false
488
- ? await selectFilterOption(client, frameNodeId, {
489
- group: spec.group || "",
490
- labels,
491
- groupOrder
492
- })
493
- : await selectFilterOptions(client, frameNodeId, {
494
- group: spec.group || "",
495
- labels,
496
- groupOrder
497
- });
498
- if (selection.selected_option) selectedOptions.push(selection.selected_option);
499
- for (const option of selection.selected_options || []) {
500
- selectedOptions.push(option);
501
- }
502
- for (const option of selection.discovered_options || []) {
503
- discoveredOptions.push(option);
504
- }
505
- }
506
-
507
- const dedupedSelected = [];
508
- const seenSelected = new Set();
509
- for (const option of selectedOptions) {
510
- const key = `${option.group || ""}:${normalizeFilterOptionLabel(option.label || "")}`;
511
- if (seenSelected.has(key)) continue;
512
- seenSelected.add(key);
513
- dedupedSelected.push(option);
514
- }
515
-
516
- return {
517
- selected_option: dedupedSelected[0] || null,
518
- selected_options: dedupedSelected,
519
- option_box: dedupedSelected.find((option) => option.option_box)?.option_box || null,
520
- discovered_options: discoveredOptions
521
- };
522
- }
523
-
524
- export async function confirmFilterPanel(client, frameNodeId, {
525
- timeoutMs = 8000
526
- } = {}) {
527
- const candidates = await readConfirmButtonCandidates(client, frameNodeId);
528
- if (!candidates.length && await getFilterPanelCount(client, frameNodeId) === 0) {
529
- return {
530
- confirmed: true,
531
- confirm_node_id: null,
532
- confirm_label: "auto-closed",
533
- confirm_candidates: [],
534
- confirm_attempts: [],
535
- panel_count: 0
536
- };
537
- }
538
- if (!candidates.length) {
539
- throw new Error("Recommend filter confirm button was not found");
540
- }
541
-
542
- const attempts = [];
543
- for (const candidate of candidates) {
544
- const clickResult = await clickFirstAvailableNode(client, [candidate.node_id]);
545
- attempts.push({
546
- node_id: candidate.node_id,
547
- label: candidate.label,
548
- clicked: clickResult.clicked,
549
- errors: clickResult.errors
550
- });
551
- if (!clickResult.clicked) continue;
552
-
553
- const started = Date.now();
554
- while (Date.now() - started <= timeoutMs) {
555
- const panelCount = await getFilterPanelCount(client, frameNodeId);
556
- if (panelCount === 0) {
557
- return {
558
- confirmed: true,
559
- confirm_node_id: clickResult.node_id,
560
- confirm_label: candidate.label,
561
- confirm_box: clickResult.box,
562
- confirm_candidates: candidates,
563
- confirm_attempts: attempts,
564
- panel_count: 0
565
- };
566
- }
567
- await sleep(250);
568
- }
569
- }
570
-
571
- return {
572
- confirmed: false,
573
- confirm_node_id: attempts.at(-1)?.node_id || null,
574
- confirm_label: attempts.at(-1)?.label || null,
575
- confirm_candidates: candidates,
576
- confirm_attempts: attempts,
577
- panel_count: await getFilterPanelCount(client, frameNodeId)
578
- };
579
- }
580
-
581
- export async function selectAndConfirmFirstSafeFilter(client, frameNodeId, options = {}) {
582
- const beforeCounts = await getRecommendFilterCounts(client, frameNodeId);
583
- const openResult = await openFilterPanel(client, frameNodeId);
584
- const afterOpenCounts = await getRecommendFilterCounts(client, frameNodeId);
585
- const filterGroups = Array.isArray(options.filterGroups) ? options.filterGroups : [];
586
- const selection = filterGroups.length
587
- ? await selectFilterGroups(client, frameNodeId, { filterGroups, groupOrder: options.groupOrder })
588
- : options.selectAllLabels
589
- ? await selectFilterOptions(client, frameNodeId, options)
590
- : await selectFilterOption(client, frameNodeId, options);
591
- const confirm = await confirmFilterPanel(client, frameNodeId);
592
- await sleep(1200);
593
- const afterConfirmCounts = await getRecommendFilterCounts(client, frameNodeId);
594
-
595
- return {
596
- opened_panel: true,
597
- trigger: {
598
- node_id: openResult.trigger.nodeId,
599
- selector: openResult.trigger.selector,
600
- center: openResult.trigger_box.center,
601
- rect: openResult.trigger_box.rect
602
- },
603
- initial_close_attempts: openResult.initial_close_attempts,
604
- before_counts: beforeCounts,
605
- after_open_counts: afterOpenCounts,
606
- ...selection,
607
- ...confirm,
608
- after_confirm_counts: afterConfirmCounts
609
- };
610
- }
1
+ import {
2
+ clickNodeCenter,
3
+ countSelectors,
4
+ DETERMINISTIC_CLICK_OPTIONS,
5
+ findFirstNode,
6
+ getAttributesMap,
7
+ getNodeBox,
8
+ getOuterHTML,
9
+ pressKey,
10
+ querySelectorAll,
11
+ sleep,
12
+ waitForSelector
13
+ } from "../../core/browser/index.js";
14
+ import { htmlToText, normalizeText } from "../../core/screening/index.js";
15
+ import {
16
+ RECOMMEND_CARD_SELECTOR,
17
+ RECOMMEND_FILTER_GROUP_ORDER,
18
+ RECOMMEND_FILTER_SELECTORS
19
+ } from "./constants.js";
20
+
21
+ const SKIP_OPTION_LABELS = new Set(["不限", "全部", "all"]);
22
+
23
+ export function normalizeFilterOptionLabel(label) {
24
+ return normalizeText(label).replace(/\s+/g, "");
25
+ }
26
+
27
+ export function isSafeFilterOptionLabel(label) {
28
+ const normalized = normalizeFilterOptionLabel(label);
29
+ return Boolean(normalized) && !SKIP_OPTION_LABELS.has(normalized.toLowerCase());
30
+ }
31
+
32
+ export function isActiveOption(attributes = {}, outerHTML = "") {
33
+ const className = attributes.class || "";
34
+ return /\bactive\b/.test(className) || /\bactive\b/.test(String(outerHTML || "").split(">")[0] || "");
35
+ }
36
+
37
+ export function chooseFirstSafeFilterOption(options = [], groupOrder = RECOMMEND_FILTER_GROUP_ORDER) {
38
+ for (const group of groupOrder) {
39
+ const option = options.find((item) => (
40
+ item.group === group
41
+ && !item.active
42
+ && isSafeFilterOptionLabel(item.label)
43
+ ));
44
+ if (option) return option;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ export function chooseFilterOptionByLabels(options = [], {
50
+ group = "",
51
+ labels = []
52
+ } = {}) {
53
+ const normalizedGroup = normalizeText(group);
54
+ const normalizedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
55
+ for (const label of normalizedLabels) {
56
+ const option = options.find((item) => (
57
+ (!normalizedGroup || item.group === normalizedGroup)
58
+ && !item.active
59
+ && normalizeFilterOptionLabel(item.label) === label
60
+ && isSafeFilterOptionLabel(item.label)
61
+ ));
62
+ if (option) return option;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ export function chooseFilterOptionsByLabels(options = [], {
68
+ group = "",
69
+ labels = []
70
+ } = {}) {
71
+ const normalizedGroup = normalizeText(group);
72
+ const normalizedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
73
+ return normalizedLabels.map((label) => {
74
+ const option = options.find((item) => (
75
+ (!normalizedGroup || item.group === normalizedGroup)
76
+ && normalizeFilterOptionLabel(item.label) === label
77
+ && isSafeFilterOptionLabel(item.label)
78
+ ));
79
+ return {
80
+ label,
81
+ option: option || null
82
+ };
83
+ });
84
+ }
85
+
86
+ export async function getFilterPanelCount(client, frameNodeId) {
87
+ return (await querySelectorAll(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.panel)).length;
88
+ }
89
+
90
+ export async function getRecommendFilterCounts(client, frameNodeId) {
91
+ return countSelectors(client, frameNodeId, {
92
+ filter_trigger: RECOMMEND_FILTER_SELECTORS.trigger,
93
+ filter_panel: RECOMMEND_FILTER_SELECTORS.panel,
94
+ check_box: RECOMMEND_FILTER_SELECTORS.checkBox,
95
+ option: `.filter-panel ${RECOMMEND_FILTER_SELECTORS.option}`,
96
+ active_option: RECOMMEND_FILTER_SELECTORS.activeOption,
97
+ recommend_card: RECOMMEND_CARD_SELECTOR
98
+ });
99
+ }
100
+
101
+ export async function findFilterTrigger(client, frameNodeId) {
102
+ return findFirstNode(client, frameNodeId, [
103
+ RECOMMEND_FILTER_SELECTORS.trigger,
104
+ ".recommend-filter.op-filter"
105
+ ]);
106
+ }
107
+
108
+ export async function ensureFilterPanelClosed(client, frameNodeId, triggerNodeId = 0) {
109
+ const attempts = [];
110
+ if (await getFilterPanelCount(client, frameNodeId) === 0) return attempts;
111
+
112
+ await pressKey(client, "Escape", {
113
+ code: "Escape",
114
+ windowsVirtualKeyCode: 27,
115
+ nativeVirtualKeyCode: 27
116
+ });
117
+ await sleep(400);
118
+ attempts.push("Escape");
119
+
120
+ if (await getFilterPanelCount(client, frameNodeId) > 0 && triggerNodeId) {
121
+ await clickNodeCenter(client, triggerNodeId, DETERMINISTIC_CLICK_OPTIONS);
122
+ await sleep(500);
123
+ attempts.push("filter-trigger-toggle");
124
+ }
125
+
126
+ return attempts;
127
+ }
128
+
129
+ async function dismissRecommendControlOverlays(client, settleMs = 250) {
130
+ if (typeof client?.Input?.dispatchKeyEvent !== "function") {
131
+ return ["Escape-unavailable"];
132
+ }
133
+ await pressKey(client, "Escape", {
134
+ code: "Escape",
135
+ windowsVirtualKeyCode: 27,
136
+ nativeVirtualKeyCode: 27
137
+ });
138
+ if (settleMs > 0) await sleep(settleMs);
139
+ return ["Escape"];
140
+ }
141
+
142
+ async function findFilterTriggerCandidates(client, frameNodeId) {
143
+ const candidates = [];
144
+ const seen = new Set();
145
+ for (const selector of [
146
+ RECOMMEND_FILTER_SELECTORS.trigger,
147
+ ".recommend-filter.op-filter"
148
+ ]) {
149
+ const candidate = await findFirstNode(client, frameNodeId, [selector]);
150
+ if (candidate && !seen.has(candidate.nodeId)) {
151
+ candidates.push(candidate);
152
+ seen.add(candidate.nodeId);
153
+ }
154
+ }
155
+ return candidates;
156
+ }
157
+
158
+ export async function openFilterPanel(client, frameNodeId) {
159
+ let triggerCandidates = await findFilterTriggerCandidates(client, frameNodeId);
160
+ if (!triggerCandidates.length) {
161
+ throw new Error("Recommend filter trigger was not found");
162
+ }
163
+
164
+ const preOpenDismissalAttempts = await dismissRecommendControlOverlays(client);
165
+ const existingPanelNodeId = await waitForSelector(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.panel, {
166
+ timeoutMs: 300,
167
+ intervalMs: 100
168
+ });
169
+ if (existingPanelNodeId) {
170
+ const triggerBox = await getNodeBox(client, triggerCandidates[0].nodeId);
171
+ return {
172
+ trigger: triggerCandidates[0],
173
+ trigger_box: triggerBox,
174
+ panel_node_id: existingPanelNodeId,
175
+ initial_close_attempts: preOpenDismissalAttempts,
176
+ already_open: true
177
+ };
178
+ }
179
+
180
+ const closeAttempts = await ensureFilterPanelClosed(client, frameNodeId, triggerCandidates[0].nodeId);
181
+
182
+ const attempts = [];
183
+ for (let round = 0; round < 3; round += 1) {
184
+ triggerCandidates = await findFilterTriggerCandidates(client, frameNodeId);
185
+ for (const trigger of triggerCandidates) {
186
+ const triggerBox = await getNodeBox(client, trigger.nodeId);
187
+ const clickBox = await clickNodeCenter(client, trigger.nodeId, DETERMINISTIC_CLICK_OPTIONS);
188
+ attempts.push({
189
+ selector: trigger.selector,
190
+ node_id: trigger.nodeId,
191
+ center: triggerBox.center,
192
+ click_target: clickBox.click_target,
193
+ click_result: clickBox.click_result
194
+ });
195
+ const panelNodeId = await waitForSelector(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.panel, {
196
+ timeoutMs: 2500,
197
+ intervalMs: 200
198
+ });
199
+ if (panelNodeId) {
200
+ return {
201
+ trigger,
202
+ trigger_box: triggerBox,
203
+ panel_node_id: panelNodeId,
204
+ initial_close_attempts: [
205
+ ...preOpenDismissalAttempts,
206
+ ...closeAttempts
207
+ ],
208
+ open_attempts: attempts
209
+ };
210
+ }
211
+ }
212
+ await sleep(500);
213
+ }
214
+
215
+ throw new Error(`Recommend filter panel did not open after ${attempts.length} trigger attempts`);
216
+ }
217
+
218
+ async function readOptionNode(client, group, nodeId) {
219
+ const [attributes, outerHTML] = await Promise.all([
220
+ getAttributesMap(client, nodeId),
221
+ getOuterHTML(client, nodeId)
222
+ ]);
223
+ const label = normalizeFilterOptionLabel(htmlToText(outerHTML));
224
+ return {
225
+ group,
226
+ node_id: nodeId,
227
+ label,
228
+ active: isActiveOption(attributes, outerHTML),
229
+ attributes: {
230
+ class: attributes.class || "",
231
+ value: attributes.value || "",
232
+ type: attributes.type || ""
233
+ }
234
+ };
235
+ }
236
+
237
+ export async function listFilterOptions(client, frameNodeId, {
238
+ groupOrder = RECOMMEND_FILTER_GROUP_ORDER
239
+ } = {}) {
240
+ const options = [];
241
+ for (const group of groupOrder) {
242
+ const groupSelector = RECOMMEND_FILTER_SELECTORS.groups[group];
243
+ if (!groupSelector) continue;
244
+ const groupNodeIds = await querySelectorAll(client, frameNodeId, groupSelector);
245
+ for (const groupNodeId of groupNodeIds) {
246
+ const optionNodeIds = await querySelectorAll(client, groupNodeId, RECOMMEND_FILTER_SELECTORS.option);
247
+ for (const optionNodeId of optionNodeIds) {
248
+ options.push(await readOptionNode(client, group, optionNodeId));
249
+ }
250
+ }
251
+ }
252
+ return options;
253
+ }
254
+
255
+ async function clickFirstAvailableNode(client, nodeIds) {
256
+ const errors = [];
257
+ for (const nodeId of nodeIds) {
258
+ try {
259
+ const box = await clickNodeCenter(client, nodeId, DETERMINISTIC_CLICK_OPTIONS);
260
+ return {
261
+ clicked: true,
262
+ node_id: nodeId,
263
+ box
264
+ };
265
+ } catch (error) {
266
+ errors.push({
267
+ node_id: nodeId,
268
+ message: error?.message || String(error)
269
+ });
270
+ }
271
+ }
272
+ return {
273
+ clicked: false,
274
+ errors
275
+ };
276
+ }
277
+
278
+ function normalizeButtonLabel(label) {
279
+ return normalizeFilterOptionLabel(label).toLowerCase();
280
+ }
281
+
282
+ function buttonRank(candidate) {
283
+ const label = normalizeButtonLabel(candidate.label);
284
+ if (/确定|确认|完成|ok|confirm/.test(label)) return 0;
285
+ if (/重置|清空|取消|reset|cancel/.test(label)) return 2;
286
+ return 1;
287
+ }
288
+
289
+ async function readButtonCandidate(client, nodeId, index) {
290
+ const [attributes, outerHTML] = await Promise.all([
291
+ getAttributesMap(client, nodeId),
292
+ getOuterHTML(client, nodeId)
293
+ ]);
294
+ return {
295
+ node_id: nodeId,
296
+ index,
297
+ label: normalizeButtonLabel(htmlToText(outerHTML)),
298
+ class_name: attributes.class || ""
299
+ };
300
+ }
301
+
302
+ async function readConfirmButtonCandidates(client, frameNodeId) {
303
+ const nodeIds = await querySelectorAll(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.confirmButton);
304
+ const candidates = [];
305
+ for (let index = 0; index < nodeIds.length; index += 1) {
306
+ candidates.push(await readButtonCandidate(client, nodeIds[index], index));
307
+ }
308
+ return candidates.sort((left, right) => {
309
+ const rankDiff = buttonRank(left) - buttonRank(right);
310
+ if (rankDiff !== 0) return rankDiff;
311
+ return right.index - left.index;
312
+ });
313
+ }
314
+
315
+ export async function selectFirstSafeFilterOption(client, frameNodeId, {
316
+ groupOrder = RECOMMEND_FILTER_GROUP_ORDER
317
+ } = {}) {
318
+ const options = await listFilterOptions(client, frameNodeId, { groupOrder });
319
+ const selected = chooseFirstSafeFilterOption(options, groupOrder);
320
+ if (!selected) {
321
+ throw new Error("No safe non-active recommend filter option was found");
322
+ }
323
+
324
+ const box = await clickNodeCenter(client, selected.node_id, {
325
+ ...DETERMINISTIC_CLICK_OPTIONS,
326
+ scrollIntoView: true
327
+ });
328
+ await sleep(300);
329
+
330
+ return {
331
+ selected_option: {
332
+ group: selected.group,
333
+ label: selected.label,
334
+ node_id: selected.node_id,
335
+ was_active: selected.active
336
+ },
337
+ option_box: box,
338
+ discovered_options: options.map((option) => ({
339
+ group: option.group,
340
+ label: option.label,
341
+ active: option.active,
342
+ node_id: option.node_id
343
+ }))
344
+ };
345
+ }
346
+
347
+ export async function selectFilterOption(client, frameNodeId, {
348
+ group = "",
349
+ labels = [],
350
+ groupOrder = RECOMMEND_FILTER_GROUP_ORDER
351
+ } = {}) {
352
+ const options = await listFilterOptions(client, frameNodeId, { groupOrder });
353
+ const selected = labels.length
354
+ ? chooseFilterOptionByLabels(options, { group, labels })
355
+ : chooseFirstSafeFilterOption(options, groupOrder);
356
+
357
+ if (!selected) {
358
+ const target = labels.length
359
+ ? `${group || "any group"} / ${labels.join(", ")}`
360
+ : "first safe non-active option";
361
+ throw new Error(`No matching recommend filter option was found for ${target}`);
362
+ }
363
+
364
+ const box = await clickNodeCenter(client, selected.node_id, {
365
+ ...DETERMINISTIC_CLICK_OPTIONS,
366
+ scrollIntoView: true
367
+ });
368
+ await sleep(300);
369
+
370
+ return {
371
+ selected_option: {
372
+ group: selected.group,
373
+ label: selected.label,
374
+ node_id: selected.node_id,
375
+ was_active: selected.active,
376
+ requested_group: group || null,
377
+ requested_labels: labels
378
+ },
379
+ option_box: box,
380
+ discovered_options: options.map((option) => ({
381
+ group: option.group,
382
+ label: option.label,
383
+ active: option.active,
384
+ node_id: option.node_id
385
+ }))
386
+ };
387
+ }
388
+
389
+ export async function selectFilterOptions(client, frameNodeId, {
390
+ group = "",
391
+ labels = [],
392
+ groupOrder = RECOMMEND_FILTER_GROUP_ORDER
393
+ } = {}) {
394
+ if (!labels.length) {
395
+ return selectFilterOption(client, frameNodeId, { group, labels, groupOrder });
396
+ }
397
+
398
+ const selectedOptions = [];
399
+ const missingLabels = [];
400
+ let discoveredOptions = [];
401
+
402
+ for (const label of labels) {
403
+ if (await getFilterPanelCount(client, frameNodeId) === 0) {
404
+ await openFilterPanel(client, frameNodeId);
405
+ }
406
+
407
+ const options = await listFilterOptions(client, frameNodeId, { groupOrder });
408
+ discoveredOptions = options.map((option) => ({
409
+ group: option.group,
410
+ label: option.label,
411
+ active: option.active,
412
+ node_id: option.node_id
413
+ }));
414
+ const selected = chooseFilterOptionByLabels(options, { group, labels: [label] });
415
+ const alreadyActive = options.find((option) => (
416
+ (!group || option.group === group)
417
+ && normalizeFilterOptionLabel(option.label) === normalizeFilterOptionLabel(label)
418
+ && option.active
419
+ ));
420
+
421
+ if (alreadyActive) {
422
+ selectedOptions.push({
423
+ group: alreadyActive.group,
424
+ label: alreadyActive.label,
425
+ node_id: alreadyActive.node_id,
426
+ was_active: true,
427
+ clicked: false,
428
+ requested_group: group || null
429
+ });
430
+ continue;
431
+ }
432
+
433
+ if (!selected) {
434
+ missingLabels.push(label);
435
+ continue;
436
+ }
437
+
438
+ const box = await clickNodeCenter(client, selected.node_id, {
439
+ ...DETERMINISTIC_CLICK_OPTIONS,
440
+ scrollIntoView: true
441
+ });
442
+ selectedOptions.push({
443
+ group: selected.group,
444
+ label: selected.label,
445
+ node_id: selected.node_id,
446
+ was_active: false,
447
+ clicked: true,
448
+ requested_group: group || null,
449
+ option_box: box
450
+ });
451
+ await sleep(450);
452
+ }
453
+
454
+ if (missingLabels.length) {
455
+ throw new Error(`No matching recommend filter options were found for ${group || "any group"} / ${missingLabels.join(", ")}`);
456
+ }
457
+
458
+ return {
459
+ selected_option: selectedOptions[0] || null,
460
+ selected_options: selectedOptions.map((option) => ({
461
+ group: option.group,
462
+ label: option.label,
463
+ node_id: option.node_id,
464
+ was_active: option.was_active,
465
+ clicked: option.clicked,
466
+ requested_group: option.requested_group,
467
+ requested_labels: labels
468
+ })),
469
+ option_box: selectedOptions.find((option) => option.option_box)?.option_box || null,
470
+ discovered_options: discoveredOptions
471
+ };
472
+ }
473
+
474
+ export async function selectFilterGroups(client, frameNodeId, {
475
+ filterGroups = [],
476
+ groupOrder = RECOMMEND_FILTER_GROUP_ORDER
477
+ } = {}) {
478
+ const selectedOptions = [];
479
+ const discoveredOptions = [];
480
+ const groups = filterGroups.filter((item) => item && (item.group || item.labels?.length));
481
+ if (!groups.length) {
482
+ return selectFilterOption(client, frameNodeId, { groupOrder });
483
+ }
484
+
485
+ for (const spec of groups) {
486
+ const labels = Array.isArray(spec.labels) ? spec.labels : [];
487
+ const selection = spec.selectAllLabels === false
488
+ ? await selectFilterOption(client, frameNodeId, {
489
+ group: spec.group || "",
490
+ labels,
491
+ groupOrder
492
+ })
493
+ : await selectFilterOptions(client, frameNodeId, {
494
+ group: spec.group || "",
495
+ labels,
496
+ groupOrder
497
+ });
498
+ if (selection.selected_option) selectedOptions.push(selection.selected_option);
499
+ for (const option of selection.selected_options || []) {
500
+ selectedOptions.push(option);
501
+ }
502
+ for (const option of selection.discovered_options || []) {
503
+ discoveredOptions.push(option);
504
+ }
505
+ }
506
+
507
+ const dedupedSelected = [];
508
+ const seenSelected = new Set();
509
+ for (const option of selectedOptions) {
510
+ const key = `${option.group || ""}:${normalizeFilterOptionLabel(option.label || "")}`;
511
+ if (seenSelected.has(key)) continue;
512
+ seenSelected.add(key);
513
+ dedupedSelected.push(option);
514
+ }
515
+
516
+ return {
517
+ selected_option: dedupedSelected[0] || null,
518
+ selected_options: dedupedSelected,
519
+ option_box: dedupedSelected.find((option) => option.option_box)?.option_box || null,
520
+ discovered_options: discoveredOptions
521
+ };
522
+ }
523
+
524
+ export async function confirmFilterPanel(client, frameNodeId, {
525
+ timeoutMs = 8000
526
+ } = {}) {
527
+ const candidates = await readConfirmButtonCandidates(client, frameNodeId);
528
+ if (!candidates.length && await getFilterPanelCount(client, frameNodeId) === 0) {
529
+ return {
530
+ confirmed: true,
531
+ confirm_node_id: null,
532
+ confirm_label: "auto-closed",
533
+ confirm_candidates: [],
534
+ confirm_attempts: [],
535
+ panel_count: 0
536
+ };
537
+ }
538
+ if (!candidates.length) {
539
+ throw new Error("Recommend filter confirm button was not found");
540
+ }
541
+
542
+ const attempts = [];
543
+ for (const candidate of candidates) {
544
+ const clickResult = await clickFirstAvailableNode(client, [candidate.node_id]);
545
+ attempts.push({
546
+ node_id: candidate.node_id,
547
+ label: candidate.label,
548
+ clicked: clickResult.clicked,
549
+ errors: clickResult.errors
550
+ });
551
+ if (!clickResult.clicked) continue;
552
+
553
+ const started = Date.now();
554
+ while (Date.now() - started <= timeoutMs) {
555
+ const panelCount = await getFilterPanelCount(client, frameNodeId);
556
+ if (panelCount === 0) {
557
+ return {
558
+ confirmed: true,
559
+ confirm_node_id: clickResult.node_id,
560
+ confirm_label: candidate.label,
561
+ confirm_box: clickResult.box,
562
+ confirm_candidates: candidates,
563
+ confirm_attempts: attempts,
564
+ panel_count: 0
565
+ };
566
+ }
567
+ await sleep(250);
568
+ }
569
+ }
570
+
571
+ return {
572
+ confirmed: false,
573
+ confirm_node_id: attempts.at(-1)?.node_id || null,
574
+ confirm_label: attempts.at(-1)?.label || null,
575
+ confirm_candidates: candidates,
576
+ confirm_attempts: attempts,
577
+ panel_count: await getFilterPanelCount(client, frameNodeId)
578
+ };
579
+ }
580
+
581
+ export async function selectAndConfirmFirstSafeFilter(client, frameNodeId, options = {}) {
582
+ const beforeCounts = await getRecommendFilterCounts(client, frameNodeId);
583
+ const openResult = await openFilterPanel(client, frameNodeId);
584
+ const afterOpenCounts = await getRecommendFilterCounts(client, frameNodeId);
585
+ const filterGroups = Array.isArray(options.filterGroups) ? options.filterGroups : [];
586
+ const selection = filterGroups.length
587
+ ? await selectFilterGroups(client, frameNodeId, { filterGroups, groupOrder: options.groupOrder })
588
+ : options.selectAllLabels
589
+ ? await selectFilterOptions(client, frameNodeId, options)
590
+ : await selectFilterOption(client, frameNodeId, options);
591
+ const confirm = await confirmFilterPanel(client, frameNodeId);
592
+ await sleep(1200);
593
+ const afterConfirmCounts = await getRecommendFilterCounts(client, frameNodeId);
594
+
595
+ return {
596
+ opened_panel: true,
597
+ trigger: {
598
+ node_id: openResult.trigger.nodeId,
599
+ selector: openResult.trigger.selector,
600
+ center: openResult.trigger_box.center,
601
+ rect: openResult.trigger_box.rect
602
+ },
603
+ initial_close_attempts: openResult.initial_close_attempts,
604
+ before_counts: beforeCounts,
605
+ after_open_counts: afterOpenCounts,
606
+ ...selection,
607
+ ...confirm,
608
+ after_confirm_counts: afterConfirmCounts
609
+ };
610
+ }