first init

This commit is contained in:
dengqichen 2025-11-13 14:31:44 +08:00
parent 5791928656
commit 6a4f0a9de3
17 changed files with 2130 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/node_modules/

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/auto-register-verdent.iml" filepath="$PROJECT_DIR$/.idea/auto-register-verdent.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

131
README.md Normal file
View File

@ -0,0 +1,131 @@
# Verdent.ai 自动注册程序
自动化注册 Verdent.ai 账号的 Node.js 程序,使用 Playwright 进行浏览器自动化。
## 功能特点
- ✅ 自动创建临时邮箱(使用 tempmail.plus
- ✅ 自动处理 Cloudflare Turnstile 验证
- ✅ 自动获取邮箱验证码
- ✅ 自动完成注册流程
- ✅ 模拟真实用户行为,避免被检测
## 项目结构
```
auto-register-verdent/
├── index.js # 主入口文件
├── emailModule.js # 邮箱处理模块
├── registerModule.js # 注册流程模块
├── config.js # 配置文件
├── package.json # 项目依赖
└── README.md # 说明文档
```
## 安装
1. 确保已安装 Node.js (v16 或更高版本)
2. 安装依赖:
```bash
npm install
```
3. 安装 Playwright 浏览器:
```bash
npx playwright install chromium
```
## 配置
编辑 `config.js` 文件以修改配置:
```javascript
export const config = {
// 临时邮箱配置
tempmail: {
url: 'https://tempmail.plus/',
username: 'qichen111', // 邮箱用户名
pinCode: '147258' // PIN 码保护
},
// Verdent 注册配置
verdent: {
signupUrl: 'https://www.verdent.ai/signup?source=verdent-deck',
password: 'Qichen5210523...' // 注册密码
},
// 浏览器配置
browser: {
headless: false, // false=显示浏览器窗口true=后台运行
slowMo: 100 // 操作延迟(毫秒)
}
};
```
## 使用方法
运行程序:
```bash
npm start
```
或者:
```bash
node index.js
```
## 工作流程
1. **设置临时邮箱**
- 访问 tempmail.plus
- 创建邮箱账号
- 设置 PIN 码保护
2. **开始注册**
- 访问 Verdent.ai 注册页面
- 填写邮箱地址
- 处理 Cloudflare Turnstile 验证
- 点击发送验证码
3. **获取验证码**
- 等待邮件到达
- 自动提取 6 位验证码
4. **完成注册**
- 填写验证码
- 填写密码
- 提交注册表单
## 注意事项
- ⚠️ 程序运行时会打开两个浏览器窗口(邮箱和注册)
- ⚠️ 如果遇到 Cloudflare 验证失败,程序会自动重试
- ⚠️ 注册成功后,浏览器会保持打开 30 秒以便查看结果
- ⚠️ 建议首次运行时将 `headless` 设置为 `false` 以观察流程
## 故障排除
### 问题:找不到元素
- 检查网站是否更新了页面结构
- 增加 `slowMo` 值以延长等待时间
### 问题Turnstile 验证失败
- Cloudflare 可能检测到自动化行为
- 尝试手动完成验证
### 问题:未收到验证码
- 检查邮箱是否正确创建
- 增加 `timeouts.emailWait`
## 技术栈
- Node.js
- Playwright (浏览器自动化)
- ES6 Modules
## 许可证
MIT

62
browser.js Normal file
View File

@ -0,0 +1,62 @@
import { chromium } from 'playwright';
import fetch from 'node-fetch';
// 连接指纹浏览器优先级:
// 1) AdsPower 本地 API设置 ADSPOWER_USER_ID
// 2) 自定义 WS 端点(设置 BROWSER_WS_ENDPOINT
// 3) 本地 Playwright 启动(默认)
export async function getBrowserAndContexts({ headless, slowMo }) {
// 尝试 AdsPower
const adspowerUserId = process.env.ADSPOWER_USER_ID;
if (adspowerUserId) {
console.log('[FP] 使用 AdsPower 指纹浏览器');
const apiBase = process.env.ADSPOWER_API || 'http://local.adspower.net:50325';
const startUrl = `${apiBase}/api/v1/browser/start?user_id=${encodeURIComponent(adspowerUserId)}`;
const resp = await fetch(startUrl);
const data = await resp.json();
if (data.code !== 0) {
throw new Error(`[AdsPower] 启动失败: ${data.msg || JSON.stringify(data)}`);
}
const wsEndpoint = data.data.ws && (data.data.ws.puppeteer || data.data.ws.selenium || data.data.ws.ws || data.data.ws);
console.log('[FP] AdsPower ws:', wsEndpoint);
const browser = await chromium.connectOverCDP(wsEndpoint);
const contexts = browser.contexts();
const baseContext = contexts[0] || await browser.newContext();
// 关闭所有已有页签,保持干净
for (const p of baseContext.pages()) { try { await p.close(); } catch {} }
// 在同一个指纹环境下创建两个页签(同上下文不同页)
return {
browser,
emailContext: baseContext,
registerContext: baseContext,
};
}
// 尝试自定义 WS 端点
const ws = process.env.BROWSER_WS_ENDPOINT;
if (ws) {
console.log('[FP] 连接到自定义指纹浏览器 WS');
const browser = await chromium.connectOverCDP(ws);
const contexts = browser.contexts();
const baseContext = contexts[0] || await browser.newContext();
for (const p of baseContext.pages()) { try { await p.close(); } catch {} }
return {
browser,
emailContext: baseContext,
registerContext: baseContext,
};
}
// 回退Playwright 本地启动
console.log('[FP] 未检测到指纹浏览器,使用内置 Chromium');
const browser = await chromium.launch({ headless, slowMo, args: ['--disable-blink-features=AutomationControlled', '--no-sandbox'] });
const emailContext = await browser.newContext({ viewport: { width: 1280, height: 720 } });
const registerContext = await browser.newContext({ viewport: { width: 1280, height: 720 } });
// 清理所有初始页面,保持上下文干净
for (const ctx of [emailContext, registerContext]) {
const pages = ctx.pages();
for (const p of pages) { try { await p.close(); } catch {} }
}
return { browser, emailContext, registerContext };
}

38
config.js Normal file
View File

@ -0,0 +1,38 @@
export const config = {
// 临时邮箱配置
tempmail: {
url: 'https://tempmail.plus/',
username: 'qichen111', // 固定的临时邮箱名称
pinCode: '147258',
domain: 'qichen.cloud' // 我们注册时使用的域名(随机前缀会投递到此)
},
// Verdent 注册配置
verdent: {
signupUrl: 'https://www.verdent.ai/signup?source=verdent-deck',
password: 'Qichen5210523...'
},
// 数据库(从环境变量读取)
db: {
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT || 3306),
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME || 'auto_register_verdent'
},
// 浏览器配置
browser: {
headless: false, // 设置为 false 可以看到浏览器操作过程
slowMo: 100, // 放慢操作速度,更像真人
keepEmailOpen: true // 结束后保留临时邮箱窗口
},
// 超时配置(毫秒)
timeouts: {
navigation: 60000, // 页面导航超时 60秒
element: 15000, // 元素查找超时 15秒
emailWait: 120000 // 等待邮件的最长时间 120秒
}
};

