first init
This commit is contained in:
parent
5791928656
commit
6a4f0a9de3
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/node_modules/
|
||||
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
12
.idea/auto-register-verdent.iml
Normal file
12
.idea/auto-register-verdent.iml
Normal 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
8
.idea/modules.xml
Normal 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
6
.idea/vcs.xml
Normal 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
131
README.md
Normal 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
62
browser.js
Normal 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
38
config.js
Normal 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
80
db.js
Normal 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
241
emailModule.js
Normal 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
121
index.js
Normal 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
1096
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal 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
234
registerModule.js
Normal 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
31
server.js
Normal 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
11
util.js
Normal 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}`;
|
||||
}
|
||||
29
我现在需要让你帮我开发一个自动注册小本地程序.ini
Normal file
29
我现在需要让你帮我开发一个自动注册小本地程序.ini
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user