@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/bin/boss-recommend-mcp.js +4 -4
- package/config/screening-config.example.json +33 -33
- package/package.json +8 -8
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +42 -42
- package/skills/boss-chat/SKILL.md +106 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +219 -214
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +127 -127
- package/src/chat-runtime-config.js +775 -775
- package/src/cli.js +573 -573
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +2419 -2415
- package/src/core/capture/index.js +1201 -1201
- package/src/core/cv-acquisition/index.js +238 -238
- package/src/core/cv-capture-target/index.js +299 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +1326 -1326
- package/src/core/reporting/legacy-csv.js +334 -332
- package/src/core/run/index.js +32 -32
- package/src/core/run/timing.js +33 -33
- package/src/core/screening/index.js +2135 -2135
- package/src/core/self-heal/index.js +973 -973
- package/src/core/self-heal/viewport.js +564 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +113 -113
- package/src/domains/chat/index.js +7 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +122 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +571 -571
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +472 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +333 -333
- package/src/domains/recommend/constants.js +228 -159
- package/src/domains/recommend/detail.js +650 -650
- package/src/domains/recommend/filters.js +748 -377
- package/src/domains/recommend/index.js +4 -3
- package/src/domains/recommend/jobs.js +542 -542
- package/src/domains/recommend/location.js +736 -0
- package/src/domains/recommend/refresh.js +504 -361
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +987 -854
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1620 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +13 -0
- package/src/parser.js +376 -8
- package/src/recommend-mcp.js +929 -915
- package/src/recommend-scheduler.js +496 -496
- package/src/recruit-mcp.js +2121 -2121
|
@@ -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,
|
|
@@ -387,114 +387,114 @@ export async function readRecommendDetailHtml(client, detailState) {
|
|
|
387
387
|
};
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
export function isStaleRecommendNodeError(error) {
|
|
391
|
-
const message = String(error?.message || error || "");
|
|
392
|
-
return /Could not find node with given id|No node with given id|Node is detached|Cannot find node|Could not compute box model/i.test(message);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export function isRecommendDetailOpenMissError(error) {
|
|
396
|
-
const message = String(error?.message || error || "");
|
|
397
|
-
return isRecommendAvatarPreviewOpenError(error)
|
|
398
|
-
|| /Candidate detail did not open|no known detail selectors mounted/i.test(message);
|
|
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
|
-
}
|
|
444
|
-
}
|
|
445
|
-
const box = await getNodeBox(client, nodeId);
|
|
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
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
async function waitForRecommendDetailOpenOutcome(client, {
|
|
456
|
-
timeoutMs = 10000,
|
|
457
|
-
intervalMs = 250
|
|
458
|
-
} = {}) {
|
|
459
|
-
const started = Date.now();
|
|
460
|
-
let detailState = null;
|
|
461
|
-
let avatarPreview = null;
|
|
462
|
-
while (Date.now() - started <= timeoutMs) {
|
|
463
|
-
detailState = await readRecommendDetailState(client);
|
|
464
|
-
if (detailState?.popup || detailState?.resumeIframe) {
|
|
465
|
-
return {
|
|
466
|
-
kind: "detail",
|
|
467
|
-
elapsed_ms: Date.now() - started,
|
|
468
|
-
detail_state: detailState
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
avatarPreview = await readRecommendAvatarPreviewState(client);
|
|
472
|
-
if (avatarPreview.open) {
|
|
473
|
-
return {
|
|
474
|
-
kind: "avatar_preview",
|
|
475
|
-
elapsed_ms: Date.now() - started,
|
|
476
|
-
avatar_preview: avatarPreview
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
await sleep(intervalMs);
|
|
480
|
-
}
|
|
481
|
-
return {
|
|
482
|
-
kind: "none",
|
|
483
|
-
elapsed_ms: Date.now() - started,
|
|
484
|
-
detail_state: detailState,
|
|
485
|
-
avatar_preview: avatarPreview
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function makeRecommendAvatarPreviewOpenedError(outcome, clickAttempts = []) {
|
|
490
|
-
const error = new Error("RECOMMEND_AVATAR_PREVIEW_OPENED: candidate avatar preview opened instead of resume detail");
|
|
491
|
-
error.code = "RECOMMEND_AVATAR_PREVIEW_OPENED";
|
|
492
|
-
error.avatar_preview = outcome?.avatar_preview || null;
|
|
493
|
-
error.click_attempts = clickAttempts;
|
|
494
|
-
return error;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
export async function findRecommendCardNodeForCandidateKey(client, {
|
|
390
|
+
export function isStaleRecommendNodeError(error) {
|
|
391
|
+
const message = String(error?.message || error || "");
|
|
392
|
+
return /Could not find node with given id|No node with given id|Node is detached|Cannot find node|Could not compute box model/i.test(message);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function isRecommendDetailOpenMissError(error) {
|
|
396
|
+
const message = String(error?.message || error || "");
|
|
397
|
+
return isRecommendAvatarPreviewOpenError(error)
|
|
398
|
+
|| /Candidate detail did not open|no known detail selectors mounted/i.test(message);
|
|
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
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const box = await getNodeBox(client, nodeId);
|
|
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
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function waitForRecommendDetailOpenOutcome(client, {
|
|
456
|
+
timeoutMs = 10000,
|
|
457
|
+
intervalMs = 250
|
|
458
|
+
} = {}) {
|
|
459
|
+
const started = Date.now();
|
|
460
|
+
let detailState = null;
|
|
461
|
+
let avatarPreview = null;
|
|
462
|
+
while (Date.now() - started <= timeoutMs) {
|
|
463
|
+
detailState = await readRecommendDetailState(client);
|
|
464
|
+
if (detailState?.popup || detailState?.resumeIframe) {
|
|
465
|
+
return {
|
|
466
|
+
kind: "detail",
|
|
467
|
+
elapsed_ms: Date.now() - started,
|
|
468
|
+
detail_state: detailState
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
avatarPreview = await readRecommendAvatarPreviewState(client);
|
|
472
|
+
if (avatarPreview.open) {
|
|
473
|
+
return {
|
|
474
|
+
kind: "avatar_preview",
|
|
475
|
+
elapsed_ms: Date.now() - started,
|
|
476
|
+
avatar_preview: avatarPreview
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
await sleep(intervalMs);
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
kind: "none",
|
|
483
|
+
elapsed_ms: Date.now() - started,
|
|
484
|
+
detail_state: detailState,
|
|
485
|
+
avatar_preview: avatarPreview
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function makeRecommendAvatarPreviewOpenedError(outcome, clickAttempts = []) {
|
|
490
|
+
const error = new Error("RECOMMEND_AVATAR_PREVIEW_OPENED: candidate avatar preview opened instead of resume detail");
|
|
491
|
+
error.code = "RECOMMEND_AVATAR_PREVIEW_OPENED";
|
|
492
|
+
error.avatar_preview = outcome?.avatar_preview || null;
|
|
493
|
+
error.click_attempts = clickAttempts;
|
|
494
|
+
return error;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export async function findRecommendCardNodeForCandidateKey(client, {
|
|
498
498
|
candidateKey = "",
|
|
499
499
|
rootState = null,
|
|
500
500
|
targetUrl = "",
|
|
@@ -524,17 +524,17 @@ export async function findRecommendCardNodeForCandidateKey(client, {
|
|
|
524
524
|
};
|
|
525
525
|
}
|
|
526
526
|
|
|
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;
|
|
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;
|
|
538
538
|
for (let visibleIndex = 0; visibleIndex < nodeIds.length; visibleIndex += 1) {
|
|
539
539
|
const nodeId = nodeIds[visibleIndex];
|
|
540
540
|
try {
|
|
@@ -580,71 +580,71 @@ export async function findRecommendCardNodeForCandidateKey(client, {
|
|
|
580
580
|
};
|
|
581
581
|
}
|
|
582
582
|
|
|
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
|
-
}
|
|
644
|
-
|
|
645
|
-
export async function openRecommendCardDetailWithFreshRetry(client, {
|
|
646
|
-
cardNodeId,
|
|
647
|
-
candidateKey = "",
|
|
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
|
+
}
|
|
644
|
+
|
|
645
|
+
export async function openRecommendCardDetailWithFreshRetry(client, {
|
|
646
|
+
cardNodeId,
|
|
647
|
+
candidateKey = "",
|
|
648
648
|
cardCandidate = null,
|
|
649
649
|
rootState = null,
|
|
650
650
|
targetUrl = "",
|
|
@@ -672,20 +672,20 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
|
|
|
672
672
|
card_candidate: currentCandidate,
|
|
673
673
|
retry_attempts: attempts
|
|
674
674
|
};
|
|
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
|
-
}
|
|
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
|
+
}
|
|
689
689
|
|
|
690
690
|
const resolved = await findRecommendCardNodeForCandidateKey(client, {
|
|
691
691
|
candidateKey,
|
|
@@ -711,100 +711,100 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
|
|
|
711
711
|
currentRootState = resolved.root_state || null;
|
|
712
712
|
}
|
|
713
713
|
}
|
|
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,
|
|
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,
|
|
808
808
|
closeWaitMs = 5000,
|
|
809
809
|
escapeWaitMs = 3500
|
|
810
810
|
} = {}) {
|
|
@@ -822,11 +822,11 @@ export async function closeRecommendDetail(client, {
|
|
|
822
822
|
const closeTarget = await findVisibleCloseTarget(client, rootState.roots, DETAIL_CLOSE_SELECTORS);
|
|
823
823
|
if (closeTarget) {
|
|
824
824
|
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
|
-
}
|
|
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
|
+
}
|
|
830
830
|
attempts.push({
|
|
831
831
|
mode: "close-selector",
|
|
832
832
|
selector: closeTarget.selector,
|
|
@@ -856,35 +856,35 @@ export async function closeRecommendDetail(client, {
|
|
|
856
856
|
closed: closedAfterClick.closed,
|
|
857
857
|
elapsed_ms: closedAfterClick.elapsed_ms
|
|
858
858
|
});
|
|
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" });
|
|
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" });
|
|
888
888
|
|
|
889
889
|
const closedAfterEscape = await waitForRecommendDetailClosed(client, {
|
|
890
890
|
timeoutMs: escapeWaitMs,
|
|
@@ -901,33 +901,33 @@ export async function closeRecommendDetail(client, {
|
|
|
901
901
|
attempts
|
|
902
902
|
};
|
|
903
903
|
}
|
|
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
|
-
}
|
|
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
|
+
}
|
|
931
931
|
|
|
932
932
|
async function findVisibleCloseTarget(client, roots, selectors) {
|
|
933
933
|
let fallback = null;
|
|
@@ -959,107 +959,107 @@ async function findVisibleCloseTarget(client, roots, selectors) {
|
|
|
959
959
|
return fallback;
|
|
960
960
|
}
|
|
961
961
|
|
|
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, {
|
|
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, {
|
|
1063
1063
|
cardCandidate,
|
|
1064
1064
|
cardNodeId,
|
|
1065
1065
|
detailState,
|