80
db.js Normal file
View File

@ -0,0 +1,80 @@
import mysql from 'mysql2/promise';
import { config } from './config.js';
let pool;
function getPool() {
if (!pool) {
if (!config.db.host || !config.db.user || !config.db.password) {
console.log('[DB] 未配置数据库环境变量,跳过持久化');
return null;
}
pool = mysql.createPool({
host: config.db.host,
port: config.db.port,
user: config.db.user,
password: config.db.password,
database: config.db.database,
waitForConnections: true,
connectionLimit: 5,
});
}
return pool;
}
export async function ensureSchema() {
const p = getPool();
if (!p) return;
await p.query(`CREATE TABLE IF NOT EXISTS accounts (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at DATETIME NOT NULL,
expires_at DATETIME NOT NULL,
UNIQUE KEY uniq_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`);
}
export async function saveAccount({ email, password, createdAt, expiresAt, status }) {
const p = getPool();
if (!p) return;
await ensureSchema();
const sql = `INSERT INTO accounts (email, password, status, created_at, expires_at)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE password=VALUES(password), status=VALUES(status), created_at=VALUES(created_at), expires_at=VALUES(expires_at)`;
await p.execute(sql, [
email,
password,
status,
formatDateTime(createdAt),
formatDateTime(expiresAt)
]);
}
export async function listAccounts({ status } = {}) {
const p = getPool();
if (!p) return [];
await ensureSchema();
let sql = 'SELECT * FROM accounts';
const params = [];
if (status) {
sql += ' WHERE status=?';
params.push(status);
}
sql += ' ORDER BY created_at DESC LIMIT 200';
const [rows] = await p.execute(sql, params);
return rows;
}
function pad2(n) { return String(n).padStart(2, '0'); }
function formatDateTime(d) {
const dt = new Date(d);
const Y = dt.getFullYear();
const M = pad2(dt.getMonth()+1);
const D = pad2(dt.getDate());
const h = pad2(dt.getHours());
const m = pad2(dt.getMinutes());
const s = pad2(dt.getSeconds());
return `${Y}-${M}-${D} ${h}:${m}:${s}`;
}

