@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.24

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 (66) 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/core/boss-cards/index.js +199 -199
  14. package/src/core/browser/index.js +286 -136
  15. package/src/core/capture/index.js +2930 -1201
  16. package/src/core/cv-acquisition/index.js +512 -238
  17. package/src/core/cv-capture-target/index.js +513 -299
  18. package/src/core/greet-quota/index.js +71 -71
  19. package/src/core/infinite-list/index.js +31 -31
  20. package/src/core/reporting/legacy-csv.js +12 -12
  21. package/src/core/run/detached-launcher.js +305 -0
  22. package/src/core/run/index.js +105 -52
  23. package/src/core/run/timing.js +33 -33
  24. package/src/core/run/windows-detached-worker.ps1 +106 -0
  25. package/src/core/screening/index.js +2135 -2135
  26. package/src/core/self-heal/index.js +989 -973
  27. package/src/core/self-heal/viewport.js +1505 -564
  28. package/src/detached-worker.js +99 -99
  29. package/src/domains/chat/action-journal.js +443 -0
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +1684 -401
  33. package/src/domains/chat/index.js +8 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +157 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +1801 -760
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +515 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +92 -92
  44. package/src/domains/recommend/detail.js +12 -12
  45. package/src/domains/recommend/filters.js +611 -611
  46. package/src/domains/recommend/index.js +9 -9
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -736
  49. package/src/domains/recommend/refresh.js +422 -422
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1791 -1205
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1817 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +16 -1
  64. package/src/parser.js +1296 -1296
  65. package/src/recommend-mcp.js +551 -362
  66. 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,223 @@ 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
-
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);
777
- }
778
-
779
- function findUnavailableGroup(unavailableGroups = [], group = "") {
780
- return unavailableGroups.find((item) => item?.group === group) || null;
781
- }
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
- }));
796
- }
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 = [];
805
- const availableSpecs = [];
806
- for (const spec of normalizedSpecs) {
807
- const unavailable = findUnavailableGroup(unavailableGroups, spec.group);
808
- if (unavailable) {
809
- groups.push({
810
- group: spec.group,
811
- requested_labels: spec.labels,
812
- active_labels: [],
813
- verified: true,
814
- unavailable: true,
815
- reason: unavailable.reason || "control_unavailable_default"
816
- });
817
- } else {
818
- 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) {
827
- const options = await listFilterOptions(client, frameNodeId, {
828
- groupOrder: [spec.group]
829
- });
830
- const requestedLabels = spec.labels.map(normalizeFilterOptionLabel).filter(Boolean);
831
- const activeLabels = options
832
- .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 : [];
762
+ confirm_attempts: attempts,
763
+ panel_count: await getFilterPanelCount(client, frameNodeId)
764
+ };
765
+ }
766
+
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);
777
+ }
778
+
779
+ function findUnavailableGroup(unavailableGroups = [], group = "") {
780
+ return unavailableGroups.find((item) => item?.group === group) || null;
781
+ }
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
+ }));
796
+ }
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 = [];
805
+ const availableSpecs = [];
806
+ for (const spec of normalizedSpecs) {
807
+ const unavailable = findUnavailableGroup(unavailableGroups, spec.group);
808
+ if (unavailable) {
809
+ groups.push({
810
+ group: spec.group,
811
+ requested_labels: spec.labels,
812
+ active_labels: [],
813
+ verified: true,
814
+ unavailable: true,
815
+ reason: unavailable.reason || "control_unavailable_default"
816
+ });
817
+ } else {
818
+ 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) {
827
+ const options = await listFilterOptions(client, frameNodeId, {
828
+ groupOrder: [spec.group]
829
+ });
830
+ const requestedLabels = spec.labels.map(normalizeFilterOptionLabel).filter(Boolean);
831
+ const activeLabels = options
832
+ .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 : [];
943
943
  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: {
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: {
967
967
  node_id: openResult.trigger.nodeId,
968
968
  selector: openResult.trigger.selector,
969
969
  center: openResult.trigger_box.center,
970
970
  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
- }
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
+ }