@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
@@ -8,60 +8,60 @@ import {
8
8
  getOuterHTML,
9
9
  pressKey,
10
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_ACTIVITY_GROUP,
17
- RECOMMEND_ACTIVITY_GROUP_HEADING,
18
- RECOMMEND_ACTIVITY_LEVELS,
19
- RECOMMEND_CARD_SELECTOR,
20
- RECOMMEND_FILTER_GROUP_ORDER,
21
- RECOMMEND_FILTER_SELECTORS
22
- } from "./constants.js";
23
-
11
+ sleep,
12
+ waitForSelector
13
+ } from "../../core/browser/index.js";
14
+ import { htmlToText, normalizeText } from "../../core/screening/index.js";
15
+ import {
16
+ RECOMMEND_ACTIVITY_GROUP,
17
+ RECOMMEND_ACTIVITY_GROUP_HEADING,
18
+ RECOMMEND_ACTIVITY_LEVELS,
19
+ RECOMMEND_CARD_SELECTOR,
20
+ RECOMMEND_FILTER_GROUP_ORDER,
21
+ RECOMMEND_FILTER_SELECTORS
22
+ } from "./constants.js";
23
+
24
24
  const SKIP_OPTION_LABELS = new Set(["不限", "全部", "all"]);
25
25
 
26
26
  export function normalizeFilterOptionLabel(label) {
27
27
  return normalizeText(label).replace(/\s+/g, "");
28
28
  }
29
29
 
