@reconcrap/boss-recommend-mcp 2.1.24 → 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.
@@ -764,7 +764,7 @@ export async function confirmFilterPanel(client, frameNodeId, {
764
764
  };
765
765
  }
766
766
 
767
- function normalizeStickyVerificationSpecs(options = {}) {
767
+ function normalizeStickyVerificationSpecs(options = {}) {
768
768
  const source = Array.isArray(options.filterGroups) && options.filterGroups.length
769
769
  ? options.filterGroups
770
770
  : [options];
@@ -774,7 +774,18 @@ function normalizeStickyVerificationSpecs(options = {}) {
774
774
  selectAllLabels: spec.selectAllLabels !== false,
775
775
  allowUnlimited: spec.allowUnlimited === true
776
776
  })).filter((spec) => spec.group && spec.labels.length);
777
- }
777
+ }
778
+
779
+ function normalizeUniqueFilterLabels(labels = []) {
780
+ return [...new Set(labels.map(normalizeFilterOptionLabel).filter(Boolean))];
781
+ }
782
+
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));
788
+ }
778
789
 
779
790
  function findUnavailableGroup(unavailableGroups = [], group = "") {
780
791
  return unavailableGroups.find((item) => item?.group === group) || null;
@@ -802,20 +813,27 @@ export async function verifyFilterGroupsSticky(client, frameNodeId, {
802
813
  } = {}) {
803
814
  const normalizedSpecs = specs.filter((spec) => spec?.group && Array.isArray(spec.labels) && spec.labels.length);
804
815
  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);
816
+ const availableSpecs = [];
817
+ for (const spec of normalizedSpecs) {
818
+ const unavailable = findUnavailableGroup(unavailableGroups, spec.group);
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);
827
+ groups.push({
828
+ group: spec.group,
829
+ requested_labels: requestedLabels,
830
+ active_labels: [],
831
+ verified: unavailableDefaultVerified,
832
+ unavailable: true,
833
+ reason
834
+ });
835
+ } else {
836
+ availableSpecs.push(spec);
819
837
  }
820
838
  }
821
839
 
