@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,457 +1,457 @@
|
|
|
1
|
-
import {
|
|
2
|
-
clickPoint,
|
|
3
|
-
getFrameDocumentNodeId,
|
|
4
|
-
getAttributesMap,
|
|
5
|
-
getNodeBox,
|
|
6
|
-
getOuterHTML,
|
|
7
|
-
querySelectorAll,
|
|
8
|
-
sleep
|
|
9
|
-
} from "../../core/browser/index.js";
|
|
10
|
-
import {
|
|
11
|
-
htmlToText,
|
|
12
|
-
normalizeText
|
|
13
|
-
} from "../../core/screening/index.js";
|
|
14
|
-
import {
|
|
15
|
-
assertGreetQuotaAvailable,
|
|
16
|
-
parseGreetQuota
|
|
17
|
-
} from "../../core/greet-quota/index.js";
|
|
18
|
-
import {
|
|
19
|
-
FAVORITE_BUTTON_SELECTORS,
|
|
20
|
-
GREET_BUTTON_RECOMMEND_SELECTORS
|
|
21
|
-
} from "./constants.js";
|
|
22
|
-
import { waitForRecommendDetail } from "./detail.js";
|
|
23
|
-
import { getRecommendRoots } from "./roots.js";
|
|
24
|
-
|
|
25
|
-
const POST_ACTIONS = new Set(["none", "favorite", "greet"]);
|
|
26
|
-
const GREET_EXACT_LABEL_PATTERN = /^(?:打招呼|聊一聊|立即沟通(?:[\((]\d+\s*[//]\s*\d+[\))])?|沟通)$/i;
|
|
27
|
-
export const RECOMMEND_DETAIL_ACTION_TEXT_SELECTORS = Object.freeze([
|
|
28
|
-
"button",
|
|
29
|
-
".btn",
|
|
30
|
-
'[role="button"]',
|
|
31
|
-
"a",
|
|
32
|
-
"span",
|
|
33
|
-
"div"
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
function uniqueSelectors(...selectorGroups) {
|
|
37
|
-
return [...new Set(selectorGroups.flat().filter(Boolean))];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function uniqueByNode(candidates = []) {
|
|
41
|
-
const seen = new Set();
|
|
42
|
-
const result = [];
|
|
43
|
-
for (const item of candidates) {
|
|
44
|
-
const key = `${item.kind}:${item.root}:${item.node_id}`;
|
|
45
|
-
if (seen.has(key)) continue;
|
|
46
|
-
seen.add(key);
|
|
47
|
-
result.push(item);
|
|
48
|
-
}
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function lowerText(...parts) {
|
|
53
|
-
return normalizeText(parts.filter(Boolean).join(" ")).toLowerCase();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function hasActiveClass(text) {
|
|
57
|
-
return /(?:^|\s)(?:active|curr|current|selected|checked)(?:\s|$)/i.test(text);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function hasDisabledSignal(text) {
|
|
61
|
-
return /(?:^|\s)(?:disabled|disable|forbidden|is-disabled)(?:\s|$)/i.test(text);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function rectArea(control) {
|
|
65
|
-
const rect = control?.rect || {};
|
|
66
|
-
return Math.max(0, Number(rect.width) || 0) * Math.max(0, Number(rect.height) || 0);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function isCompactLabel(control, limit = 80) {
|
|
70
|
-
const label = normalizeText(control?.label || "");
|
|
71
|
-
return label.length > 0 && label.length <= limit;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function controlRank(control, exactLabelPattern) {
|
|
75
|
-
const label = normalizeText(control?.label || "");
|
|
76
|
-
const selector = String(control?.selector || "");
|
|
77
|
-
const className = String(control?.class_name || "");
|
|
78
|
-
let score = 0;
|
|
79
|
-
if (exactLabelPattern.test(label)) score -= 1000;
|
|
80
|
-
if (/button|\[role=|\.btn/.test(selector) || /btn|button/i.test(className)) score -= 250;
|
|
81
|
-
if (selector === "div") score += 300;
|
|
82
|
-
if (!isCompactLabel(control)) score += 500;
|
|
83
|
-
score += Math.min(rectArea(control), 100000) / 1000;
|
|
84
|
-
score += label.length / 10;
|
|
85
|
-
return score;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function bestControl(controls, exactLabelPattern) {
|
|
89
|
-
return [...controls].sort((left, right) => (
|
|
90
|
-
controlRank(left, exactLabelPattern) - controlRank(right, exactLabelPattern)
|
|
91
|
-
))[0] || null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function normalizeRecommendPostAction(value) {
|
|
95
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
96
|
-
if (["", "none", "skip", "no", "不执行", "无"].includes(normalized)) return "none";
|
|
97
|
-
if (["favorite", "fav", "collect", "收藏", "感兴趣"].includes(normalized)) return "favorite";
|
|
98
|
-
if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
|
|
99
|
-
return POST_ACTIONS.has(normalized) ? normalized : "";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function resolveRecommendPostAction({
|
|
103
|
-
postAction = "none",
|
|
104
|
-
greetCount = 0,
|
|
105
|
-
maxGreetCount = null
|
|
106
|
-
} = {}) {
|
|
107
|
-
const requested = normalizeRecommendPostAction(postAction) || "none";
|
|
108
|
-
const currentGreetCount = Number.isInteger(greetCount) && greetCount >= 0 ? greetCount : 0;
|
|
109
|
-
const limit = Number.isInteger(maxGreetCount) && maxGreetCount > 0 ? maxGreetCount : null;
|
|
110
|
-
if (requested === "greet" && limit !== null && currentGreetCount >= limit) {
|
|
111
|
-
return {
|
|
112
|
-
requested,
|
|
113
|
-
effective: "favorite",
|
|
114
|
-
reason: "greet_limit_reached",
|
|
115
|
-
greet_count: currentGreetCount,
|
|
116
|
-
max_greet_count: limit
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
requested,
|
|
121
|
-
effective: requested,
|
|
122
|
-
reason: "requested_action",
|
|
123
|
-
greet_count: currentGreetCount,
|
|
124
|
-
max_greet_count: limit
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function classifyFavoriteControl({
|
|
129
|
-
outerHTML = "",
|
|
130
|
-
attributes = {}
|
|
131
|
-
} = {}) {
|
|
132
|
-
const label = htmlToText(outerHTML);
|
|
133
|
-
const labelText = normalizeText(label);
|
|
134
|
-
const className = normalizeText(attributes.class || "");
|
|
135
|
-
const title = normalizeText(attributes.title || attributes["aria-label"] || "");
|
|
136
|
-
const combined = lowerText(className, title);
|
|
137
|
-
const labelMatches = /^(?:收藏|已收藏|感兴趣|已感兴趣)$/.test(labelText);
|
|
138
|
-
const classMatches = /favorite|collect|interest|like/.test(combined);
|
|
139
|
-
const matches = labelMatches || classMatches;
|
|
140
|
-
const active = (
|
|
141
|
-
/已收藏|已感兴趣/.test(label)
|
|
142
|
-
|| /like-icon-active|favorite-active|collect-active/i.test(outerHTML)
|
|
143
|
-
|| hasActiveClass(className)
|
|
144
|
-
);
|
|
145
|
-
const disabled = (
|
|
146
|
-
Object.prototype.hasOwnProperty.call(attributes, "disabled")
|
|
147
|
-
|| hasDisabledSignal(className)
|
|
148
|
-
|| /disabled/i.test(outerHTML)
|
|
149
|
-
);
|
|
150
|
-
return {
|
|
151
|
-
kind: "favorite",
|
|
152
|
-
matches,
|
|
153
|
-
active,
|
|
154
|
-
disabled,
|
|
155
|
-
label: label || title || null,
|
|
156
|
-
class_name: className || null
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function classifyGreetControl({
|
|
161
|
-
outerHTML = "",
|
|
162
|
-
attributes = {}
|
|
163
|
-
} = {}) {
|
|
164
|
-
const label = htmlToText(outerHTML);
|
|
165
|
-
const labelText = normalizeText(label);
|
|
166
|
-
const className = normalizeText(attributes.class || "");
|
|
167
|
-
const title = normalizeText(attributes.title || attributes["aria-label"] || "");
|
|
168
|
-
const combined = lowerText(className, title);
|
|
169
|
-
const continueChat = labelText.length <= 40 && /继续沟通/.test(labelText);
|
|
170
|
-
const greetQuota = parseGreetQuota(labelText || title);
|
|
171
|
-
const greetEntry = (
|
|
172
|
-
GREET_EXACT_LABEL_PATTERN.test(labelText)
|
|
173
|
-
|| greetQuota.found
|
|
174
|
-
|| /greet/i.test(combined)
|
|
175
|
-
);
|
|
176
|
-
const disabled = (
|
|
177
|
-
Object.prototype.hasOwnProperty.call(attributes, "disabled")
|
|
178
|
-
|| hasDisabledSignal(className)
|
|
179
|
-
|| /disabled/i.test(outerHTML)
|
|
180
|
-
);
|
|
181
|
-
return {
|
|
182
|
-
kind: "greet",
|
|
183
|
-
matches: greetEntry || continueChat,
|
|
184
|
-
available: greetEntry && !continueChat && !disabled,
|
|
185
|
-
continue_chat: continueChat,
|
|
186
|
-
disabled,
|
|
187
|
-
label: label || title || null,
|
|
188
|
-
greet_quota: greetQuota.found ? greetQuota : null,
|
|
189
|
-
class_name: className || null
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function readActionNode(client, {
|
|
194
|
-
root,
|
|
195
|
-
selector,
|
|
196
|
-
nodeId,
|
|
197
|
-
kind
|
|
198
|
-
}) {
|
|
199
|
-
const [attributes, outerHTML] = await Promise.all([
|
|
200
|
-
getAttributesMap(client, nodeId),
|
|
201
|
-
getOuterHTML(client, nodeId)
|
|
202
|
-
]);
|
|
203
|
-
let box = null;
|
|
204
|
-
let visible = false;
|
|
205
|
-
try {
|
|
206
|
-
box = await getNodeBox(client, nodeId);
|
|
207
|
-
visible = box.rect.width > 2 && box.rect.height > 2;
|
|
208
|
-
} catch {}
|
|
209
|
-
const classification = kind === "favorite"
|
|
210
|
-
? classifyFavoriteControl({ outerHTML, attributes })
|
|
211
|
-
: classifyGreetControl({ outerHTML, attributes });
|
|
212
|
-
return {
|
|
213
|
-
kind,
|
|
214
|
-
root: root.name,
|
|
215
|
-
root_node_id: root.nodeId,
|
|
216
|
-
selector,
|
|
217
|
-
node_id: nodeId,
|
|
218
|
-
visible,
|
|
219
|
-
center: box?.center || null,
|
|
220
|
-
rect: box?.rect || null,
|
|
221
|
-
attributes,
|
|
222
|
-
outer_html_length: outerHTML.length,
|
|
223
|
-
html_preview: outerHTML.slice(0, 500),
|
|
224
|
-
...classification
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export async function collectRecommendActionControls(client, roots, {
|
|
229
|
-
favoriteSelectors = FAVORITE_BUTTON_SELECTORS,
|
|
230
|
-
greetSelectors = GREET_BUTTON_RECOMMEND_SELECTORS,
|
|
231
|
-
detailTextFallback = false
|
|
232
|
-
} = {}) {
|
|
233
|
-
const candidates = [];
|
|
234
|
-
const favoriteScanSelectors = detailTextFallback
|
|
235
|
-
? uniqueSelectors(favoriteSelectors, RECOMMEND_DETAIL_ACTION_TEXT_SELECTORS)
|
|
236
|
-
: favoriteSelectors;
|
|
237
|
-
const greetScanSelectors = detailTextFallback
|
|
238
|
-
? uniqueSelectors(greetSelectors, RECOMMEND_DETAIL_ACTION_TEXT_SELECTORS)
|
|
239
|
-
: greetSelectors;
|
|
240
|
-
for (const root of roots) {
|
|
241
|
-
if (!root?.nodeId) continue;
|
|
242
|
-
for (const [kind, selectors] of [
|
|
243
|
-
["favorite", favoriteScanSelectors],
|
|
244
|
-
["greet", greetScanSelectors]
|
|
245
|
-
]) {
|
|
246
|
-
for (const selector of selectors) {
|
|
247
|
-
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
248
|
-
for (const nodeId of nodeIds) {
|
|
249
|
-
candidates.push(await readActionNode(client, {
|
|
250
|
-
root,
|
|
251
|
-
selector,
|
|
252
|
-
nodeId,
|
|
253
|
-
kind
|
|
254
|
-
}));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return uniqueByNode(candidates);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export function summarizeRecommendActionControls(controls = []) {
|
|
263
|
-
const visibleControls = controls.filter((item) => item.visible && item.matches);
|
|
264
|
-
const favoriteControls = visibleControls.filter((item) => item.kind === "favorite");
|
|
265
|
-
const greetControls = visibleControls.filter((item) => item.kind === "greet");
|
|
266
|
-
const favorite = bestControl(
|
|
267
|
-
favoriteControls.filter((item) => item.matches),
|
|
268
|
-
/^(?:收藏|已收藏|感兴趣|已感兴趣)$/i
|
|
269
|
-
);
|
|
270
|
-
const greet = bestControl(
|
|
271
|
-
greetControls.filter((item) => item.available),
|
|
272
|
-
GREET_EXACT_LABEL_PATTERN
|
|
273
|
-
) || bestControl(
|
|
274
|
-
greetControls.filter((item) => item.continue_chat),
|
|
275
|
-
/^继续沟通$/i
|
|
276
|
-
);
|
|
277
|
-
return {
|
|
278
|
-
favorite: favorite
|
|
279
|
-
? {
|
|
280
|
-
found: true,
|
|
281
|
-
active: favorite.active,
|
|
282
|
-
disabled: favorite.disabled,
|
|
283
|
-
label: favorite.label,
|
|
284
|
-
selector: favorite.selector,
|
|
285
|
-
root: favorite.root,
|
|
286
|
-
node_id: favorite.node_id,
|
|
287
|
-
center: favorite.center
|
|
288
|
-
}
|
|
289
|
-
: { found: false },
|
|
290
|
-
greet: greet
|
|
291
|
-
? {
|
|
292
|
-
found: true,
|
|
293
|
-
available: greet.available,
|
|
294
|
-
continue_chat: greet.continue_chat,
|
|
295
|
-
disabled: greet.disabled,
|
|
296
|
-
label: greet.label,
|
|
297
|
-
greet_quota: greet.greet_quota || null,
|
|
298
|
-
selector: greet.selector,
|
|
299
|
-
root: greet.root,
|
|
300
|
-
node_id: greet.node_id,
|
|
301
|
-
center: greet.center
|
|
302
|
-
}
|
|
303
|
-
: { found: false },
|
|
304
|
-
counts: {
|
|
305
|
-
total: controls.length,
|
|
306
|
-
visible_matching: visibleControls.length,
|
|
307
|
-
favorite: favoriteControls.length,
|
|
308
|
-
greet: greetControls.length
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
export async function discoverRecommendActionControls(client, {
|
|
314
|
-
roots = null,
|
|
315
|
-
selectors = {},
|
|
316
|
-
detailTextFallback = false
|
|
317
|
-
} = {}) {
|
|
318
|
-
const rootState = roots ? { roots } : await getRecommendRoots(client);
|
|
319
|
-
const controls = await collectRecommendActionControls(client, rootState.roots, {
|
|
320
|
-
...selectors,
|
|
321
|
-
detailTextFallback
|
|
322
|
-
});
|
|
323
|
-
return {
|
|
324
|
-
controls,
|
|
325
|
-
summary: summarizeRecommendActionControls(controls)
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export async function waitForRecommendActionControls(client, {
|
|
330
|
-
timeoutMs = 6000,
|
|
331
|
-
intervalMs = 250,
|
|
332
|
-
requireAny = true,
|
|
333
|
-
...discoveryOptions
|
|
334
|
-
} = {}) {
|
|
335
|
-
const started = Date.now();
|
|
336
|
-
let lastDiscovery = null;
|
|
337
|
-
while (Date.now() - started <= timeoutMs) {
|
|
338
|
-
lastDiscovery = await discoverRecommendActionControls(client, discoveryOptions);
|
|
339
|
-
const hasControl = Boolean(
|
|
340
|
-
lastDiscovery.summary.favorite.found
|
|
341
|
-
|| lastDiscovery.summary.greet.found
|
|
342
|
-
);
|
|
343
|
-
if (!requireAny || hasControl) {
|
|
344
|
-
return {
|
|
345
|
-
...lastDiscovery,
|
|
346
|
-
elapsed_ms: Date.now() - started,
|
|
347
|
-
timed_out: false
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
await sleep(intervalMs);
|
|
351
|
-
}
|
|
352
|
-
return {
|
|
353
|
-
...(lastDiscovery || { controls: [], summary: summarizeRecommendActionControls([]) }),
|
|
354
|
-
elapsed_ms: Date.now() - started,
|
|
355
|
-
timed_out: true
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
export async function getRecommendDetailActionRoots(client, detailState) {
|
|
360
|
-
const roots = [];
|
|
361
|
-
if (detailState?.popup?.node_id) {
|
|
362
|
-
roots.push({
|
|
363
|
-
name: `${detailState.popup.root || "unknown"}:detail-popup`,
|
|
364
|
-
nodeId: detailState.popup.node_id
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
if (detailState?.resumeIframe?.node_id) {
|
|
368
|
-
try {
|
|
369
|
-
roots.push({
|
|
370
|
-
name: `${detailState.resumeIframe.root || "unknown"}:resume-iframe-document`,
|
|
371
|
-
nodeId: await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id)
|
|
372
|
-
});
|
|
373
|
-
} catch {
|
|
374
|
-
roots.push({
|
|
375
|
-
name: `${detailState.resumeIframe.root || "unknown"}:resume-iframe-node`,
|
|
376
|
-
nodeId: detailState.resumeIframe.node_id
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
return roots;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export async function waitForRecommendDetailActionControls(client, {
|
|
384
|
-
timeoutMs = 8000,
|
|
385
|
-
intervalMs = 350,
|
|
386
|
-
selectors = {},
|
|
387
|
-
requireAny = true
|
|
388
|
-
} = {}) {
|
|
389
|
-
const started = Date.now();
|
|
390
|
-
let lastDiscovery = null;
|
|
391
|
-
let lastError = null;
|
|
392
|
-
let lastRootCount = 0;
|
|
393
|
-
while (Date.now() - started <= timeoutMs) {
|
|
394
|
-
const detailState = await waitForRecommendDetail(client, {
|
|
395
|
-
timeoutMs: Math.min(intervalMs, 500),
|
|
396
|
-
intervalMs: 100
|
|
397
|
-
});
|
|
398
|
-
const roots = await getRecommendDetailActionRoots(client, detailState);
|
|
399
|
-
lastRootCount = roots.length;
|
|
400
|
-
if (roots.length) {
|
|
401
|
-
try {
|
|
402
|
-
lastDiscovery = await discoverRecommendActionControls(client, {
|
|
403
|
-
roots,
|
|
404
|
-
selectors,
|
|
405
|
-
detailTextFallback: true
|
|
406
|
-
});
|
|
407
|
-
const hasControl = Boolean(
|
|
408
|
-
lastDiscovery.summary.favorite.found
|
|
409
|
-
|| lastDiscovery.summary.greet.found
|
|
410
|
-
);
|
|
411
|
-
if (!requireAny || hasControl) {
|
|
412
|
-
return {
|
|
413
|
-
...lastDiscovery,
|
|
414
|
-
elapsed_ms: Date.now() - started,
|
|
415
|
-
timed_out: false,
|
|
416
|
-
detail_root_count: roots.length
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
} catch (error) {
|
|
420
|
-
lastError = error?.message || String(error);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
await sleep(intervalMs);
|
|
424
|
-
}
|
|
425
|
-
return {
|
|
426
|
-
...(lastDiscovery || { controls: [], summary: summarizeRecommendActionControls([]) }),
|
|
427
|
-
elapsed_ms: Date.now() - started,
|
|
428
|
-
timed_out: true,
|
|
429
|
-
detail_root_count: lastRootCount,
|
|
430
|
-
last_error: lastError
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
export async function clickRecommendActionControl(client, control, {
|
|
435
|
-
allowDisabled = false
|
|
436
|
-
} = {}) {
|
|
437
|
-
if (!control?.center) {
|
|
438
|
-
throw new Error("Action control has no clickable center");
|
|
439
|
-
}
|
|
440
|
-
const greetQuota = control.kind === "greet"
|
|
441
|
-
? assertGreetQuotaAvailable(control.greet_quota || control.label || "")
|
|
442
|
-
: null;
|
|
443
|
-
if (control.disabled && !allowDisabled) {
|
|
444
|
-
throw new Error(`Action control is disabled: ${control.kind}`);
|
|
445
|
-
}
|
|
446
|
-
await clickPoint(client, control.center.x, control.center.y);
|
|
447
|
-
return {
|
|
448
|
-
clicked: true,
|
|
449
|
-
kind: control.kind,
|
|
450
|
-
label: control.label,
|
|
451
|
-
greet_quota: greetQuota?.found ? greetQuota : null,
|
|
452
|
-
selector: control.selector,
|
|
453
|
-
root: control.root,
|
|
454
|
-
node_id: control.node_id,
|
|
455
|
-
center: control.center
|
|
456
|
-
};
|
|
457
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
clickPoint,
|
|
3
|
+
getFrameDocumentNodeId,
|
|
4
|
+
getAttributesMap,
|
|
5
|
+
getNodeBox,
|
|
6
|
+
getOuterHTML,
|
|
7
|
+
querySelectorAll,
|
|
8
|
+
sleep
|
|
9
|
+
} from "../../core/browser/index.js";
|
|
10
|
+
import {
|
|
11
|
+
htmlToText,
|
|
12
|
+
normalizeText
|
|
13
|
+
} from "../../core/screening/index.js";
|
|
14
|
+
import {
|
|
15
|
+
assertGreetQuotaAvailable,
|
|
16
|
+
parseGreetQuota
|
|
17
|
+
} from "../../core/greet-quota/index.js";
|
|
18
|
+
import {
|
|
19
|
+
FAVORITE_BUTTON_SELECTORS,
|
|
20
|
+
GREET_BUTTON_RECOMMEND_SELECTORS
|
|
21
|
+
} from "./constants.js";
|
|
22
|
+
import { waitForRecommendDetail } from "./detail.js";
|
|
23
|
+
import { getRecommendRoots } from "./roots.js";
|
|
24
|
+
|
|
25
|
+
const POST_ACTIONS = new Set(["none", "favorite", "greet"]);
|
|
26
|
+
const GREET_EXACT_LABEL_PATTERN = /^(?:打招呼|聊一聊|立即沟通(?:[\((]\d+\s*[//]\s*\d+[\))])?|沟通)$/i;
|
|
27
|
+
export const RECOMMEND_DETAIL_ACTION_TEXT_SELECTORS = Object.freeze([
|
|
28
|
+
"button",
|
|
29
|
+
".btn",
|
|
30
|
+
'[role="button"]',
|
|
31
|
+
"a",
|
|
32
|
+
"span",
|
|
33
|
+
"div"
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function uniqueSelectors(...selectorGroups) {
|
|
37
|
+
return [...new Set(selectorGroups.flat().filter(Boolean))];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function uniqueByNode(candidates = []) {
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const result = [];
|
|
43
|
+
for (const item of candidates) {
|
|
44
|
+
const key = `${item.kind}:${item.root}:${item.node_id}`;
|
|
45
|
+
if (seen.has(key)) continue;
|
|
46
|
+
seen.add(key);
|
|
47
|
+
result.push(item);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function lowerText(...parts) {
|
|
53
|
+
return normalizeText(parts.filter(Boolean).join(" ")).toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasActiveClass(text) {
|
|
57
|
+
return /(?:^|\s)(?:active|curr|current|selected|checked)(?:\s|$)/i.test(text);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hasDisabledSignal(text) {
|
|
61
|
+
return /(?:^|\s)(?:disabled|disable|forbidden|is-disabled)(?:\s|$)/i.test(text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function rectArea(control) {
|
|
65
|
+
const rect = control?.rect || {};
|
|
66
|
+
return Math.max(0, Number(rect.width) || 0) * Math.max(0, Number(rect.height) || 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isCompactLabel(control, limit = 80) {
|
|
70
|
+
const label = normalizeText(control?.label || "");
|
|
71
|
+
return label.length > 0 && label.length <= limit;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function controlRank(control, exactLabelPattern) {
|
|
75
|
+
const label = normalizeText(control?.label || "");
|
|
76
|
+
const selector = String(control?.selector || "");
|
|
77
|
+
const className = String(control?.class_name || "");
|
|
78
|
+
let score = 0;
|
|
79
|
+
if (exactLabelPattern.test(label)) score -= 1000;
|
|
80
|
+
if (/button|\[role=|\.btn/.test(selector) || /btn|button/i.test(className)) score -= 250;
|
|
81
|
+
if (selector === "div") score += 300;
|
|
82
|
+
if (!isCompactLabel(control)) score += 500;
|
|
83
|
+
score += Math.min(rectArea(control), 100000) / 1000;
|
|
84
|
+
score += label.length / 10;
|
|
85
|
+
return score;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function bestControl(controls, exactLabelPattern) {
|
|
89
|
+
return [...controls].sort((left, right) => (
|
|
90
|
+
controlRank(left, exactLabelPattern) - controlRank(right, exactLabelPattern)
|
|
91
|
+
))[0] || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function normalizeRecommendPostAction(value) {
|
|
95
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
96
|
+
if (["", "none", "skip", "no", "不执行", "无"].includes(normalized)) return "none";
|
|
97
|
+
if (["favorite", "fav", "collect", "收藏", "感兴趣"].includes(normalized)) return "favorite";
|
|
98
|
+
if (["greet", "chat", "打招呼", "直接沟通", "沟通"].includes(normalized)) return "greet";
|
|
99
|
+
return POST_ACTIONS.has(normalized) ? normalized : "";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function resolveRecommendPostAction({
|
|
103
|
+
postAction = "none",
|
|
104
|
+
greetCount = 0,
|
|
105
|
+
maxGreetCount = null
|
|
106
|
+
} = {}) {
|
|
107
|
+
const requested = normalizeRecommendPostAction(postAction) || "none";
|
|
108
|
+
const currentGreetCount = Number.isInteger(greetCount) && greetCount >= 0 ? greetCount : 0;
|
|
109
|
+
const limit = Number.isInteger(maxGreetCount) && maxGreetCount > 0 ? maxGreetCount : null;
|
|
110
|
+
if (requested === "greet" && limit !== null && currentGreetCount >= limit) {
|
|
111
|
+
return {
|
|
112
|
+
requested,
|
|
113
|
+
effective: "favorite",
|
|
114
|
+
reason: "greet_limit_reached",
|
|
115
|
+
greet_count: currentGreetCount,
|
|
116
|
+
max_greet_count: limit
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
requested,
|
|
121
|
+
effective: requested,
|
|
122
|
+
reason: "requested_action",
|
|
123
|
+
greet_count: currentGreetCount,
|
|
124
|
+
max_greet_count: limit
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function classifyFavoriteControl({
|
|
129
|
+
outerHTML = "",
|
|
130
|
+
attributes = {}
|
|
131
|
+
} = {}) {
|
|
132
|
+
const label = htmlToText(outerHTML);
|
|
133
|
+
const labelText = normalizeText(label);
|
|
134
|
+
const className = normalizeText(attributes.class || "");
|
|
135
|
+
const title = normalizeText(attributes.title || attributes["aria-label"] || "");
|
|
136
|
+
const combined = lowerText(className, title);
|
|
137
|
+
const labelMatches = /^(?:收藏|已收藏|感兴趣|已感兴趣)$/.test(labelText);
|
|
138
|
+
const classMatches = /favorite|collect|interest|like/.test(combined);
|
|
139
|
+
const matches = labelMatches || classMatches;
|
|
140
|
+
const active = (
|
|
141
|
+
/已收藏|已感兴趣/.test(label)
|
|
142
|
+
|| /like-icon-active|favorite-active|collect-active/i.test(outerHTML)
|
|
143
|
+
|| hasActiveClass(className)
|
|
144
|
+
);
|
|
145
|
+
const disabled = (
|
|
146
|
+
Object.prototype.hasOwnProperty.call(attributes, "disabled")
|
|
147
|
+
|| hasDisabledSignal(className)
|
|
148
|
+
|| /disabled/i.test(outerHTML)
|
|
149
|
+
);
|
|
150
|
+
return {
|
|
151
|
+
kind: "favorite",
|
|
152
|
+
matches,
|
|
153
|
+
active,
|
|
154
|
+
disabled,
|
|
155
|
+
label: label || title || null,
|
|
156
|
+
class_name: className || null
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function classifyGreetControl({
|
|
161
|
+
outerHTML = "",
|
|
162
|
+
attributes = {}
|
|
163
|
+
} = {}) {
|
|
164
|
+
const label = htmlToText(outerHTML);
|
|
165
|
+
const labelText = normalizeText(label);
|
|
166
|
+
const className = normalizeText(attributes.class || "");
|
|
167
|
+
const title = normalizeText(attributes.title || attributes["aria-label"] || "");
|
|
168
|
+
const combined = lowerText(className, title);
|
|
169
|
+
const continueChat = labelText.length <= 40 && /继续沟通/.test(labelText);
|
|
170
|
+
const greetQuota = parseGreetQuota(labelText || title);
|
|
171
|
+
const greetEntry = (
|
|
172
|
+
GREET_EXACT_LABEL_PATTERN.test(labelText)
|
|
173
|
+
|| greetQuota.found
|
|
174
|
+
|| /greet/i.test(combined)
|
|
175
|
+
);
|
|
176
|
+
const disabled = (
|
|
177
|
+
Object.prototype.hasOwnProperty.call(attributes, "disabled")
|
|
178
|
+
|| hasDisabledSignal(className)
|
|
179
|
+
|| /disabled/i.test(outerHTML)
|
|
180
|
+
);
|
|
181
|
+
return {
|
|
182
|
+
kind: "greet",
|
|
183
|
+
matches: greetEntry || continueChat,
|
|
184
|
+
available: greetEntry && !continueChat && !disabled,
|
|
185
|
+
continue_chat: continueChat,
|
|
186
|
+
disabled,
|
|
187
|
+
label: label || title || null,
|
|
188
|
+
greet_quota: greetQuota.found ? greetQuota : null,
|
|
189
|
+
class_name: className || null
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function readActionNode(client, {
|
|
194
|
+
root,
|
|
195
|
+
selector,
|
|
196
|
+
nodeId,
|
|
197
|
+
kind
|
|
198
|
+
}) {
|
|
199
|
+
const [attributes, outerHTML] = await Promise.all([
|
|
200
|
+
getAttributesMap(client, nodeId),
|
|
201
|
+
getOuterHTML(client, nodeId)
|
|
202
|
+
]);
|
|
203
|
+
let box = null;
|
|
204
|
+
let visible = false;
|
|
205
|
+
try {
|
|
206
|
+
box = await getNodeBox(client, nodeId);
|
|
207
|
+
visible = box.rect.width > 2 && box.rect.height > 2;
|
|
208
|
+
} catch {}
|
|
209
|
+
const classification = kind === "favorite"
|
|
210
|
+
? classifyFavoriteControl({ outerHTML, attributes })
|
|
211
|
+
: classifyGreetControl({ outerHTML, attributes });
|
|
212
|
+
return {
|
|
213
|
+
kind,
|
|
214
|
+
root: root.name,
|
|
215
|
+
root_node_id: root.nodeId,
|
|
216
|
+
selector,
|
|
217
|
+
node_id: nodeId,
|
|
218
|
+
visible,
|
|
219
|
+
center: box?.center || null,
|
|
220
|
+
rect: box?.rect || null,
|
|
221
|
+
attributes,
|
|
222
|
+
outer_html_length: outerHTML.length,
|
|
223
|
+
html_preview: outerHTML.slice(0, 500),
|
|
224
|
+
...classification
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function collectRecommendActionControls(client, roots, {
|
|
229
|
+
favoriteSelectors = FAVORITE_BUTTON_SELECTORS,
|
|
230
|
+
greetSelectors = GREET_BUTTON_RECOMMEND_SELECTORS,
|
|
231
|
+
detailTextFallback = false
|
|
232
|
+
} = {}) {
|
|
233
|
+
const candidates = [];
|
|
234
|
+
const favoriteScanSelectors = detailTextFallback
|
|
235
|
+
? uniqueSelectors(favoriteSelectors, RECOMMEND_DETAIL_ACTION_TEXT_SELECTORS)
|
|
236
|
+
: favoriteSelectors;
|
|
237
|
+
const greetScanSelectors = detailTextFallback
|
|
238
|
+
? uniqueSelectors(greetSelectors, RECOMMEND_DETAIL_ACTION_TEXT_SELECTORS)
|
|
239
|
+
: greetSelectors;
|
|
240
|
+
for (const root of roots) {
|
|
241
|
+
if (!root?.nodeId) continue;
|
|
242
|
+
for (const [kind, selectors] of [
|
|
243
|
+
["favorite", favoriteScanSelectors],
|
|
244
|
+
["greet", greetScanSelectors]
|
|
245
|
+
]) {
|
|
246
|
+
for (const selector of selectors) {
|
|
247
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
248
|
+
for (const nodeId of nodeIds) {
|
|
249
|
+
candidates.push(await readActionNode(client, {
|
|
250
|
+
root,
|
|
251
|
+
selector,
|
|
252
|
+
nodeId,
|
|
253
|
+
kind
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return uniqueByNode(candidates);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function summarizeRecommendActionControls(controls = []) {
|
|
263
|
+
const visibleControls = controls.filter((item) => item.visible && item.matches);
|
|
264
|
+
const favoriteControls = visibleControls.filter((item) => item.kind === "favorite");
|
|
265
|
+
const greetControls = visibleControls.filter((item) => item.kind === "greet");
|
|
266
|
+
const favorite = bestControl(
|
|
267
|
+
favoriteControls.filter((item) => item.matches),
|
|
268
|
+
/^(?:收藏|已收藏|感兴趣|已感兴趣)$/i
|
|
269
|
+
);
|
|
270
|
+
const greet = bestControl(
|
|
271
|
+
greetControls.filter((item) => item.available),
|
|
272
|
+
GREET_EXACT_LABEL_PATTERN
|
|
273
|
+
) || bestControl(
|
|
274
|
+
greetControls.filter((item) => item.continue_chat),
|
|
275
|
+
/^继续沟通$/i
|
|
276
|
+
);
|
|
277
|
+
return {
|
|
278
|
+
favorite: favorite
|
|
279
|
+
? {
|
|
280
|
+
found: true,
|
|
281
|
+
active: favorite.active,
|
|
282
|
+
disabled: favorite.disabled,
|
|
283
|
+
label: favorite.label,
|
|
284
|
+
selector: favorite.selector,
|
|
285
|
+
root: favorite.root,
|
|
286
|
+
node_id: favorite.node_id,
|
|
287
|
+
center: favorite.center
|
|
288
|
+
}
|
|
289
|
+
: { found: false },
|
|
290
|
+
greet: greet
|
|
291
|
+
? {
|
|
292
|
+
found: true,
|
|
293
|
+
available: greet.available,
|
|
294
|
+
continue_chat: greet.continue_chat,
|
|
295
|
+
disabled: greet.disabled,
|
|
296
|
+
label: greet.label,
|
|
297
|
+
greet_quota: greet.greet_quota || null,
|
|
298
|
+
selector: greet.selector,
|
|
299
|
+
root: greet.root,
|
|
300
|
+
node_id: greet.node_id,
|
|
301
|
+
center: greet.center
|
|
302
|
+
}
|
|
303
|
+
: { found: false },
|
|
304
|
+
counts: {
|
|
305
|
+
total: controls.length,
|
|
306
|
+
visible_matching: visibleControls.length,
|
|
307
|
+
favorite: favoriteControls.length,
|
|
308
|
+
greet: greetControls.length
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export async function discoverRecommendActionControls(client, {
|
|
314
|
+
roots = null,
|
|
315
|
+
selectors = {},
|
|
316
|
+
detailTextFallback = false
|
|
317
|
+
} = {}) {
|
|
318
|
+
const rootState = roots ? { roots } : await getRecommendRoots(client);
|
|
319
|
+
const controls = await collectRecommendActionControls(client, rootState.roots, {
|
|
320
|
+
...selectors,
|
|
321
|
+
detailTextFallback
|
|
322
|
+
});
|
|
323
|
+
return {
|
|
324
|
+
controls,
|
|
325
|
+
summary: summarizeRecommendActionControls(controls)
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function waitForRecommendActionControls(client, {
|
|
330
|
+
timeoutMs = 6000,
|
|
331
|
+
intervalMs = 250,
|
|
332
|
+
requireAny = true,
|
|
333
|
+
...discoveryOptions
|
|
334
|
+
} = {}) {
|
|
335
|
+
const started = Date.now();
|
|
336
|
+
let lastDiscovery = null;
|
|
337
|
+
while (Date.now() - started <= timeoutMs) {
|
|
338
|
+
lastDiscovery = await discoverRecommendActionControls(client, discoveryOptions);
|
|
339
|
+
const hasControl = Boolean(
|
|
340
|
+
lastDiscovery.summary.favorite.found
|
|
341
|
+
|| lastDiscovery.summary.greet.found
|
|
342
|
+
);
|
|
343
|
+
if (!requireAny || hasControl) {
|
|
344
|
+
return {
|
|
345
|
+
...lastDiscovery,
|
|
346
|
+
elapsed_ms: Date.now() - started,
|
|
347
|
+
timed_out: false
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
await sleep(intervalMs);
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
...(lastDiscovery || { controls: [], summary: summarizeRecommendActionControls([]) }),
|
|
354
|
+
elapsed_ms: Date.now() - started,
|
|
355
|
+
timed_out: true
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function getRecommendDetailActionRoots(client, detailState) {
|
|
360
|
+
const roots = [];
|
|
361
|
+
if (detailState?.popup?.node_id) {
|
|
362
|
+
roots.push({
|
|
363
|
+
name: `${detailState.popup.root || "unknown"}:detail-popup`,
|
|
364
|
+
nodeId: detailState.popup.node_id
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (detailState?.resumeIframe?.node_id) {
|
|
368
|
+
try {
|
|
369
|
+
roots.push({
|
|
370
|
+
name: `${detailState.resumeIframe.root || "unknown"}:resume-iframe-document`,
|
|
371
|
+
nodeId: await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id)
|
|
372
|
+
});
|
|
373
|
+
} catch {
|
|
374
|
+
roots.push({
|
|
375
|
+
name: `${detailState.resumeIframe.root || "unknown"}:resume-iframe-node`,
|
|
376
|
+
nodeId: detailState.resumeIframe.node_id
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return roots;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export async function waitForRecommendDetailActionControls(client, {
|
|
384
|
+
timeoutMs = 8000,
|
|
385
|
+
intervalMs = 350,
|
|
386
|
+
selectors = {},
|
|
387
|
+
requireAny = true
|
|
388
|
+
} = {}) {
|
|
389
|
+
const started = Date.now();
|
|
390
|
+
let lastDiscovery = null;
|
|
391
|
+
let lastError = null;
|
|
392
|
+
let lastRootCount = 0;
|
|
393
|
+
while (Date.now() - started <= timeoutMs) {
|
|
394
|
+
const detailState = await waitForRecommendDetail(client, {
|
|
395
|
+
timeoutMs: Math.min(intervalMs, 500),
|
|
396
|
+
intervalMs: 100
|
|
397
|
+
});
|
|
398
|
+
const roots = await getRecommendDetailActionRoots(client, detailState);
|
|
399
|
+
lastRootCount = roots.length;
|
|
400
|
+
if (roots.length) {
|
|
401
|
+
try {
|
|
402
|
+
lastDiscovery = await discoverRecommendActionControls(client, {
|
|
403
|
+
roots,
|
|
404
|
+
selectors,
|
|
405
|
+
detailTextFallback: true
|
|
406
|
+
});
|
|
407
|
+
const hasControl = Boolean(
|
|
408
|
+
lastDiscovery.summary.favorite.found
|
|
409
|
+
|| lastDiscovery.summary.greet.found
|
|
410
|
+
);
|
|
411
|
+
if (!requireAny || hasControl) {
|
|
412
|
+
return {
|
|
413
|
+
...lastDiscovery,
|
|
414
|
+
elapsed_ms: Date.now() - started,
|
|
415
|
+
timed_out: false,
|
|
416
|
+
detail_root_count: roots.length
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
lastError = error?.message || String(error);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
await sleep(intervalMs);
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
...(lastDiscovery || { controls: [], summary: summarizeRecommendActionControls([]) }),
|
|
427
|
+
elapsed_ms: Date.now() - started,
|
|
428
|
+
timed_out: true,
|
|
429
|
+
detail_root_count: lastRootCount,
|
|
430
|
+
last_error: lastError
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export async function clickRecommendActionControl(client, control, {
|
|
435
|
+
allowDisabled = false
|
|
436
|
+
} = {}) {
|
|
437
|
+
if (!control?.center) {
|
|
438
|
+
throw new Error("Action control has no clickable center");
|
|
439
|
+
}
|
|
440
|
+
const greetQuota = control.kind === "greet"
|
|
441
|
+
? assertGreetQuotaAvailable(control.greet_quota || control.label || "")
|
|
442
|
+
: null;
|
|
443
|
+
if (control.disabled && !allowDisabled) {
|
|
444
|
+
throw new Error(`Action control is disabled: ${control.kind}`);
|
|
445
|
+
}
|
|
446
|
+
await clickPoint(client, control.center.x, control.center.y);
|
|
447
|
+
return {
|
|
448
|
+
clicked: true,
|
|
449
|
+
kind: control.kind,
|
|
450
|
+
label: control.label,
|
|
451
|
+
greet_quota: greetQuota?.found ? greetQuota : null,
|
|
452
|
+
selector: control.selector,
|
|
453
|
+
root: control.root,
|
|
454
|
+
node_id: control.node_id,
|
|
455
|
+
center: control.center
|
|
456
|
+
};
|
|
457
|
+
}
|