@reconcrap/boss-recommend-mcp 2.0.46 → 2.0.47
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/bin/boss-recommend-mcp.js +4 -4
- package/config/screening-config.example.json +27 -27
- package/package.json +1 -1
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +39 -39
- package/skills/boss-chat/SKILL.md +93 -93
- package/skills/boss-recommend-pipeline/README.md +12 -12
- package/skills/boss-recommend-pipeline/SKILL.md +180 -180
- package/skills/boss-recruit-pipeline/README.md +17 -17
- package/skills/boss-recruit-pipeline/SKILL.md +58 -58
- package/src/chat-mcp.js +1780 -1780
- package/src/chat-runtime-config.js +749 -749
- package/src/cli.js +3054 -3054
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +1453 -1453
- 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 +54 -54
- package/src/core/infinite-list/index.js +1326 -1326
- package/src/core/reporting/legacy-csv.js +341 -341
- package/src/core/run/timing.js +33 -33
- package/src/core/screening/index.js +50 -3
- package/src/core/self-heal/index.js +973 -973
- package/src/core/self-heal/viewport.js +564 -564
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +221 -221
- package/src/domains/chat/detail.js +1668 -1668
- package/src/domains/chat/index.js +7 -7
- package/src/domains/chat/jobs.js +592 -592
- package/src/domains/chat/page-guard.js +98 -98
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +1977 -1977
- package/src/domains/recommend/actions.js +457 -457
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/constants.js +165 -165
- package/src/domains/recommend/detail.js +25 -18
- package/src/domains/recommend/filters.js +610 -610
- package/src/domains/recommend/index.js +10 -10
- package/src/domains/recommend/jobs.js +316 -316
- package/src/domains/recommend/refresh.js +472 -472
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +27 -20
- 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 +167 -167
- package/src/domains/recruit/detail.js +461 -461
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +451 -451
- package/src/domains/recruit/refresh.js +44 -44
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1207 -1207
- package/src/domains/recruit/search.js +1202 -1202
- package/src/recommend-mcp.js +22 -22
- package/src/recruit-mcp.js +1338 -1338
|
@@ -1,610 +1,610 @@
|
|
|
1
|
-
import {
|
|
2
|
-
clickNodeCenter,
|
|
3
|
-
countSelectors,
|
|
4
|
-
DETERMINISTIC_CLICK_OPTIONS,
|
|
5
|
-
findFirstNode,
|
|
6
|
-
getAttributesMap,
|
|
7
|
-
getNodeBox,
|
|
8
|
-
getOuterHTML,
|
|
9
|
-
pressKey,
|
|
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_CARD_SELECTOR,
|
|
17
|
-
RECOMMEND_FILTER_GROUP_ORDER,
|
|
18
|
-
RECOMMEND_FILTER_SELECTORS
|
|
19
|
-
} from "./constants.js";
|
|
20
|
-
|
|
21
|
-
const SKIP_OPTION_LABELS = new Set(["不限", "全部", "all"]);
|
|
22
|
-
|
|
23
|
-
export function normalizeFilterOptionLabel(label) {
|
|
24
|
-
return normalizeText(label).replace(/\s+/g, "");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function isSafeFilterOptionLabel(label) {
|
|
28
|
-
const normalized = normalizeFilterOptionLabel(label);
|
|
29
|
-
return Boolean(normalized) && !SKIP_OPTION_LABELS.has(normalized.toLowerCase());
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function isActiveOption(attributes = {}, outerHTML = "") {
|
|
33
|
-
const className = attributes.class || "";
|
|
34
|
-
return /\bactive\b/.test(className) || /\bactive\b/.test(String(outerHTML || "").split(">")[0] || "");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function chooseFirstSafeFilterOption(options = [], groupOrder = RECOMMEND_FILTER_GROUP_ORDER) {
|
|
38
|
-
for (const group of groupOrder) {
|
|
39
|
-
const option = options.find((item) => (
|
|
40
|
-
item.group === group
|
|
41
|
-
&& !item.active
|
|
42
|
-
&& isSafeFilterOptionLabel(item.label)
|
|
43
|
-
));
|
|
44
|
-
if (option) return option;
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function chooseFilterOptionByLabels(options = [], {
|
|
50
|
-
group = "",
|
|
51
|
-
labels = []
|
|
52
|
-
} = {}) {
|
|
53
|
-
const normalizedGroup = normalizeText(group);
|
|
54
|
-
const normalizedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
|
|
55
|
-
for (const label of normalizedLabels) {
|
|
56
|
-
const option = options.find((item) => (
|
|
57
|
-
(!normalizedGroup || item.group === normalizedGroup)
|
|
58
|
-
&& !item.active
|
|
59
|
-
&& normalizeFilterOptionLabel(item.label) === label
|
|
60
|
-
&& isSafeFilterOptionLabel(item.label)
|
|
61
|
-
));
|
|
62
|
-
if (option) return option;
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function chooseFilterOptionsByLabels(options = [], {
|
|
68
|
-
group = "",
|
|
69
|
-
labels = []
|
|
70
|
-
} = {}) {
|
|
71
|
-
const normalizedGroup = normalizeText(group);
|
|
72
|
-
const normalizedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
|
|
73
|
-
return normalizedLabels.map((label) => {
|
|
74
|
-
const option = options.find((item) => (
|
|
75
|
-
(!normalizedGroup || item.group === normalizedGroup)
|
|
76
|
-
&& normalizeFilterOptionLabel(item.label) === label
|
|
77
|
-
&& isSafeFilterOptionLabel(item.label)
|
|
78
|
-
));
|
|
79
|
-
return {
|
|
80
|
-
label,
|
|
81
|
-
option: option || null
|
|
82
|
-
};
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export async function getFilterPanelCount(client, frameNodeId) {
|
|
87
|
-
return (await querySelectorAll(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.panel)).length;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function getRecommendFilterCounts(client, frameNodeId) {
|
|
91
|
-
return countSelectors(client, frameNodeId, {
|
|
92
|
-
filter_trigger: RECOMMEND_FILTER_SELECTORS.trigger,
|
|
93
|
-
filter_panel: RECOMMEND_FILTER_SELECTORS.panel,
|
|
94
|
-
check_box: RECOMMEND_FILTER_SELECTORS.checkBox,
|
|
95
|
-
option: `.filter-panel ${RECOMMEND_FILTER_SELECTORS.option}`,
|
|
96
|
-
active_option: RECOMMEND_FILTER_SELECTORS.activeOption,
|
|
97
|
-
recommend_card: RECOMMEND_CARD_SELECTOR
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function findFilterTrigger(client, frameNodeId) {
|
|
102
|
-
return findFirstNode(client, frameNodeId, [
|
|
103
|
-
RECOMMEND_FILTER_SELECTORS.trigger,
|
|
104
|
-
".recommend-filter.op-filter"
|
|
105
|
-
]);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export async function ensureFilterPanelClosed(client, frameNodeId, triggerNodeId = 0) {
|
|
109
|
-
const attempts = [];
|
|
110
|
-
if (await getFilterPanelCount(client, frameNodeId) === 0) return attempts;
|
|
111
|
-
|
|
112
|
-
await pressKey(client, "Escape", {
|
|
113
|
-
code: "Escape",
|
|
114
|
-
windowsVirtualKeyCode: 27,
|
|
115
|
-
nativeVirtualKeyCode: 27
|
|
116
|
-
});
|
|
117
|
-
await sleep(400);
|
|
118
|
-
attempts.push("Escape");
|
|
119
|
-
|
|
120
|
-
if (await getFilterPanelCount(client, frameNodeId) > 0 && triggerNodeId) {
|
|
121
|
-
await clickNodeCenter(client, triggerNodeId, DETERMINISTIC_CLICK_OPTIONS);
|
|
122
|
-
await sleep(500);
|
|
123
|
-
attempts.push("filter-trigger-toggle");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return attempts;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function dismissRecommendControlOverlays(client, settleMs = 250) {
|
|
130
|
-
if (typeof client?.Input?.dispatchKeyEvent !== "function") {
|
|
131
|
-
return ["Escape-unavailable"];
|
|
132
|
-
}
|
|
133
|
-
await pressKey(client, "Escape", {
|
|
134
|
-
code: "Escape",
|
|
135
|
-
windowsVirtualKeyCode: 27,
|
|
136
|
-
nativeVirtualKeyCode: 27
|
|
137
|
-
});
|
|
138
|
-
if (settleMs > 0) await sleep(settleMs);
|
|
139
|
-
return ["Escape"];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function findFilterTriggerCandidates(client, frameNodeId) {
|
|
143
|
-
const candidates = [];
|
|
144
|
-
const seen = new Set();
|
|
145
|
-
for (const selector of [
|
|
146
|
-
RECOMMEND_FILTER_SELECTORS.trigger,
|
|
147
|
-
".recommend-filter.op-filter"
|
|
148
|
-
]) {
|
|
149
|
-
const candidate = await findFirstNode(client, frameNodeId, [selector]);
|
|
150
|
-
if (candidate && !seen.has(candidate.nodeId)) {
|
|
151
|
-
candidates.push(candidate);
|
|
152
|
-
seen.add(candidate.nodeId);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return candidates;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export async function openFilterPanel(client, frameNodeId) {
|
|
159
|
-
let triggerCandidates = await findFilterTriggerCandidates(client, frameNodeId);
|
|
160
|
-
if (!triggerCandidates.length) {
|
|
161
|
-
throw new Error("Recommend filter trigger was not found");
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const preOpenDismissalAttempts = await dismissRecommendControlOverlays(client);
|
|
165
|
-
const existingPanelNodeId = await waitForSelector(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.panel, {
|
|
166
|
-
timeoutMs: 300,
|
|
167
|
-
intervalMs: 100
|
|
168
|
-
});
|
|
169
|
-
if (existingPanelNodeId) {
|
|
170
|
-
const triggerBox = await getNodeBox(client, triggerCandidates[0].nodeId);
|
|
171
|
-
return {
|
|
172
|
-
trigger: triggerCandidates[0],
|
|
173
|
-
trigger_box: triggerBox,
|
|
174
|
-
panel_node_id: existingPanelNodeId,
|
|
175
|
-
initial_close_attempts: preOpenDismissalAttempts,
|
|
176
|
-
already_open: true
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const closeAttempts = await ensureFilterPanelClosed(client, frameNodeId, triggerCandidates[0].nodeId);
|
|
181
|
-
|
|
182
|
-
const attempts = [];
|
|
183
|
-
for (let round = 0; round < 3; round += 1) {
|
|
184
|
-
triggerCandidates = await findFilterTriggerCandidates(client, frameNodeId);
|
|
185
|
-
for (const trigger of triggerCandidates) {
|
|
186
|
-
const triggerBox = await getNodeBox(client, trigger.nodeId);
|
|
187
|
-
const clickBox = await clickNodeCenter(client, trigger.nodeId, DETERMINISTIC_CLICK_OPTIONS);
|
|
188
|
-
attempts.push({
|
|
189
|
-
selector: trigger.selector,
|
|
190
|
-
node_id: trigger.nodeId,
|
|
191
|
-
center: triggerBox.center,
|
|
192
|
-
click_target: clickBox.click_target,
|
|
193
|
-
click_result: clickBox.click_result
|
|
194
|
-
});
|
|
195
|
-
const panelNodeId = await waitForSelector(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.panel, {
|
|
196
|
-
timeoutMs: 2500,
|
|
197
|
-
intervalMs: 200
|
|
198
|
-
});
|
|
199
|
-
if (panelNodeId) {
|
|
200
|
-
return {
|
|
201
|
-
trigger,
|
|
202
|
-
trigger_box: triggerBox,
|
|
203
|
-
panel_node_id: panelNodeId,
|
|
204
|
-
initial_close_attempts: [
|
|
205
|
-
...preOpenDismissalAttempts,
|
|
206
|
-
...closeAttempts
|
|
207
|
-
],
|
|
208
|
-
open_attempts: attempts
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
await sleep(500);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
throw new Error(`Recommend filter panel did not open after ${attempts.length} trigger attempts`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function readOptionNode(client, group, nodeId) {
|
|
219
|
-
const [attributes, outerHTML] = await Promise.all([
|
|
220
|
-
getAttributesMap(client, nodeId),
|
|
221
|
-
getOuterHTML(client, nodeId)
|
|
222
|
-
]);
|
|
223
|
-
const label = normalizeFilterOptionLabel(htmlToText(outerHTML));
|
|
224
|
-
return {
|
|
225
|
-
group,
|
|
226
|
-
node_id: nodeId,
|
|
227
|
-
label,
|
|
228
|
-
active: isActiveOption(attributes, outerHTML),
|
|
229
|
-
attributes: {
|
|
230
|
-
class: attributes.class || "",
|
|
231
|
-
value: attributes.value || "",
|
|
232
|
-
type: attributes.type || ""
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export async function listFilterOptions(client, frameNodeId, {
|
|
238
|
-
groupOrder = RECOMMEND_FILTER_GROUP_ORDER
|
|
239
|
-
} = {}) {
|
|
240
|
-
const options = [];
|
|
241
|
-
for (const group of groupOrder) {
|
|
242
|
-
const groupSelector = RECOMMEND_FILTER_SELECTORS.groups[group];
|
|
243
|
-
if (!groupSelector) continue;
|
|
244
|
-
const groupNodeIds = await querySelectorAll(client, frameNodeId, groupSelector);
|
|
245
|
-
for (const groupNodeId of groupNodeIds) {
|
|
246
|
-
const optionNodeIds = await querySelectorAll(client, groupNodeId, RECOMMEND_FILTER_SELECTORS.option);
|
|
247
|
-
for (const optionNodeId of optionNodeIds) {
|
|
248
|
-
options.push(await readOptionNode(client, group, optionNodeId));
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return options;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async function clickFirstAvailableNode(client, nodeIds) {
|
|
256
|
-
const errors = [];
|
|
257
|
-
for (const nodeId of nodeIds) {
|
|
258
|
-
try {
|
|
259
|
-
const box = await clickNodeCenter(client, nodeId, DETERMINISTIC_CLICK_OPTIONS);
|
|
260
|
-
return {
|
|
261
|
-
clicked: true,
|
|
262
|
-
node_id: nodeId,
|
|
263
|
-
box
|
|
264
|
-
};
|
|
265
|
-
} catch (error) {
|
|
266
|
-
errors.push({
|
|
267
|
-
node_id: nodeId,
|
|
268
|
-
message: error?.message || String(error)
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return {
|
|
273
|
-
clicked: false,
|
|
274
|
-
errors
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function normalizeButtonLabel(label) {
|
|
279
|
-
return normalizeFilterOptionLabel(label).toLowerCase();
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function buttonRank(candidate) {
|
|
283
|
-
const label = normalizeButtonLabel(candidate.label);
|
|
284
|
-
if (/确定|确认|完成|ok|confirm/.test(label)) return 0;
|
|
285
|
-
if (/重置|清空|取消|reset|cancel/.test(label)) return 2;
|
|
286
|
-
return 1;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function readButtonCandidate(client, nodeId, index) {
|
|
290
|
-
const [attributes, outerHTML] = await Promise.all([
|
|
291
|
-
getAttributesMap(client, nodeId),
|
|
292
|
-
getOuterHTML(client, nodeId)
|
|
293
|
-
]);
|
|
294
|
-
return {
|
|
295
|
-
node_id: nodeId,
|
|
296
|
-
index,
|
|
297
|
-
label: normalizeButtonLabel(htmlToText(outerHTML)),
|
|
298
|
-
class_name: attributes.class || ""
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async function readConfirmButtonCandidates(client, frameNodeId) {
|
|
303
|
-
const nodeIds = await querySelectorAll(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.confirmButton);
|
|
304
|
-
const candidates = [];
|
|
305
|
-
for (let index = 0; index < nodeIds.length; index += 1) {
|
|
306
|
-
candidates.push(await readButtonCandidate(client, nodeIds[index], index));
|
|
307
|
-
}
|
|
308
|
-
return candidates.sort((left, right) => {
|
|
309
|
-
const rankDiff = buttonRank(left) - buttonRank(right);
|
|
310
|
-
if (rankDiff !== 0) return rankDiff;
|
|
311
|
-
return right.index - left.index;
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export async function selectFirstSafeFilterOption(client, frameNodeId, {
|
|
316
|
-
groupOrder = RECOMMEND_FILTER_GROUP_ORDER
|
|
317
|
-
} = {}) {
|
|
318
|
-
const options = await listFilterOptions(client, frameNodeId, { groupOrder });
|
|
319
|
-
const selected = chooseFirstSafeFilterOption(options, groupOrder);
|
|
320
|
-
if (!selected) {
|
|
321
|
-
throw new Error("No safe non-active recommend filter option was found");
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const box = await clickNodeCenter(client, selected.node_id, {
|
|
325
|
-
...DETERMINISTIC_CLICK_OPTIONS,
|
|
326
|
-
scrollIntoView: true
|
|
327
|
-
});
|
|
328
|
-
await sleep(300);
|
|
329
|
-
|
|
330
|
-
return {
|
|
331
|
-
selected_option: {
|
|
332
|
-
group: selected.group,
|
|
333
|
-
label: selected.label,
|
|
334
|
-
node_id: selected.node_id,
|
|
335
|
-
was_active: selected.active
|
|
336
|
-
},
|
|
337
|
-
option_box: box,
|
|
338
|
-
discovered_options: options.map((option) => ({
|
|
339
|
-
group: option.group,
|
|
340
|
-
label: option.label,
|
|
341
|
-
active: option.active,
|
|
342
|
-
node_id: option.node_id
|
|
343
|
-
}))
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export async function selectFilterOption(client, frameNodeId, {
|
|
348
|
-
group = "",
|
|
349
|
-
labels = [],
|
|
350
|
-
groupOrder = RECOMMEND_FILTER_GROUP_ORDER
|
|
351
|
-
} = {}) {
|
|
352
|
-
const options = await listFilterOptions(client, frameNodeId, { groupOrder });
|
|
353
|
-
const selected = labels.length
|
|
354
|
-
? chooseFilterOptionByLabels(options, { group, labels })
|
|
355
|
-
: chooseFirstSafeFilterOption(options, groupOrder);
|
|
356
|
-
|
|
357
|
-
if (!selected) {
|
|
358
|
-
const target = labels.length
|
|
359
|
-
? `${group || "any group"} / ${labels.join(", ")}`
|
|
360
|
-
: "first safe non-active option";
|
|
361
|
-
throw new Error(`No matching recommend filter option was found for ${target}`);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const box = await clickNodeCenter(client, selected.node_id, {
|
|
365
|
-
...DETERMINISTIC_CLICK_OPTIONS,
|
|
366
|
-
scrollIntoView: true
|
|
367
|
-
});
|
|
368
|
-
await sleep(300);
|
|
369
|
-
|
|
370
|
-
return {
|
|
371
|
-
selected_option: {
|
|
372
|
-
group: selected.group,
|
|
373
|
-
label: selected.label,
|
|
374
|
-
node_id: selected.node_id,
|
|
375
|
-
was_active: selected.active,
|
|
376
|
-
requested_group: group || null,
|
|
377
|
-
requested_labels: labels
|
|
378
|
-
},
|
|
379
|
-
option_box: box,
|
|
380
|
-
discovered_options: options.map((option) => ({
|
|
381
|
-
group: option.group,
|
|
382
|
-
label: option.label,
|
|
383
|
-
active: option.active,
|
|
384
|
-
node_id: option.node_id
|
|
385
|
-
}))
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export async function selectFilterOptions(client, frameNodeId, {
|
|
390
|
-
group = "",
|
|
391
|
-
labels = [],
|
|
392
|
-
groupOrder = RECOMMEND_FILTER_GROUP_ORDER
|
|
393
|
-
} = {}) {
|
|
394
|
-
if (!labels.length) {
|
|
395
|
-
return selectFilterOption(client, frameNodeId, { group, labels, groupOrder });
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const selectedOptions = [];
|
|
399
|
-
const missingLabels = [];
|
|
400
|
-
let discoveredOptions = [];
|
|
401
|
-
|
|
402
|
-
for (const label of labels) {
|
|
403
|
-
if (await getFilterPanelCount(client, frameNodeId) === 0) {
|
|
404
|
-
await openFilterPanel(client, frameNodeId);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const options = await listFilterOptions(client, frameNodeId, { groupOrder });
|
|
408
|
-
discoveredOptions = options.map((option) => ({
|
|
409
|
-
group: option.group,
|
|
410
|
-
label: option.label,
|
|
411
|
-
active: option.active,
|
|
412
|
-
node_id: option.node_id
|
|
413
|
-
}));
|
|
414
|
-
const selected = chooseFilterOptionByLabels(options, { group, labels: [label] });
|
|
415
|
-
const alreadyActive = options.find((option) => (
|
|
416
|
-
(!group || option.group === group)
|
|
417
|
-
&& normalizeFilterOptionLabel(option.label) === normalizeFilterOptionLabel(label)
|
|
418
|
-
&& option.active
|
|
419
|
-
));
|
|
420
|
-
|
|
421
|
-
if (alreadyActive) {
|
|
422
|
-
selectedOptions.push({
|
|
423
|
-
group: alreadyActive.group,
|
|
424
|
-
label: alreadyActive.label,
|
|
425
|
-
node_id: alreadyActive.node_id,
|
|
426
|
-
was_active: true,
|
|
427
|
-
clicked: false,
|
|
428
|
-
requested_group: group || null
|
|
429
|
-
});
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (!selected) {
|
|
434
|
-
missingLabels.push(label);
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const box = await clickNodeCenter(client, selected.node_id, {
|
|
439
|
-
...DETERMINISTIC_CLICK_OPTIONS,
|
|
440
|
-
scrollIntoView: true
|
|
441
|
-
});
|
|
442
|
-
selectedOptions.push({
|
|
443
|
-
group: selected.group,
|
|
444
|
-
label: selected.label,
|
|
445
|
-
node_id: selected.node_id,
|
|
446
|
-
was_active: false,
|
|
447
|
-
clicked: true,
|
|
448
|
-
requested_group: group || null,
|
|
449
|
-
option_box: box
|
|
450
|
-
});
|
|
451
|
-
await sleep(450);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (missingLabels.length) {
|
|
455
|
-
throw new Error(`No matching recommend filter options were found for ${group || "any group"} / ${missingLabels.join(", ")}`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
selected_option: selectedOptions[0] || null,
|
|
460
|
-
selected_options: selectedOptions.map((option) => ({
|
|
461
|
-
group: option.group,
|
|
462
|
-
label: option.label,
|
|
463
|
-
node_id: option.node_id,
|
|
464
|
-
was_active: option.was_active,
|
|
465
|
-
clicked: option.clicked,
|
|
466
|
-
requested_group: option.requested_group,
|
|
467
|
-
requested_labels: labels
|
|
468
|
-
})),
|
|
469
|
-
option_box: selectedOptions.find((option) => option.option_box)?.option_box || null,
|
|
470
|
-
discovered_options: discoveredOptions
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
export async function selectFilterGroups(client, frameNodeId, {
|
|
475
|
-
filterGroups = [],
|
|
476
|
-
groupOrder = RECOMMEND_FILTER_GROUP_ORDER
|
|
477
|
-
} = {}) {
|
|
478
|
-
const selectedOptions = [];
|
|
479
|
-
const discoveredOptions = [];
|
|
480
|
-
const groups = filterGroups.filter((item) => item && (item.group || item.labels?.length));
|
|
481
|
-
if (!groups.length) {
|
|
482
|
-
return selectFilterOption(client, frameNodeId, { groupOrder });
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
for (const spec of groups) {
|
|
486
|
-
const labels = Array.isArray(spec.labels) ? spec.labels : [];
|
|
487
|
-
const selection = spec.selectAllLabels === false
|
|
488
|
-
? await selectFilterOption(client, frameNodeId, {
|
|
489
|
-
group: spec.group || "",
|
|
490
|
-
labels,
|
|
491
|
-
groupOrder
|
|
492
|
-
})
|
|
493
|
-
: await selectFilterOptions(client, frameNodeId, {
|
|
494
|
-
group: spec.group || "",
|
|
495
|
-
labels,
|
|
496
|
-
groupOrder
|
|
497
|
-
});
|
|
498
|
-
if (selection.selected_option) selectedOptions.push(selection.selected_option);
|
|
499
|
-
for (const option of selection.selected_options || []) {
|
|
500
|
-
selectedOptions.push(option);
|
|
501
|
-
}
|
|
502
|
-
for (const option of selection.discovered_options || []) {
|
|
503
|
-
discoveredOptions.push(option);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const dedupedSelected = [];
|
|
508
|
-
const seenSelected = new Set();
|
|
509
|
-
for (const option of selectedOptions) {
|
|
510
|
-
const key = `${option.group || ""}:${normalizeFilterOptionLabel(option.label || "")}`;
|
|
511
|
-
if (seenSelected.has(key)) continue;
|
|
512
|
-
seenSelected.add(key);
|
|
513
|
-
dedupedSelected.push(option);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return {
|
|
517
|
-
selected_option: dedupedSelected[0] || null,
|
|
518
|
-
selected_options: dedupedSelected,
|
|
519
|
-
option_box: dedupedSelected.find((option) => option.option_box)?.option_box || null,
|
|
520
|
-
discovered_options: discoveredOptions
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
export async function confirmFilterPanel(client, frameNodeId, {
|
|
525
|
-
timeoutMs = 8000
|
|
526
|
-
} = {}) {
|
|
527
|
-
const candidates = await readConfirmButtonCandidates(client, frameNodeId);
|
|
528
|
-
if (!candidates.length && await getFilterPanelCount(client, frameNodeId) === 0) {
|
|
529
|
-
return {
|
|
530
|
-
confirmed: true,
|
|
531
|
-
confirm_node_id: null,
|
|
532
|
-
confirm_label: "auto-closed",
|
|
533
|
-
confirm_candidates: [],
|
|
534
|
-
confirm_attempts: [],
|
|
535
|
-
panel_count: 0
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
if (!candidates.length) {
|
|
539
|
-
throw new Error("Recommend filter confirm button was not found");
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const attempts = [];
|
|
543
|
-
for (const candidate of candidates) {
|
|
544
|
-
const clickResult = await clickFirstAvailableNode(client, [candidate.node_id]);
|
|
545
|
-
attempts.push({
|
|
546
|
-
node_id: candidate.node_id,
|
|
547
|
-
label: candidate.label,
|
|
548
|
-
clicked: clickResult.clicked,
|
|
549
|
-
errors: clickResult.errors
|
|
550
|
-
});
|
|
551
|
-
if (!clickResult.clicked) continue;
|
|
552
|
-
|
|
553
|
-
const started = Date.now();
|
|
554
|
-
while (Date.now() - started <= timeoutMs) {
|
|
555
|
-
const panelCount = await getFilterPanelCount(client, frameNodeId);
|
|
556
|
-
if (panelCount === 0) {
|
|
557
|
-
return {
|
|
558
|
-
confirmed: true,
|
|
559
|
-
confirm_node_id: clickResult.node_id,
|
|
560
|
-
confirm_label: candidate.label,
|
|
561
|
-
confirm_box: clickResult.box,
|
|
562
|
-
confirm_candidates: candidates,
|
|
563
|
-
confirm_attempts: attempts,
|
|
564
|
-
panel_count: 0
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
await sleep(250);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return {
|
|
572
|
-
confirmed: false,
|
|
573
|
-
confirm_node_id: attempts.at(-1)?.node_id || null,
|
|
574
|
-
confirm_label: attempts.at(-1)?.label || null,
|
|
575
|
-
confirm_candidates: candidates,
|
|
576
|
-
confirm_attempts: attempts,
|
|
577
|
-
panel_count: await getFilterPanelCount(client, frameNodeId)
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
export async function selectAndConfirmFirstSafeFilter(client, frameNodeId, options = {}) {
|
|
582
|
-
const beforeCounts = await getRecommendFilterCounts(client, frameNodeId);
|
|
583
|
-
const openResult = await openFilterPanel(client, frameNodeId);
|
|
584
|
-
const afterOpenCounts = await getRecommendFilterCounts(client, frameNodeId);
|
|
585
|
-
const filterGroups = Array.isArray(options.filterGroups) ? options.filterGroups : [];
|
|
586
|
-
const selection = filterGroups.length
|
|
587
|
-
? await selectFilterGroups(client, frameNodeId, { filterGroups, groupOrder: options.groupOrder })
|
|
588
|
-
: options.selectAllLabels
|
|
589
|
-
? await selectFilterOptions(client, frameNodeId, options)
|
|
590
|
-
: await selectFilterOption(client, frameNodeId, options);
|
|
591
|
-
const confirm = await confirmFilterPanel(client, frameNodeId);
|
|
592
|
-
await sleep(1200);
|
|
593
|
-
const afterConfirmCounts = await getRecommendFilterCounts(client, frameNodeId);
|
|
594
|
-
|
|
595
|
-
return {
|
|
596
|
-
opened_panel: true,
|
|
597
|
-
trigger: {
|
|
598
|
-
node_id: openResult.trigger.nodeId,
|
|
599
|
-
selector: openResult.trigger.selector,
|
|
600
|
-
center: openResult.trigger_box.center,
|
|
601
|
-
rect: openResult.trigger_box.rect
|
|
602
|
-
},
|
|
603
|
-
initial_close_attempts: openResult.initial_close_attempts,
|
|
604
|
-
before_counts: beforeCounts,
|
|
605
|
-
after_open_counts: afterOpenCounts,
|
|
606
|
-
...selection,
|
|
607
|
-
...confirm,
|
|
608
|
-
after_confirm_counts: afterConfirmCounts
|
|
609
|
-
};
|
|
610
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
clickNodeCenter,
|
|
3
|
+
countSelectors,
|
|
4
|
+
DETERMINISTIC_CLICK_OPTIONS,
|
|
5
|
+
findFirstNode,
|
|
6
|
+
getAttributesMap,
|
|
7
|
+
getNodeBox,
|
|
8
|
+
getOuterHTML,
|
|
9
|
+
pressKey,
|
|
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_CARD_SELECTOR,
|
|
17
|
+
RECOMMEND_FILTER_GROUP_ORDER,
|
|
18
|
+
RECOMMEND_FILTER_SELECTORS
|
|
19
|
+
} from "./constants.js";
|
|
20
|
+
|
|
21
|
+
const SKIP_OPTION_LABELS = new Set(["不限", "全部", "all"]);
|
|
22
|
+
|
|
23
|
+
export function normalizeFilterOptionLabel(label) {
|
|
24
|
+
return normalizeText(label).replace(/\s+/g, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isSafeFilterOptionLabel(label) {
|
|
28
|
+
const normalized = normalizeFilterOptionLabel(label);
|
|
29
|
+
return Boolean(normalized) && !SKIP_OPTION_LABELS.has(normalized.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isActiveOption(attributes = {}, outerHTML = "") {
|
|
33
|
+
const className = attributes.class || "";
|
|
34
|
+
return /\bactive\b/.test(className) || /\bactive\b/.test(String(outerHTML || "").split(">")[0] || "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function chooseFirstSafeFilterOption(options = [], groupOrder = RECOMMEND_FILTER_GROUP_ORDER) {
|
|
38
|
+
for (const group of groupOrder) {
|
|
39
|
+
const option = options.find((item) => (
|
|
40
|
+
item.group === group
|
|
41
|
+
&& !item.active
|
|
42
|
+
&& isSafeFilterOptionLabel(item.label)
|
|
43
|
+
));
|
|
44
|
+
if (option) return option;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function chooseFilterOptionByLabels(options = [], {
|
|
50
|
+
group = "",
|
|
51
|
+
labels = []
|
|
52
|
+
} = {}) {
|
|
53
|
+
const normalizedGroup = normalizeText(group);
|
|
54
|
+
const normalizedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
|
|
55
|
+
for (const label of normalizedLabels) {
|
|
56
|
+
const option = options.find((item) => (
|
|
57
|
+
(!normalizedGroup || item.group === normalizedGroup)
|
|
58
|
+
&& !item.active
|
|
59
|
+
&& normalizeFilterOptionLabel(item.label) === label
|
|
60
|
+
&& isSafeFilterOptionLabel(item.label)
|
|
61
|
+
));
|
|
62
|
+
if (option) return option;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function chooseFilterOptionsByLabels(options = [], {
|
|
68
|
+
group = "",
|
|
69
|
+
labels = []
|
|
70
|
+
} = {}) {
|
|
71
|
+
const normalizedGroup = normalizeText(group);
|
|
72
|
+
const normalizedLabels = labels.map(normalizeFilterOptionLabel).filter(Boolean);
|
|
73
|
+
return normalizedLabels.map((label) => {
|
|
74
|
+
const option = options.find((item) => (
|
|
75
|
+
(!normalizedGroup || item.group === normalizedGroup)
|
|
76
|
+
&& normalizeFilterOptionLabel(item.label) === label
|
|
77
|
+
&& isSafeFilterOptionLabel(item.label)
|
|
78
|
+
));
|
|
79
|
+
return {
|
|
80
|
+
label,
|
|
81
|
+
option: option || null
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function getFilterPanelCount(client, frameNodeId) {
|
|
87
|
+
return (await querySelectorAll(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.panel)).length;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function getRecommendFilterCounts(client, frameNodeId) {
|
|
91
|
+
return countSelectors(client, frameNodeId, {
|
|
92
|
+
filter_trigger: RECOMMEND_FILTER_SELECTORS.trigger,
|
|
93
|
+
filter_panel: RECOMMEND_FILTER_SELECTORS.panel,
|
|
94
|
+
check_box: RECOMMEND_FILTER_SELECTORS.checkBox,
|
|
95
|
+
option: `.filter-panel ${RECOMMEND_FILTER_SELECTORS.option}`,
|
|
96
|
+
active_option: RECOMMEND_FILTER_SELECTORS.activeOption,
|
|
97
|
+
recommend_card: RECOMMEND_CARD_SELECTOR
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function findFilterTrigger(client, frameNodeId) {
|
|
102
|
+
return findFirstNode(client, frameNodeId, [
|
|
103
|
+
RECOMMEND_FILTER_SELECTORS.trigger,
|
|
104
|
+
".recommend-filter.op-filter"
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function ensureFilterPanelClosed(client, frameNodeId, triggerNodeId = 0) {
|
|
109
|
+
const attempts = [];
|
|
110
|
+
if (await getFilterPanelCount(client, frameNodeId) === 0) return attempts;
|
|
111
|
+
|
|
112
|
+
await pressKey(client, "Escape", {
|
|
113
|
+
code: "Escape",
|
|
114
|
+
windowsVirtualKeyCode: 27,
|
|
115
|
+
nativeVirtualKeyCode: 27
|
|
116
|
+
});
|
|
117
|
+
await sleep(400);
|
|
118
|
+
attempts.push("Escape");
|
|
119
|
+
|
|
120
|
+
if (await getFilterPanelCount(client, frameNodeId) > 0 && triggerNodeId) {
|
|
121
|
+
await clickNodeCenter(client, triggerNodeId, DETERMINISTIC_CLICK_OPTIONS);
|
|
122
|
+
await sleep(500);
|
|
123
|
+
attempts.push("filter-trigger-toggle");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return attempts;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function dismissRecommendControlOverlays(client, settleMs = 250) {
|
|
130
|
+
if (typeof client?.Input?.dispatchKeyEvent !== "function") {
|
|
131
|
+
return ["Escape-unavailable"];
|
|
132
|
+
}
|
|
133
|
+
await pressKey(client, "Escape", {
|
|
134
|
+
code: "Escape",
|
|
135
|
+
windowsVirtualKeyCode: 27,
|
|
136
|
+
nativeVirtualKeyCode: 27
|
|
137
|
+
});
|
|
138
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
139
|
+
return ["Escape"];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function findFilterTriggerCandidates(client, frameNodeId) {
|
|
143
|
+
const candidates = [];
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
for (const selector of [
|
|
146
|
+
RECOMMEND_FILTER_SELECTORS.trigger,
|
|
147
|
+
".recommend-filter.op-filter"
|
|
148
|
+
]) {
|
|
149
|
+
const candidate = await findFirstNode(client, frameNodeId, [selector]);
|
|
150
|
+
if (candidate && !seen.has(candidate.nodeId)) {
|
|
151
|
+
candidates.push(candidate);
|
|
152
|
+
seen.add(candidate.nodeId);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return candidates;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function openFilterPanel(client, frameNodeId) {
|
|
159
|
+
let triggerCandidates = await findFilterTriggerCandidates(client, frameNodeId);
|
|
160
|
+
if (!triggerCandidates.length) {
|
|
161
|
+
throw new Error("Recommend filter trigger was not found");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const preOpenDismissalAttempts = await dismissRecommendControlOverlays(client);
|
|
165
|
+
const existingPanelNodeId = await waitForSelector(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.panel, {
|
|
166
|
+
timeoutMs: 300,
|
|
167
|
+
intervalMs: 100
|
|
168
|
+
});
|
|
169
|
+
if (existingPanelNodeId) {
|
|
170
|
+
const triggerBox = await getNodeBox(client, triggerCandidates[0].nodeId);
|
|
171
|
+
return {
|
|
172
|
+
trigger: triggerCandidates[0],
|
|
173
|
+
trigger_box: triggerBox,
|
|
174
|
+
panel_node_id: existingPanelNodeId,
|
|
175
|
+
initial_close_attempts: preOpenDismissalAttempts,
|
|
176
|
+
already_open: true
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const closeAttempts = await ensureFilterPanelClosed(client, frameNodeId, triggerCandidates[0].nodeId);
|
|
181
|
+
|
|
182
|
+
const attempts = [];
|
|
183
|
+
for (let round = 0; round < 3; round += 1) {
|
|
184
|
+
triggerCandidates = await findFilterTriggerCandidates(client, frameNodeId);
|
|
185
|
+
for (const trigger of triggerCandidates) {
|
|
186
|
+
const triggerBox = await getNodeBox(client, trigger.nodeId);
|
|
187
|
+
const clickBox = await clickNodeCenter(client, trigger.nodeId, DETERMINISTIC_CLICK_OPTIONS);
|
|
188
|
+
attempts.push({
|
|
189
|
+
selector: trigger.selector,
|
|
190
|
+
node_id: trigger.nodeId,
|
|
191
|
+
center: triggerBox.center,
|
|
192
|
+
click_target: clickBox.click_target,
|
|
193
|
+
click_result: clickBox.click_result
|
|
194
|
+
});
|
|
195
|
+
const panelNodeId = await waitForSelector(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.panel, {
|
|
196
|
+
timeoutMs: 2500,
|
|
197
|
+
intervalMs: 200
|
|
198
|
+
});
|
|
199
|
+
if (panelNodeId) {
|
|
200
|
+
return {
|
|
201
|
+
trigger,
|
|
202
|
+
trigger_box: triggerBox,
|
|
203
|
+
panel_node_id: panelNodeId,
|
|
204
|
+
initial_close_attempts: [
|
|
205
|
+
...preOpenDismissalAttempts,
|
|
206
|
+
...closeAttempts
|
|
207
|
+
],
|
|
208
|
+
open_attempts: attempts
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
await sleep(500);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
throw new Error(`Recommend filter panel did not open after ${attempts.length} trigger attempts`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function readOptionNode(client, group, nodeId) {
|
|
219
|
+
const [attributes, outerHTML] = await Promise.all([
|
|
220
|
+
getAttributesMap(client, nodeId),
|
|
221
|
+
getOuterHTML(client, nodeId)
|
|
222
|
+
]);
|
|
223
|
+
const label = normalizeFilterOptionLabel(htmlToText(outerHTML));
|
|
224
|
+
return {
|
|
225
|
+
group,
|
|
226
|
+
node_id: nodeId,
|
|
227
|
+
label,
|
|
228
|
+
active: isActiveOption(attributes, outerHTML),
|
|
229
|
+
attributes: {
|
|
230
|
+
class: attributes.class || "",
|
|
231
|
+
value: attributes.value || "",
|
|
232
|
+
type: attributes.type || ""
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function listFilterOptions(client, frameNodeId, {
|
|
238
|
+
groupOrder = RECOMMEND_FILTER_GROUP_ORDER
|
|
239
|
+
} = {}) {
|
|
240
|
+
const options = [];
|
|
241
|
+
for (const group of groupOrder) {
|
|
242
|
+
const groupSelector = RECOMMEND_FILTER_SELECTORS.groups[group];
|
|
243
|
+
if (!groupSelector) continue;
|
|
244
|
+
const groupNodeIds = await querySelectorAll(client, frameNodeId, groupSelector);
|
|
245
|
+
for (const groupNodeId of groupNodeIds) {
|
|
246
|
+
const optionNodeIds = await querySelectorAll(client, groupNodeId, RECOMMEND_FILTER_SELECTORS.option);
|
|
247
|
+
for (const optionNodeId of optionNodeIds) {
|
|
248
|
+
options.push(await readOptionNode(client, group, optionNodeId));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return options;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function clickFirstAvailableNode(client, nodeIds) {
|
|
256
|
+
const errors = [];
|
|
257
|
+
for (const nodeId of nodeIds) {
|
|
258
|
+
try {
|
|
259
|
+
const box = await clickNodeCenter(client, nodeId, DETERMINISTIC_CLICK_OPTIONS);
|
|
260
|
+
return {
|
|
261
|
+
clicked: true,
|
|
262
|
+
node_id: nodeId,
|
|
263
|
+
box
|
|
264
|
+
};
|
|
265
|
+
} catch (error) {
|
|
266
|
+
errors.push({
|
|
267
|
+
node_id: nodeId,
|
|
268
|
+
message: error?.message || String(error)
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
clicked: false,
|
|
274
|
+
errors
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function normalizeButtonLabel(label) {
|
|
279
|
+
return normalizeFilterOptionLabel(label).toLowerCase();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function buttonRank(candidate) {
|
|
283
|
+
const label = normalizeButtonLabel(candidate.label);
|
|
284
|
+
if (/确定|确认|完成|ok|confirm/.test(label)) return 0;
|
|
285
|
+
if (/重置|清空|取消|reset|cancel/.test(label)) return 2;
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function readButtonCandidate(client, nodeId, index) {
|
|
290
|
+
const [attributes, outerHTML] = await Promise.all([
|
|
291
|
+
getAttributesMap(client, nodeId),
|
|
292
|
+
getOuterHTML(client, nodeId)
|
|
293
|
+
]);
|
|
294
|
+
return {
|
|
295
|
+
node_id: nodeId,
|
|
296
|
+
index,
|
|
297
|
+
label: normalizeButtonLabel(htmlToText(outerHTML)),
|
|
298
|
+
class_name: attributes.class || ""
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function readConfirmButtonCandidates(client, frameNodeId) {
|
|
303
|
+
const nodeIds = await querySelectorAll(client, frameNodeId, RECOMMEND_FILTER_SELECTORS.confirmButton);
|
|
304
|
+
const candidates = [];
|
|
305
|
+
for (let index = 0; index < nodeIds.length; index += 1) {
|
|
306
|
+
candidates.push(await readButtonCandidate(client, nodeIds[index], index));
|
|
307
|
+
}
|
|
308
|
+
return candidates.sort((left, right) => {
|
|
309
|
+
const rankDiff = buttonRank(left) - buttonRank(right);
|
|
310
|
+
if (rankDiff !== 0) return rankDiff;
|
|
311
|
+
return right.index - left.index;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function selectFirstSafeFilterOption(client, frameNodeId, {
|
|
316
|
+
groupOrder = RECOMMEND_FILTER_GROUP_ORDER
|
|
317
|
+
} = {}) {
|
|
318
|
+
const options = await listFilterOptions(client, frameNodeId, { groupOrder });
|
|
319
|
+
const selected = chooseFirstSafeFilterOption(options, groupOrder);
|
|
320
|
+
if (!selected) {
|
|
321
|
+
throw new Error("No safe non-active recommend filter option was found");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const box = await clickNodeCenter(client, selected.node_id, {
|
|
325
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
326
|
+
scrollIntoView: true
|
|
327
|
+
});
|
|
328
|
+
await sleep(300);
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
selected_option: {
|
|
332
|
+
group: selected.group,
|
|
333
|
+
label: selected.label,
|
|
334
|
+
node_id: selected.node_id,
|
|
335
|
+
was_active: selected.active
|
|
336
|
+
},
|
|
337
|
+
option_box: box,
|
|
338
|
+
discovered_options: options.map((option) => ({
|
|
339
|
+
group: option.group,
|
|
340
|
+
label: option.label,
|
|
341
|
+
active: option.active,
|
|
342
|
+
node_id: option.node_id
|
|
343
|
+
}))
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function selectFilterOption(client, frameNodeId, {
|
|
348
|
+
group = "",
|
|
349
|
+
labels = [],
|
|
350
|
+
groupOrder = RECOMMEND_FILTER_GROUP_ORDER
|
|
351
|
+
} = {}) {
|
|
352
|
+
const options = await listFilterOptions(client, frameNodeId, { groupOrder });
|
|
353
|
+
const selected = labels.length
|
|
354
|
+
? chooseFilterOptionByLabels(options, { group, labels })
|
|
355
|
+
: chooseFirstSafeFilterOption(options, groupOrder);
|
|
356
|
+
|
|
357
|
+
if (!selected) {
|
|
358
|
+
const target = labels.length
|
|
359
|
+
? `${group || "any group"} / ${labels.join(", ")}`
|
|
360
|
+
: "first safe non-active option";
|
|
361
|
+
throw new Error(`No matching recommend filter option was found for ${target}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const box = await clickNodeCenter(client, selected.node_id, {
|
|
365
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
366
|
+
scrollIntoView: true
|
|
367
|
+
});
|
|
368
|
+
await sleep(300);
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
selected_option: {
|
|
372
|
+
group: selected.group,
|
|
373
|
+
label: selected.label,
|
|
374
|
+
node_id: selected.node_id,
|
|
375
|
+
was_active: selected.active,
|
|
376
|
+
requested_group: group || null,
|
|
377
|
+
requested_labels: labels
|
|
378
|
+
},
|
|
379
|
+
option_box: box,
|
|
380
|
+
discovered_options: options.map((option) => ({
|
|
381
|
+
group: option.group,
|
|
382
|
+
label: option.label,
|
|
383
|
+
active: option.active,
|
|
384
|
+
node_id: option.node_id
|
|
385
|
+
}))
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function selectFilterOptions(client, frameNodeId, {
|
|
390
|
+
group = "",
|
|
391
|
+
labels = [],
|
|
392
|
+
groupOrder = RECOMMEND_FILTER_GROUP_ORDER
|
|
393
|
+
} = {}) {
|
|
394
|
+
if (!labels.length) {
|
|
395
|
+
return selectFilterOption(client, frameNodeId, { group, labels, groupOrder });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const selectedOptions = [];
|
|
399
|
+
const missingLabels = [];
|
|
400
|
+
let discoveredOptions = [];
|
|
401
|
+
|
|
402
|
+
for (const label of labels) {
|
|
403
|
+
if (await getFilterPanelCount(client, frameNodeId) === 0) {
|
|
404
|
+
await openFilterPanel(client, frameNodeId);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const options = await listFilterOptions(client, frameNodeId, { groupOrder });
|
|
408
|
+
discoveredOptions = options.map((option) => ({
|
|
409
|
+
group: option.group,
|
|
410
|
+
label: option.label,
|
|
411
|
+
active: option.active,
|
|
412
|
+
node_id: option.node_id
|
|
413
|
+
}));
|
|
414
|
+
const selected = chooseFilterOptionByLabels(options, { group, labels: [label] });
|
|
415
|
+
const alreadyActive = options.find((option) => (
|
|
416
|
+
(!group || option.group === group)
|
|
417
|
+
&& normalizeFilterOptionLabel(option.label) === normalizeFilterOptionLabel(label)
|
|
418
|
+
&& option.active
|
|
419
|
+
));
|
|
420
|
+
|
|
421
|
+
if (alreadyActive) {
|
|
422
|
+
selectedOptions.push({
|
|
423
|
+
group: alreadyActive.group,
|
|
424
|
+
label: alreadyActive.label,
|
|
425
|
+
node_id: alreadyActive.node_id,
|
|
426
|
+
was_active: true,
|
|
427
|
+
clicked: false,
|
|
428
|
+
requested_group: group || null
|
|
429
|
+
});
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!selected) {
|
|
434
|
+
missingLabels.push(label);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const box = await clickNodeCenter(client, selected.node_id, {
|
|
439
|
+
...DETERMINISTIC_CLICK_OPTIONS,
|
|
440
|
+
scrollIntoView: true
|
|
441
|
+
});
|
|
442
|
+
selectedOptions.push({
|
|
443
|
+
group: selected.group,
|
|
444
|
+
label: selected.label,
|
|
445
|
+
node_id: selected.node_id,
|
|
446
|
+
was_active: false,
|
|
447
|
+
clicked: true,
|
|
448
|
+
requested_group: group || null,
|
|
449
|
+
option_box: box
|
|
450
|
+
});
|
|
451
|
+
await sleep(450);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (missingLabels.length) {
|
|
455
|
+
throw new Error(`No matching recommend filter options were found for ${group || "any group"} / ${missingLabels.join(", ")}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
selected_option: selectedOptions[0] || null,
|
|
460
|
+
selected_options: selectedOptions.map((option) => ({
|
|
461
|
+
group: option.group,
|
|
462
|
+
label: option.label,
|
|
463
|
+
node_id: option.node_id,
|
|
464
|
+
was_active: option.was_active,
|
|
465
|
+
clicked: option.clicked,
|
|
466
|
+
requested_group: option.requested_group,
|
|
467
|
+
requested_labels: labels
|
|
468
|
+
})),
|
|
469
|
+
option_box: selectedOptions.find((option) => option.option_box)?.option_box || null,
|
|
470
|
+
discovered_options: discoveredOptions
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export async function selectFilterGroups(client, frameNodeId, {
|
|
475
|
+
filterGroups = [],
|
|
476
|
+
groupOrder = RECOMMEND_FILTER_GROUP_ORDER
|
|
477
|
+
} = {}) {
|
|
478
|
+
const selectedOptions = [];
|
|
479
|
+
const discoveredOptions = [];
|
|
480
|
+
const groups = filterGroups.filter((item) => item && (item.group || item.labels?.length));
|
|
481
|
+
if (!groups.length) {
|
|
482
|
+
return selectFilterOption(client, frameNodeId, { groupOrder });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
for (const spec of groups) {
|
|
486
|
+
const labels = Array.isArray(spec.labels) ? spec.labels : [];
|
|
487
|
+
const selection = spec.selectAllLabels === false
|
|
488
|
+
? await selectFilterOption(client, frameNodeId, {
|
|
489
|
+
group: spec.group || "",
|
|
490
|
+
labels,
|
|
491
|
+
groupOrder
|
|
492
|
+
})
|
|
493
|
+
: await selectFilterOptions(client, frameNodeId, {
|
|
494
|
+
group: spec.group || "",
|
|
495
|
+
labels,
|
|
496
|
+
groupOrder
|
|
497
|
+
});
|
|
498
|
+
if (selection.selected_option) selectedOptions.push(selection.selected_option);
|
|
499
|
+
for (const option of selection.selected_options || []) {
|
|
500
|
+
selectedOptions.push(option);
|
|
501
|
+
}
|
|
502
|
+
for (const option of selection.discovered_options || []) {
|
|
503
|
+
discoveredOptions.push(option);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const dedupedSelected = [];
|
|
508
|
+
const seenSelected = new Set();
|
|
509
|
+
for (const option of selectedOptions) {
|
|
510
|
+
const key = `${option.group || ""}:${normalizeFilterOptionLabel(option.label || "")}`;
|
|
511
|
+
if (seenSelected.has(key)) continue;
|
|
512
|
+
seenSelected.add(key);
|
|
513
|
+
dedupedSelected.push(option);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
selected_option: dedupedSelected[0] || null,
|
|
518
|
+
selected_options: dedupedSelected,
|
|
519
|
+
option_box: dedupedSelected.find((option) => option.option_box)?.option_box || null,
|
|
520
|
+
discovered_options: discoveredOptions
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export async function confirmFilterPanel(client, frameNodeId, {
|
|
525
|
+
timeoutMs = 8000
|
|
526
|
+
} = {}) {
|
|
527
|
+
const candidates = await readConfirmButtonCandidates(client, frameNodeId);
|
|
528
|
+
if (!candidates.length && await getFilterPanelCount(client, frameNodeId) === 0) {
|
|
529
|
+
return {
|
|
530
|
+
confirmed: true,
|
|
531
|
+
confirm_node_id: null,
|
|
532
|
+
confirm_label: "auto-closed",
|
|
533
|
+
confirm_candidates: [],
|
|
534
|
+
confirm_attempts: [],
|
|
535
|
+
panel_count: 0
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (!candidates.length) {
|
|
539
|
+
throw new Error("Recommend filter confirm button was not found");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const attempts = [];
|
|
543
|
+
for (const candidate of candidates) {
|
|
544
|
+
const clickResult = await clickFirstAvailableNode(client, [candidate.node_id]);
|
|
545
|
+
attempts.push({
|
|
546
|
+
node_id: candidate.node_id,
|
|
547
|
+
label: candidate.label,
|
|
548
|
+
clicked: clickResult.clicked,
|
|
549
|
+
errors: clickResult.errors
|
|
550
|
+
});
|
|
551
|
+
if (!clickResult.clicked) continue;
|
|
552
|
+
|
|
553
|
+
const started = Date.now();
|
|
554
|
+
while (Date.now() - started <= timeoutMs) {
|
|
555
|
+
const panelCount = await getFilterPanelCount(client, frameNodeId);
|
|
556
|
+
if (panelCount === 0) {
|
|
557
|
+
return {
|
|
558
|
+
confirmed: true,
|
|
559
|
+
confirm_node_id: clickResult.node_id,
|
|
560
|
+
confirm_label: candidate.label,
|
|
561
|
+
confirm_box: clickResult.box,
|
|
562
|
+
confirm_candidates: candidates,
|
|
563
|
+
confirm_attempts: attempts,
|
|
564
|
+
panel_count: 0
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
await sleep(250);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
confirmed: false,
|
|
573
|
+
confirm_node_id: attempts.at(-1)?.node_id || null,
|
|
574
|
+
confirm_label: attempts.at(-1)?.label || null,
|
|
575
|
+
confirm_candidates: candidates,
|
|
576
|
+
confirm_attempts: attempts,
|
|
577
|
+
panel_count: await getFilterPanelCount(client, frameNodeId)
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export async function selectAndConfirmFirstSafeFilter(client, frameNodeId, options = {}) {
|
|
582
|
+
const beforeCounts = await getRecommendFilterCounts(client, frameNodeId);
|
|
583
|
+
const openResult = await openFilterPanel(client, frameNodeId);
|
|
584
|
+
const afterOpenCounts = await getRecommendFilterCounts(client, frameNodeId);
|
|
585
|
+
const filterGroups = Array.isArray(options.filterGroups) ? options.filterGroups : [];
|
|
586
|
+
const selection = filterGroups.length
|
|
587
|
+
? await selectFilterGroups(client, frameNodeId, { filterGroups, groupOrder: options.groupOrder })
|
|
588
|
+
: options.selectAllLabels
|
|
589
|
+
? await selectFilterOptions(client, frameNodeId, options)
|
|
590
|
+
: await selectFilterOption(client, frameNodeId, options);
|
|
591
|
+
const confirm = await confirmFilterPanel(client, frameNodeId);
|
|
592
|
+
await sleep(1200);
|
|
593
|
+
const afterConfirmCounts = await getRecommendFilterCounts(client, frameNodeId);
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
opened_panel: true,
|
|
597
|
+
trigger: {
|
|
598
|
+
node_id: openResult.trigger.nodeId,
|
|
599
|
+
selector: openResult.trigger.selector,
|
|
600
|
+
center: openResult.trigger_box.center,
|
|
601
|
+
rect: openResult.trigger_box.rect
|
|
602
|
+
},
|
|
603
|
+
initial_close_attempts: openResult.initial_close_attempts,
|
|
604
|
+
before_counts: beforeCounts,
|
|
605
|
+
after_open_counts: afterOpenCounts,
|
|
606
|
+
...selection,
|
|
607
|
+
...confirm,
|
|
608
|
+
after_confirm_counts: afterConfirmCounts
|
|
609
|
+
};
|
|
610
|
+
}
|