@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.22

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 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +8 -8
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2419 -2415
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1326 -1326
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +32 -32
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  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 +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  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 +472 -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 +228 -159
  44. package/src/domains/recommend/detail.js +650 -650
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +504 -361
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +987 -854
  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 +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +13 -0
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +929 -915
  66. package/src/recommend-scheduler.js +496 -496
  67. package/src/recruit-mcp.js +2121 -2121
@@ -0,0 +1,736 @@
1
+ import {
2
+ clickNodeCenter,
3
+ DETERMINISTIC_CLICK_OPTIONS,
4
+ getAttributesMap,
5
+ getNodeBox,
6
+ getOuterHTML,
7
+ pressKey,
8
+ querySelectorAll,
9
+ sleep
10
+ } from "../../core/browser/index.js";
11
+ import { htmlToText, normalizeText } from "../../core/screening/index.js";
12
+ import {
13
+ RECOMMEND_CURRENT_CITY_ONLY_LABEL,
14
+ RECOMMEND_LOCATION_SELECTORS
15
+ } from "./constants.js";
16
+
17
+ function normalizeControlText(value) {
18
+ return normalizeText(value).replace(/\s+/g, "");
19
+ }
20
+
21
+ function hasOwn(object, key) {
22
+ return Object.prototype.hasOwnProperty.call(object || {}, key);
23
+ }
24
+
25
+ function parseBooleanState(value) {
26
+ if (typeof value === "boolean") return value;
27
+ const normalized = String(value ?? "").trim().toLowerCase();
28
+ if (["true", "1", "yes", "on", "checked", "selected"].includes(normalized)) return true;
29
+ if (["false", "0", "no", "off", "unchecked", "unselected"].includes(normalized)) return false;
30
+ return null;
31
+ }
32
+
33
+ function uniqueNodeIds(nodeIds = []) {
34
+ return [...new Set(nodeIds.filter((nodeId) => Number(nodeId) > 0))];
35
+ }
36
+
37
+ async function getVisibleBox(client, nodeId) {
38
+ try {
39
+ const box = await getNodeBox(client, nodeId);
40
+ if (box.rect.width <= 0 || box.rect.height <= 0) return null;
41
+ return box;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function extractCityLabel(attributes = {}, outerHTML = "") {
48
+ const cleanCityLabel = (value) => normalizeText(value).replace(/^仅看\s*/, "").trim();
49
+ const attributeValue = [
50
+ attributes["data-city"],
51
+ attributes["data-name"],
52
+ attributes.title,
53
+ attributes["aria-label"]
54
+ ].map(cleanCityLabel).find((value) => value && value.length <= 24);
55
+ if (attributeValue) return attributeValue;
56
+ const text = cleanCityLabel(htmlToText(outerHTML)).replace(/\s+/g, " ").trim();
57
+ if (!text || text.length > 24 || text.includes(RECOMMEND_CURRENT_CITY_ONLY_LABEL)) return null;
58
+ return text;
59
+ }
60
+
61
+ async function readLocationTriggerCandidate(client, nodeId, selector) {
62
+ const [attributes, outerHTML, box] = await Promise.all([
63
+ getAttributesMap(client, nodeId),
64
+ getOuterHTML(client, nodeId),
65
+ getVisibleBox(client, nodeId)
66
+ ]);
67
+ if (!box) return null;
68
+ const currentCityLabel = extractCityLabel(attributes, outerHTML);
69
+ return {
70
+ node_id: nodeId,
71
+ selector,
72
+ current_city_label: currentCityLabel,
73
+ class_name: attributes.class || "",
74
+ box
75
+ };
76
+ }
77
+
78
+ export async function findRecommendLocationTrigger(client, frameNodeId) {
79
+ const candidates = [];
80
+ const seen = new Set();
81
+ for (const selector of RECOMMEND_LOCATION_SELECTORS.trigger) {
82
+ const nodeIds = await querySelectorAll(client, frameNodeId, selector);
83
+ for (const nodeId of nodeIds) {
84
+ if (seen.has(nodeId)) continue;
85
+ seen.add(nodeId);
86
+ const candidate = await readLocationTriggerCandidate(client, nodeId, selector);
87
+ if (candidate) candidates.push(candidate);
88
+ }
89
+ }
90
+ candidates.sort((left, right) => {
91
+ const labelDiff = Number(Boolean(right.current_city_label)) - Number(Boolean(left.current_city_label));
92
+ if (labelDiff !== 0) return labelDiff;
93
+ return left.box.rect.width - right.box.rect.width;
94
+ });
95
+ return candidates[0] || null;
96
+ }
97
+
98
+ async function readAccessibilityCheckedState(client, nodeId) {
99
+ if (typeof client?.Accessibility?.getPartialAXTree !== "function") return null;
100
+ try {
101
+ const result = await client.Accessibility.getPartialAXTree({
102
+ nodeId,
103
+ fetchRelatives: true
104
+ });
105
+ for (const node of result.nodes || []) {
106
+ for (const property of node.properties || []) {
107
+ if (property.name !== "checked") continue;
108
+ const checked = parseBooleanState(property.value?.value);
109
+ if (checked !== null) {
110
+ return {
111
+ checked,
112
+ source: "accessibility.checked"
113
+ };
114
+ }
115
+ }
116
+ }
117
+ } catch {
118
+ // Attribute and verified class fallbacks remain CDP-only and cover custom controls.
119
+ }
120
+ return null;
121
+ }
122
+
123
+ function readAttributeCheckedState(attributes = {}, source = "attributes") {
124
+ for (const key of ["aria-checked", "data-checked", "data-state"]) {
125
+ if (!hasOwn(attributes, key)) continue;
126
+ const checked = parseBooleanState(attributes[key]);
127
+ if (checked !== null) return { checked, source: `${source}.${key}` };
128
+ }
129
+ if (hasOwn(attributes, "checked")) {
130
+ return { checked: true, source: `${source}.checked` };
131
+ }
132
+ const className = String(attributes.class || "");
133
+ if (/(?:^|\s)(?:is-checked|checked|selected)(?:\s|$)/i.test(className)) {
134
+ return { checked: true, source: `${source}.class` };
135
+ }
136
+ if (/(?:^|\s)(?:is-unchecked|unchecked)(?:\s|$)/i.test(className)) {
137
+ return { checked: false, source: `${source}.class` };
138
+ }
139
+ return null;
140
+ }
141
+
142
+ async function readCurrentCityCheckboxState(client, nodeId) {
143
+ const nestedNodeIds = uniqueNodeIds(await querySelectorAll(
144
+ client,
145
+ nodeId,
146
+ RECOMMEND_LOCATION_SELECTORS.checkboxInput
147
+ ));
148
+ const stateNodeIds = uniqueNodeIds([nodeId, ...nestedNodeIds]);
149
+ for (const stateNodeId of stateNodeIds) {
150
+ const accessibility = await readAccessibilityCheckedState(client, stateNodeId);
151
+ if (accessibility) {
152
+ return {
153
+ ...accessibility,
154
+ state_node_id: stateNodeId,
155
+ input_node_ids: nestedNodeIds
156
+ };
157
+ }
158
+ }
159
+ const attributesByNode = [];
160
+ for (const stateNodeId of stateNodeIds) {
161
+ const attributes = await getAttributesMap(client, stateNodeId);
162
+ attributesByNode.push({ node_id: stateNodeId, attributes });
163
+ const state = readAttributeCheckedState(attributes, `node_${stateNodeId}`);
164
+ if (state) {
165
+ return {
166
+ ...state,
167
+ state_node_id: stateNodeId,
168
+ input_node_ids: nestedNodeIds
169
+ };
170
+ }
171
+ }
172
+ const checkboxInput = attributesByNode.find(({ attributes }) => (
173
+ String(attributes.type || "").toLowerCase() === "checkbox"
174
+ || String(attributes.role || "").toLowerCase() === "checkbox"
175
+ ));
176
+ if (checkboxInput) {
177
+ return {
178
+ checked: false,
179
+ source: `node_${checkboxInput.node_id}.unchecked_input`,
180
+ state_node_id: checkboxInput.node_id,
181
+ input_node_ids: nestedNodeIds
182
+ };
183
+ }
184
+ const wrapper = attributesByNode[0]?.attributes || {};
185
+ if (/(?:^|\s)(?:checkbox|check-box)(?:\s|$)/i.test(String(wrapper.class || ""))) {
186
+ return {
187
+ checked: false,
188
+ source: `node_${nodeId}.unchecked_class`,
189
+ state_node_id: nodeId,
190
+ input_node_ids: nestedNodeIds
191
+ };
192
+ }
193
+ return {
194
+ checked: null,
195
+ source: "unreadable",
196
+ state_node_id: null,
197
+ input_node_ids: nestedNodeIds
198
+ };
199
+ }
200
+
201
+ async function readCurrentCityControlCandidates(client, nodeIds) {
202
+ const matchingCandidates = [];
203
+ for (const nodeId of nodeIds) {
204
+ let outerHTML;
205
+ try {
206
+ outerHTML = await getOuterHTML(client, nodeId);
207
+ } catch {
208
+ continue;
209
+ }
210
+ const label = normalizeControlText(htmlToText(outerHTML));
211
+ if (!label.includes(normalizeControlText(RECOMMEND_CURRENT_CITY_ONLY_LABEL))) continue;
212
+ const box = await getVisibleBox(client, nodeId);
213
+ if (!box) continue;
214
+ const state = await readCurrentCityCheckboxState(client, nodeId);
215
+ matchingCandidates.push({
216
+ node_id: nodeId,
217
+ label,
218
+ exact_label: label === normalizeControlText(RECOMMEND_CURRENT_CITY_ONLY_LABEL),
219
+ box,
220
+ state
221
+ });
222
+ }
223
+ matchingCandidates.sort((left, right) => {
224
+ const readableDiff = Number(right.state.checked !== null) - Number(left.state.checked !== null);
225
+ if (readableDiff !== 0) return readableDiff;
226
+ const exactDiff = Number(right.exact_label) - Number(left.exact_label);
227
+ if (exactDiff !== 0) return exactDiff;
228
+ return left.label.length - right.label.length;
229
+ });
230
+ return matchingCandidates;
231
+ }
232
+
233
+ export async function findRecommendCurrentCityControl(client, frameNodeId) {
234
+ const calibratedNodeIds = uniqueNodeIds(await querySelectorAll(
235
+ client,
236
+ frameNodeId,
237
+ RECOMMEND_LOCATION_SELECTORS.checkboxCalibrated
238
+ ));
239
+ let matchingCandidates = await readCurrentCityControlCandidates(client, calibratedNodeIds);
240
+ if (!matchingCandidates.length) {
241
+ const fallbackNodeIds = uniqueNodeIds(await querySelectorAll(
242
+ client,
243
+ frameNodeId,
244
+ RECOMMEND_LOCATION_SELECTORS.checkboxCandidates
245
+ ));
246
+ matchingCandidates = await readCurrentCityControlCandidates(client, fallbackNodeIds);
247
+ }
248
+ if (!matchingCandidates.length) return null;
249
+ const best = matchingCandidates[0];
250
+ return {
251
+ ...best,
252
+ visible: true,
253
+ readable: best.state.checked !== null,
254
+ matching_candidate_count: matchingCandidates.length
255
+ };
256
+ }
257
+
258
+ async function getControlAncestorNodeIds(client, nodeId, frameNodeId, maxDepth = 10) {
259
+ const ancestors = [];
260
+ let currentNodeId = nodeId;
261
+ for (let depth = 0; depth < maxDepth; depth += 1) {
262
+ const described = await client.DOM.describeNode({
263
+ nodeId: currentNodeId,
264
+ depth: 0,
265
+ pierce: true
266
+ });
267
+ const parentId = described.node?.parentId || 0;
268
+ if (!parentId || parentId === frameNodeId) break;
269
+ ancestors.push(parentId);
270
+ currentNodeId = parentId;
271
+ }
272
+ return uniqueNodeIds(ancestors);
273
+ }
274
+
275
+ async function findExactLocationConfirmCandidates(client, frameNodeId, {
276
+ controlNodeId
277
+ } = {}) {
278
+ if (!controlNodeId) return [];
279
+ const popoverNodeIds = uniqueNodeIds(await querySelectorAll(
280
+ client,
281
+ frameNodeId,
282
+ RECOMMEND_LOCATION_SELECTORS.popoverCandidates
283
+ ));
284
+ for (const popoverNodeId of popoverNodeIds) {
285
+ const popoverHTML = await getOuterHTML(client, popoverNodeId);
286
+ if (!normalizeControlText(htmlToText(popoverHTML)).includes(
287
+ normalizeControlText(RECOMMEND_CURRENT_CITY_ONLY_LABEL)
288
+ )) continue;
289
+ const nodeIds = uniqueNodeIds(await querySelectorAll(
290
+ client,
291
+ popoverNodeId,
292
+ RECOMMEND_LOCATION_SELECTORS.confirmCandidates
293
+ ));
294
+ const candidates = [];
295
+ for (const nodeId of nodeIds) {
296
+ const outerHTML = await getOuterHTML(client, nodeId);
297
+ const label = normalizeControlText(htmlToText(outerHTML));
298
+ if (label !== normalizeControlText("确认")) continue;
299
+ const box = await getVisibleBox(client, nodeId);
300
+ if (!box) continue;
301
+ candidates.push({
302
+ node_id: nodeId,
303
+ label: "确认",
304
+ box,
305
+ scope_node_id: popoverNodeId
306
+ });
307
+ }
308
+ if (candidates.length) return candidates;
309
+ }
310
+ const ancestorNodeIds = await getControlAncestorNodeIds(client, controlNodeId, frameNodeId);
311
+ for (const ancestorNodeId of ancestorNodeIds) {
312
+ const nodeIds = uniqueNodeIds(await querySelectorAll(
313
+ client,
314
+ ancestorNodeId,
315
+ RECOMMEND_LOCATION_SELECTORS.confirmCandidates
316
+ ));
317
+ const candidates = [];
318
+ for (const nodeId of nodeIds) {
319
+ const outerHTML = await getOuterHTML(client, nodeId);
320
+ const label = normalizeControlText(htmlToText(outerHTML));
321
+ if (label !== normalizeControlText("确认")) continue;
322
+ const box = await getVisibleBox(client, nodeId);
323
+ if (!box) continue;
324
+ candidates.push({
325
+ node_id: nodeId,
326
+ label: "确认",
327
+ box,
328
+ scope_node_id: ancestorNodeId
329
+ });
330
+ }
331
+ if (candidates.length) return candidates;
332
+ }
333
+ return [];
334
+ }
335
+
336
+ async function waitForCurrentCityControl(client, frameNodeId, {
337
+ timeoutMs = 1800,
338
+ intervalMs = 150
339
+ } = {}) {
340
+ const started = Date.now();
341
+ while (true) {
342
+ const control = await findRecommendCurrentCityControl(client, frameNodeId);
343
+ if (control) return control;
344
+ if (Date.now() - started >= timeoutMs) break;
345
+ if (intervalMs > 0) await sleep(intervalMs);
346
+ }
347
+ return null;
348
+ }
349
+
350
+ async function findVisibleRecommendLocationPopover(client, frameNodeId) {
351
+ const nodeIds = uniqueNodeIds(await querySelectorAll(
352
+ client,
353
+ frameNodeId,
354
+ RECOMMEND_LOCATION_SELECTORS.popoverCandidates
355
+ ));
356
+ for (const nodeId of nodeIds) {
357
+ const box = await getVisibleBox(client, nodeId);
358
+ if (box) return { node_id: nodeId, box };
359
+ }
360
+ return null;
361
+ }
362
+
363
+ async function waitForRecommendLocationPopoverState(client, frameNodeId, {
364
+ timeoutMs = 1800,
365
+ intervalMs = 150
366
+ } = {}) {
367
+ const started = Date.now();
368
+ let popover = null;
369
+ while (true) {
370
+ const control = await findRecommendCurrentCityControl(client, frameNodeId);
371
+ if (control) return { opened: true, control, popover };
372
+ popover = await findVisibleRecommendLocationPopover(client, frameNodeId) || popover;
373
+ if (Date.now() - started >= timeoutMs) break;
374
+ if (intervalMs > 0) await sleep(intervalMs);
375
+ }
376
+ return {
377
+ opened: Boolean(popover),
378
+ control: null,
379
+ popover
380
+ };
381
+ }
382
+
383
+ async function openRecommendLocationPopover(client, frameNodeId, {
384
+ timeoutMs = 1800,
385
+ intervalMs = 150,
386
+ attemptsLimit = 3
387
+ } = {}) {
388
+ const attempts = [];
389
+ const alreadyOpenControl = await findRecommendCurrentCityControl(client, frameNodeId);
390
+ if (alreadyOpenControl) {
391
+ const trigger = await findRecommendLocationTrigger(client, frameNodeId);
392
+ return {
393
+ opened: true,
394
+ already_open: true,
395
+ trigger,
396
+ control: alreadyOpenControl,
397
+ attempts,
398
+ reason: "control_already_open"
399
+ };
400
+ }
401
+ const alreadyOpenPopover = await findVisibleRecommendLocationPopover(client, frameNodeId);
402
+ if (alreadyOpenPopover) {
403
+ const state = await waitForRecommendLocationPopoverState(client, frameNodeId, {
404
+ timeoutMs,
405
+ intervalMs
406
+ });
407
+ const trigger = await findRecommendLocationTrigger(client, frameNodeId);
408
+ return {
409
+ opened: true,
410
+ already_open: true,
411
+ trigger,
412
+ control: state.control,
413
+ popover: state.popover || alreadyOpenPopover,
414
+ attempts,
415
+ reason: state.control ? "control_already_open" : "popover_already_open_without_control"
416
+ };
417
+ }
418
+ for (let attempt = 1; attempt <= attemptsLimit; attempt += 1) {
419
+ const trigger = await findRecommendLocationTrigger(client, frameNodeId);
420
+ if (!trigger) {
421
+ return {
422
+ opened: false,
423
+ trigger: null,
424
+ control: null,
425
+ attempts,
426
+ reason: "location_trigger_unavailable"
427
+ };
428
+ }
429
+ try {
430
+ const click = await clickNodeCenter(client, trigger.node_id, DETERMINISTIC_CLICK_OPTIONS);
431
+ const state = await waitForRecommendLocationPopoverState(client, frameNodeId, {
432
+ timeoutMs,
433
+ intervalMs
434
+ });
435
+ attempts.push({
436
+ attempt,
437
+ trigger_node_id: trigger.node_id,
438
+ trigger_selector: trigger.selector,
439
+ clicked: true,
440
+ click_target: click.click_target,
441
+ popover_found: Boolean(state.popover),
442
+ control_found: Boolean(state.control)
443
+ });
444
+ if (state.opened) {
445
+ return {
446
+ opened: true,
447
+ trigger,
448
+ control: state.control,
449
+ popover: state.popover,
450
+ attempts,
451
+ reason: state.control ? "control_found" : "control_unavailable"
452
+ };
453
+ }
454
+ } catch (error) {
455
+ attempts.push({
456
+ attempt,
457
+ trigger_node_id: trigger.node_id,
458
+ trigger_selector: trigger.selector,
459
+ clicked: false,
460
+ error: error?.message || String(error)
461
+ });
462
+ if (typeof client?.Input?.dispatchKeyEvent === "function") {
463
+ await pressKey(client, "Escape", {
464
+ code: "Escape",
465
+ windowsVirtualKeyCode: 27,
466
+ nativeVirtualKeyCode: 27
467
+ });
468
+ attempts.at(-1).recovery = "Escape";
469
+ }
470
+ }
471
+ }
472
+ return {
473
+ opened: false,
474
+ trigger: null,
475
+ control: null,
476
+ attempts,
477
+ reason: "location_popover_did_not_open"
478
+ };
479
+ }
480
+
481
+ async function confirmRecommendLocationPopover(client, frameNodeId, {
482
+ timeoutMs = 1800,
483
+ intervalMs = 150,
484
+ controlNodeId
485
+ } = {}) {
486
+ const candidates = await findExactLocationConfirmCandidates(client, frameNodeId, { controlNodeId });
487
+ if (!candidates.length) {
488
+ throw new Error("Recommend location exact 确认 button was not found");
489
+ }
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");
515
+ error.click_errors = clickErrors;
516
+ throw error;
517
+ }
518
+
519
+ async function clickCurrentCityControl(client, control) {
520
+ const candidates = uniqueNodeIds([
521
+ control.node_id,
522
+ ...(control.state.input_node_ids || [])
523
+ ]);
524
+ const errors = [];
525
+ for (const nodeId of candidates) {
526
+ try {
527
+ const box = await clickNodeCenter(client, nodeId, DETERMINISTIC_CLICK_OPTIONS);
528
+ return { clicked: true, node_id: nodeId, box };
529
+ } catch (error) {
530
+ errors.push({ node_id: nodeId, message: error?.message || String(error) });
531
+ }
532
+ }
533
+ const error = new Error("Recommend current-city checkbox could not be clicked");
534
+ error.click_errors = errors;
535
+ throw error;
536
+ }
537
+
538
+ function compactControlState(control) {
539
+ if (!control) return null;
540
+ return {
541
+ checked: control.state.checked,
542
+ state_source: control.state.source,
543
+ node_id: control.node_id,
544
+ state_node_id: control.state.state_node_id
545
+ };
546
+ }
547
+
548
+ function isStaleLocationNodeError(error) {
549
+ return /Could not find node|Could not compute box model|No node with given id|stale/i.test(
550
+ String(error?.message || "")
551
+ );
552
+ }
553
+
554
+ function unavailableResult({ requested, currentCityLabel, reason, attempts }) {
555
+ return {
556
+ requested,
557
+ effective: false,
558
+ available: false,
559
+ unavailable: true,
560
+ reason,
561
+ clicked: false,
562
+ current_city_label: currentCityLabel || null,
563
+ before: null,
564
+ after_toggle: null,
565
+ confirmation: null,
566
+ sticky_verification: {
567
+ verified: true,
568
+ expected: false,
569
+ actual: null,
570
+ unavailable: true,
571
+ state_source: "control_unavailable",
572
+ close_confirmation: null
573
+ },
574
+ attempts
575
+ };
576
+ }
577
+
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
+ } = {}) {
586
+ const requested = enabled === true;
587
+ const attempts = [];
588
+ for (let attempt = 1; attempt <= attemptsLimit; attempt += 1) {
589
+ try {
590
+ const opened = await openRecommendLocationPopover(client, frameNodeId, {
591
+ timeoutMs,
592
+ intervalMs,
593
+ attemptsLimit: openAttemptsLimit
594
+ });
595
+ const currentCityLabel = opened.trigger?.current_city_label || null;
596
+ attempts.push({
597
+ attempt,
598
+ opened: opened.opened,
599
+ reason: opened.reason,
600
+ open_attempts: opened.attempts
601
+ });
602
+ if (!opened.opened) {
603
+ if (!requested && opened.reason === "location_trigger_unavailable") {
604
+ return unavailableResult({
605
+ requested,
606
+ currentCityLabel,
607
+ reason: "location_trigger_unavailable",
608
+ attempts
609
+ });
610
+ }
611
+ throw new Error(`Recommend location popover could not be opened: ${opened.reason}`);
612
+ }
613
+ if (!opened.control) {
614
+ if (typeof client?.Input?.dispatchKeyEvent === "function") {
615
+ await pressKey(client, "Escape", {
616
+ code: "Escape",
617
+ windowsVirtualKeyCode: 27,
618
+ nativeVirtualKeyCode: 27
619
+ });
620
+ attempts.at(-1).unavailable_close = "Escape";
621
+ }
622
+ if (!requested) {
623
+ return unavailableResult({
624
+ requested,
625
+ currentCityLabel,
626
+ reason: "current_city_control_unavailable",
627
+ attempts
628
+ });
629
+ }
630
+ throw new Error("Recommend current-city checkbox is unavailable for an enabled request");
631
+ }
632
+ if (!opened.control.readable) {
633
+ throw new Error("Recommend current-city checkbox is visible but its state is unreadable");
634
+ }
635
+
636
+ const before = compactControlState(opened.control);
637
+ let clicked = false;
638
+ let clickEvidence = null;
639
+ let afterToggleControl = opened.control;
640
+ if (opened.control.state.checked !== requested) {
641
+ clickEvidence = await clickCurrentCityControl(client, opened.control);
642
+ clicked = true;
643
+ afterToggleControl = await waitForCurrentCityControl(client, frameNodeId, { timeoutMs, intervalMs });
644
+ if (!afterToggleControl?.readable || afterToggleControl.state.checked !== requested) {
645
+ throw new Error("Recommend current-city checkbox did not reach the requested state after click");
646
+ }
647
+ }
648
+ const afterToggle = compactControlState(afterToggleControl);
649
+ const confirmation = await confirmRecommendLocationPopover(client, frameNodeId, {
650
+ timeoutMs,
651
+ intervalMs,
652
+ controlNodeId: afterToggleControl.node_id
653
+ });
654
+ if (!confirmation.confirmed) {
655
+ throw new Error("Recommend location state was not confirmed");
656
+ }
657
+ if (settleMs > 0) await sleep(settleMs);
658
+
659
+ const reopened = await openRecommendLocationPopover(client, frameNodeId, {
660
+ timeoutMs,
661
+ intervalMs,
662
+ attemptsLimit: openAttemptsLimit
663
+ });
664
+ if (!reopened.opened || !reopened.control) {
665
+ throw new Error("Recommend current-city checkbox was unavailable during sticky verification");
666
+ }
667
+ if (!reopened.control.readable) {
668
+ throw new Error("Recommend current-city checkbox was visible but unreadable during sticky verification");
669
+ }
670
+ const actual = reopened.control.state.checked;
671
+ const stickyClose = await confirmRecommendLocationPopover(client, frameNodeId, {
672
+ timeoutMs,
673
+ intervalMs,
674
+ controlNodeId: reopened.control.node_id
675
+ });
676
+ if (!stickyClose.confirmed) {
677
+ throw new Error("Recommend location sticky verification was not confirmed");
678
+ }
679
+ const stickyVerification = {
680
+ verified: actual === requested,
681
+ expected: requested,
682
+ actual,
683
+ unavailable: false,
684
+ state_source: reopened.control.state.source,
685
+ close_confirmation: {
686
+ confirmed: stickyClose.confirmed,
687
+ label: stickyClose.label,
688
+ node_id: stickyClose.node_id
689
+ }
690
+ };
691
+ if (!stickyVerification.verified) {
692
+ const error = new Error("Recommend current-city checkbox failed sticky verification");
693
+ error.sticky_verification = stickyVerification;
694
+ throw error;
695
+ }
696
+ attempts.at(-1).click = clickEvidence
697
+ ? { clicked: true, node_id: clickEvidence.node_id }
698
+ : { clicked: false, reason: "already_in_requested_state" };
699
+ attempts.at(-1).verified = true;
700
+ return {
701
+ requested,
702
+ effective: actual,
703
+ available: true,
704
+ unavailable: false,
705
+ reason: clicked ? "state_updated" : "already_in_requested_state",
706
+ clicked,
707
+ current_city_label: currentCityLabel,
708
+ before,
709
+ after_toggle: afterToggle,
710
+ confirmation: {
711
+ confirmed: confirmation.confirmed,
712
+ label: confirmation.label,
713
+ node_id: confirmation.node_id
714
+ },
715
+ sticky_verification: stickyVerification,
716
+ attempts
717
+ };
718
+ } catch (error) {
719
+ attempts.push({
720
+ attempt,
721
+ error: error?.message || String(error),
722
+ stale_node: isStaleLocationNodeError(error)
723
+ });
724
+ if (!isStaleLocationNodeError(error) || attempt >= attemptsLimit) throw error;
725
+ if (typeof client?.Input?.dispatchKeyEvent === "function") {
726
+ await pressKey(client, "Escape", {
727
+ code: "Escape",
728
+ windowsVirtualKeyCode: 27,
729
+ nativeVirtualKeyCode: 27
730
+ });
731
+ }
732
+ if (settleMs > 0) await sleep(settleMs);
733
+ }
734
+ }
735
+ throw new Error("Recommend current-city checkbox state was not ensured");
736
+ }