241
emailModule.js Normal file
View File

@ -0,0 +1,241 @@
import { config } from './config.js';
/**
* 设置临时邮箱并获取验证码
* @param {Page} page - Playwright 页面对象
* @returns {Promise<{email: string, code: string}>}
*/
export async function setupTempMail(page) {
console.log('📧 开始设置临时邮箱...');
// 访问 tempmail.plus增加超时时间因为网络较慢
await page.goto(config.tempmail.url, {
waitUntil: 'domcontentloaded', // 改为 domcontentloaded不等待所有网络请求
timeout: 60000 // 60秒超时
});
await page.waitForTimeout(3000); // 等待页面稳定
// 输入邮箱用户名(固定为配置的 qichen111
console.log('输入邮箱用户名:', config.tempmail.username);
await page.fill('#pre_button', config.tempmail.username);
// 点击页面其他位置或按回车来确认输入,触发邮箱地址更新
console.log('确认邮箱用户名...');
await page.click('body'); // 点击页面空白处
await page.waitForTimeout(2000);
// 检查 PIN 码弹窗是否已经显示
console.log('[LOG] 检查 PIN 码弹窗状态...');
const pinModal = page.locator('#modal-verify');
await page.waitForTimeout(2000); // 等待页面稳定
const initialModalClass = await pinModal.getAttribute('class');
console.log('[LOG] 弹窗 class:', initialModalClass);
// 判断弹窗是否已经显示(包含 "show" class
const isModalShown = initialModalClass && initialModalClass.includes('show');
console.log('[LOG] 弹窗是否已显示:', isModalShown);
if (!isModalShown) {
// 如果弹窗未显示,才需要点击 PIN 保护元素
console.log('[LOG] 弹窗未显示,点击 PIN 码保护元素...');
const pinProtectSpan = page.locator('span.pin-text[data-tr="box_protected"]');
await pinProtectSpan.waitFor({ state: 'visible', timeout: 15000 });
await pinProtectSpan.click({ force: true });
console.log('[LOG] ✅ 已点击 PIN 保护元素');
await page.waitForTimeout(2000);
} else {
console.log('[LOG] ✅ 弹窗已自动显示,无需点击');
}
// 查找 PIN 码输入框
console.log('[LOG] 查找 PIN 码输入框...');
const pinInput = page.locator('#pin');
const pinInputCount = await pinInput.count();
console.log('[LOG] 找到 PIN 输入框数量:', pinInputCount);
if (pinInputCount > 0) {
// 检查输入框是否可见
const isVisible = await pinInput.isVisible();
console.log('[LOG] PIN 输入框是否可见:', isVisible);
// 输入 PIN 码
console.log('[LOG] 输入 PIN 码:', config.tempmail.pinCode);
await pinInput.fill(config.tempmail.pinCode);
console.log('[LOG] ✅ PIN 码已输入');
await page.waitForTimeout(500);
// 查找提交按钮
console.log('[LOG] 查找提交按钮 #verify...');
const verifyButton = page.locator('#verify');
const verifyButtonCount = await verifyButton.count();
console.log('[LOG] 找到提交按钮数量:', verifyButtonCount);
if (verifyButtonCount > 0) {
const buttonText = await verifyButton.textContent();
console.log('[LOG] 按钮文本:', buttonText);
console.log('[LOG] 点击提交按钮...');
await verifyButton.click();
console.log('[LOG] ✅ 已点击提交按钮');
} else {
console.log('[LOG] ⚠️ 未找到提交按钮,尝试按回车');
await pinInput.press('Enter');
}
// 等待验证完成
await page.waitForTimeout(2000);
console.log('[LOG] ✅ PIN 码验证流程完成');
} else {
console.log('[LOG] ❌ 未找到 PIN 输入框!');
throw new Error('未找到 PIN 码输入框');
}
// 等待弹窗关闭后获取邮箱地址
await page.waitForTimeout(2000);
console.log('[LOG] 获取生成的邮箱地址...');
// 尝试多个可能的邮箱地址元素
let emailAddress = '';
// 方法 1: 查找特定的邮箱显示元素
const emailDisplay = page.locator('#email-address, #mail, .email-address, [id*="email"]').first();
if (await emailDisplay.count() > 0 && await emailDisplay.isVisible()) {
const text = await emailDisplay.textContent();
const match = text.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/);
if (match) {
emailAddress = match[0];
}
}
// 方法 2: 如果没找到,尝试从页面中提取所有包含 @ 的文本
if (!emailAddress) {
const pageContent = await page.content();
const emailMatches = pageContent.match(/qichen111@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/);
if (emailMatches && emailMatches.length > 0) {
emailAddress = emailMatches[0];
}
}
// 方法 3: 查找复制按钮附近的文本
if (!emailAddress) {
const copyButton = page.locator('button:has-text("复制")').first();
if (await copyButton.count() > 0) {
const parent = copyButton.locator('..');
const text = await parent.textContent();
const match = text.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/);
if (match) {
emailAddress = match[0];
}
}
}
console.log('[LOG] 提取到的邮箱地址:', emailAddress);
if (!emailAddress || !emailAddress.includes('@')) {
console.log('[LOG] ⚠️ 未能正确获取邮箱地址,尝试使用默认格式');
emailAddress = `${config.tempmail.username}@mailto.plus`;
}
console.log('✅ 邮箱地址:', emailAddress);
return { inboxReady: true, page };
}
/**
* 等待并获取验证码
* @param {Page} page - Playwright 页面对象
* @returns {Promise<string>} 验证码
*/
export async function waitForVerificationCode(page, sentAfterMs) {
console.log('⏳ 等待接收验证码邮件...');
console.log('[LOG] 不会刷新页面,只等待新邮件出现...');
const maxWaitTime = 120000; // 最多等待 120 秒
const checkInterval = 1000; // 每 1 秒检查一次
let elapsed = 0;
async function extractCodeFromOpenedMail() {
// 1) 优先从特定内容区域读取
const contentLoc = page.locator('.mail-content, .email-body, #mail-body, [class*="mail-content"], .pm-text, .view, .message-body').first();
if (await contentLoc.count() > 0) {
const text = await contentLoc.textContent();
const code = /\b(\d{6})\b/.exec(text || '');
if (code) return code[1];
}
// 2) 遍历所有 iframe 提取文本
const frames = page.frames();
console.log(`[LOG] 当前 frame 数量: ${frames.length}`);
for (const f of frames) {
try {
const body = await f.$('body');
if (!body) continue;
const text = await f.evaluate(() => document.body.innerText || '');
const m = /\b(\d{6})\b/.exec(text);
if (m) return m[1];
} catch {}
}
// 3) 退化为整页文本
const all = await page.textContent('body');
const m = /\b(\d{6})\b/.exec(all || '');
return m ? m[1] : null;
}
while (elapsed < maxWaitTime) {
await page.waitForTimeout(checkInterval);
elapsed += checkInterval;
console.log(`[LOG] 检查邮件... (已等待 ${elapsed / 1000} 秒)`);
// 邮件列表行(兼容多种结构)
const emailRows = page.locator('.inbox-dataList .mail-item, .inbox-dataList > div, div.row.no-gutters');
const emailCount = await emailRows.count();
console.log(`[LOG] 当前邮件数量: ${emailCount}`);
if (emailCount > 0) {
console.log('📬 发现邮件,尝试读取...');
// 遍历所有邮件,优先读取发送之后的邮件(按顺序尝试)
for (let i = 0; i < emailCount; i++) {
const emailRow = emailRows.nth(i);
// 过滤发送时间之前的旧邮件(根据 data-date
try {
const timeSpan = emailRow.locator('span[data-date]').first();
if (await timeSpan.count() > 0) {
const dateStr = await timeSpan.getAttribute('data-date');
if (dateStr) {
const parsed = Date.parse(dateStr.replace(' ', 'T')) || new Date(dateStr).getTime();
if (sentAfterMs && parsed && parsed < sentAfterMs) {
console.log(`[LOG] 跳过旧邮件 ${dateStr}`);
continue;
}
}
}
} catch {}
try {
// 点击打开邮件(有些需要双击/再次点击切换到详情)
await emailRow.click({ timeout: 5000 });
await page.waitForTimeout(500);
try { await emailRow.dblclick({ timeout: 1000 }); } catch {}
// 等待内容区域或 iframe 加载
await page.waitForTimeout(1500);
const code = await extractCodeFromOpenedMail();
if (code) {
console.log('✅ 获取到验证码:', code);
return code;
}
console.log(`[LOG] 邮件 ${i + 1} 中未找到验证码`);
} catch (error) {
console.log(`[LOG] 读取邮件 ${i + 1} 失败:`, error.message);
}
}
console.log('⚠️ 所有邮件中未找到验证码,继续等待...');
}
}
throw new Error('超时:未能在规定时间内收到验证码');
}