30
- export function isSafeFilterOptionLabel(label) {
31
- const normalized = normalizeFilterOptionLabel(label);
32
- return Boolean(normalized) && !SKIP_OPTION_LABELS.has(normalized.toLowerCase());
33
- }
34
-
35
- function isSelectableFilterOptionLabel(label, { allowUnlimited = false } = {}) {
36
- const normalized = normalizeFilterOptionLabel(label);
37
- if (!normalized) return false;
38
- return allowUnlimited || !SKIP_OPTION_LABELS.has(normalized.toLowerCase());
39
- }
40
-
41
- function normalizeActivityGroupText(text) {
42
- return normalizeFilterOptionLabel(text)
43
- .replace(/【/g, "[")
44
- .replace(/】/g, "]");
45
- }
46
-
47
- export function isRecommendActivityGroupText(text) {
48
- const normalized = normalizeActivityGroupText(text);
49
- const heading = normalizeActivityGroupText(RECOMMEND_ACTIVITY_GROUP_HEADING);
50
- const exactOptionCount = RECOMMEND_ACTIVITY_LEVELS.filter((label) => (
51
- normalized.includes(normalizeFilterOptionLabel(label))
52
- )).length;
53
- const hasUnlimited = normalized.includes(normalizeFilterOptionLabel("不限"));
54
- return (
55
- (normalized.startsWith(heading) && exactOptionCount >= 2 && hasUnlimited)
56
- || (exactOptionCount === RECOMMEND_ACTIVITY_LEVELS.length && hasUnlimited)
57
- );
58
- }
59
-
60
- export function isActiveOption(attributes = {}, outerHTML = "") {
61
- const className = attributes.class || "";
62
- return /\bactive\b/.test(className) || /\bactive\b/.test(String(outerHTML || "").split(">")[0] || "");
63
- }
64
-
30
+ export function isSafeFilterOptionLabel(label) {
31
+ const normalized = normalizeFilterOptionLabel(label);
32
+ return Boolean(normalized) && !SKIP_OPTION_LABELS.has(normalized.toLowerCase());
33
+ }
34
+
35
+ function isSelectableFilterOptionLabel(label, { allowUnlimited = false } = {}) {
36
+ const normalized = normalizeFilterOptionLabel(label);
37
+ if (!normalized) return false;
38
+ return allowUnlimited || !SKIP_OPTION_LABELS.has(normalized.toLowerCase());
39
+ }
40
+
41
+ function normalizeActivityGroupText(text) {
42
+ return normalizeFilterOptionLabel(text)
43
+ .replace(/【/g, "[")
44
+ .replace(/】/g, "]");
45
+ }
46
+
47
+ export function isRecommendActivityGroupText(text) {
48
+ const normalized = normalizeActivityGroupText(text);
49
+ const heading = normalizeActivityGroupText(RECOMMEND_ACTIVITY_GROUP_HEADING);
50
+ const exactOptionCount = RECOMMEND_ACTIVITY_LEVELS.filter((label) => (
51
+ normalized.includes(normalizeFilterOptionLabel(label))
52
+ )).length;
53
+ const hasUnlimited = normalized.includes(normalizeFilterOptionLabel("不限"));
54
+ return (
55
+ (normalized.startsWith(heading) && exactOptionCount >= 2 && hasUnlimited)
56
+ || (exactOptionCount === RECOMMEND_ACTIVITY_LEVELS.length && hasUnlimited)
57
+ );
58
+ }
59
+
60
+ export function isActiveOption(attributes = {}, outerHTML = "") {
61
+ const className = attributes.class || "";
62
+ return /\bactive\b/.test(className) || /\bactive\b/.test(String(outerHTML || "").split(">")[0] || "");
63
+ }
64
+
65
65
  export function chooseFirstSafeFilterOption(options = [], groupOrder = RECOMMEND_FILTER_GROUP_ORDER) {
66
66
  for (const group of groupOrder) {
67
67
  const option = options.find((item) => (
@@ -71,45 +71,45 @@ export function chooseFirstSafeFilterOption(options = [], groupOrder = RECOMMEND
71
71
  ));
72
72
  if (option) return option;
73
73
  }
74
- return null;
75
- }
76
-
77
- export function chooseFilterOptionByLabels(options = [], {
78
- group = "",
79
- labels = [],
80
- allowUnlimited = false
81
- } = {}) {
82
- const normalizedGroup = normalizeText(group);
83
- const normalizedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
84
- for (const label of normalizedLabels) {
85
- const option = options.find((item) => (
86
- (!normalizedGroup || item.group === normalizedGroup)
87
- && !item.active
88
- && normalizeFilterOptionLabel(item.label) === label
89
- && isSelectableFilterOptionLabel(item.label, { allowUnlimited })
90
- ));
91
- if (option) return option;
92
- }
93
- return null;
94
- }
95
-
96
- export function chooseFilterOptionsByLabels(options = [], {
97
- group = "",
98
- labels = [],
99
- allowUnlimited = false
100
- } = {}) {
101
- const normalizedGroup = normalizeText(group);
102
- const normalizedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
103
- return normalizedLabels.map((label) => {
104
- const option = options.find((item) => (
105
- (!normalizedGroup || item.group === normalizedGroup)
106
- && normalizeFilterOptionLabel(item.label) === label
107
- && isSelectableFilterOptionLabel(item.label, { allowUnlimited })
108
- ));
109
- return {
110
- label,
111
- option: option || null
112
- };
74
+ return null;
75
+ }
76
+
77
+ export function chooseFilterOptionByLabels(options = [], {
78
+ group = "",
79
+ labels = [],
80
+ allowUnlimited = false
81
+ } = {}) {
82
+ const normalizedGroup = normalizeText(group);
83
+ const normalizedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
84
+ for (const label of normalizedLabels) {
85
+ const option = options.find((item) => (
86
+ (!normalizedGroup || item.group === normalizedGroup)
87
+ && !item.active
88
+ && normalizeFilterOptionLabel(item.label) === label
89
+ && isSelectableFilterOptionLabel(item.label, { allowUnlimited })
90
+ ));
91
+ if (option) return option;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ export function chooseFilterOptionsByLabels(options = [], {
97
+ group = "",
98
+ labels = [],
99
+ allowUnlimited = false
100
+ } = {}) {
101
+ const normalizedGroup = normalizeText(group);
102
+ const normalizedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
103
+ return normalizedLabels.map((label) => {
104
+ const option = options.find((item) => (
105
+ (!normalizedGroup || item.group === normalizedGroup)
106
+ && normalizeFilterOptionLabel(item.label) === label
107
+ && isSelectableFilterOptionLabel(item.label, { allowUnlimited })
108
+ ));
109
+ return {
110
+ label,
111
+ option: option || null
112
+ };
113
113
  });
114
114
  }
115
115
 
@@ -185,12 +185,12 @@ async function findFilterTriggerCandidates(client, frameNodeId) {
185
185
  return candidates;
186
186
  }
187
187
 
188
- export async function openFilterPanel(client, frameNodeId) {
189
- let triggerCandidates = await findFilterTriggerCandidates(client, frameNodeId);
190
- if (!triggerCandidates.length) {
191
- const error = new Error("Recommend filter trigger was not found");
192
- error.code = "RECOMMEND_FILTER_TRIGGER_UNAVAILABLE";
193
- throw error;
188
+ export async function openFilterPanel(client, frameNodeId) {
189
+ let triggerCandidates = await findFilterTriggerCandidates(client, frameNodeId);
190
+ if (!triggerCandidates.length) {
191
+ const error = new Error("Recommend filter trigger was not found");
192
+ error.code = "RECOMMEND_FILTER_TRIGGER_UNAVAILABLE";
193
+ throw error;
194
194
  }
195
195
 
196
196
  const preOpenDismissalAttempts = await dismissRecommendControlOverlays(client);
@@ -261,76 +261,76 @@ async function readOptionNode(client, group, nodeId) {
261
261
  attributes: {
262
262
  class: attributes.class || "",
263
263
  value: attributes.value || "",
264
- type: attributes.type || ""
265
- }
266
- };
267
- }
268
-
269
- function uniqueNodeIds(nodeIds = []) {
270
- return [...new Set(nodeIds.filter((nodeId) => Number(nodeId) > 0))];
271
- }
272
-
273
- async function nodeMatchesActivityGroup(client, nodeId) {
274
- const outerHTML = await getOuterHTML(client, nodeId);
275
- return isRecommendActivityGroupText(htmlToText(outerHTML));
276
- }
277
-
278
- export async function resolveRecommendFilterGroupNodeIds(client, frameNodeId, group) {
279
- const groupSelector = RECOMMEND_FILTER_SELECTORS.groups[group];
280
- if (!groupSelector) return [];
281
- if (group !== RECOMMEND_ACTIVITY_GROUP) {
282
- return querySelectorAll(client, frameNodeId, groupSelector);
283
- }
284
-
285
- const selectorCandidates = uniqueNodeIds(await querySelectorAll(client, frameNodeId, groupSelector));
286
- const semanticMatches = [];
287
- for (const nodeId of selectorCandidates) {
288
- if (await nodeMatchesActivityGroup(client, nodeId)) semanticMatches.push(nodeId);
289
- }
290
- if (semanticMatches.length) return uniqueNodeIds(semanticMatches);
291
-
292
- const broadCandidates = uniqueNodeIds(await querySelectorAll(
293
- client,
294
- frameNodeId,
295
- RECOMMEND_FILTER_SELECTORS.checkBox
296
- ));
297
- for (const nodeId of broadCandidates) {
298
- if (selectorCandidates.includes(nodeId)) continue;
299
- try {
300
- if (await nodeMatchesActivityGroup(client, nodeId)) semanticMatches.push(nodeId);
301
- } catch {
302
- // An unrelated stale filter row does not make the activity control visible.
303
- }
304
- }
305
- if (semanticMatches.length) return uniqueNodeIds(semanticMatches);
306
- if (selectorCandidates.length) {
307
- throw new Error("Recommend activity filter control was visible but its semantic row could not be verified");
308
- }
309
- return [];
310
- }
311
-
312
- export async function listFilterOptions(client, frameNodeId, {
313
- groupOrder = RECOMMEND_FILTER_GROUP_ORDER
314
- } = {}) {
315
- const options = [];
316
- for (const group of groupOrder) {
317
- const groupNodeIds = await resolveRecommendFilterGroupNodeIds(client, frameNodeId, group);
318
- let optionCount = 0;
319
- for (const groupNodeId of groupNodeIds) {
320
- const optionNodeIds = await querySelectorAll(client, groupNodeId, RECOMMEND_FILTER_SELECTORS.option);
321
- for (const optionNodeId of optionNodeIds) {
322
- options.push(await readOptionNode(client, group, optionNodeId));
323
- optionCount += 1;
324
- }
325
- }
326
- if (group === RECOMMEND_ACTIVITY_GROUP && groupNodeIds.length && optionCount === 0) {
327
- throw new Error("Recommend activity filter control was visible but its options could not be read");
328
- }
329
- }
330
- return options;
331
- }
332
-
333
- async function clickFirstAvailableNode(client, nodeIds) {
264
+ type: attributes.type || ""
265
+ }
266
+ };
267
+ }
268
+
269
+ function uniqueNodeIds(nodeIds = []) {
270
+ return [...new Set(nodeIds.filter((nodeId) => Number(nodeId) > 0))];
271
+ }
272
+
273
+ async function nodeMatchesActivityGroup(client, nodeId) {
274
+ const outerHTML = await getOuterHTML(client, nodeId);
275
+ return isRecommendActivityGroupText(htmlToText(outerHTML));
276
+ }
277
+
278
+ export async function resolveRecommendFilterGroupNodeIds(client, frameNodeId, group) {
279
+ const groupSelector = RECOMMEND_FILTER_SELECTORS.groups[group];
280
+ if (!groupSelector) return [];
281
+ if (group !== RECOMMEND_ACTIVITY_GROUP) {
282
+ return querySelectorAll(client, frameNodeId, groupSelector);
283
+ }
284
+
285
+ const selectorCandidates = uniqueNodeIds(await querySelectorAll(client, frameNodeId, groupSelector));
286
+ const semanticMatches = [];
287
+ for (const nodeId of selectorCandidates) {
288
+ if (await nodeMatchesActivityGroup(client, nodeId)) semanticMatches.push(nodeId);
289
+ }
290
+ if (semanticMatches.length) return uniqueNodeIds(semanticMatches);
291
+
292
+ const broadCandidates = uniqueNodeIds(await querySelectorAll(
293
+ client,
294
+ frameNodeId,
295
+ RECOMMEND_FILTER_SELECTORS.checkBox
296
+ ));
297
+ for (const nodeId of broadCandidates) {
298
+ if (selectorCandidates.includes(nodeId)) continue;
299
+ try {
300
+ if (await nodeMatchesActivityGroup(client, nodeId)) semanticMatches.push(nodeId);
301
+ } catch {
302
+ // An unrelated stale filter row does not make the activity control visible.
303
+ }
304
+ }
305
+ if (semanticMatches.length) return uniqueNodeIds(semanticMatches);
306
+ if (selectorCandidates.length) {
307
+ throw new Error("Recommend activity filter control was visible but its semantic row could not be verified");
308
+ }
309
+ return [];
310
+ }
311
+
312
+ export async function listFilterOptions(client, frameNodeId, {
313
+ groupOrder = RECOMMEND_FILTER_GROUP_ORDER
314
+ } = {}) {
315
+ const options = [];
316
+ for (const group of groupOrder) {
317
+ const groupNodeIds = await resolveRecommendFilterGroupNodeIds(client, frameNodeId, group);
318
+ let optionCount = 0;
319
+ for (const groupNodeId of groupNodeIds) {
320
+ const optionNodeIds = await querySelectorAll(client, groupNodeId, RECOMMEND_FILTER_SELECTORS.option);
321
+ for (const optionNodeId of optionNodeIds) {
322
+ options.push(await readOptionNode(client, group, optionNodeId));
323
+ optionCount += 1;
324
+ }
325
+ }
326
+ if (group === RECOMMEND_ACTIVITY_GROUP && groupNodeIds.length && optionCount === 0) {
327
+ throw new Error("Recommend activity filter control was visible but its options could not be read");
328
+ }
329
+ }
330
+ return options;
331
+ }
332
+
333
+ async function clickFirstAvailableNode(client, nodeIds) {
334
334
  const errors = [];
335
335
  for (const nodeId of nodeIds) {
336
336
  try {
@@ -420,86 +420,86 @@ export async function selectFirstSafeFilterOption(client, frameNodeId, {
420
420
  node_id: option.node_id
421
421
  }))
422
422
  };
423
- }
424
-
425
- export async function selectFilterOption(client, frameNodeId, {
426
- group = "",
427
- labels = [],
428
- groupOrder = RECOMMEND_FILTER_GROUP_ORDER,
429
- allowUnlimited = false
430
- } = {}) {
431
- const options = await listFilterOptions(client, frameNodeId, { groupOrder });
432
- const alreadyActive = labels.length
433
- ? options.find((option) => (
434
- (!group || option.group === group)
435
- && labels.some((label) => (
436
- normalizeFilterOptionLabel(option.label) === normalizeFilterOptionLabel(label)
437
- ))
438
- && option.active
439
- && isSelectableFilterOptionLabel(option.label, { allowUnlimited })
440
- ))
441
- : null;
442
- if (alreadyActive) {
443
- return {
444
- selected_option: {
445
- group: alreadyActive.group,
446
- label: alreadyActive.label,
447
- node_id: alreadyActive.node_id,
448
- was_active: true,
449
- clicked: false,
450
- requested_group: group || null,
451
- requested_labels: labels
452
- },
453
- selected_options: [],
454
- option_box: null,
455
- unavailable: false,
456
- unavailable_groups: [],
457
- discovered_options: options.map((option) => ({
458
- group: option.group,
459
- label: option.label,
460
- active: option.active,
461
- node_id: option.node_id
462
- }))
463
- };
464
- }
465
- const selected = labels.length
466
- ? chooseFilterOptionByLabels(options, { group, labels, allowUnlimited })
467
- : chooseFirstSafeFilterOption(options, groupOrder);
468
-
469
- if (!selected) {
470
- const requestedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
471
- const groupOptions = options.filter((option) => !group || option.group === group);
472
- const unavailableDefault = (
473
- group === RECOMMEND_ACTIVITY_GROUP
474
- && allowUnlimited
475
- && requestedLabels.length === 1
476
- && requestedLabels[0] === normalizeFilterOptionLabel("不限")
477
- && groupOptions.length === 0
478
- );
479
- if (unavailableDefault) {
480
- return {
481
- selected_option: null,
482
- selected_options: [],
483
- option_box: null,
484
- unavailable: true,
485
- unavailable_groups: [{
486
- group,
487
- requested_labels: labels,
488
- reason: "activity_control_unavailable_default"
489
- }],
490
- discovered_options: options.map((option) => ({
491
- group: option.group,
492
- label: option.label,
493
- active: option.active,
494
- node_id: option.node_id
495
- }))
496
- };
497
- }
498
- const target = labels.length
499
- ? `${group || "any group"} / ${labels.join(", ")}`
500
- : "first safe non-active option";
501
- throw new Error(`No matching recommend filter option was found for ${target}`);
502
- }
423
+ }
424
+
425
+ export async function selectFilterOption(client, frameNodeId, {
426
+ group = "",
427
+ labels = [],
428
+ groupOrder = RECOMMEND_FILTER_GROUP_ORDER,
429
+ allowUnlimited = false
430
+ } = {}) {
431
+ const options = await listFilterOptions(client, frameNodeId, { groupOrder });
432
+ const alreadyActive = labels.length
433
+ ? options.find((option) => (
434
+ (!group || option.group === group)
435
+ && labels.some((label) => (
436
+ normalizeFilterOptionLabel(option.label) === normalizeFilterOptionLabel(label)
437
+ ))
438
+ && option.active
439
+ && isSelectableFilterOptionLabel(option.label, { allowUnlimited })
440
+ ))
441
+ : null;
442
+ if (alreadyActive) {
443
+ return {
444
+ selected_option: {
445
+ group: alreadyActive.group,
446
+ label: alreadyActive.label,
447
+ node_id: alreadyActive.node_id,
448
+ was_active: true,
449
+ clicked: false,
450
+ requested_group: group || null,
451
+ requested_labels: labels
452
+ },
453
+ selected_options: [],
454
+ option_box: null,
455
+ unavailable: false,
456
+ unavailable_groups: [],
457
+ discovered_options: options.map((option) => ({
458
+ group: option.group,
459
+ label: option.label,
460
+ active: option.active,
461
+ node_id: option.node_id
462
+ }))
463
+ };
464
+ }
465
+ const selected = labels.length
466
+ ? chooseFilterOptionByLabels(options, { group, labels, allowUnlimited })
467
+ : chooseFirstSafeFilterOption(options, groupOrder);
468
+
469
+ if (!selected) {
470
+ const requestedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
471
+ const groupOptions = options.filter((option) => !group || option.group === group);
472
+ const unavailableDefault = (
473
+ group === RECOMMEND_ACTIVITY_GROUP
474
+ && allowUnlimited
475
+ && requestedLabels.length === 1
476
+ && requestedLabels[0] === normalizeFilterOptionLabel("不限")
477
+ && groupOptions.length === 0
478
+ );
479
+ if (unavailableDefault) {
480
+ return {
481
+ selected_option: null,
482
+ selected_options: [],
483
+ option_box: null,
484
+ unavailable: true,
485
+ unavailable_groups: [{
486
+ group,
487
+ requested_labels: labels,
488
+ reason: "activity_control_unavailable_default"
489
+ }],
490
+ discovered_options: options.map((option) => ({
491
+ group: option.group,
492
+ label: option.label,
493
+ active: option.active,
494
+ node_id: option.node_id
495
+ }))
496
+ };
497
+ }
498
+ const target = labels.length
499
+ ? `${group || "any group"} / ${labels.join(", ")}`
500
+ : "first safe non-active option";
501
+ throw new Error(`No matching recommend filter option was found for ${target}`);
502
+ }
503
503
 
504
504
  const box = await clickNodeCenter(client, selected.node_id, {
505
505
  ...DETERMINISTIC_CLICK_OPTIONS,
@@ -508,45 +508,45 @@ export async function selectFilterOption(client, frameNodeId, {
508
508
  await sleep(300);
509
509
 
510
510
  return {
511
- selected_option: {
512
- group: selected.group,
513
- label: selected.label,
514
- node_id: selected.node_id,
515
- was_active: selected.active,
516
- clicked: true,
517
- requested_group: group || null,
518
- requested_labels: labels
519
- },
520
- option_box: box,
521
- unavailable: false,
522
- unavailable_groups: [],
523
- discovered_options: options.map((option) => ({
524
- group: option.group,
525
- label: option.label,
526
- active: option.active,
527
- node_id: option.node_id
511
+ selected_option: {
512
+ group: selected.group,
513
+ label: selected.label,
514
+ node_id: selected.node_id,
515
+ was_active: selected.active,
516
+ clicked: true,
517
+ requested_group: group || null,
518
+ requested_labels: labels
519
+ },
520
+ option_box: box,
521
+ unavailable: false,
522
+ unavailable_groups: [],
523
+ discovered_options: options.map((option) => ({
524
+ group: option.group,
525
+ label: option.label,
526
+ active: option.active,
527
+ node_id: option.node_id
528
528
  }))
529
529
  };
530
- }
531
-
532
- export async function selectFilterOptions(client, frameNodeId, {
533
- group = "",
534
- labels = [],
535
- groupOrder = RECOMMEND_FILTER_GROUP_ORDER,
536
- allowUnlimited = false
537
- } = {}) {
538
- if (!labels.length) {
539
- return selectFilterOption(client, frameNodeId, {
540
- group,
541
- labels,
542
- groupOrder,
543
- allowUnlimited
544
- });
545
- }
546
-
547
- const selectedOptions = [];
548
- const missingLabels = [];
549
- let discoveredOptions = [];
530
+ }
531
+
532
+ export async function selectFilterOptions(client, frameNodeId, {
533
+ group = "",
534
+ labels = [],
535
+ groupOrder = RECOMMEND_FILTER_GROUP_ORDER,
536
+ allowUnlimited = false
537
+ } = {}) {
538
+ if (!labels.length) {
539
+ return selectFilterOption(client, frameNodeId, {
540
+ group,
541
+ labels,
542
+ groupOrder,
543
+ allowUnlimited
544
+ });
545
+ }
546
+
547
+ const selectedOptions = [];
548
+ const missingLabels = [];
549
+ let discoveredOptions = [];
550
550
 
551
551
  for (const label of labels) {
552
552
  if (await getFilterPanelCount(client, frameNodeId) === 0) {
@@ -555,26 +555,26 @@ export async function selectFilterOptions(client, frameNodeId, {
555
555
 
556
556
  const options = await listFilterOptions(client, frameNodeId, { groupOrder });
557
557
  discoveredOptions = options.map((option) => ({
558
- group: option.group,
559
- label: option.label,
560
- active: option.active,
561
- node_id: option.node_id
562
- }));
563
- const selected = chooseFilterOptionByLabels(options, {
564
- group,
565
- labels: [label],
566
- allowUnlimited
567
- });
568
- const alreadyActive = options.find((option) => (
569
- (!group || option.group === group)
570
- && normalizeFilterOptionLabel(option.label) === normalizeFilterOptionLabel(label)
571
- && option.active
572
- && isSelectableFilterOptionLabel(option.label, { allowUnlimited })
573
- ));
574
-
575
- if (alreadyActive) {
576
- selectedOptions.push({
577
- group: alreadyActive.group,
558
+ group: option.group,
559
+ label: option.label,
560
+ active: option.active,
561
+ node_id: option.node_id
562
+ }));
563
+ const selected = chooseFilterOptionByLabels(options, {
564
+ group,
565
+ labels: [label],
566
+ allowUnlimited
567
+ });
568
+ const alreadyActive = options.find((option) => (
569
+ (!group || option.group === group)
570
+ && normalizeFilterOptionLabel(option.label) === normalizeFilterOptionLabel(label)
571
+ && option.active
572
+ && isSelectableFilterOptionLabel(option.label, { allowUnlimited })
573
+ ));
574
+
575
+ if (alreadyActive) {
576
+ selectedOptions.push({
577
+ group: alreadyActive.group,
578
578
  label: alreadyActive.label,
579
579
  node_id: alreadyActive.node_id,
580
580
  was_active: true,
@@ -601,113 +601,113 @@ export async function selectFilterOptions(client, frameNodeId, {
601
601
  clicked: true,
602
602
  requested_group: group || null,
603
603
  option_box: box
604
- });
605
- await sleep(450);
606
- }
607
-
608
- if (missingLabels.length) {
609
- const groupOptions = discoveredOptions.filter((option) => !group || option.group === group);
610
- const unavailableDefault = (
611
- group === RECOMMEND_ACTIVITY_GROUP
612
- && allowUnlimited
613
- && missingLabels.length === 1
614
- && normalizeFilterOptionLabel(missingLabels[0]) === normalizeFilterOptionLabel("不限")
615
- && groupOptions.length === 0
616
- );
617
- if (unavailableDefault) {
618
- return {
619
- selected_option: null,
620
- selected_options: [],
621
- option_box: null,
622
- unavailable: true,
623
- unavailable_groups: [{
624
- group,
625
- requested_labels: labels,
626
- reason: "activity_control_unavailable_default"
627
- }],
628
- discovered_options: discoveredOptions
629
- };
630
- }
631
- throw new Error(`No matching recommend filter options were found for ${group || "any group"} / ${missingLabels.join(", ")}`);
632
- }
633
-
634
- return {
635
- selected_option: selectedOptions[0] || null,
604
+ });
605
+ await sleep(450);
606
+ }
607
+
608
+ if (missingLabels.length) {
609
+ const groupOptions = discoveredOptions.filter((option) => !group || option.group === group);
610
+ const unavailableDefault = (
611
+ group === RECOMMEND_ACTIVITY_GROUP
612
+ && allowUnlimited
613
+ && missingLabels.length === 1
614
+ && normalizeFilterOptionLabel(missingLabels[0]) === normalizeFilterOptionLabel("不限")
615
+ && groupOptions.length === 0
616
+ );
617
+ if (unavailableDefault) {
618
+ return {
619
+ selected_option: null,
620
+ selected_options: [],
621
+ option_box: null,
622
+ unavailable: true,
623
+ unavailable_groups: [{
624
+ group,
625
+ requested_labels: labels,
626
+ reason: "activity_control_unavailable_default"
627
+ }],
628
+ discovered_options: discoveredOptions
629
+ };
630
+ }
631
+ throw new Error(`No matching recommend filter options were found for ${group || "any group"} / ${missingLabels.join(", ")}`);
632
+ }
633
+
634
+ return {
635
+ selected_option: selectedOptions[0] || null,
636
636
  selected_options: selectedOptions.map((option) => ({
637
637
  group: option.group,
638
638
  label: option.label,
639
639
  node_id: option.node_id,
640
640
  was_active: option.was_active,
641
- clicked: option.clicked,
642
- requested_group: option.requested_group,
643
- requested_labels: labels
644
- })),
645
- option_box: selectedOptions.find((option) => option.option_box)?.option_box || null,
646
- unavailable: false,
647
- unavailable_groups: [],
648
- discovered_options: discoveredOptions
649
- };
650
- }
651
-
652
- export async function selectFilterGroups(client, frameNodeId, {
653
- filterGroups = [],
654
- groupOrder = RECOMMEND_FILTER_GROUP_ORDER
655
- } = {}) {
656
- const selectedOptions = [];
657
- const discoveredOptions = [];
658
- const unavailableGroups = [];
659
- const groups = filterGroups.filter((item) => item && (item.group || item.labels?.length));
660
- if (!groups.length) {
661
- return selectFilterOption(client, frameNodeId, { groupOrder });
662
- }
663
-
641
+ clicked: option.clicked,
642
+ requested_group: option.requested_group,
643
+ requested_labels: labels
644
+ })),
645
+ option_box: selectedOptions.find((option) => option.option_box)?.option_box || null,
646
+ unavailable: false,
647
+ unavailable_groups: [],
648
+ discovered_options: discoveredOptions
649
+ };
650
+ }
651
+
652
+ export async function selectFilterGroups(client, frameNodeId, {
653
+ filterGroups = [],
654
+ groupOrder = RECOMMEND_FILTER_GROUP_ORDER
655
+ } = {}) {
656
+ const selectedOptions = [];
657
+ const discoveredOptions = [];
658
+ const unavailableGroups = [];
659
+ const groups = filterGroups.filter((item) => item && (item.group || item.labels?.length));
660
+ if (!groups.length) {
661
+ return selectFilterOption(client, frameNodeId, { groupOrder });
662
+ }
663
+
664
664
  for (const spec of groups) {
665
- const labels = Array.isArray(spec.labels) ? spec.labels : [];
666
- const selection = spec.selectAllLabels === false
667
- ? await selectFilterOption(client, frameNodeId, {
668
- group: spec.group || "",
669
- labels,
670
- groupOrder,
671
- allowUnlimited: spec.allowUnlimited === true
672
- })
673
- : await selectFilterOptions(client, frameNodeId, {
674
- group: spec.group || "",
675
- labels,
676
- groupOrder,
677
- allowUnlimited: spec.allowUnlimited === true
678
- });
679
- if (selection.selected_option) selectedOptions.push(selection.selected_option);
680
- for (const option of selection.selected_options || []) {
681
- selectedOptions.push(option);
682
- }
683
- for (const option of selection.discovered_options || []) {
684
- discoveredOptions.push(option);
685
- }
686
- for (const unavailable of selection.unavailable_groups || []) {
687
- unavailableGroups.push(unavailable);
688
- }
689
- }
690
-
691
- const dedupedSelected = [];
692
- const seenSelected = new Set();
693
- for (const option of selectedOptions) {
665
+ const labels = Array.isArray(spec.labels) ? spec.labels : [];
666
+ const selection = spec.selectAllLabels === false
667
+ ? await selectFilterOption(client, frameNodeId, {
668
+ group: spec.group || "",
669
+ labels,
670
+ groupOrder,
671
+ allowUnlimited: spec.allowUnlimited === true
672
+ })
673
+ : await selectFilterOptions(client, frameNodeId, {
674
+ group: spec.group || "",
675
+ labels,
676
+ groupOrder,
677
+ allowUnlimited: spec.allowUnlimited === true
678
+ });
679
+ if (selection.selected_option) selectedOptions.push(selection.selected_option);
680
+ for (const option of selection.selected_options || []) {
681
+ selectedOptions.push(option);
682
+ }
683
+ for (const option of selection.discovered_options || []) {
684
+ discoveredOptions.push(option);
685
+ }
686
+ for (const unavailable of selection.unavailable_groups || []) {
687
+ unavailableGroups.push(unavailable);
688
+ }
689
+ }
690
+
691
+ const dedupedSelected = [];
692
+ const seenSelected = new Set();
693
+ for (const option of selectedOptions) {
694
694
  const key = `${option.group || ""}:${normalizeFilterOptionLabel(option.label || "")}`;
695
695
  if (seenSelected.has(key)) continue;
696
696
  seenSelected.add(key);
697
697
  dedupedSelected.push(option);
698
698
  }
699
-
700
- return {
701
- selected_option: dedupedSelected[0] || null,
702
- selected_options: dedupedSelected,
703
- option_box: dedupedSelected.find((option) => option.option_box)?.option_box || null,
704
- unavailable: unavailableGroups.length > 0,
705
- unavailable_groups: unavailableGroups,
706
- discovered_options: discoveredOptions
707
- };
708
- }
709
-
710
- export async function confirmFilterPanel(client, frameNodeId, {
699
+
700
+ return {
701
+ selected_option: dedupedSelected[0] || null,
702
+ selected_options: dedupedSelected,
703
+ option_box: dedupedSelected.find((option) => option.option_box)?.option_box || null,
704
+ unavailable: unavailableGroups.length > 0,
705
+ unavailable_groups: unavailableGroups,
706
+ discovered_options: discoveredOptions
707
+ };
708
+ }
709
+
710
+ export async function confirmFilterPanel(client, frameNodeId, {
711
711
  timeoutMs = 8000
712
712
  } = {}) {
713
713
  const candidates = await readConfirmButtonCandidates(client, frameNodeId);
@@ -759,223 +759,241 @@ export async function confirmFilterPanel(client, frameNodeId, {
759
759
  confirm_node_id: attempts.at(-1)?.node_id || null,
760
760
  confirm_label: attempts.at(-1)?.label || null,
761
761
  confirm_candidates: candidates,
762
- confirm_attempts: attempts,
763
- panel_count: await getFilterPanelCount(client, frameNodeId)
764
- };
765
- }
766
-
762
+ confirm_attempts: attempts,
763
+ panel_count: await getFilterPanelCount(client, frameNodeId)
764
+ };
765
+ }
766
+
767
767
  function normalizeStickyVerificationSpecs(options = {}) {
768
- const source = Array.isArray(options.filterGroups) && options.filterGroups.length
769
- ? options.filterGroups
770
- : [options];
771
- return source.filter((spec) => spec?.verifySticky === true).map((spec) => ({
772
- group: String(spec.group || ""),
773
- labels: Array.isArray(spec.labels) ? spec.labels : [],
774
- selectAllLabels: spec.selectAllLabels !== false,
775
- allowUnlimited: spec.allowUnlimited === true
776
- })).filter((spec) => spec.group && spec.labels.length);
768
+ const source = Array.isArray(options.filterGroups) && options.filterGroups.length
769
+ ? options.filterGroups
770
+ : [options];
771
+ return source.filter((spec) => spec?.verifySticky === true).map((spec) => ({
772
+ group: String(spec.group || ""),
773
+ labels: Array.isArray(spec.labels) ? spec.labels : [],
774
+ selectAllLabels: spec.selectAllLabels !== false,
775
+ allowUnlimited: spec.allowUnlimited === true
776
+ })).filter((spec) => spec.group && spec.labels.length);
777
777
  }
778
778
 
779
- function findUnavailableGroup(unavailableGroups = [], group = "") {
780
- return unavailableGroups.find((item) => item?.group === group) || null;
779
+ function normalizeUniqueFilterLabels(labels = []) {
780
+ return [...new Set(labels.map(normalizeFilterOptionLabel).filter(Boolean))];
781
781
  }
782
782
 
783
- function normalizeRequestedFilterGroups(options = {}) {
784
- const source = Array.isArray(options.filterGroups) && options.filterGroups.length
785
- ? options.filterGroups.filter(Boolean)
786
- : (options.group || options.labels?.length ? [options] : []);
787
- return source.map((spec) => ({
788
- group: String(spec?.group || ""),
789
- labels: Array.isArray(spec?.labels)
790
- ? spec.labels.map(normalizeFilterOptionLabel).filter(Boolean)
791
- : [],
792
- select_all_labels: spec?.selectAllLabels !== false,
793
- allow_unlimited: spec?.allowUnlimited === true,
794
- verify_sticky: spec?.verifySticky === true
795
- }));
783
+ function activeFilterLabelsMatchRequestedExactly(requestedLabels = [], activeLabels = []) {
784
+ const requested = normalizeUniqueFilterLabels(requestedLabels);
785
+ const active = normalizeUniqueFilterLabels(activeLabels);
786
+ return requested.length === active.length
787
+ && requested.every((label) => active.includes(label));
796
788
  }
797
-
798
- export async function verifyFilterGroupsSticky(client, frameNodeId, {
799
- specs = [],
800
- unavailableGroups = [],
801
- settleMs = 300
802
- } = {}) {
803
- const normalizedSpecs = specs.filter((spec) => spec?.group && Array.isArray(spec.labels) && spec.labels.length);
804
- const groups = [];
789
+
790
+ function findUnavailableGroup(unavailableGroups = [], group = "") {
791
+ return unavailableGroups.find((item) => item?.group === group) || null;
792
+ }
793
+
794
+ function normalizeRequestedFilterGroups(options = {}) {
795
+ const source = Array.isArray(options.filterGroups) && options.filterGroups.length
796
+ ? options.filterGroups.filter(Boolean)
797
+ : (options.group || options.labels?.length ? [options] : []);
798
+ return source.map((spec) => ({
799
+ group: String(spec?.group || ""),
800
+ labels: Array.isArray(spec?.labels)
801
+ ? spec.labels.map(normalizeFilterOptionLabel).filter(Boolean)
802
+ : [],
803
+ select_all_labels: spec?.selectAllLabels !== false,
804
+ allow_unlimited: spec?.allowUnlimited === true,
805
+ verify_sticky: spec?.verifySticky === true
806
+ }));
807
+ }
808
+
809
+ export async function verifyFilterGroupsSticky(client, frameNodeId, {
810
+ specs = [],
811
+ unavailableGroups = [],
812
+ settleMs = 300
813
+ } = {}) {
814
+ const normalizedSpecs = specs.filter((spec) => spec?.group && Array.isArray(spec.labels) && spec.labels.length);
815
+ const groups = [];
805
816
  const availableSpecs = [];
806
817
  for (const spec of normalizedSpecs) {
807
818
  const unavailable = findUnavailableGroup(unavailableGroups, spec.group);
808
819
  if (unavailable) {
820
+ const requestedLabels = normalizeUniqueFilterLabels(spec.labels);
821
+ const reason = unavailable.reason || "control_unavailable_default";
822
+ const unavailableDefaultVerified = spec.group === RECOMMEND_ACTIVITY_GROUP
823
+ && spec.allowUnlimited === true
824
+ && requestedLabels.length === 1
825
+ && requestedLabels[0] === normalizeFilterOptionLabel("不限")
826
+ && /control_unavailable_default$/.test(reason);
809
827
  groups.push({
810
828
  group: spec.group,
811
- requested_labels: spec.labels,
829
+ requested_labels: requestedLabels,
812
830
  active_labels: [],
813
- verified: true,
831
+ verified: unavailableDefaultVerified,
814
832
  unavailable: true,
815
- reason: unavailable.reason || "control_unavailable_default"
833
+ reason
816
834
  });
817
835
  } else {
818
836
  availableSpecs.push(spec);
819
- }
820
- }
821
-
822
- let reopen = null;
823
- let closeConfirmation = null;
824
- if (availableSpecs.length) {
825
- reopen = await openFilterPanel(client, frameNodeId);
826
- for (const spec of availableSpecs) {
837
+ }
838
+ }
839
+
840
+ let reopen = null;
841
+ let closeConfirmation = null;
842
+ if (availableSpecs.length) {
843
+ reopen = await openFilterPanel(client, frameNodeId);
844
+ for (const spec of availableSpecs) {
827
845
  const options = await listFilterOptions(client, frameNodeId, {
828
846
  groupOrder: [spec.group]
829
847
  });
830
- const requestedLabels = spec.labels.map(normalizeFilterOptionLabel).filter(Boolean);
831
- const activeLabels = options
848
+ const requestedLabels = normalizeUniqueFilterLabels(spec.labels);
849
+ const activeLabels = normalizeUniqueFilterLabels(options
832
850
  .filter((option) => option.group === spec.group && option.active)
833
- .map((option) => normalizeFilterOptionLabel(option.label));
834
- const verified = spec.selectAllLabels === false
835
- ? requestedLabels.length === 1
836
- && activeLabels.length === 1
837
- && activeLabels[0] === requestedLabels[0]
838
- : requestedLabels.every((label) => activeLabels.includes(label));
839
- groups.push({
840
- group: spec.group,
841
- requested_labels: requestedLabels,
842
- active_labels: activeLabels,
843
- verified,
844
- unavailable: false,
845
- single_select: spec.selectAllLabels === false,
846
- discovered_options: options.map((option) => ({
847
- label: option.label,
848
- active: option.active,
849
- node_id: option.node_id
850
- }))
851
- });
852
- }
853
- closeConfirmation = await confirmFilterPanel(client, frameNodeId);
854
- if (!closeConfirmation.confirmed) {
855
- throw new Error("Recommend filter sticky verification panel could not be closed");
856
- }
857
- if (settleMs > 0) await sleep(settleMs);
858
- }
859
-
860
- const verified = groups.length === normalizedSpecs.length && groups.every((item) => item.verified);
861
- const result = {
862
- verified,
863
- groups,
864
- reopened: Boolean(reopen),
865
- reopen_panel_node_id: reopen?.panel_node_id || null,
866
- close_confirmation: closeConfirmation
867
- ? {
868
- confirmed: closeConfirmation.confirmed,
869
- label: closeConfirmation.confirm_label,
870
- node_id: closeConfirmation.confirm_node_id
871
- }
872
- : null
873
- };
874
- if (!verified) {
875
- const failedGroups = groups.filter((item) => !item.verified).map((item) => item.group);
876
- const error = new Error(`Recommend filter sticky verification failed for: ${failedGroups.join(", ")}`);
877
- error.sticky_verification = result;
878
- throw error;
879
- }
880
- return result;
881
- }
882
-
883
- export async function selectAndConfirmFirstSafeFilter(client, frameNodeId, options = {}) {
884
- const beforeCounts = await getRecommendFilterCounts(client, frameNodeId);
885
- const requestedGroups = normalizeRequestedFilterGroups(options);
886
- let openResult;
887
- try {
888
- openResult = await openFilterPanel(client, frameNodeId);
889
- } catch (error) {
890
- const onlyGroup = requestedGroups.length === 1 ? requestedGroups[0] : null;
891
- const requestedLabels = onlyGroup?.labels || [];
892
- const unavailableActivityDefault = (
893
- error?.code === "RECOMMEND_FILTER_TRIGGER_UNAVAILABLE"
894
- && normalizeText(onlyGroup?.group) === RECOMMEND_ACTIVITY_GROUP
895
- && onlyGroup?.allow_unlimited === true
896
- && requestedLabels.length === 1
897
- && requestedLabels[0] === normalizeFilterOptionLabel("不限")
898
- );
899
- if (!unavailableActivityDefault) throw error;
900
-
901
- const unavailableGroups = [{
902
- group: RECOMMEND_ACTIVITY_GROUP,
903
- requested_labels: requestedLabels,
904
- reason: "activity_control_unavailable_default",
905
- scope: "filter_panel"
906
- }];
907
- const stickySpecs = normalizeStickyVerificationSpecs(options);
908
- const stickyVerification = stickySpecs.length
909
- ? await verifyFilterGroupsSticky(client, frameNodeId, {
910
- specs: stickySpecs,
911
- unavailableGroups,
912
- settleMs: 0
913
- })
914
- : null;
915
- const afterCounts = await getRecommendFilterCounts(client, frameNodeId);
916
- return {
917
- opened_panel: false,
918
- trigger: null,
919
- initial_close_attempts: [],
920
- open_attempts: [],
921
- before_counts: beforeCounts,
922
- after_open_counts: afterCounts,
923
- requested_groups: requestedGroups,
924
- selected_option: null,
925
- selected_options: [],
926
- option_box: null,
927
- unavailable: true,
928
- unavailable_default: true,
929
- unavailable_groups: unavailableGroups,
930
- discovered_options: [],
931
- confirmed: true,
932
- confirm_node_id: null,
933
- confirm_label: "unavailable-default",
934
- confirm_candidates: [],
935
- confirm_attempts: [],
936
- panel_count: Number(afterCounts.filter_panel || 0),
937
- sticky_verification: stickyVerification,
938
- after_confirm_counts: afterCounts
939
- };
940
- }
941
- const afterOpenCounts = await getRecommendFilterCounts(client, frameNodeId);
942
- const filterGroups = Array.isArray(options.filterGroups) ? options.filterGroups : [];
851
+ .map((option) => option.label));
852
+ const verified = requestedLabels.length > 0 && (
853
+ spec.selectAllLabels === false
854
+ ? activeLabels.length === 1 && requestedLabels.includes(activeLabels[0])
855
+ : activeFilterLabelsMatchRequestedExactly(requestedLabels, activeLabels)
856
+ );
857
+ groups.push({
858
+ group: spec.group,
859
+ requested_labels: requestedLabels,
860
+ active_labels: activeLabels,
861
+ verified,
862
+ unavailable: false,
863
+ single_select: spec.selectAllLabels === false,
864
+ discovered_options: options.map((option) => ({
865
+ label: option.label,
866
+ active: option.active,
867
+ node_id: option.node_id
868
+ }))
869
+ });
870
+ }
871
+ closeConfirmation = await confirmFilterPanel(client, frameNodeId);
872
+ if (!closeConfirmation.confirmed) {
873
+ throw new Error("Recommend filter sticky verification panel could not be closed");
874
+ }
875
+ if (settleMs > 0) await sleep(settleMs);
876
+ }
877
+
878
+ const verified = groups.length === normalizedSpecs.length && groups.every((item) => item.verified);
879
+ const result = {
880
+ verified,
881
+ groups,
882
+ reopened: Boolean(reopen),
883
+ reopen_panel_node_id: reopen?.panel_node_id || null,
884
+ close_confirmation: closeConfirmation
885
+ ? {
886
+ confirmed: closeConfirmation.confirmed,
887
+ label: closeConfirmation.confirm_label,
888
+ node_id: closeConfirmation.confirm_node_id
889
+ }
890
+ : null
891
+ };
892
+ if (!verified) {
893
+ const failedGroups = groups.filter((item) => !item.verified).map((item) => item.group);
894
+ const error = new Error(`Recommend filter sticky verification failed for: ${failedGroups.join(", ")}`);
895
+ error.sticky_verification = result;
896
+ throw error;
897
+ }
898
+ return result;
899
+ }
900
+
901
+ export async function selectAndConfirmFirstSafeFilter(client, frameNodeId, options = {}) {
902
+ const beforeCounts = await getRecommendFilterCounts(client, frameNodeId);
903
+ const requestedGroups = normalizeRequestedFilterGroups(options);
904
+ let openResult;
905
+ try {
906
+ openResult = await openFilterPanel(client, frameNodeId);
907
+ } catch (error) {
908
+ const onlyGroup = requestedGroups.length === 1 ? requestedGroups[0] : null;
909
+ const requestedLabels = onlyGroup?.labels || [];
910
+ const unavailableActivityDefault = (
911
+ error?.code === "RECOMMEND_FILTER_TRIGGER_UNAVAILABLE"
912
+ && normalizeText(onlyGroup?.group) === RECOMMEND_ACTIVITY_GROUP
913
+ && onlyGroup?.allow_unlimited === true
914
+ && requestedLabels.length === 1
915
+ && requestedLabels[0] === normalizeFilterOptionLabel("不限")
916
+ );
917
+ if (!unavailableActivityDefault) throw error;
918
+
919
+ const unavailableGroups = [{
920
+ group: RECOMMEND_ACTIVITY_GROUP,
921
+ requested_labels: requestedLabels,
922
+ reason: "activity_control_unavailable_default",
923
+ scope: "filter_panel"
924
+ }];
925
+ const stickySpecs = normalizeStickyVerificationSpecs(options);
926
+ const stickyVerification = stickySpecs.length
927
+ ? await verifyFilterGroupsSticky(client, frameNodeId, {
928
+ specs: stickySpecs,
929
+ unavailableGroups,
930
+ settleMs: 0
931
+ })
932
+ : null;
933
+ const afterCounts = await getRecommendFilterCounts(client, frameNodeId);
934
+ return {
935
+ opened_panel: false,
936
+ trigger: null,
937
+ initial_close_attempts: [],
938
+ open_attempts: [],
939
+ before_counts: beforeCounts,
940
+ after_open_counts: afterCounts,
941
+ requested_groups: requestedGroups,
942
+ selected_option: null,
943
+ selected_options: [],
944
+ option_box: null,
945
+ unavailable: true,
946
+ unavailable_default: true,
947
+ unavailable_groups: unavailableGroups,
948
+ discovered_options: [],
949
+ confirmed: true,
950
+ confirm_node_id: null,
951
+ confirm_label: "unavailable-default",
952
+ confirm_candidates: [],
953
+ confirm_attempts: [],
954
+ panel_count: Number(afterCounts.filter_panel || 0),
955
+ sticky_verification: stickyVerification,
956
+ after_confirm_counts: afterCounts
957
+ };
958
+ }
959
+ const afterOpenCounts = await getRecommendFilterCounts(client, frameNodeId);
960
+ const filterGroups = Array.isArray(options.filterGroups) ? options.filterGroups : [];
943
961
  const selection = filterGroups.length
944
- ? await selectFilterGroups(client, frameNodeId, { filterGroups, groupOrder: options.groupOrder })
945
- : options.selectAllLabels
946
- ? await selectFilterOptions(client, frameNodeId, options)
947
- : await selectFilterOption(client, frameNodeId, options);
948
- const confirm = await confirmFilterPanel(client, frameNodeId);
949
- const stickySpecs = normalizeStickyVerificationSpecs(options);
950
- const stickyVerification = confirm.confirmed && stickySpecs.length
951
- ? await verifyFilterGroupsSticky(client, frameNodeId, {
952
- specs: stickySpecs,
953
- unavailableGroups: selection.unavailable_groups || [],
954
- settleMs: Number.isFinite(options.stickySettleMs) ? Math.max(0, options.stickySettleMs) : 300
955
- })
956
- : null;
957
- const afterConfirmSettleMs = Number.isFinite(options.afterConfirmSettleMs)
958
- ? Math.max(0, options.afterConfirmSettleMs)
959
- : 1200;
960
- if (afterConfirmSettleMs > 0) await sleep(afterConfirmSettleMs);
961
- const afterConfirmCounts = await getRecommendFilterCounts(client, frameNodeId);
962
-
963
- return {
964
- opened_panel: true,
965
- requested_groups: requestedGroups,
966
- trigger: {
962
+ ? await selectFilterGroups(client, frameNodeId, { filterGroups, groupOrder: options.groupOrder })
963
+ : options.selectAllLabels
964
+ ? await selectFilterOptions(client, frameNodeId, options)
965
+ : await selectFilterOption(client, frameNodeId, options);
966
+ const confirm = await confirmFilterPanel(client, frameNodeId);
967
+ const stickySpecs = normalizeStickyVerificationSpecs(options);
968
+ const stickyVerification = confirm.confirmed && stickySpecs.length
969
+ ? await verifyFilterGroupsSticky(client, frameNodeId, {
970
+ specs: stickySpecs,
971
+ unavailableGroups: selection.unavailable_groups || [],
972
+ settleMs: Number.isFinite(options.stickySettleMs) ? Math.max(0, options.stickySettleMs) : 300
973
+ })
974
+ : null;
975
+ const afterConfirmSettleMs = Number.isFinite(options.afterConfirmSettleMs)
976
+ ? Math.max(0, options.afterConfirmSettleMs)
977
+ : 1200;
978
+ if (afterConfirmSettleMs > 0) await sleep(afterConfirmSettleMs);
979
+ const afterConfirmCounts = await getRecommendFilterCounts(client, frameNodeId);
980
+
981
+ return {
982
+ opened_panel: true,
983
+ requested_groups: requestedGroups,
984
+ trigger: {
967
985
  node_id: openResult.trigger.nodeId,
968
986
  selector: openResult.trigger.selector,
969
987
  center: openResult.trigger_box.center,
970
988
  rect: openResult.trigger_box.rect
971
- },
972
- initial_close_attempts: openResult.initial_close_attempts,
973
- open_attempts: openResult.open_attempts || [],
974
- before_counts: beforeCounts,
975
- after_open_counts: afterOpenCounts,
976
- ...selection,
977
- ...confirm,
978
- sticky_verification: stickyVerification,
979
- after_confirm_counts: afterConfirmCounts
980
- };
981
- }
989
+ },
990
+ initial_close_attempts: openResult.initial_close_attempts,
991
+ open_attempts: openResult.open_attempts || [],
992
+ before_counts: beforeCounts,
993
+ after_open_counts: afterOpenCounts,
994
+ ...selection,
995
+ ...confirm,
996
+ sticky_verification: stickyVerification,
997
+ after_confirm_counts: afterConfirmCounts
998
+ };
999
+ }