@reconcrap/boss-recommend-mcp 1.3.38 → 2.0.0

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