121
index.js Normal file
View File

@ -0,0 +1,121 @@
import { chromium } from 'playwright';
import { config } from './config.js';
import { setupTempMail, waitForVerificationCode } from './emailModule.js';
import { registerOnVerdent, completeRegistration } from './registerModule.js';
import { getBrowserAndContexts } from './browser.js';
import { generateUsername } from './util.js';
import { saveAccount } from './db.js';
/**
* 主自动注册流程
*/
async function main() {
console.log('🚀 开始自动注册流程...\n');
let browser;
let emailContext;
let registerContext;
try {
// 启动或连接到浏览器(支持指纹浏览器/AdsPower/自定义 WS
console.log('🌐 启动浏览器...');
({ browser, emailContext, registerContext } = await getBrowserAndContexts({
headless: config.browser.headless,
slowMo: config.browser.slowMo,
}));
// 创建页面
const emailPage = await emailContext.newPage();
let registerPage = await registerContext.newPage();
// 生成唯一用户名(随机 + 时间)
const localPart = generateUsername('qichen');
// ========== 第一步:设置临时邮箱 ==========
console.log('\n📧 === 步骤 1: 设置临时邮箱 ===');
const { inboxReady } = await setupTempMail(emailPage);
if (!inboxReady) {
throw new Error('未能成功创建临时邮箱');
}
const email = `${localPart}@${config.tempmail.domain}`;
console.log('📧 将用于注册的邮箱:', email);
// ========== 第二步:在 Verdent 上开始注册 ==========
console.log('\n🌐 === 步骤 2: 开始 Verdent.ai 注册 ===');
const { sentAtMs } = await registerOnVerdent(registerPage, email);
// ========== 第三步:等待验证码邮件(只取发送后的邮件) ==========
console.log('\n📬 === 步骤 3: 等待验证码邮件 ===');
const verificationCode = await waitForVerificationCode(emailPage, sentAtMs);
if (!verificationCode) {
throw new Error('未能获取验证码');
}
// ========== 第四步:完成注册 ==========
console.log('\n✅ === 步骤 4: 完成注册 ===');
const success = await completeRegistration(registerPage, verificationCode);
// 注册页本次任务结束后关闭,并为下次准备新页签
try { await registerPage.close(); } catch {}
registerPage = await registerContext.newPage();
if (success) {
console.log('\n🎉 ============================================');
console.log(' 注册流程成功完成!');
console.log('============================================');
console.log('📧 邮箱:', email);
console.log('🔑 密码:', config.verdent.password);
console.log('============================================\n');
// 持久化保存(注册时间与过期时间=7天
const createdAt = new Date();
const expiresAt = new Date(createdAt.getTime() + 7 * 24 * 60 * 60 * 1000);
try {
await saveAccount({ email, password: config.verdent.password, createdAt, expiresAt, status: 'active' });
console.log('[DB] 账号已写入数据库');
} catch (e) {
console.log('[DB] 写入失败:', e.message);
}
} else if (success === false) {
console.log('\n❌ 注册失败,请检查错误信息');
} else {
console.log('\n⚠ 注册状态未知,请手动检查浏览器窗口');
}
// 保持浏览器打开 30 秒以便查看结果
console.log('⏰ 浏览器将在 30 秒后关闭...');
await new Promise(resolve => setTimeout(resolve, 30000));
} catch (error) {
console.error('\n❌ 发生错误:', error.message);
console.error(error.stack);
// 错误时保持浏览器打开以便调试
console.log('⏰ 浏览器将在 60 秒后关闭,请检查问题...');
await new Promise(resolve => setTimeout(resolve, 60000));
} finally {
// 清理资源
try {
if (!config.browser.keepEmailOpen) {
if (emailContext && emailContext.close) await emailContext.close();
} else {
console.log('[INFO] 已根据配置保留临时邮箱窗口,未关闭 emailContext');
}
} catch {}
try {
if (registerContext && registerContext.close) await registerContext.close();
} catch {}
try {
if (!config.browser.keepEmailOpen) {
if (browser && browser.close) await browser.close();
}
} catch {}
console.log('👋 程序结束');
}
}
// 运行主程序
main().catch(console.error);

