tt-help-cli-ycl 1.4.7 → 1.4.8
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/package.json +1 -1
- package/src/cli/comments.js +676 -407
- package/src/cli/tag.js +1 -1
- package/src/lib/args.js +2 -2
- package/src/lib/constants.js +1 -1
- package/src/lib/modal-handler.js +592 -0
package/src/cli/tag.js
CHANGED
|
@@ -735,7 +735,7 @@ export async function handleScoreAll(parsed) {
|
|
|
735
735
|
),
|
|
736
736
|
nodeHeapMb: Math.max(
|
|
737
737
|
128,
|
|
738
|
-
parseInt(process.env.TAG_SCOREALL_RECYCLE_HEAP_MB || "
|
|
738
|
+
parseInt(process.env.TAG_SCOREALL_RECYCLE_HEAP_MB || "2000", 10) || 2000,
|
|
739
739
|
),
|
|
740
740
|
edgeMb: Math.max(
|
|
741
741
|
500,
|
package/src/lib/args.js
CHANGED
|
@@ -668,7 +668,7 @@ function parseCommentsArgs(args) {
|
|
|
668
668
|
let commentsParallel = 1;
|
|
669
669
|
let commentsInterval = 10;
|
|
670
670
|
let commentsServer = defaultServer;
|
|
671
|
-
let commentsBasePort =
|
|
671
|
+
let commentsBasePort = 6222;
|
|
672
672
|
let commentsProxy = null;
|
|
673
673
|
|
|
674
674
|
const positional = [];
|
|
@@ -686,7 +686,7 @@ function parseCommentsArgs(args) {
|
|
|
686
686
|
} else if (arg === "-m" || arg === "--max-comments") {
|
|
687
687
|
commentsMax = parseInt(args[++i]) || 20;
|
|
688
688
|
} else if (arg === "--base-port") {
|
|
689
|
-
commentsBasePort = parseInt(args[++i]) ||
|
|
689
|
+
commentsBasePort = parseInt(args[++i]) || 6222;
|
|
690
690
|
} else if (arg === "--proxy") {
|
|
691
691
|
commentsProxy = args[++i];
|
|
692
692
|
} else {
|
package/src/lib/constants.js
CHANGED
|
@@ -268,7 +268,7 @@ const HELP_TEXT = [
|
|
|
268
268
|
" 环境变量:",
|
|
269
269
|
" TAG_SCOREALL_PORT_POOL_SIZE=N 端口池大小(默认 10)",
|
|
270
270
|
" TAG_SCOREALL_RECYCLE_RSS_MB=N 内存回收阈值 RSS MB(默认 900)",
|
|
271
|
-
" TAG_SCOREALL_RECYCLE_HEAP_MB=N 内存回收阈值 Heap MB(默认
|
|
271
|
+
" TAG_SCOREALL_RECYCLE_HEAP_MB=N 内存回收阈值 Heap MB(默认 2000)",
|
|
272
272
|
" 示例: tt-help tag score-all",
|
|
273
273
|
" tt-help tag score-all --countries ES -s http://127.0.0.1:3001",
|
|
274
274
|
" tt-help tag score-all --port 9222 -s http://127.0.0.1:3001 --discover",
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用弹窗检测与关闭模块
|
|
3
|
+
*
|
|
4
|
+
* 用于识别 TikTok 页面上的各类弹窗,并提供关闭方法。
|
|
5
|
+
* 支持:登录弹窗、新手引导、Cookie/同意弹窗、验证码弹窗等。
|
|
6
|
+
*
|
|
7
|
+
* 基于实际 CDP 探索发现的 TikTok DOM 结构编写。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ============================================================
|
|
11
|
+
// 弹窗检测
|
|
12
|
+
// ============================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 已知的弹窗类型常量
|
|
16
|
+
*/
|
|
17
|
+
export const MODAL_TYPES = {
|
|
18
|
+
MODAL_CONTAINER: "modal-container", // TikTok DivModalContainer
|
|
19
|
+
LOGIN_MODAL: "login-modal", // 登录弹窗
|
|
20
|
+
ROLE_DIALOG: "role-dialog", // [role="dialog"]
|
|
21
|
+
USER_GUIDE: "user-guide", // 新手引导 #new-user-guide-overlay
|
|
22
|
+
CAPTCHA: "captcha", // 验证码 .captcha-verify-container
|
|
23
|
+
COOKIE_BANNER: "cookie-banner", // Cookie/同意弹窗
|
|
24
|
+
GENERIC_OVERLAY: "generic-overlay", // 通用高 z-index 浮层
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 检测页面上所有可见的弹窗。
|
|
29
|
+
*
|
|
30
|
+
* 扫描策略(按优先级):
|
|
31
|
+
* 1. TikTok 通用弹窗容器 `div[class*="DivModalContainer"]`
|
|
32
|
+
* 2. 登录弹窗 `#login-modal`
|
|
33
|
+
* 3. ARIA dialog `[role="dialog"]`
|
|
34
|
+
* 4. 新手引导 `#new-user-guide-overlay`
|
|
35
|
+
* 5. 验证码 `.captcha-verify-container`
|
|
36
|
+
* 6. Cookie/同意弹窗
|
|
37
|
+
* 7. 通用高 z-index 固定定位浮层
|
|
38
|
+
*
|
|
39
|
+
* @param {import('playwright').Page} page
|
|
40
|
+
* @returns {Promise<Array<{type: string, visible: boolean, text?: string, zIndex?: number, rect?: object}>>}
|
|
41
|
+
*/
|
|
42
|
+
export async function detectModals(page) {
|
|
43
|
+
return page.evaluate(() => {
|
|
44
|
+
const results = [];
|
|
45
|
+
|
|
46
|
+
// --- 1. TikTok 通用弹窗容器 ---
|
|
47
|
+
const containers = document.querySelectorAll(
|
|
48
|
+
'div[class*="DivModalContainer"]',
|
|
49
|
+
);
|
|
50
|
+
for (const el of containers) {
|
|
51
|
+
const style = window.getComputedStyle(el);
|
|
52
|
+
if (style.display === "none" || style.visibility === "hidden") continue;
|
|
53
|
+
const rect = el.getBoundingClientRect();
|
|
54
|
+
results.push({
|
|
55
|
+
type: "modal-container",
|
|
56
|
+
visible: true,
|
|
57
|
+
text: el.textContent?.trim()?.substring(0, 120),
|
|
58
|
+
zIndex: parseInt(style.zIndex) || 0,
|
|
59
|
+
rect: {
|
|
60
|
+
x: Math.round(rect.x),
|
|
61
|
+
y: Math.round(rect.y),
|
|
62
|
+
w: Math.round(rect.width),
|
|
63
|
+
h: Math.round(rect.height),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- 2. 登录弹窗 ---
|
|
69
|
+
const loginModal = document.getElementById("login-modal");
|
|
70
|
+
if (loginModal && loginModal.offsetParent !== null) {
|
|
71
|
+
results.push({
|
|
72
|
+
type: "login-modal",
|
|
73
|
+
visible: true,
|
|
74
|
+
text: loginModal.textContent?.trim()?.substring(0, 120),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- 3. ARIA dialog ---
|
|
79
|
+
const dialogs = document.querySelectorAll(
|
|
80
|
+
'[role="dialog"], [role="alertdialog"]',
|
|
81
|
+
);
|
|
82
|
+
for (const el of dialogs) {
|
|
83
|
+
if (el.offsetParent !== null) {
|
|
84
|
+
const rect = el.getBoundingClientRect();
|
|
85
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
86
|
+
results.push({
|
|
87
|
+
type: "role-dialog",
|
|
88
|
+
visible: true,
|
|
89
|
+
text: el.textContent?.trim()?.substring(0, 100),
|
|
90
|
+
rect: {
|
|
91
|
+
x: Math.round(rect.x),
|
|
92
|
+
y: Math.round(rect.y),
|
|
93
|
+
w: Math.round(rect.width),
|
|
94
|
+
h: Math.round(rect.height),
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- 4. 新手引导 ---
|
|
102
|
+
const guide = document.getElementById("new-user-guide-overlay");
|
|
103
|
+
if (guide && guide.offsetParent !== null) {
|
|
104
|
+
results.push({
|
|
105
|
+
type: "user-guide",
|
|
106
|
+
visible: true,
|
|
107
|
+
text: guide.textContent?.trim()?.substring(0, 80),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- 5. 验证码 ---
|
|
112
|
+
const captcha = document.querySelector(".captcha-verify-container");
|
|
113
|
+
if (captcha && captcha.offsetParent !== null) {
|
|
114
|
+
results.push({
|
|
115
|
+
type: "captcha",
|
|
116
|
+
visible: true,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- 6. Cookie/同意弹窗 ---
|
|
121
|
+
const cookieEls = document.querySelectorAll(
|
|
122
|
+
'[class*="cookie"], [class*="consent"], [class*="gdpr"], [class*="Cookie"], [class*="Consent"]',
|
|
123
|
+
);
|
|
124
|
+
for (const el of cookieEls) {
|
|
125
|
+
if (el.offsetParent !== null) {
|
|
126
|
+
const rect = el.getBoundingClientRect();
|
|
127
|
+
if (rect.width > 100 && rect.height > 40) {
|
|
128
|
+
results.push({
|
|
129
|
+
type: "cookie-banner",
|
|
130
|
+
visible: true,
|
|
131
|
+
text: el.textContent?.trim()?.substring(0, 100),
|
|
132
|
+
rect: {
|
|
133
|
+
x: Math.round(rect.x),
|
|
134
|
+
y: Math.round(rect.y),
|
|
135
|
+
w: Math.round(rect.width),
|
|
136
|
+
h: Math.round(rect.height),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
break; // 一个已足够
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- 7. 通用高 z-index 固定定位浮层(兜底) ---
|
|
145
|
+
for (const el of document.querySelectorAll("div")) {
|
|
146
|
+
const style = window.getComputedStyle(el);
|
|
147
|
+
const z = parseInt(style.zIndex) || 0;
|
|
148
|
+
const rect = el.getBoundingClientRect();
|
|
149
|
+
if (
|
|
150
|
+
(style.position === "fixed" || style.position === "absolute") &&
|
|
151
|
+
z > 1000 &&
|
|
152
|
+
rect.width > 200 &&
|
|
153
|
+
rect.height > 100 &&
|
|
154
|
+
style.display !== "none" &&
|
|
155
|
+
style.visibility !== "hidden" &&
|
|
156
|
+
el.offsetParent !== null
|
|
157
|
+
) {
|
|
158
|
+
const text = el.textContent?.trim();
|
|
159
|
+
// 排除空内容(如 TUXToastProvider)
|
|
160
|
+
if (text && text.length > 10) {
|
|
161
|
+
results.push({
|
|
162
|
+
type: "generic-overlay",
|
|
163
|
+
visible: true,
|
|
164
|
+
text: text.substring(0, 100),
|
|
165
|
+
zIndex: z,
|
|
166
|
+
rect: {
|
|
167
|
+
x: Math.round(rect.x),
|
|
168
|
+
y: Math.round(rect.y),
|
|
169
|
+
w: Math.round(rect.width),
|
|
170
|
+
h: Math.round(rect.height),
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return results;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 检测页面是否有任何可见弹窗。
|
|
183
|
+
*
|
|
184
|
+
* @param {import('playwright').Page} page
|
|
185
|
+
* @returns {Promise<boolean>}
|
|
186
|
+
*/
|
|
187
|
+
export async function hasAnyModal(page) {
|
|
188
|
+
const modals = await detectModals(page);
|
|
189
|
+
return modals.some((m) => m.visible);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ============================================================
|
|
193
|
+
// 弹窗关闭
|
|
194
|
+
// ============================================================
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 关闭页面上所有检测到的弹窗。
|
|
198
|
+
*
|
|
199
|
+
* 对每种弹窗类型使用对应的关闭策略:
|
|
200
|
+
* - modal-container: 点击遮罩层 > 按 Escape > 找关闭按钮
|
|
201
|
+
* - login-modal: 同上
|
|
202
|
+
* - role-dialog: 按 Escape > 找关闭按钮
|
|
203
|
+
* - user-guide: 点击"知道了"按钮
|
|
204
|
+
* - captcha: 委托给 closeCaptcha
|
|
205
|
+
* - cookie-banner: 找 Accept/同意按钮
|
|
206
|
+
* - generic-overlay: 按 Escape
|
|
207
|
+
*
|
|
208
|
+
* @param {import('playwright').Page} page
|
|
209
|
+
* @param {object} [options]
|
|
210
|
+
* @param {number} [options.waitMs=500] - 每次关闭后等待时间(ms)
|
|
211
|
+
* @returns {Promise<{closed: string[], failed: string[]}>}
|
|
212
|
+
*/
|
|
213
|
+
export async function closeAllModals(page, options = {}) {
|
|
214
|
+
const { waitMs = 500 } = options;
|
|
215
|
+
const closed = [];
|
|
216
|
+
const failed = [];
|
|
217
|
+
|
|
218
|
+
const modals = await detectModals(page);
|
|
219
|
+
if (modals.length === 0) return { closed, failed };
|
|
220
|
+
|
|
221
|
+
for (const modal of modals) {
|
|
222
|
+
// 在尝试关闭前,先检查弹窗是否还存在(可能已被上一轮关闭连带清除)
|
|
223
|
+
const stillPresent = await isModalStillPresent(page, modal);
|
|
224
|
+
if (!stillPresent) {
|
|
225
|
+
// 弹窗已随父级一同消失,算作已关闭
|
|
226
|
+
closed.push(modal.type + "(parent-closed)");
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const result = await closeModalByType(page, modal);
|
|
232
|
+
if (result) {
|
|
233
|
+
closed.push(modal.type);
|
|
234
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
235
|
+
} else {
|
|
236
|
+
// 关闭函数返回 false,但也许弹窗已经不在了
|
|
237
|
+
const gone = await isModalStillPresent(page, modal);
|
|
238
|
+
if (!gone) {
|
|
239
|
+
closed.push(modal.type + "(gone-after-close)");
|
|
240
|
+
} else {
|
|
241
|
+
failed.push(modal.type);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch (e) {
|
|
245
|
+
// 如果关闭时出错但弹窗已经不在了,算作成功
|
|
246
|
+
const gone = await isModalStillPresent(page, modal);
|
|
247
|
+
if (!gone) {
|
|
248
|
+
closed.push(modal.type + "(gone-after-error)");
|
|
249
|
+
} else {
|
|
250
|
+
failed.push(`${modal.type}: ${e.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { closed, failed };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 检查某类型弹窗是否仍然存在于页面上。
|
|
260
|
+
* @param {import('playwright').Page} page
|
|
261
|
+
* @param {{type: string}} modal
|
|
262
|
+
* @returns {Promise<boolean>}
|
|
263
|
+
*/
|
|
264
|
+
async function isModalStillPresent(page, modal) {
|
|
265
|
+
return page.evaluate((type) => {
|
|
266
|
+
switch (type) {
|
|
267
|
+
case "modal-container": {
|
|
268
|
+
const c = document.querySelector('div[class*="DivModalContainer"]');
|
|
269
|
+
return c !== null && window.getComputedStyle(c).display !== "none";
|
|
270
|
+
}
|
|
271
|
+
case "login-modal": {
|
|
272
|
+
const m = document.getElementById("login-modal");
|
|
273
|
+
return m !== null && m.offsetParent !== null;
|
|
274
|
+
}
|
|
275
|
+
case "role-dialog": {
|
|
276
|
+
const d = document.querySelector('[role="dialog"]');
|
|
277
|
+
return d !== null && d.offsetParent !== null;
|
|
278
|
+
}
|
|
279
|
+
case "user-guide": {
|
|
280
|
+
const g = document.getElementById("new-user-guide-overlay");
|
|
281
|
+
return g !== null && g.offsetParent !== null;
|
|
282
|
+
}
|
|
283
|
+
case "captcha": {
|
|
284
|
+
const c = document.querySelector(".captcha-verify-container");
|
|
285
|
+
return c !== null && c.offsetParent !== null;
|
|
286
|
+
}
|
|
287
|
+
default:
|
|
288
|
+
return true; // 无法判断时假设还存在
|
|
289
|
+
}
|
|
290
|
+
}, modal.type);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 根据弹窗类型执行关闭操作。
|
|
295
|
+
* @param {import('playwright').Page} page
|
|
296
|
+
* @param {{type: string}} modal
|
|
297
|
+
* @returns {Promise<boolean>}
|
|
298
|
+
*/
|
|
299
|
+
async function closeModalByType(page, modal) {
|
|
300
|
+
switch (modal.type) {
|
|
301
|
+
case MODAL_TYPES.MODAL_CONTAINER:
|
|
302
|
+
return await closeDivModalContainer(page);
|
|
303
|
+
|
|
304
|
+
case MODAL_TYPES.LOGIN_MODAL:
|
|
305
|
+
return await closeLoginModal(page);
|
|
306
|
+
|
|
307
|
+
case MODAL_TYPES.ROLE_DIALOG:
|
|
308
|
+
return await closeRoleDialog(page);
|
|
309
|
+
|
|
310
|
+
case MODAL_TYPES.USER_GUIDE:
|
|
311
|
+
return await closeUserGuide(page);
|
|
312
|
+
|
|
313
|
+
case MODAL_TYPES.CAPTCHA:
|
|
314
|
+
return await closeCaptchaModal(page);
|
|
315
|
+
|
|
316
|
+
case MODAL_TYPES.COOKIE_BANNER:
|
|
317
|
+
return await closeCookieBanner(page);
|
|
318
|
+
|
|
319
|
+
case MODAL_TYPES.GENERIC_OVERLAY:
|
|
320
|
+
return await closeGenericOverlay(page);
|
|
321
|
+
|
|
322
|
+
default:
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---- 各类弹窗关闭实现 ----
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 关闭 TikTok DivModalContainer 弹窗。
|
|
331
|
+
*
|
|
332
|
+
* 策略:
|
|
333
|
+
* 1. 点击遮罩层 (DivModalMask)
|
|
334
|
+
* 2. 找关闭按钮(data-e2e="modal-close" 或 class 含 close 的元素)
|
|
335
|
+
* 3. 按 Escape 键
|
|
336
|
+
*/
|
|
337
|
+
async function closeDivModalContainer(page) {
|
|
338
|
+
// 策略1:点击遮罩层(点击弹窗外的黑色半透明区域)
|
|
339
|
+
const maskClicked = await page.evaluate(() => {
|
|
340
|
+
const mask = document.querySelector('div[class*="DivModalMask"]');
|
|
341
|
+
if (mask && mask.offsetParent !== null) {
|
|
342
|
+
mask.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
343
|
+
mask.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
344
|
+
mask.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
return false;
|
|
348
|
+
});
|
|
349
|
+
if (maskClicked) return true;
|
|
350
|
+
|
|
351
|
+
// 策略2:找关闭按钮
|
|
352
|
+
const closeBtnClicked = await page.evaluate(() => {
|
|
353
|
+
const container = document.querySelector('div[class*="DivModalContainer"]');
|
|
354
|
+
if (!container) return false;
|
|
355
|
+
|
|
356
|
+
// 尝试多种选择器
|
|
357
|
+
const selectors = [
|
|
358
|
+
'[data-e2e="modal-close"]',
|
|
359
|
+
'div[class*="close"]',
|
|
360
|
+
'div[class*="Close"]',
|
|
361
|
+
'button[class*="close"]',
|
|
362
|
+
'button[class*="Close"]',
|
|
363
|
+
'span[class*="close"]',
|
|
364
|
+
'span[class*="Close"]',
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
for (const sel of selectors) {
|
|
368
|
+
const btn = container.querySelector(sel);
|
|
369
|
+
if (btn && btn.offsetParent !== null) {
|
|
370
|
+
btn.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
371
|
+
btn.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
372
|
+
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 兜底:找右上角的小型可点击元素(关闭按钮通常位于弹窗右上角)
|
|
378
|
+
const allEls = container.querySelectorAll("div, button, span");
|
|
379
|
+
const containerRect = container.getBoundingClientRect();
|
|
380
|
+
for (const el of allEls) {
|
|
381
|
+
const r = el.getBoundingClientRect();
|
|
382
|
+
// 右上角、小尺寸
|
|
383
|
+
if (
|
|
384
|
+
r.width > 15 &&
|
|
385
|
+
r.width < 60 &&
|
|
386
|
+
r.height > 15 &&
|
|
387
|
+
r.height < 60 &&
|
|
388
|
+
r.x > containerRect.x + containerRect.width / 2 &&
|
|
389
|
+
r.y < containerRect.y + 60 &&
|
|
390
|
+
el.offsetParent !== null
|
|
391
|
+
) {
|
|
392
|
+
el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
393
|
+
el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
394
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return false;
|
|
400
|
+
});
|
|
401
|
+
if (closeBtnClicked) return true;
|
|
402
|
+
|
|
403
|
+
// 策略3:Escape 键
|
|
404
|
+
await page.keyboard.press("Escape");
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 关闭登录弹窗。
|
|
410
|
+
*/
|
|
411
|
+
async function closeLoginModal(page) {
|
|
412
|
+
return await closeDivModalContainer(page);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* 关闭 role="dialog" 弹窗。
|
|
417
|
+
*/
|
|
418
|
+
async function closeRoleDialog(page) {
|
|
419
|
+
// 先尝试 Escape
|
|
420
|
+
await page.keyboard.press("Escape");
|
|
421
|
+
|
|
422
|
+
// 再尝试找关闭按钮
|
|
423
|
+
return page.evaluate(() => {
|
|
424
|
+
const dialog = document.querySelector(
|
|
425
|
+
'[role="dialog"], [role="alertdialog"]',
|
|
426
|
+
);
|
|
427
|
+
if (!dialog) return false;
|
|
428
|
+
|
|
429
|
+
const closeBtn =
|
|
430
|
+
dialog.querySelector('[data-e2e="modal-close"]') ||
|
|
431
|
+
dialog.querySelector(
|
|
432
|
+
'div[class*="close"], button[class*="close"], span[class*="close"]',
|
|
433
|
+
) ||
|
|
434
|
+
dialog.querySelector(
|
|
435
|
+
'div[class*="Close"], button[class*="Close"], span[class*="Close"]',
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
if (closeBtn) {
|
|
439
|
+
closeBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* 关闭新手引导弹窗。
|
|
448
|
+
*/
|
|
449
|
+
async function closeUserGuide(page) {
|
|
450
|
+
return page.evaluate(() => {
|
|
451
|
+
const overlay = document.getElementById("new-user-guide-overlay");
|
|
452
|
+
if (!overlay || overlay.offsetParent === null) return false;
|
|
453
|
+
|
|
454
|
+
// 点击"知道了"按钮
|
|
455
|
+
const btns = overlay.querySelectorAll("button, div, span");
|
|
456
|
+
for (const btn of btns) {
|
|
457
|
+
if (btn.textContent?.trim() === "知道了" && btn.offsetParent !== null) {
|
|
458
|
+
btn.click();
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 兜底:直接点击 overlay
|
|
464
|
+
overlay.click();
|
|
465
|
+
return true;
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* 关闭验证码弹窗(复用现有 capcha-handler 逻辑)。
|
|
471
|
+
*/
|
|
472
|
+
async function closeCaptchaModal(page) {
|
|
473
|
+
return page.evaluate(() => {
|
|
474
|
+
const closeBtn = document.getElementById("captcha_close_button");
|
|
475
|
+
if (!closeBtn) return false;
|
|
476
|
+
|
|
477
|
+
closeBtn.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
478
|
+
closeBtn.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
479
|
+
closeBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
480
|
+
return true;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 关闭 Cookie / Consent 弹窗。
|
|
486
|
+
*/
|
|
487
|
+
async function closeCookieBanner(page) {
|
|
488
|
+
const clicked = await page.evaluate(() => {
|
|
489
|
+
// 找 Accept / Agree / 同意 / 允许 按钮
|
|
490
|
+
const acceptPatterns = [
|
|
491
|
+
/^accept/i,
|
|
492
|
+
/^agree/i,
|
|
493
|
+
/^allow/i,
|
|
494
|
+
/^ok$/i,
|
|
495
|
+
/^同意/,
|
|
496
|
+
/^允许/,
|
|
497
|
+
/^接受/,
|
|
498
|
+
/^知道了/,
|
|
499
|
+
/^got it/i,
|
|
500
|
+
/^i understand/i,
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
const banners = document.querySelectorAll(
|
|
504
|
+
'[class*="cookie"], [class*="consent"], [class*="gdpr"], [class*="Cookie"], [class*="Consent"]',
|
|
505
|
+
);
|
|
506
|
+
for (const banner of banners) {
|
|
507
|
+
if (banner.offsetParent === null) continue;
|
|
508
|
+
|
|
509
|
+
const btns = banner.querySelectorAll('button, a, div[role="button"]');
|
|
510
|
+
for (const btn of btns) {
|
|
511
|
+
const text = btn.textContent?.trim();
|
|
512
|
+
if (text && acceptPatterns.some((p) => p.test(text))) {
|
|
513
|
+
btn.click();
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return false;
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (!clicked) {
|
|
522
|
+
await page.keyboard.press("Escape");
|
|
523
|
+
}
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* 关闭通用浮层(使用 Escape)。
|
|
529
|
+
*/
|
|
530
|
+
async function closeGenericOverlay(page) {
|
|
531
|
+
await page.keyboard.press("Escape");
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ============================================================
|
|
536
|
+
// 便捷函数
|
|
537
|
+
// ============================================================
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* 等待并关闭弹窗(轮询模式)。
|
|
541
|
+
*
|
|
542
|
+
* 持续检测弹窗直到超时或弹窗被关闭。
|
|
543
|
+
* 适用于:需要等待用户手动解决验证码,或等待弹窗自行出现的场景。
|
|
544
|
+
*
|
|
545
|
+
* @param {import('playwright').Page} page
|
|
546
|
+
* @param {object} [options]
|
|
547
|
+
* @param {number} [options.timeout=30000] - 总超时时间(ms)
|
|
548
|
+
* @param {number} [options.pollInterval=1000] - 轮询间隔(ms)
|
|
549
|
+
* @param {boolean} [options.closeOnDetect=true] - 检测到弹窗后是否自动关闭
|
|
550
|
+
* @param {function} [options.onDetect] - 检测到弹窗时的回调
|
|
551
|
+
* @returns {Promise<{detected: boolean, closed: boolean, elapsed: number}>}
|
|
552
|
+
*/
|
|
553
|
+
export async function waitAndHandleModal(page, options = {}) {
|
|
554
|
+
const {
|
|
555
|
+
timeout = 30000,
|
|
556
|
+
pollInterval = 1000,
|
|
557
|
+
closeOnDetect = true,
|
|
558
|
+
onDetect,
|
|
559
|
+
} = options;
|
|
560
|
+
|
|
561
|
+
const startTime = Date.now();
|
|
562
|
+
const deadline = startTime + timeout;
|
|
563
|
+
|
|
564
|
+
while (Date.now() < deadline) {
|
|
565
|
+
const modals = await detectModals(page);
|
|
566
|
+
|
|
567
|
+
if (modals.length > 0) {
|
|
568
|
+
if (onDetect) {
|
|
569
|
+
await onDetect(modals);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (closeOnDetect) {
|
|
573
|
+
const { closed, failed } = await closeAllModals(page);
|
|
574
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
575
|
+
return {
|
|
576
|
+
detected: true,
|
|
577
|
+
closed: failed.length === 0,
|
|
578
|
+
elapsed,
|
|
579
|
+
details: { closed, failed },
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
584
|
+
return { detected: true, closed: false, elapsed };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
591
|
+
return { detected: false, closed: false, elapsed };
|
|
592
|
+
}
|