@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.
- package/README.md +5 -2
- package/bin/boss-recommend-mcp.js +4 -4
- package/config/screening-config.example.json +33 -33
- package/package.json +8 -8
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +42 -42
- package/skills/boss-chat/SKILL.md +106 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +219 -214
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +127 -127
- package/src/chat-runtime-config.js +775 -775
- package/src/cli.js +573 -573
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +2419 -2415
- package/src/core/capture/index.js +1201 -1201
- package/src/core/cv-acquisition/index.js +238 -238
- package/src/core/cv-capture-target/index.js +299 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +1326 -1326
- package/src/core/reporting/legacy-csv.js +334 -332
- package/src/core/run/index.js +32 -32
- package/src/core/run/timing.js +33 -33
- package/src/core/screening/index.js +2135 -2135
- package/src/core/self-heal/index.js +973 -973
- package/src/core/self-heal/viewport.js +564 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +113 -113
- package/src/domains/chat/index.js +7 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +122 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +571 -571
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +472 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +333 -333
- package/src/domains/recommend/constants.js +228 -159
- package/src/domains/recommend/detail.js +650 -650
- package/src/domains/recommend/filters.js +748 -377
- package/src/domains/recommend/index.js +4 -3
- package/src/domains/recommend/jobs.js +542 -542
- package/src/domains/recommend/location.js +736 -0
- package/src/domains/recommend/refresh.js +504 -361
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +987 -854
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1620 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +13 -0
- package/src/parser.js +376 -8
- package/src/recommend-mcp.js +929 -915
- package/src/recommend-scheduler.js +496 -496
- 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
|
+
}
|