1096
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "auto-register-verdent",
"version": "1.0.0",
"description": "Auto registration for Verdent.ai",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node index.js",
"serve": "node server.js",
"test": "node index.js"
},
"keywords": ["automation", "registration"],
"author": "",
"license": "MIT",
"dependencies": {
"express": "^4.19.2",
"mysql2": "^3.11.0",
"node-fetch": "^3.3.2",
"playwright": "^1.40.0"
}
}

234
registerModule.js Normal file
View File

@ -0,0 +1,234 @@
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 注册流程...');
// 访问注册页面
await page.goto(config.verdent.signupUrl, {
waitUntil: 'domcontentloaded',
timeout: 60000
});
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('🎉 注册成功!');
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;
}
}

31
server.js Normal file
View File

@ -0,0 +1,31 @@
import express from 'express';
import { listAccounts } from './db.js';
const app = express();
const port = process.env.PORT || 3210;
app.get('/', async (req, res) => {
const rows = await listAccounts();
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(`<!doctype html>
<html><head><meta charset="utf-8"/>
<title>Verdent Accounts</title>
<style>body{font-family:system-ui,Arial;padding:20px;} table{border-collapse:collapse;width:100%} th,td{border:1px solid #ccc;padding:8px;text-align:left} .expired{color:#b00}</style>
</head><body>
<h2>Registered Accounts</h2>
<table>
<thead><tr><th>Email</th><th>Password</th><th>Status</th><th>Created</th><th>Expires</th></tr></thead>
<tbody>
${rows.map(r => `<tr class="${new Date(r.expires_at) < new Date() ? 'expired' : ''}"><td>${r.email}</td><td>${r.password}</td><td>${r.status}</td><td>${fmt(r.created_at)}</td><td>${fmt(r.expires_at)}</td></tr>`).join('')}
</tbody>
</table>
</body></html>`);
});
function fmt(d){
const dt=new Date(d);return dt.toISOString().replace('T',' ').slice(0,19);
}
app.listen(port, () => {
console.log(`[WEB] Query page running at http://localhost:${port}`);
});

11
util.js Normal file
View File

@ -0,0 +1,11 @@
export function generateUsername(prefix = 'user') {
const now = new Date();
const YYYY = now.getFullYear();
const MM = String(now.getMonth() + 1).padStart(2, '0');
const DD = String(now.getDate()).padStart(2, '0');
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
const rand = Math.random().toString(36).slice(2, 6);
return `${prefix}${YYYY}${MM}${DD}${hh}${mm}${ss}${rand}`;
}

File diff suppressed because one or more lines are too long