287 lines
10 KiB
JavaScript
287 lines
10 KiB
JavaScript
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<void>}
|
||
*/
|
||
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);
|
||
|
||
// 第三步:获取额度信息
|
||
await page.waitForTimeout(2000);
|
||
let credits = null;
|
||
try {
|
||
console.log('💳 步骤 3: 获取额度信息...');
|
||
const creditsElement = page.locator('.left-number').first();
|
||
const creditsText = await creditsElement.textContent({ timeout: 5000 });
|
||
credits = creditsText.trim();
|
||
console.log('✅ 额度:', credits);
|
||
} catch (e) {
|
||
console.log('⚠️ 获取额度失败:', e.message);
|
||
}
|
||
|
||
return { success: true, credits };
|
||
} catch (e) {
|
||
console.log('⚠️ 跳转 Dashboard 失败:', e.message);
|
||
}
|
||
|
||
return { success: true, credits: null };
|
||
} 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;
|
||
}
|
||
}
|