@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23
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 +2 -1
- 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 +2416 -2385
- 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 +1315 -1306
- package/src/core/reporting/legacy-csv.js +334 -332
- package/src/core/run/index.js +44 -27
- 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 +655 -646
- 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 +575 -351
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +1616 -878
- 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 +121 -4
- package/src/parser.js +376 -8
- package/src/recommend-mcp.js +1174 -914
- package/src/recommend-scheduler.js +535 -469
- package/src/recruit-mcp.js +2121 -2121
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
import {
|
|
2
|
-
clickNodeCenter,
|
|
3
|
-
clickPoint,
|
|
4
|
-
DETERMINISTIC_CLICK_OPTIONS,
|
|
5
|
-
getFrameDocumentNodeId,
|
|
6
|
-
getNodeBox,
|
|
7
|
-
getOuterHTML,
|
|
8
|
-
pressKey,
|
|
9
|
-
querySelectorAll,
|
|
10
|
-
scrollNodeIntoView,
|
|
11
|
-
sleep
|
|
12
|
-
} from "../../core/browser/index.js";
|
|
13
|
-
import { candidateKeyFromProfile } from "../../core/infinite-list/index.js";
|
|
14
|
-
import {
|
|
15
|
-
buildScreeningCandidateFromDetail,
|
|
16
|
-
htmlToText
|
|
17
|
-
} from "../../core/screening/index.js";
|
|
18
|
-
import {
|
|
19
|
-
closeBossAccountRightsBlockingPanel,
|
|
20
|
-
findBossAccountRightsBlockingPanel
|
|
21
|
-
} from "../common/account-rights-panel.js";
|
|
22
|
-
import {
|
|
23
|
-
DETAIL_CLOSE_SELECTORS,
|
|
24
|
-
DETAIL_NETWORK_PATTERNS,
|
|
25
|
-
DETAIL_POPUP_SELECTORS,
|
|
26
|
-
DETAIL_RESUME_IFRAME_SELECTORS,
|
|
27
|
-
RECOMMEND_AVATAR_PREVIEW_CLOSE_SELECTORS,
|
|
28
|
-
RECOMMEND_AVATAR_PREVIEW_SELECTORS
|
|
29
|
-
} from "./constants.js";
|
|
1
|
+
import {
|
|
2
|
+
clickNodeCenter,
|
|
3
|
+
clickPoint,
|
|
4
|
+
DETERMINISTIC_CLICK_OPTIONS,
|
|
5
|
+
getFrameDocumentNodeId,
|
|
6
|
+
getNodeBox,
|
|
7
|
+
getOuterHTML,
|
|
8
|
+
pressKey,
|
|
9
|
+
querySelectorAll,
|
|
10
|
+
scrollNodeIntoView,
|
|
11
|
+
sleep
|
|
12
|
+
} from "../../core/browser/index.js";
|
|
13
|
+
import { candidateKeyFromProfile } from "../../core/infinite-list/index.js";
|
|
14
|
+
import {
|
|
15
|
+
buildScreeningCandidateFromDetail,
|
|
16
|
+
htmlToText
|
|
17
|
+
} from "../../core/screening/index.js";
|
|
18
|
+
import {
|
|
19
|
+
closeBossAccountRightsBlockingPanel,
|
|
20
|
+
findBossAccountRightsBlockingPanel
|
|
21
|
+
} from "../common/account-rights-panel.js";
|
|
22
|
+
import {
|
|
23
|
+
DETAIL_CLOSE_SELECTORS,
|
|
24
|
+
DETAIL_NETWORK_PATTERNS,
|
|
25
|
+
DETAIL_POPUP_SELECTORS,
|
|
26
|
+
DETAIL_RESUME_IFRAME_SELECTORS,
|
|
27
|
+
RECOMMEND_AVATAR_PREVIEW_CLOSE_SELECTORS,
|
|
28
|
+
RECOMMEND_AVATAR_PREVIEW_SELECTORS
|
|
29
|
+
} from "./constants.js";
|
|
30
30
|
import {
|
|
31
31
|
getRecommendRoots
|
|
32
32
|
} from "./roots.js";
|
|
33
|
-
import {
|
|
34
|
-
findRecommendCardNodeIds,
|
|
35
|
-
readRecommendCardCandidate
|
|
36
|
-
} from "./cards.js";
|
|
37
|
-
|
|
38
|
-
const DETAIL_OUTSIDE_CLOSE_BOUNDARY_SELECTORS = Object.freeze([
|
|
39
|
-
".resume-center-side .resume-detail-wrap",
|
|
40
|
-
".resume-detail-wrap",
|
|
41
|
-
".boss-popup__wrapper .boss-popup__body",
|
|
42
|
-
".boss-popup__wrapper .dialog-body",
|
|
43
|
-
".dialog-wrap.active .resume-detail-wrap",
|
|
44
|
-
".geek-detail-modal .resume-detail-wrap"
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
export function matchesRecommendDetailNetwork(url) {
|
|
33
|
+
import {
|
|
34
|
+
findRecommendCardNodeIds,
|
|
35
|
+
readRecommendCardCandidate
|
|
36
|
+
} from "./cards.js";
|
|
37
|
+
|
|
38
|
+
const DETAIL_OUTSIDE_CLOSE_BOUNDARY_SELECTORS = Object.freeze([
|
|
39
|
+
".resume-center-side .resume-detail-wrap",
|
|
40
|
+
".resume-detail-wrap",
|
|
41
|
+
".boss-popup__wrapper .boss-popup__body",
|
|
42
|
+
".boss-popup__wrapper .dialog-body",
|
|
43
|
+
".dialog-wrap.active .resume-detail-wrap",
|
|
44
|
+
".geek-detail-modal .resume-detail-wrap"
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export function matchesRecommendDetailNetwork(url) {
|
|
48
48
|
return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export function createRecommendDetailNetworkRecorder(client) {
|
|
51
|
+
export function createRecommendDetailNetworkRecorder(client) {
|
|
52
52
|
const events = [];
|
|
53
53
|
client.Network.responseReceived((event) => {
|
|
54
54
|
const url = event?.response?.url || "";
|
|
@@ -83,29 +83,29 @@ export function createRecommendDetailNetworkRecorder(client) {
|
|
|
83
83
|
events.length = 0;
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export async function findRecommendBlockingPanel(client, options = {}) {
|
|
89
|
-
return findBossAccountRightsBlockingPanel(client, options);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export async function closeRecommendBlockingPanels(client, options = {}) {
|
|
93
|
-
return closeBossAccountRightsBlockingPanel(client, {
|
|
94
|
-
resolveRoots: getRecommendRoots,
|
|
95
|
-
...options
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function looksLikeRecommendAvatarPreviewHtml(html = "") {
|
|
100
|
-
return /\bavatar-preview\b|\bfigure-preview\b/i.test(String(html || ""));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function isRecommendAvatarPreviewOpenError(error) {
|
|
104
|
-
return error?.code === "RECOMMEND_AVATAR_PREVIEW_OPENED"
|
|
105
|
-
|| /RECOMMEND_AVATAR_PREVIEW_OPENED/i.test(String(error?.message || error || ""));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export async function waitForRecommendDetailNetworkEvents(recorder, {
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function findRecommendBlockingPanel(client, options = {}) {
|
|
89
|
+
return findBossAccountRightsBlockingPanel(client, options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function closeRecommendBlockingPanels(client, options = {}) {
|
|
93
|
+
return closeBossAccountRightsBlockingPanel(client, {
|
|
94
|
+
resolveRoots: getRecommendRoots,
|
|
95
|
+
...options
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function looksLikeRecommendAvatarPreviewHtml(html = "") {
|
|
100
|
+
return /\bavatar-preview\b|\bfigure-preview\b/i.test(String(html || ""));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isRecommendAvatarPreviewOpenError(error) {
|
|
104
|
+
return error?.code === "RECOMMEND_AVATAR_PREVIEW_OPENED"
|
|
105
|
+
|| /RECOMMEND_AVATAR_PREVIEW_OPENED/i.test(String(error?.message || error || ""));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function waitForRecommendDetailNetworkEvents(recorder, {
|
|
109
109
|
minCount = 1,
|
|
110
110
|
requireLoaded = true,
|
|
111
111
|
timeoutMs = 3500,
|
|
@@ -175,10 +175,10 @@ export async function waitForRecommendDetail(client, {
|
|
|
175
175
|
return lastState;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
async function readRecommendDetailState(client) {
|
|
179
|
-
const rootState = await getRecommendRoots(client);
|
|
180
|
-
const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
|
|
181
|
-
const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
|
|
178
|
+
async function readRecommendDetailState(client) {
|
|
179
|
+
const rootState = await getRecommendRoots(client);
|
|
180
|
+
const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
|
|
181
|
+
const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
|
|
182
182
|
return {
|
|
183
183
|
iframe: rootState.iframe,
|
|
184
184
|
roots: rootState.roots,
|
|
@@ -187,10 +187,10 @@ async function readRecommendDetailState(client) {
|
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
export async function waitForRecommendDetailClosed(client, {
|
|
191
|
-
timeoutMs = 4000,
|
|
192
|
-
intervalMs = 250
|
|
193
|
-
} = {}) {
|
|
190
|
+
export async function waitForRecommendDetailClosed(client, {
|
|
191
|
+
timeoutMs = 4000,
|
|
192
|
+
intervalMs = 250
|
|
193
|
+
} = {}) {
|
|
194
194
|
const started = Date.now();
|
|
195
195
|
let lastState = null;
|
|
196
196
|
while (Date.now() - started <= timeoutMs) {
|
|
@@ -208,126 +208,126 @@ export async function waitForRecommendDetailClosed(client, {
|
|
|
208
208
|
closed: false,
|
|
209
209
|
elapsed_ms: Date.now() - started,
|
|
210
210
|
state: lastState
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export async function readRecommendAvatarPreviewState(client) {
|
|
215
|
-
const rootState = await getRecommendRoots(client);
|
|
216
|
-
const topRoot = rootState.rootNodes?.top
|
|
217
|
-
? [{ name: "top", nodeId: rootState.rootNodes.top }]
|
|
218
|
-
: rootState.roots.filter((root) => root?.name === "top");
|
|
219
|
-
const preview = await findVisibleDetailTarget(client, topRoot, RECOMMEND_AVATAR_PREVIEW_SELECTORS, {
|
|
220
|
-
includeAvatarPreview: true
|
|
221
|
-
});
|
|
222
|
-
return {
|
|
223
|
-
open: Boolean(preview),
|
|
224
|
-
root: rootState.topRoot,
|
|
225
|
-
roots: topRoot,
|
|
226
|
-
preview
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export async function waitForRecommendAvatarPreviewClosed(client, {
|
|
231
|
-
timeoutMs = 1200,
|
|
232
|
-
intervalMs = 120
|
|
233
|
-
} = {}) {
|
|
234
|
-
const started = Date.now();
|
|
235
|
-
let state = null;
|
|
236
|
-
while (Date.now() - started <= timeoutMs) {
|
|
237
|
-
state = await readRecommendAvatarPreviewState(client);
|
|
238
|
-
if (!state.open) {
|
|
239
|
-
return {
|
|
240
|
-
closed: true,
|
|
241
|
-
elapsed_ms: Date.now() - started,
|
|
242
|
-
state
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
await sleep(intervalMs);
|
|
246
|
-
}
|
|
247
|
-
return {
|
|
248
|
-
closed: false,
|
|
249
|
-
elapsed_ms: Date.now() - started,
|
|
250
|
-
state
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function compactRect(rect) {
|
|
255
|
-
if (!rect) return null;
|
|
256
|
-
return {
|
|
257
|
-
x: Math.round(Number(rect.x) || 0),
|
|
258
|
-
y: Math.round(Number(rect.y) || 0),
|
|
259
|
-
width: Math.round(Number(rect.width) || 0),
|
|
260
|
-
height: Math.round(Number(rect.height) || 0)
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function compactDetailTarget(target) {
|
|
265
|
-
if (!target) return null;
|
|
266
|
-
return {
|
|
267
|
-
root: target.root || "",
|
|
268
|
-
root_node_id: target.root_node_id || null,
|
|
269
|
-
selector: target.selector || "",
|
|
270
|
-
node_id: target.node_id || null,
|
|
271
|
-
rect: compactRect(target.rect)
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function compactDetailOpenState(state) {
|
|
276
|
-
if (!state) {
|
|
277
|
-
return {
|
|
278
|
-
open: false,
|
|
279
|
-
popup: null,
|
|
280
|
-
resume_iframe: null,
|
|
281
|
-
iframe_document_node_id: null
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
return {
|
|
285
|
-
open: Boolean(state.popup || state.resumeIframe),
|
|
286
|
-
popup: compactDetailTarget(state.popup),
|
|
287
|
-
resume_iframe: compactDetailTarget(state.resumeIframe),
|
|
288
|
-
iframe_document_node_id: state.iframe?.documentNodeId || null
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
async function verifyRecommendDetailStillOpen(client, {
|
|
293
|
-
settleMs = 350
|
|
294
|
-
} = {}) {
|
|
295
|
-
const firstState = await readRecommendDetailState(client);
|
|
296
|
-
if (settleMs > 0) await sleep(settleMs);
|
|
297
|
-
const secondState = await readRecommendDetailState(client);
|
|
298
|
-
const first = compactDetailOpenState(firstState);
|
|
299
|
-
const second = compactDetailOpenState(secondState);
|
|
300
|
-
const stableOpen = Boolean(first.open && second.open);
|
|
301
|
-
return {
|
|
302
|
-
open: Boolean(second.open),
|
|
303
|
-
stable_open: stableOpen,
|
|
304
|
-
first,
|
|
305
|
-
second
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async function findVisibleDetailTarget(client, roots, selectors, {
|
|
310
|
-
includeAvatarPreview = false
|
|
311
|
-
} = {}) {
|
|
312
|
-
for (const root of roots) {
|
|
313
|
-
if (!root?.nodeId) continue;
|
|
314
|
-
for (const selector of selectors) {
|
|
315
|
-
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
316
|
-
for (const nodeId of nodeIds) {
|
|
317
|
-
try {
|
|
318
|
-
const box = await getNodeBox(client, nodeId);
|
|
319
|
-
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
320
|
-
if (!includeAvatarPreview) {
|
|
321
|
-
try {
|
|
322
|
-
const html = await getOuterHTML(client, nodeId);
|
|
323
|
-
if (looksLikeRecommendAvatarPreviewHtml(html)) continue;
|
|
324
|
-
} catch {
|
|
325
|
-
// If the node went stale, let the next candidate decide.
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return {
|
|
329
|
-
root: root.name,
|
|
330
|
-
root_node_id: root.nodeId,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function readRecommendAvatarPreviewState(client) {
|
|
215
|
+
const rootState = await getRecommendRoots(client);
|
|
216
|
+
const topRoot = rootState.rootNodes?.top
|
|
217
|
+
? [{ name: "top", nodeId: rootState.rootNodes.top }]
|
|
218
|
+
: rootState.roots.filter((root) => root?.name === "top");
|
|
219
|
+
const preview = await findVisibleDetailTarget(client, topRoot, RECOMMEND_AVATAR_PREVIEW_SELECTORS, {
|
|
220
|
+
includeAvatarPreview: true
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
open: Boolean(preview),
|
|
224
|
+
root: rootState.topRoot,
|
|
225
|
+
roots: topRoot,
|
|
226
|
+
preview
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function waitForRecommendAvatarPreviewClosed(client, {
|
|
231
|
+
timeoutMs = 1200,
|
|
232
|
+
intervalMs = 120
|
|
233
|
+
} = {}) {
|
|
234
|
+
const started = Date.now();
|
|
235
|
+
let state = null;
|
|
236
|
+
while (Date.now() - started <= timeoutMs) {
|
|
237
|
+
state = await readRecommendAvatarPreviewState(client);
|
|
238
|
+
if (!state.open) {
|
|
239
|
+
return {
|
|
240
|
+
closed: true,
|
|
241
|
+
elapsed_ms: Date.now() - started,
|
|
242
|
+
state
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
await sleep(intervalMs);
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
closed: false,
|
|
249
|
+
elapsed_ms: Date.now() - started,
|
|
250
|
+
state
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function compactRect(rect) {
|
|
255
|
+
if (!rect) return null;
|
|
256
|
+
return {
|
|
257
|
+
x: Math.round(Number(rect.x) || 0),
|
|
258
|
+
y: Math.round(Number(rect.y) || 0),
|
|
259
|
+
width: Math.round(Number(rect.width) || 0),
|
|
260
|
+
height: Math.round(Number(rect.height) || 0)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function compactDetailTarget(target) {
|
|
265
|
+
if (!target) return null;
|
|
266
|
+
return {
|
|
267
|
+
root: target.root || "",
|
|
268
|
+
root_node_id: target.root_node_id || null,
|
|
269
|
+
selector: target.selector || "",
|
|
270
|
+
node_id: target.node_id || null,
|
|
271
|
+
rect: compactRect(target.rect)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function compactDetailOpenState(state) {
|
|
276
|
+
if (!state) {
|
|
277
|
+
return {
|
|
278
|
+
open: false,
|
|
279
|
+
popup: null,
|
|
280
|
+
resume_iframe: null,
|
|
281
|
+
iframe_document_node_id: null
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
open: Boolean(state.popup || state.resumeIframe),
|
|
286
|
+
popup: compactDetailTarget(state.popup),
|
|
287
|
+
resume_iframe: compactDetailTarget(state.resumeIframe),
|
|
288
|
+
iframe_document_node_id: state.iframe?.documentNodeId || null
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function verifyRecommendDetailStillOpen(client, {
|
|
293
|
+
settleMs = 350
|
|
294
|
+
} = {}) {
|
|
295
|
+
const firstState = await readRecommendDetailState(client);
|
|
296
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
297
|
+
const secondState = await readRecommendDetailState(client);
|
|
298
|
+
const first = compactDetailOpenState(firstState);
|
|
299
|
+
const second = compactDetailOpenState(secondState);
|
|
300
|
+
const stableOpen = Boolean(first.open && second.open);
|
|
301
|
+
return {
|
|
302
|
+
open: Boolean(second.open),
|
|
303
|
+
stable_open: stableOpen,
|
|
304
|
+
first,
|
|
305
|
+
second
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function findVisibleDetailTarget(client, roots, selectors, {
|
|
310
|
+
includeAvatarPreview = false
|
|
311
|
+
} = {}) {
|
|
312
|
+
for (const root of roots) {
|
|
313
|
+
if (!root?.nodeId) continue;
|
|
314
|
+
for (const selector of selectors) {
|
|
315
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
316
|
+
for (const nodeId of nodeIds) {
|
|
317
|
+
try {
|
|
318
|
+
const box = await getNodeBox(client, nodeId);
|
|
319
|
+
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
320
|
+
if (!includeAvatarPreview) {
|
|
321
|
+
try {
|
|
322
|
+
const html = await getOuterHTML(client, nodeId);
|
|
323
|
+
if (looksLikeRecommendAvatarPreviewHtml(html)) continue;
|
|
324
|
+
} catch {
|
|
325
|
+
// If the node went stale, let the next candidate decide.
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
root: root.name,
|
|
330
|
+
root_node_id: root.nodeId,
|
|
331
331
|
selector,
|
|
332
332
|
node_id: nodeId,
|
|
333
333
|
center: box.center,
|
|
@@ -388,113 +388,122 @@ export async function readRecommendDetailHtml(client, detailState) {
|
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
export function isStaleRecommendNodeError(error) {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
export function resolveRecommendCardDetailClickPoint(cardBox, {
|
|
402
|
-
attemptIndex = 0
|
|
403
|
-
} = {}) {
|
|
404
|
-
const rect = cardBox?.rect || {};
|
|
405
|
-
const width = Number(rect.width) || 0;
|
|
406
|
-
const height = Number(rect.height) || 0;
|
|
407
|
-
if (width <= 2 || height <= 2) {
|
|
408
|
-
return {
|
|
409
|
-
...(cardBox?.center || { x: 0, y: 0 }),
|
|
410
|
-
mode: "card-center-fallback",
|
|
411
|
-
reason: "invalid_card_rect"
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const xFractions = [0.22, 0.50, 0.72];
|
|
416
|
-
const xFraction = xFractions[Math.min(Math.max(0, attemptIndex), xFractions.length - 1)];
|
|
417
|
-
const minOffsetX = Math.min(width - 12, Math.max(110, Math.min(180, width * 0.18)));
|
|
418
|
-
const maxOffsetX = Math.max(minOffsetX, width - Math.min(220, Math.max(90, width * 0.22)));
|
|
419
|
-
const rawOffsetX = width * xFraction;
|
|
420
|
-
const offsetX = clampPointCoordinate(rawOffsetX, minOffsetX, maxOffsetX);
|
|
421
|
-
const offsetY = clampPointCoordinate(height * 0.28, Math.min(34, height / 2), Math.max(36, height - 28));
|
|
422
|
-
return {
|
|
423
|
-
x: rect.x + offsetX,
|
|
424
|
-
y: rect.y + offsetY,
|
|
425
|
-
mode: "card-body-safe-point",
|
|
426
|
-
attempt_index: attemptIndex,
|
|
427
|
-
offset_x: Math.round(offsetX),
|
|
428
|
-
offset_y: Math.round(offsetY)
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
async function clickRecommendCardDetailPoint(client, nodeId, {
|
|
433
|
-
scrollIntoView = true,
|
|
434
|
-
attemptIndex = 0
|
|
435
|
-
} = {}) {
|
|
436
|
-
if (scrollIntoView) {
|
|
437
|
-
try {
|
|
438
|
-
await scrollNodeIntoView(client, nodeId);
|
|
439
|
-
await sleep(150);
|
|
440
|
-
} catch {
|
|
441
|
-
// Recommend list cards are selected from visible nodes; if this CDP
|
|
442
|
-
// helper races the virtual list, let the box lookup/retry decide.
|
|
443
|
-
}
|
|
391
|
+
const pattern = /Could not find node with given id|No node with given id|Node is detached|Cannot find node|Could not compute box model/i;
|
|
392
|
+
const seen = new Set();
|
|
393
|
+
let current = error;
|
|
394
|
+
for (let depth = 0; current && depth < 5; depth += 1) {
|
|
395
|
+
if ((typeof current === "object" || typeof current === "function") && seen.has(current)) break;
|
|
396
|
+
if (typeof current === "object" || typeof current === "function") seen.add(current);
|
|
397
|
+
const message = String(current?.message || current || "");
|
|
398
|
+
if (pattern.test(message)) return true;
|
|
399
|
+
current = current?.cause || null;
|
|
444
400
|
}
|
|
445
|
-
|
|
446
|
-
const clickTarget = resolveRecommendCardDetailClickPoint(box, { attemptIndex });
|
|
447
|
-
const clickResult = await clickPoint(client, clickTarget.x, clickTarget.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
448
|
-
return {
|
|
449
|
-
...box,
|
|
450
|
-
click_target: clickTarget,
|
|
451
|
-
click_result: clickResult
|
|
452
|
-
};
|
|
401
|
+
return false;
|
|
453
402
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
|
|
403
|
+
|
|
404
|
+
export function isRecommendDetailOpenMissError(error) {
|
|
405
|
+
const message = String(error?.message || error || "");
|
|
406
|
+
return isRecommendAvatarPreviewOpenError(error)
|
|
407
|
+
|| /Candidate detail did not open|no known detail selectors mounted/i.test(message);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function resolveRecommendCardDetailClickPoint(cardBox, {
|
|
411
|
+
attemptIndex = 0
|
|
412
|
+
} = {}) {
|
|
413
|
+
const rect = cardBox?.rect || {};
|
|
414
|
+
const width = Number(rect.width) || 0;
|
|
415
|
+
const height = Number(rect.height) || 0;
|
|
416
|
+
if (width <= 2 || height <= 2) {
|
|
417
|
+
return {
|
|
418
|
+
...(cardBox?.center || { x: 0, y: 0 }),
|
|
419
|
+
mode: "card-center-fallback",
|
|
420
|
+
reason: "invalid_card_rect"
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const xFractions = [0.22, 0.50, 0.72];
|
|
425
|
+
const xFraction = xFractions[Math.min(Math.max(0, attemptIndex), xFractions.length - 1)];
|
|
426
|
+
const minOffsetX = Math.min(width - 12, Math.max(110, Math.min(180, width * 0.18)));
|
|
427
|
+
const maxOffsetX = Math.max(minOffsetX, width - Math.min(220, Math.max(90, width * 0.22)));
|
|
428
|
+
const rawOffsetX = width * xFraction;
|
|
429
|
+
const offsetX = clampPointCoordinate(rawOffsetX, minOffsetX, maxOffsetX);
|
|
430
|
+
const offsetY = clampPointCoordinate(height * 0.28, Math.min(34, height / 2), Math.max(36, height - 28));
|
|
431
|
+
return {
|
|
432
|
+
x: rect.x + offsetX,
|
|
433
|
+
y: rect.y + offsetY,
|
|
434
|
+
mode: "card-body-safe-point",
|
|
435
|
+
attempt_index: attemptIndex,
|
|
436
|
+
offset_x: Math.round(offsetX),
|
|
437
|
+
offset_y: Math.round(offsetY)
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function clickRecommendCardDetailPoint(client, nodeId, {
|
|
442
|
+
scrollIntoView = true,
|
|
443
|
+
attemptIndex = 0
|
|
444
|
+
} = {}) {
|
|
445
|
+
if (scrollIntoView) {
|
|
446
|
+
try {
|
|
447
|
+
await scrollNodeIntoView(client, nodeId);
|
|
448
|
+
await sleep(150);
|
|
449
|
+
} catch {
|
|
450
|
+
// Recommend list cards are selected from visible nodes; if this CDP
|
|
451
|
+
// helper races the virtual list, let the box lookup/retry decide.
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const box = await getNodeBox(client, nodeId);
|
|
455
|
+
const clickTarget = resolveRecommendCardDetailClickPoint(box, { attemptIndex });
|
|
456
|
+
const clickResult = await clickPoint(client, clickTarget.x, clickTarget.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
457
|
+
return {
|
|
458
|
+
...box,
|
|
459
|
+
click_target: clickTarget,
|
|
460
|
+
click_result: clickResult
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function waitForRecommendDetailOpenOutcome(client, {
|
|
465
|
+
timeoutMs = 10000,
|
|
466
|
+
intervalMs = 250
|
|
467
|
+
} = {}) {
|
|
468
|
+
const started = Date.now();
|
|
469
|
+
let detailState = null;
|
|
470
|
+
let avatarPreview = null;
|
|
471
|
+
while (Date.now() - started <= timeoutMs) {
|
|
472
|
+
detailState = await readRecommendDetailState(client);
|
|
473
|
+
if (detailState?.popup || detailState?.resumeIframe) {
|
|
474
|
+
return {
|
|
475
|
+
kind: "detail",
|
|
476
|
+
elapsed_ms: Date.now() - started,
|
|
477
|
+
detail_state: detailState
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
avatarPreview = await readRecommendAvatarPreviewState(client);
|
|
481
|
+
if (avatarPreview.open) {
|
|
482
|
+
return {
|
|
483
|
+
kind: "avatar_preview",
|
|
484
|
+
elapsed_ms: Date.now() - started,
|
|
485
|
+
avatar_preview: avatarPreview
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
await sleep(intervalMs);
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
kind: "none",
|
|
492
|
+
elapsed_ms: Date.now() - started,
|
|
493
|
+
detail_state: detailState,
|
|
494
|
+
avatar_preview: avatarPreview
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function makeRecommendAvatarPreviewOpenedError(outcome, clickAttempts = []) {
|
|
499
|
+
const error = new Error("RECOMMEND_AVATAR_PREVIEW_OPENED: candidate avatar preview opened instead of resume detail");
|
|
500
|
+
error.code = "RECOMMEND_AVATAR_PREVIEW_OPENED";
|
|
501
|
+
error.avatar_preview = outcome?.avatar_preview || null;
|
|
502
|
+
error.click_attempts = clickAttempts;
|
|
503
|
+
return error;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export async function findRecommendCardNodeForCandidateKey(client, {
|
|
498
507
|
candidateKey = "",
|
|
499
508
|
rootState = null,
|
|
500
509
|
targetUrl = "",
|
|
@@ -524,17 +533,17 @@ export async function findRecommendCardNodeForCandidateKey(client, {
|
|
|
524
533
|
};
|
|
525
534
|
}
|
|
526
535
|
|
|
527
|
-
let nodeIds = [];
|
|
528
|
-
try {
|
|
529
|
-
nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
|
|
530
|
-
} catch (error) {
|
|
531
|
-
lastError = error;
|
|
532
|
-
if (!isStaleRecommendNodeError(error)) throw error;
|
|
533
|
-
rootState = null;
|
|
534
|
-
if (intervalMs > 0) await sleep(intervalMs);
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
537
|
-
lastCardCount = nodeIds.length;
|
|
536
|
+
let nodeIds = [];
|
|
537
|
+
try {
|
|
538
|
+
nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
lastError = error;
|
|
541
|
+
if (!isStaleRecommendNodeError(error)) throw error;
|
|
542
|
+
rootState = null;
|
|
543
|
+
if (intervalMs > 0) await sleep(intervalMs);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
lastCardCount = nodeIds.length;
|
|
538
547
|
for (let visibleIndex = 0; visibleIndex < nodeIds.length; visibleIndex += 1) {
|
|
539
548
|
const nodeId = nodeIds[visibleIndex];
|
|
540
549
|
try {
|
|
@@ -580,71 +589,71 @@ export async function findRecommendCardNodeForCandidateKey(client, {
|
|
|
580
589
|
};
|
|
581
590
|
}
|
|
582
591
|
|
|
583
|
-
export async function openRecommendCardDetail(client, cardNodeId, {
|
|
584
|
-
timeoutMs = 12000,
|
|
585
|
-
scrollIntoView = true
|
|
586
|
-
} = {}) {
|
|
587
|
-
const started = Date.now();
|
|
588
|
-
const clickAttempts = [];
|
|
589
|
-
const maxClickAttempts = 3;
|
|
590
|
-
let lastOutcome = null;
|
|
591
|
-
let lastCardBox = null;
|
|
592
|
-
let candidateClickMs = 0;
|
|
593
|
-
let detailOpenMs = 0;
|
|
594
|
-
|
|
595
|
-
for (let attemptIndex = 0; attemptIndex < maxClickAttempts; attemptIndex += 1) {
|
|
596
|
-
const clickStarted = Date.now();
|
|
597
|
-
lastCardBox = await clickRecommendCardDetailPoint(client, cardNodeId, {
|
|
598
|
-
scrollIntoView: attemptIndex === 0 ? scrollIntoView : false,
|
|
599
|
-
attemptIndex
|
|
600
|
-
});
|
|
601
|
-
candidateClickMs += Date.now() - clickStarted;
|
|
602
|
-
const detailStarted = Date.now();
|
|
603
|
-
lastOutcome = await waitForRecommendDetailOpenOutcome(client, {
|
|
604
|
-
timeoutMs: attemptIndex === 0 ? timeoutMs : Math.max(2500, Math.floor(timeoutMs / 3)),
|
|
605
|
-
intervalMs: 250
|
|
606
|
-
});
|
|
607
|
-
detailOpenMs += Date.now() - detailStarted;
|
|
608
|
-
clickAttempts.push({
|
|
609
|
-
attempt: attemptIndex + 1,
|
|
610
|
-
click_target: lastCardBox.click_target,
|
|
611
|
-
click_result: lastCardBox.click_result,
|
|
612
|
-
outcome: lastOutcome.kind,
|
|
613
|
-
elapsed_ms: lastOutcome.elapsed_ms
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
if (lastOutcome.kind === "detail") {
|
|
617
|
-
return {
|
|
618
|
-
card_box: lastCardBox,
|
|
619
|
-
click_attempts: clickAttempts,
|
|
620
|
-
detail_state: lastOutcome.detail_state,
|
|
621
|
-
timings: {
|
|
622
|
-
candidate_click_ms: candidateClickMs,
|
|
623
|
-
detail_open_ms: detailOpenMs,
|
|
624
|
-
open_total_ms: Date.now() - started
|
|
625
|
-
}
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
if (lastOutcome.kind === "avatar_preview") {
|
|
630
|
-
await closeRecommendAvatarPreview(client, { attemptsLimit: 2, waitMs: 350 });
|
|
631
|
-
throw makeRecommendAvatarPreviewOpenedError(lastOutcome, clickAttempts);
|
|
632
|
-
}
|
|
633
|
-
break;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (lastOutcome?.kind === "avatar_preview") {
|
|
637
|
-
throw makeRecommendAvatarPreviewOpenedError(lastOutcome, clickAttempts);
|
|
638
|
-
}
|
|
639
|
-
const error = new Error("Candidate detail did not open or no known detail selectors mounted");
|
|
640
|
-
error.click_attempts = clickAttempts;
|
|
641
|
-
error.last_open_outcome = lastOutcome;
|
|
642
|
-
throw error;
|
|
643
|
-
}
|
|
592
|
+
export async function openRecommendCardDetail(client, cardNodeId, {
|
|
593
|
+
timeoutMs = 12000,
|
|
594
|
+
scrollIntoView = true
|
|
595
|
+
} = {}) {
|
|
596
|
+
const started = Date.now();
|
|
597
|
+
const clickAttempts = [];
|
|
598
|
+
const maxClickAttempts = 3;
|
|
599
|
+
let lastOutcome = null;
|
|
600
|
+
let lastCardBox = null;
|
|
601
|
+
let candidateClickMs = 0;
|
|
602
|
+
let detailOpenMs = 0;
|
|
603
|
+
|
|
604
|
+
for (let attemptIndex = 0; attemptIndex < maxClickAttempts; attemptIndex += 1) {
|
|
605
|
+
const clickStarted = Date.now();
|
|
606
|
+
lastCardBox = await clickRecommendCardDetailPoint(client, cardNodeId, {
|
|
607
|
+
scrollIntoView: attemptIndex === 0 ? scrollIntoView : false,
|
|
608
|
+
attemptIndex
|
|
609
|
+
});
|
|
610
|
+
candidateClickMs += Date.now() - clickStarted;
|
|
611
|
+
const detailStarted = Date.now();
|
|
612
|
+
lastOutcome = await waitForRecommendDetailOpenOutcome(client, {
|
|
613
|
+
timeoutMs: attemptIndex === 0 ? timeoutMs : Math.max(2500, Math.floor(timeoutMs / 3)),
|
|
614
|
+
intervalMs: 250
|
|
615
|
+
});
|
|
616
|
+
detailOpenMs += Date.now() - detailStarted;
|
|
617
|
+
clickAttempts.push({
|
|
618
|
+
attempt: attemptIndex + 1,
|
|
619
|
+
click_target: lastCardBox.click_target,
|
|
620
|
+
click_result: lastCardBox.click_result,
|
|
621
|
+
outcome: lastOutcome.kind,
|
|
622
|
+
elapsed_ms: lastOutcome.elapsed_ms
|
|
623
|
+
});
|
|
644
624
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
625
|
+
if (lastOutcome.kind === "detail") {
|
|
626
|
+
return {
|
|
627
|
+
card_box: lastCardBox,
|
|
628
|
+
click_attempts: clickAttempts,
|
|
629
|
+
detail_state: lastOutcome.detail_state,
|
|
630
|
+
timings: {
|
|
631
|
+
candidate_click_ms: candidateClickMs,
|
|
632
|
+
detail_open_ms: detailOpenMs,
|
|
633
|
+
open_total_ms: Date.now() - started
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (lastOutcome.kind === "avatar_preview") {
|
|
639
|
+
await closeRecommendAvatarPreview(client, { attemptsLimit: 2, waitMs: 350 });
|
|
640
|
+
throw makeRecommendAvatarPreviewOpenedError(lastOutcome, clickAttempts);
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (lastOutcome?.kind === "avatar_preview") {
|
|
646
|
+
throw makeRecommendAvatarPreviewOpenedError(lastOutcome, clickAttempts);
|
|
647
|
+
}
|
|
648
|
+
const error = new Error("Candidate detail did not open or no known detail selectors mounted");
|
|
649
|
+
error.click_attempts = clickAttempts;
|
|
650
|
+
error.last_open_outcome = lastOutcome;
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export async function openRecommendCardDetailWithFreshRetry(client, {
|
|
655
|
+
cardNodeId,
|
|
656
|
+
candidateKey = "",
|
|
648
657
|
cardCandidate = null,
|
|
649
658
|
rootState = null,
|
|
650
659
|
targetUrl = "",
|
|
@@ -672,20 +681,20 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
|
|
|
672
681
|
card_candidate: currentCandidate,
|
|
673
682
|
retry_attempts: attempts
|
|
674
683
|
};
|
|
675
|
-
} catch (error) {
|
|
676
|
-
const stale = isStaleRecommendNodeError(error);
|
|
677
|
-
const detailOpenMiss = isRecommendDetailOpenMissError(error);
|
|
678
|
-
attempts.push({
|
|
679
|
-
attempt: attemptIndex + 1,
|
|
680
|
-
node_id: currentNodeId,
|
|
681
|
-
stale_node: stale,
|
|
682
|
-
detail_open_miss: detailOpenMiss,
|
|
683
|
-
error: error?.message || String(error)
|
|
684
|
-
});
|
|
685
|
-
if ((!stale && !detailOpenMiss) || attemptIndex >= limit - 1 || !candidateKey) {
|
|
686
|
-
error.recommend_detail_open_attempts = attempts;
|
|
687
|
-
throw error;
|
|
688
|
-
}
|
|
684
|
+
} catch (error) {
|
|
685
|
+
const stale = isStaleRecommendNodeError(error);
|
|
686
|
+
const detailOpenMiss = isRecommendDetailOpenMissError(error);
|
|
687
|
+
attempts.push({
|
|
688
|
+
attempt: attemptIndex + 1,
|
|
689
|
+
node_id: currentNodeId,
|
|
690
|
+
stale_node: stale,
|
|
691
|
+
detail_open_miss: detailOpenMiss,
|
|
692
|
+
error: error?.message || String(error)
|
|
693
|
+
});
|
|
694
|
+
if ((!stale && !detailOpenMiss) || attemptIndex >= limit - 1 || !candidateKey) {
|
|
695
|
+
error.recommend_detail_open_attempts = attempts;
|
|
696
|
+
throw error;
|
|
697
|
+
}
|
|
689
698
|
|
|
690
699
|
const resolved = await findRecommendCardNodeForCandidateKey(client, {
|
|
691
700
|
candidateKey,
|
|
@@ -711,100 +720,100 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
|
|
|
711
720
|
currentRootState = resolved.root_state || null;
|
|
712
721
|
}
|
|
713
722
|
}
|
|
714
|
-
|
|
715
|
-
throw new Error("Recommend detail retry exhausted");
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
export async function closeRecommendAvatarPreview(client, {
|
|
719
|
-
attemptsLimit = 2,
|
|
720
|
-
waitMs = 500
|
|
721
|
-
} = {}) {
|
|
722
|
-
const attempts = [];
|
|
723
|
-
for (let index = 0; index < attemptsLimit; index += 1) {
|
|
724
|
-
const state = await readRecommendAvatarPreviewState(client);
|
|
725
|
-
if (!state.open) {
|
|
726
|
-
return {
|
|
727
|
-
closed: true,
|
|
728
|
-
already_closed: true,
|
|
729
|
-
attempts
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const closeTarget = await findVisibleCloseTarget(client, state.roots, RECOMMEND_AVATAR_PREVIEW_CLOSE_SELECTORS);
|
|
734
|
-
if (closeTarget) {
|
|
735
|
-
try {
|
|
736
|
-
if (closeTarget.center) {
|
|
737
|
-
await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
738
|
-
} else {
|
|
739
|
-
await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
|
|
740
|
-
}
|
|
741
|
-
attempts.push({
|
|
742
|
-
mode: "avatar-preview-close-selector",
|
|
743
|
-
selector: closeTarget.selector,
|
|
744
|
-
root: closeTarget.root
|
|
745
|
-
});
|
|
746
|
-
} catch (error) {
|
|
747
|
-
attempts.push({
|
|
748
|
-
mode: "avatar-preview-close-selector-error",
|
|
749
|
-
selector: closeTarget.selector,
|
|
750
|
-
root: closeTarget.root,
|
|
751
|
-
error: error?.message || String(error)
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
} else {
|
|
755
|
-
await pressEscape(client);
|
|
756
|
-
attempts.push({ mode: "avatar-preview-Escape" });
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
const closed = await waitForRecommendAvatarPreviewClosed(client, {
|
|
760
|
-
timeoutMs: waitMs,
|
|
761
|
-
intervalMs: 100
|
|
762
|
-
});
|
|
763
|
-
attempts.push({
|
|
764
|
-
mode: "wait-avatar-preview-closed",
|
|
765
|
-
closed: closed.closed,
|
|
766
|
-
elapsed_ms: closed.elapsed_ms
|
|
767
|
-
});
|
|
768
|
-
if (closed.closed) {
|
|
769
|
-
return {
|
|
770
|
-
closed: true,
|
|
771
|
-
already_closed: false,
|
|
772
|
-
attempts
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
await pressEscape(client);
|
|
777
|
-
attempts.push({ mode: "avatar-preview-Escape-fallback" });
|
|
778
|
-
const closedAfterEscape = await waitForRecommendAvatarPreviewClosed(client, {
|
|
779
|
-
timeoutMs: waitMs,
|
|
780
|
-
intervalMs: 100
|
|
781
|
-
});
|
|
782
|
-
attempts.push({
|
|
783
|
-
mode: "wait-avatar-preview-closed-after-escape",
|
|
784
|
-
closed: closedAfterEscape.closed,
|
|
785
|
-
elapsed_ms: closedAfterEscape.elapsed_ms
|
|
786
|
-
});
|
|
787
|
-
if (closedAfterEscape.closed) {
|
|
788
|
-
return {
|
|
789
|
-
closed: true,
|
|
790
|
-
already_closed: false,
|
|
791
|
-
attempts
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
const state = await readRecommendAvatarPreviewState(client);
|
|
797
|
-
return {
|
|
798
|
-
closed: !state.open,
|
|
799
|
-
already_closed: false,
|
|
800
|
-
reason: state.open ? "avatar_preview_still_visible_after_close_attempts" : null,
|
|
801
|
-
attempts,
|
|
802
|
-
state
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
export async function closeRecommendDetail(client, {
|
|
807
|
-
attemptsLimit = 4,
|
|
723
|
+
|
|
724
|
+
throw new Error("Recommend detail retry exhausted");
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export async function closeRecommendAvatarPreview(client, {
|
|
728
|
+
attemptsLimit = 2,
|
|
729
|
+
waitMs = 500
|
|
730
|
+
} = {}) {
|
|
731
|
+
const attempts = [];
|
|
732
|
+
for (let index = 0; index < attemptsLimit; index += 1) {
|
|
733
|
+
const state = await readRecommendAvatarPreviewState(client);
|
|
734
|
+
if (!state.open) {
|
|
735
|
+
return {
|
|
736
|
+
closed: true,
|
|
737
|
+
already_closed: true,
|
|
738
|
+
attempts
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const closeTarget = await findVisibleCloseTarget(client, state.roots, RECOMMEND_AVATAR_PREVIEW_CLOSE_SELECTORS);
|
|
743
|
+
if (closeTarget) {
|
|
744
|
+
try {
|
|
745
|
+
if (closeTarget.center) {
|
|
746
|
+
await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
747
|
+
} else {
|
|
748
|
+
await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
|
|
749
|
+
}
|
|
750
|
+
attempts.push({
|
|
751
|
+
mode: "avatar-preview-close-selector",
|
|
752
|
+
selector: closeTarget.selector,
|
|
753
|
+
root: closeTarget.root
|
|
754
|
+
});
|
|
755
|
+
} catch (error) {
|
|
756
|
+
attempts.push({
|
|
757
|
+
mode: "avatar-preview-close-selector-error",
|
|
758
|
+
selector: closeTarget.selector,
|
|
759
|
+
root: closeTarget.root,
|
|
760
|
+
error: error?.message || String(error)
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
await pressEscape(client);
|
|
765
|
+
attempts.push({ mode: "avatar-preview-Escape" });
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const closed = await waitForRecommendAvatarPreviewClosed(client, {
|
|
769
|
+
timeoutMs: waitMs,
|
|
770
|
+
intervalMs: 100
|
|
771
|
+
});
|
|
772
|
+
attempts.push({
|
|
773
|
+
mode: "wait-avatar-preview-closed",
|
|
774
|
+
closed: closed.closed,
|
|
775
|
+
elapsed_ms: closed.elapsed_ms
|
|
776
|
+
});
|
|
777
|
+
if (closed.closed) {
|
|
778
|
+
return {
|
|
779
|
+
closed: true,
|
|
780
|
+
already_closed: false,
|
|
781
|
+
attempts
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
await pressEscape(client);
|
|
786
|
+
attempts.push({ mode: "avatar-preview-Escape-fallback" });
|
|
787
|
+
const closedAfterEscape = await waitForRecommendAvatarPreviewClosed(client, {
|
|
788
|
+
timeoutMs: waitMs,
|
|
789
|
+
intervalMs: 100
|
|
790
|
+
});
|
|
791
|
+
attempts.push({
|
|
792
|
+
mode: "wait-avatar-preview-closed-after-escape",
|
|
793
|
+
closed: closedAfterEscape.closed,
|
|
794
|
+
elapsed_ms: closedAfterEscape.elapsed_ms
|
|
795
|
+
});
|
|
796
|
+
if (closedAfterEscape.closed) {
|
|
797
|
+
return {
|
|
798
|
+
closed: true,
|
|
799
|
+
already_closed: false,
|
|
800
|
+
attempts
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const state = await readRecommendAvatarPreviewState(client);
|
|
806
|
+
return {
|
|
807
|
+
closed: !state.open,
|
|
808
|
+
already_closed: false,
|
|
809
|
+
reason: state.open ? "avatar_preview_still_visible_after_close_attempts" : null,
|
|
810
|
+
attempts,
|
|
811
|
+
state
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export async function closeRecommendDetail(client, {
|
|
816
|
+
attemptsLimit = 4,
|
|
808
817
|
closeWaitMs = 5000,
|
|
809
818
|
escapeWaitMs = 3500
|
|
810
819
|
} = {}) {
|
|
@@ -822,11 +831,11 @@ export async function closeRecommendDetail(client, {
|
|
|
822
831
|
const closeTarget = await findVisibleCloseTarget(client, rootState.roots, DETAIL_CLOSE_SELECTORS);
|
|
823
832
|
if (closeTarget) {
|
|
824
833
|
try {
|
|
825
|
-
if (closeTarget.center) {
|
|
826
|
-
await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
827
|
-
} else {
|
|
828
|
-
await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
|
|
829
|
-
}
|
|
834
|
+
if (closeTarget.center) {
|
|
835
|
+
await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
836
|
+
} else {
|
|
837
|
+
await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
|
|
838
|
+
}
|
|
830
839
|
attempts.push({
|
|
831
840
|
mode: "close-selector",
|
|
832
841
|
selector: closeTarget.selector,
|
|
@@ -856,35 +865,35 @@ export async function closeRecommendDetail(client, {
|
|
|
856
865
|
closed: closedAfterClick.closed,
|
|
857
866
|
elapsed_ms: closedAfterClick.elapsed_ms
|
|
858
867
|
});
|
|
859
|
-
if (closedAfterClick.closed) {
|
|
860
|
-
return {
|
|
861
|
-
closed: true,
|
|
862
|
-
attempts
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
const outsideClick = await clickOutsideRecommendDetail(client, closedAfterClick.state || existingState);
|
|
867
|
-
attempts.push(outsideClick);
|
|
868
|
-
if (outsideClick.clicked) {
|
|
869
|
-
const closedAfterOutsideClick = await waitForRecommendDetailClosed(client, {
|
|
870
|
-
timeoutMs: closeWaitMs,
|
|
871
|
-
intervalMs: 250
|
|
872
|
-
});
|
|
873
|
-
attempts.push({
|
|
874
|
-
mode: "wait-closed-after-outside-click",
|
|
875
|
-
closed: closedAfterOutsideClick.closed,
|
|
876
|
-
elapsed_ms: closedAfterOutsideClick.elapsed_ms
|
|
877
|
-
});
|
|
878
|
-
if (closedAfterOutsideClick.closed) {
|
|
879
|
-
return {
|
|
880
|
-
closed: true,
|
|
881
|
-
attempts
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
await pressEscape(client);
|
|
887
|
-
attempts.push({ mode: "Escape-fallback" });
|
|
868
|
+
if (closedAfterClick.closed) {
|
|
869
|
+
return {
|
|
870
|
+
closed: true,
|
|
871
|
+
attempts
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const outsideClick = await clickOutsideRecommendDetail(client, closedAfterClick.state || existingState);
|
|
876
|
+
attempts.push(outsideClick);
|
|
877
|
+
if (outsideClick.clicked) {
|
|
878
|
+
const closedAfterOutsideClick = await waitForRecommendDetailClosed(client, {
|
|
879
|
+
timeoutMs: closeWaitMs,
|
|
880
|
+
intervalMs: 250
|
|
881
|
+
});
|
|
882
|
+
attempts.push({
|
|
883
|
+
mode: "wait-closed-after-outside-click",
|
|
884
|
+
closed: closedAfterOutsideClick.closed,
|
|
885
|
+
elapsed_ms: closedAfterOutsideClick.elapsed_ms
|
|
886
|
+
});
|
|
887
|
+
if (closedAfterOutsideClick.closed) {
|
|
888
|
+
return {
|
|
889
|
+
closed: true,
|
|
890
|
+
attempts
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
await pressEscape(client);
|
|
896
|
+
attempts.push({ mode: "Escape-fallback" });
|
|
888
897
|
|
|
889
898
|
const closedAfterEscape = await waitForRecommendDetailClosed(client, {
|
|
890
899
|
timeoutMs: escapeWaitMs,
|
|
@@ -901,33 +910,33 @@ export async function closeRecommendDetail(client, {
|
|
|
901
910
|
attempts
|
|
902
911
|
};
|
|
903
912
|
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
const verification = await verifyRecommendDetailStillOpen(client);
|
|
907
|
-
attempts.push({
|
|
908
|
-
mode: "final-close-verification",
|
|
909
|
-
open: verification.open,
|
|
910
|
-
stable_open: verification.stable_open,
|
|
911
|
-
popup: verification.second.popup,
|
|
912
|
-
resume_iframe: verification.second.resume_iframe
|
|
913
|
-
});
|
|
914
|
-
if (!verification.open) {
|
|
915
|
-
return {
|
|
916
|
-
closed: true,
|
|
917
|
-
attempts,
|
|
918
|
-
verification
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
return {
|
|
923
|
-
closed: false,
|
|
924
|
-
reason: verification.stable_open
|
|
925
|
-
? "detail_still_visible_after_close_attempts"
|
|
926
|
-
: "detail_visibility_ambiguous_after_close_attempts",
|
|
927
|
-
attempts,
|
|
928
|
-
verification
|
|
929
|
-
};
|
|
930
|
-
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const verification = await verifyRecommendDetailStillOpen(client);
|
|
916
|
+
attempts.push({
|
|
917
|
+
mode: "final-close-verification",
|
|
918
|
+
open: verification.open,
|
|
919
|
+
stable_open: verification.stable_open,
|
|
920
|
+
popup: verification.second.popup,
|
|
921
|
+
resume_iframe: verification.second.resume_iframe
|
|
922
|
+
});
|
|
923
|
+
if (!verification.open) {
|
|
924
|
+
return {
|
|
925
|
+
closed: true,
|
|
926
|
+
attempts,
|
|
927
|
+
verification
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return {
|
|
932
|
+
closed: false,
|
|
933
|
+
reason: verification.stable_open
|
|
934
|
+
? "detail_still_visible_after_close_attempts"
|
|
935
|
+
: "detail_visibility_ambiguous_after_close_attempts",
|
|
936
|
+
attempts,
|
|
937
|
+
verification
|
|
938
|
+
};
|
|
939
|
+
}
|
|
931
940
|
|
|
932
941
|
async function findVisibleCloseTarget(client, roots, selectors) {
|
|
933
942
|
let fallback = null;
|
|
@@ -959,107 +968,107 @@ async function findVisibleCloseTarget(client, roots, selectors) {
|
|
|
959
968
|
return fallback;
|
|
960
969
|
}
|
|
961
970
|
|
|
962
|
-
async function pressEscape(client) {
|
|
963
|
-
await pressKey(client, "Escape", {
|
|
964
|
-
code: "Escape",
|
|
965
|
-
windowsVirtualKeyCode: 27,
|
|
966
|
-
nativeVirtualKeyCode: 27
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function clampPointCoordinate(value, min, max) {
|
|
971
|
-
return Math.max(min, Math.min(max, value));
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
async function getClickViewport(client) {
|
|
975
|
-
try {
|
|
976
|
-
const metrics = typeof client?.Page?.getLayoutMetrics === "function"
|
|
977
|
-
? await client.Page.getLayoutMetrics()
|
|
978
|
-
: null;
|
|
979
|
-
const viewport = metrics?.cssLayoutViewport || metrics?.layoutViewport || metrics?.visualViewport || {};
|
|
980
|
-
return {
|
|
981
|
-
width: Number(viewport.clientWidth || viewport.width || 1440),
|
|
982
|
-
height: Number(viewport.clientHeight || viewport.height || 900)
|
|
983
|
-
};
|
|
984
|
-
} catch {
|
|
985
|
-
return {
|
|
986
|
-
width: 1440,
|
|
987
|
-
height: 900
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
function getOutsideClickPoint(rect, viewport) {
|
|
993
|
-
if (!rect || rect.width <= 2 || rect.height <= 2) return null;
|
|
994
|
-
const margin = 24;
|
|
995
|
-
const minX = 8;
|
|
996
|
-
const minY = 8;
|
|
997
|
-
const maxX = Math.max(minX, (Number(viewport?.width) || 1440) - 8);
|
|
998
|
-
const maxY = Math.max(minY, (Number(viewport?.height) || 900) - 8);
|
|
999
|
-
const midX = rect.x + rect.width / 2;
|
|
1000
|
-
const midY = rect.y + Math.min(Math.max(rect.height * 0.2, 48), Math.max(48, rect.height - 24));
|
|
1001
|
-
const candidates = [
|
|
1002
|
-
{ side: "left", x: rect.x - margin, y: midY },
|
|
1003
|
-
{ side: "right", x: rect.x + rect.width + margin, y: midY },
|
|
1004
|
-
{ side: "above", x: midX, y: rect.y - margin },
|
|
1005
|
-
{ side: "below", x: midX, y: rect.y + rect.height + margin },
|
|
1006
|
-
{ side: "viewport-corner", x: 16, y: 16 }
|
|
1007
|
-
];
|
|
1008
|
-
|
|
1009
|
-
for (const candidate of candidates) {
|
|
1010
|
-
const x = clampPointCoordinate(candidate.x, minX, maxX);
|
|
1011
|
-
const y = clampPointCoordinate(candidate.y, minY, maxY);
|
|
1012
|
-
const insideRect = (
|
|
1013
|
-
x >= rect.x
|
|
1014
|
-
&& x <= rect.x + rect.width
|
|
1015
|
-
&& y >= rect.y
|
|
1016
|
-
&& y <= rect.y + rect.height
|
|
1017
|
-
);
|
|
1018
|
-
if (!insideRect) {
|
|
1019
|
-
return {
|
|
1020
|
-
...candidate,
|
|
1021
|
-
x,
|
|
1022
|
-
y
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
return null;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
async function clickOutsideRecommendDetail(client, detailState) {
|
|
1030
|
-
const rootState = detailState?.roots?.length
|
|
1031
|
-
? detailState
|
|
1032
|
-
: await readRecommendDetailState(client);
|
|
1033
|
-
const boundaryTarget = await findVisibleDetailTarget(
|
|
1034
|
-
client,
|
|
1035
|
-
rootState.roots || [],
|
|
1036
|
-
DETAIL_OUTSIDE_CLOSE_BOUNDARY_SELECTORS
|
|
1037
|
-
);
|
|
1038
|
-
const target = boundaryTarget || rootState.resumeIframe || rootState.popup || null;
|
|
1039
|
-
const viewport = await getClickViewport(client);
|
|
1040
|
-
const point = getOutsideClickPoint(target?.rect, viewport);
|
|
1041
|
-
if (!point) {
|
|
1042
|
-
return {
|
|
1043
|
-
clicked: false,
|
|
1044
|
-
mode: "outside-modal-click",
|
|
1045
|
-
reason: "no_outside_click_point",
|
|
1046
|
-
selector: target?.selector || null,
|
|
1047
|
-
root: target?.root || null
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
await clickPoint(client, point.x, point.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
1051
|
-
return {
|
|
1052
|
-
clicked: true,
|
|
1053
|
-
mode: "outside-modal-click",
|
|
1054
|
-
selector: target?.selector || null,
|
|
1055
|
-
root: target?.root || null,
|
|
1056
|
-
side: point.side,
|
|
1057
|
-
x: Math.round(point.x),
|
|
1058
|
-
y: Math.round(point.y)
|
|
1059
|
-
};
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
export async function extractRecommendDetailCandidate(client, {
|
|
971
|
+
async function pressEscape(client) {
|
|
972
|
+
await pressKey(client, "Escape", {
|
|
973
|
+
code: "Escape",
|
|
974
|
+
windowsVirtualKeyCode: 27,
|
|
975
|
+
nativeVirtualKeyCode: 27
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function clampPointCoordinate(value, min, max) {
|
|
980
|
+
return Math.max(min, Math.min(max, value));
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async function getClickViewport(client) {
|
|
984
|
+
try {
|
|
985
|
+
const metrics = typeof client?.Page?.getLayoutMetrics === "function"
|
|
986
|
+
? await client.Page.getLayoutMetrics()
|
|
987
|
+
: null;
|
|
988
|
+
const viewport = metrics?.cssLayoutViewport || metrics?.layoutViewport || metrics?.visualViewport || {};
|
|
989
|
+
return {
|
|
990
|
+
width: Number(viewport.clientWidth || viewport.width || 1440),
|
|
991
|
+
height: Number(viewport.clientHeight || viewport.height || 900)
|
|
992
|
+
};
|
|
993
|
+
} catch {
|
|
994
|
+
return {
|
|
995
|
+
width: 1440,
|
|
996
|
+
height: 900
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function getOutsideClickPoint(rect, viewport) {
|
|
1002
|
+
if (!rect || rect.width <= 2 || rect.height <= 2) return null;
|
|
1003
|
+
const margin = 24;
|
|
1004
|
+
const minX = 8;
|
|
1005
|
+
const minY = 8;
|
|
1006
|
+
const maxX = Math.max(minX, (Number(viewport?.width) || 1440) - 8);
|
|
1007
|
+
const maxY = Math.max(minY, (Number(viewport?.height) || 900) - 8);
|
|
1008
|
+
const midX = rect.x + rect.width / 2;
|
|
1009
|
+
const midY = rect.y + Math.min(Math.max(rect.height * 0.2, 48), Math.max(48, rect.height - 24));
|
|
1010
|
+
const candidates = [
|
|
1011
|
+
{ side: "left", x: rect.x - margin, y: midY },
|
|
1012
|
+
{ side: "right", x: rect.x + rect.width + margin, y: midY },
|
|
1013
|
+
{ side: "above", x: midX, y: rect.y - margin },
|
|
1014
|
+
{ side: "below", x: midX, y: rect.y + rect.height + margin },
|
|
1015
|
+
{ side: "viewport-corner", x: 16, y: 16 }
|
|
1016
|
+
];
|
|
1017
|
+
|
|
1018
|
+
for (const candidate of candidates) {
|
|
1019
|
+
const x = clampPointCoordinate(candidate.x, minX, maxX);
|
|
1020
|
+
const y = clampPointCoordinate(candidate.y, minY, maxY);
|
|
1021
|
+
const insideRect = (
|
|
1022
|
+
x >= rect.x
|
|
1023
|
+
&& x <= rect.x + rect.width
|
|
1024
|
+
&& y >= rect.y
|
|
1025
|
+
&& y <= rect.y + rect.height
|
|
1026
|
+
);
|
|
1027
|
+
if (!insideRect) {
|
|
1028
|
+
return {
|
|
1029
|
+
...candidate,
|
|
1030
|
+
x,
|
|
1031
|
+
y
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async function clickOutsideRecommendDetail(client, detailState) {
|
|
1039
|
+
const rootState = detailState?.roots?.length
|
|
1040
|
+
? detailState
|
|
1041
|
+
: await readRecommendDetailState(client);
|
|
1042
|
+
const boundaryTarget = await findVisibleDetailTarget(
|
|
1043
|
+
client,
|
|
1044
|
+
rootState.roots || [],
|
|
1045
|
+
DETAIL_OUTSIDE_CLOSE_BOUNDARY_SELECTORS
|
|
1046
|
+
);
|
|
1047
|
+
const target = boundaryTarget || rootState.resumeIframe || rootState.popup || null;
|
|
1048
|
+
const viewport = await getClickViewport(client);
|
|
1049
|
+
const point = getOutsideClickPoint(target?.rect, viewport);
|
|
1050
|
+
if (!point) {
|
|
1051
|
+
return {
|
|
1052
|
+
clicked: false,
|
|
1053
|
+
mode: "outside-modal-click",
|
|
1054
|
+
reason: "no_outside_click_point",
|
|
1055
|
+
selector: target?.selector || null,
|
|
1056
|
+
root: target?.root || null
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
await clickPoint(client, point.x, point.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
1060
|
+
return {
|
|
1061
|
+
clicked: true,
|
|
1062
|
+
mode: "outside-modal-click",
|
|
1063
|
+
selector: target?.selector || null,
|
|
1064
|
+
root: target?.root || null,
|
|
1065
|
+
side: point.side,
|
|
1066
|
+
x: Math.round(point.x),
|
|
1067
|
+
y: Math.round(point.y)
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
export async function extractRecommendDetailCandidate(client, {
|
|
1063
1072
|
cardCandidate,
|
|
1064
1073
|
cardNodeId,
|
|
1065
1074
|
detailState,
|