@@ -824,18 +842,18 @@ export async function verifyFilterGroupsSticky(client, frameNodeId, {
824
842
  if (availableSpecs.length) {
825
843
  reopen = await openFilterPanel(client, frameNodeId);
826
844
  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));
845
+ const options = await listFilterOptions(client, frameNodeId, {
846
+ groupOrder: [spec.group]
847
+ });
848
+ const requestedLabels = normalizeUniqueFilterLabels(spec.labels);
849
+ const activeLabels = normalizeUniqueFilterLabels(options
850
+ .filter((option) => option.group === spec.group && option.active)
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
+ );
839
857
  groups.push({
840
858
  group: spec.group,
841
859
  requested_labels: requestedLabels,
@@ -36,17 +36,46 @@ function trimSalarySuffix(label) {
36
36
  return stripSalaryText(label);
37
37
  }
38
38
 
39
+ function splitStructuredJobLabel(label) {
40
+ const withoutSalary = normalizeText(stripSalaryText(label));
41
+ const separatorIndex = withoutSalary.lastIndexOf(" _ ");
42
+ if (separatorIndex < 0) {
43
+ return {
44
+ full: normalizeJobText(withoutSalary),
45
+ role: normalizeJobText(withoutSalary),
46
+ location: ""
47
+ };
48
+ }
49
+ return {
50
+ full: normalizeJobText(withoutSalary),
51
+ role: normalizeJobText(withoutSalary.slice(0, separatorIndex)),
52
+ location: normalizeJobText(withoutSalary.slice(separatorIndex + 3))
53
+ };
54
+ }
55
+
39
56
  export function jobLabelMatches(optionLabel, targetLabel) {
40
57
  const option = normalizeJobText(optionLabel);
41
58
  const target = normalizeJobText(targetLabel);
42
59
  const optionWithoutSalary = normalizeJobText(stripSalaryText(optionLabel));
43
60
  const targetWithoutSalary = normalizeJobText(stripSalaryText(targetLabel));
44
61
  if (!option || !target) return false;
45
- return option === target
46
- || option.startsWith(target)
62
+ if (option === target
47
63
  || optionWithoutSalary === target
48
64
  || option === targetWithoutSalary
49
- || optionWithoutSalary === targetWithoutSalary;
65
+ || optionWithoutSalary === targetWithoutSalary) {
66
+ return true;
67
+ }
68
+
69
+ const optionParts = splitStructuredJobLabel(optionLabel);
70
+ const targetParts = splitStructuredJobLabel(targetLabel);
71
+ // A caller may provide only the role name while Boss renders the option as
72
+ // "role _ city [salary]". Accept only that structured metadata suffix;
73
+ // never restore arbitrary startsWith matching between two role names.
74
+ return Boolean(
75
+ !targetParts.location
76
+ && optionParts.location
77
+ && optionParts.role === targetParts.role
78
+ );
50
79
  }
51
80
 
52
81
  function isVisibleBox(box) {
@@ -478,40 +478,84 @@ async function openRecommendLocationPopover(client, frameNodeId, {
478
478
  };
479
479
  }
480
480
 
481
- async function confirmRecommendLocationPopover(client, frameNodeId, {
482
- timeoutMs = 1800,
483
- intervalMs = 150,
484
- controlNodeId
485
- } = {}) {
481
+ async function confirmRecommendLocationPopover(client, frameNodeId, {
482
+ timeoutMs = 1800,
483
+ intervalMs = 150,
484
+ controlNodeId,
485
+ stableCloseMs = 300
486
+ } = {}) {
486
487
  const candidates = await findExactLocationConfirmCandidates(client, frameNodeId, { controlNodeId });
487
488
  if (!candidates.length) {
488
489
  throw new Error("Recommend location exact 确认 button was not found");
489
490
  }
490
- const clickErrors = [];
491
- for (const candidate of candidates) {
492
- try {
493
- const box = await clickNodeCenter(client, candidate.node_id, DETERMINISTIC_CLICK_OPTIONS);
494
- const started = Date.now();
495
- while (Date.now() - started <= timeoutMs) {
496
- const control = await findRecommendCurrentCityControl(client, frameNodeId);
497
- if (!control) {
498
- return {
499
- confirmed: true,
500
- label: "确认",
501
- node_id: candidate.node_id,
502
- box
503
- };
504
- }
505
- if (intervalMs > 0) await sleep(intervalMs);
506
- }
507
- } catch (error) {
508
- clickErrors.push({
509
- node_id: candidate.node_id,
510
- message: error?.message || String(error)
511
- });
512
- }
513
- }
514
- const error = new Error("Recommend location popover did not close after exact 确认 click");
491
+ const clickErrors = [];
492
+ for (const candidate of candidates) {
493
+ let box;
494
+ try {
495
+ box = await clickNodeCenter(client, candidate.node_id, DETERMINISTIC_CLICK_OPTIONS);
496
+ } catch (error) {
497
+ clickErrors.push({
498
+ node_id: candidate.node_id,
499
+ message: error?.message || String(error)
500
+ });
501
+ continue;
502
+ }
503
+
504
+ const started = Date.now();
505
+ const requiredStableCloseMs = Math.max(0, Number(stableCloseMs) || 0);
506
+ let absentSince = null;
507
+ let absentObservations = 0;
508
+ let lastObservation = null;
509
+ while (Date.now() - started <= timeoutMs) {
510
+ try {
511
+ const [control, popover] = await Promise.all([
512
+ findRecommendCurrentCityControl(client, frameNodeId),
513
+ findVisibleRecommendLocationPopover(client, frameNodeId)
514
+ ]);
515
+ const observedAt = Date.now();
516
+ lastObservation = {
517
+ control_visible: Boolean(control),
518
+ popover_visible: Boolean(popover),
519
+ observed_after_ms: observedAt - started
520
+ };
521
+ if (!control && !popover) {
522
+ absentSince ??= observedAt;
523
+ absentObservations += 1;
524
+ if (observedAt - absentSince >= requiredStableCloseMs) {
525
+ return {
526
+ confirmed: true,
527
+ label: "确认",
528
+ node_id: candidate.node_id,
529
+ box,
530
+ stable_close_ms: observedAt - absentSince,
531
+ stable_close_observations: absentObservations,
532
+ control_absent: true,
533
+ popover_invisible: true
534
+ };
535
+ }
536
+ } else {
537
+ absentSince = null;
538
+ absentObservations = 0;
539
+ }
540
+ } catch (error) {
541
+ const uncertain = new Error("Recommend location popover close state was uncertain after exact 确认 click");
542
+ uncertain.cause = error;
543
+ uncertain.confirm_node_id = candidate.node_id;
544
+ uncertain.last_observation = lastObservation;
545
+ throw uncertain;
546
+ }
547
+ if (intervalMs > 0) await sleep(intervalMs);
548
+ }
549
+
550
+ const error = new Error("Recommend location popover did not close after exact 确认 click");
551
+ error.confirm_node_id = candidate.node_id;
552
+ error.last_observation = lastObservation;
553
+ error.stable_close_ms = absentSince === null ? 0 : Date.now() - absentSince;
554
+ error.stable_close_observations = absentObservations;
555
+ error.click_errors = clickErrors;
556
+ throw error;
557
+ }
558
+ const error = new Error("Recommend location popover did not close after exact 确认 click");
515
559
  error.click_errors = clickErrors;
516
560
  throw error;
517
561
  }
@@ -575,14 +619,15 @@ function unavailableResult({ requested, currentCityLabel, reason, attempts }) {
575
619
  };
576
620
  }
577
621
 
578
- export async function ensureRecommendCurrentCityOnly(client, frameNodeId, {
579
- enabled = false,
580
- timeoutMs = 1800,
581
- intervalMs = 150,
582
- attemptsLimit = 2,
583
- openAttemptsLimit = 3,
584
- settleMs = 250
585
- } = {}) {
622
+ export async function ensureRecommendCurrentCityOnly(client, frameNodeId, {
623
+ enabled = false,
624
+ timeoutMs = 1800,
625
+ intervalMs = 150,
626
+ attemptsLimit = 2,
627
+ openAttemptsLimit = 3,
628
+ settleMs = 250,
629
+ closeStableMs = 300
630
+ } = {}) {
586
631
  const requested = enabled === true;
587
632
  const attempts = [];
588
633
  for (let attempt = 1; attempt <= attemptsLimit; attempt += 1) {
@@ -646,10 +691,11 @@ export async function ensureRecommendCurrentCityOnly(client, frameNodeId, {
646
691
  }
647
692
  }
648
693
  const afterToggle = compactControlState(afterToggleControl);
649
- const confirmation = await confirmRecommendLocationPopover(client, frameNodeId, {
650
- timeoutMs,
651
- intervalMs,
652
- controlNodeId: afterToggleControl.node_id
694
+ const confirmation = await confirmRecommendLocationPopover(client, frameNodeId, {
695
+ timeoutMs,
696
+ intervalMs,
697
+ controlNodeId: afterToggleControl.node_id,
698
+ stableCloseMs: closeStableMs
653
699
  });
654
700
  if (!confirmation.confirmed) {
655
701
  throw new Error("Recommend location state was not confirmed");
@@ -668,10 +714,11 @@ export async function ensureRecommendCurrentCityOnly(client, frameNodeId, {
668
714
  throw new Error("Recommend current-city checkbox was visible but unreadable during sticky verification");
669
715
  }
670
716
  const actual = reopened.control.state.checked;
671
- const stickyClose = await confirmRecommendLocationPopover(client, frameNodeId, {
672
- timeoutMs,
673
- intervalMs,
674
- controlNodeId: reopened.control.node_id
717
+ const stickyClose = await confirmRecommendLocationPopover(client, frameNodeId, {
718
+ timeoutMs,
719
+ intervalMs,
720
+ controlNodeId: reopened.control.node_id,
721
+ stableCloseMs: closeStableMs
675
722
  });
676
723
  if (!stickyClose.confirmed) {
677
724
  throw new Error("Recommend location sticky verification was not confirmed");