import { config } from './config.js'; /** * 处理 Cloudflare Turnstile 验证 * @param {Page} page - Playwright 页面对象 */ async function handleTurnstile(page) { console.log('🔐 检查 Cloudflare Turnstile 验证...'); try { // 使用 frame 或 frameLocator 两种方式尝试定位 Cloudflare 挑战 iframe const cfFrames = page.frames().filter(f => /cloudflare|turnstile|challenges/i.test(f.url())); const cfFrameLocator = page.frameLocator('iframe[title*="Cloudflare" i], iframe[src*="challenges.cloudflare.com" i], iframe[src*="turnstile" i]').first(); const hasLocator = await cfFrameLocator.locator('body').count().catch(() => 0); const hasFrame = cfFrames.length > 0; if (hasLocator || hasFrame) { console.log('[LOG] 发现 Cloudflare 验证 iframe'); async function tryClickIn(locatorBase) { const possibleTargets = [ 'input[type="checkbox"]', 'div[role="checkbox"]', 'label:has-text("确认您是真人")', 'label:has-text("I am human")', 'span:has-text("确认您是真人")', 'span:has-text("I am human")', '#cf-stage label', 'button' ]; for (const sel of possibleTargets) { const loc = locatorBase.locator(sel).first(); if (await loc.count() > 0 && await loc.isVisible()) { console.log('[LOG] 尝试点击选择器:', sel); try { await loc.click({ force: true }); return true; } catch {} } } return false; } let clicked = false; // 方案 A:用 frameLocator 点击 try { clicked = await tryClickIn(cfFrameLocator); } catch {} // 方案 B:直接用 frame 对象点击 if (!clicked) { for (const f of cfFrames) { const base = f.locator('html'); try { if (await tryClickIn(base)) { clicked = true; break; } } catch {} } } // 如果没有匹配元素,尝试点击 iframe 中心 if (!clicked) { console.log('[LOG] 未找到明确的控件,点击 iframe 中心尝试通过'); const handle = await cfFrameLocator.elementHandle().catch(() => null); if (handle) { const box = await handle.boundingBox(); if (box) await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); } } // 等待验证成功,观察注册按钮是否可点击 const signUpCandidate = page.locator('.sign-button [role="button"], .sign-button button, button:has-text("Sign Up"), [role="button"]:has-text("Sign Up")').first(); try { await page.waitForFunction( (el) => { if (!el) return false; const cls = el.getAttribute('class') || ''; const aria = el.getAttribute('aria-disabled'); return !cls.includes('disabled') && aria !== 'true'; }, signUpCandidate, { timeout: 10000 } ); console.log('✅ Turnstile 验证完成(按钮已可点击)'); } catch { console.log('ℹ️ 未检测到按钮状态变化,继续流程'); } } else { console.log('ℹ️ 未找到 Cloudflare 验证 iframe,可能已自动通过'); } } catch (error) { console.log('ℹ️ Turnstile 处理异常:', error.message); } } /** * 在 Verdent.ai 上执行注册流程 * @param {Page} page - Playwright 页面对象 * @param {string} email - 邮箱地址 * @returns {Promise} */ export async function registerOnVerdent(page, email) { console.log('🌐 开始 Verdent.ai 注册流程...'); // 打开注册页面(带重试) let pageOpened = false; for (let attempt = 1; attempt <= 3; attempt++) { try { console.log(`[尝试 ${attempt}/3] 打开注册页面...`); await page.goto(config.verdent.signupUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }); pageOpened = true; console.log('✅ 页面加载成功'); break; } catch (e) { console.log(`⚠️ 第 ${attempt} 次打开失败:`, e.message); if (attempt < 3) { console.log('等待 3 秒后重试...'); await page.waitForTimeout(3000); } } } if (!pageOpened) { throw new Error('打开注册页面失败,已重试 3 次'); } await page.waitForTimeout(3000); // 输入邮箱地址 console.log('📝 输入邮箱:', email); const emailInput = page.locator('input[type="email"], input[placeholder*="email" i], input[placeholder*="邮箱"]').first(); await emailInput.fill(email); await page.waitForTimeout(1000); // 处理 Turnstile 验证(如果存在) await handleTurnstile(page); // 点击发送验证码按钮 console.log('📤 点击发送验证码...'); const sendCodeButton = page.locator('button.send-code-button').first(); // 等待按钮变为可点击状态 await page.waitForTimeout(2000); // 检查按钮是否被禁用 const isDisabled = await sendCodeButton.getAttribute('disabled'); if (isDisabled !== null) { console.log('⚠️ 发送验证码按钮被禁用,可能需要先完成其他验证'); // 再次尝试处理 Turnstile await handleTurnstile(page); await page.waitForTimeout(2000); } await sendCodeButton.click(); const sentAtMs = Date.now(); console.log('✅ 验证码已发送 at', new Date(sentAtMs).toISOString()); return { page, sentAtMs }; } /** * 填写验证码和密码,完成注册 * @param {Page} page - Playwright 页面对象 * @param {string} verificationCode - 验证码 */ export async function completeRegistration(page, verificationCode) { console.log('✍️ 填写验证码和密码...'); // 输入验证码 console.log('输入验证码:', verificationCode); const codeInput = page.locator('input[placeholder*="Verification code" i], input[autocomplete="one-time-code"]').first(); await codeInput.fill(verificationCode); await page.waitForTimeout(1000); // 输入密码 console.log('输入密码...'); const passwordInput = page.locator('input[type="password"][placeholder*="Password" i], input[autocomplete="new-password"]').first(); await passwordInput.fill(config.verdent.password); await page.waitForTimeout(1000); // 点击注册按钮 console.log('🚀 点击注册按钮...'); console.log('[LOG] 查找注册按钮...'); // 等待一下,让表单验证完成 await page.waitForTimeout(2000); // 尝试多种方式定位按钮(包含 div[role=button]) let signUpButton = page.locator('.sign-button [role="button"], .sign-button button').first(); let buttonCount = await signUpButton.count(); console.log('[LOG] 找到 .sign-button [role=button]/button:', buttonCount); if (buttonCount === 0) { signUpButton = page.locator('button:has-text("Sign Up"), [role="button"]:has-text("Sign Up"), [role="button"]:has-text("注册")').first(); buttonCount = await signUpButton.count(); console.log('[LOG] 找到包含 Sign Up/注册 文本的按钮:', buttonCount); } if (buttonCount === 0) { console.log('[LOG] 未找到按钮,输出页面内容调试...'); const roles = page.locator('[role="button"]'); const allRoleBtnCount = await roles.count(); console.log('[LOG] 页面上 role=button 数量', allRoleBtnCount); for (let i = 0; i < Math.min(allRoleBtnCount, 5); i++) { const btn = roles.nth(i); const text = (await btn.textContent() || '').trim(); console.log(`[LOG] role 按钮 ${i+1}:`, text); } throw new Error('未找到注册按钮'); } // 检查按钮状态并等待启用 try { await page.waitForFunction( (el) => { if (!el) return false; const cls = el.getAttribute('class') || ''; const aria = el.getAttribute('aria-disabled'); return !cls.includes('disabled') && aria !== 'true'; }, signUpButton, { timeout: 15000 } ); } catch (error) { console.log('[LOG] 等待按钮启用超时:', error.message); } try { await signUpButton.click({ timeout: 5000, force: true }); console.log('✅ 注册请求已提交'); // 等待跳转或成功提示 await page.waitForTimeout(5000); // 检查是否注册成功 const currentUrl = page.url(); console.log('当前页面:', currentUrl); if (!currentUrl.includes('/signup')) { console.log('🎉 注册成功!'); // 第一步:点击右上角用户头像展开菜单 try { console.log('👤 步骤 1: 点击用户头像展开菜单...'); const userAvatar = page.locator('.user-avatar').first(); await userAvatar.click({ timeout: 5000 }); await page.waitForTimeout(1500); // 第二步:点击 Dashboard 链接 console.log('📊 步骤 2: 点击 Dashboard 链接...'); const dashboardLink = page.locator('.link:has-text("Dashboard")').first(); await dashboardLink.click({ timeout: 5000 }); await page.waitForTimeout(2000); const finalUrl = page.url(); console.log('✅ 已跳转到 Dashboard:', finalUrl); } catch (e) { console.log('⚠️ 跳转 Dashboard 失败:', e.message); } return true; } else { // 检查是否有错误提示 const errorElement = page.locator('[class*="error"], [class*="alert"]').first(); if (await errorElement.count() > 0) { const errorText = await errorElement.textContent(); console.log('❌ 注册失败:', errorText); return false; } console.log('⚠️ 注册状态未知,请手动检查'); return null; } } catch (error) { console.log('❌ 点击注册按钮失败:', error.message); return false; } }