aaaaa
This commit is contained in:
parent
bf55bcee27
commit
c1d1381edb
@ -22,7 +22,9 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^11.0.0",
|
||||
"puppeteer": "^21.11.0"
|
||||
"puppeteer": "npm:rebrowser-puppeteer@^23.9.0",
|
||||
"imap": "^0.8.19",
|
||||
"mailparser": "^3.6.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
||||
@ -56,6 +56,7 @@ if (registerTool) {
|
||||
.option('-o, --output <file>', '保存账号数据到文件')
|
||||
.option('--from-step <number>', '从第几步开始执行', '1')
|
||||
.option('--to-step <number>', '执行到第几步(不指定则执行全部)')
|
||||
.option('--password-strategy <strategy>', '密码策略 (email=使用邮箱, random=随机)', 'email')
|
||||
.option('--keep-browser-open', '保持浏览器打开', false)
|
||||
.action(async (options) => {
|
||||
// 转换步骤参数为数字
|
||||
|
||||
@ -0,0 +1,242 @@
|
||||
/**
|
||||
* IMAP Connector - IMAP邮箱连接器
|
||||
* 支持QQ邮箱、Gmail、Outlook等支持IMAP的邮箱服务
|
||||
*/
|
||||
|
||||
const Imap = require('imap');
|
||||
const { simpleParser } = require('mailparser');
|
||||
const logger = require('../../../../shared/logger');
|
||||
|
||||
class ImapConnector {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.imap = null;
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到邮箱
|
||||
*/
|
||||
async connect() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.imap = new Imap({
|
||||
user: this.config.user,
|
||||
password: this.config.password,
|
||||
host: this.config.host,
|
||||
port: this.config.port,
|
||||
tls: this.config.tls,
|
||||
tlsOptions: this.config.tlsOptions || { rejectUnauthorized: false }
|
||||
});
|
||||
|
||||
this.imap.once('ready', () => {
|
||||
this.connected = true;
|
||||
logger.success('IMAP', `已连接到邮箱: ${this.config.user}`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.imap.once('error', (err) => {
|
||||
logger.error('IMAP', `连接失败: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.imap.once('end', () => {
|
||||
this.connected = false;
|
||||
logger.info('IMAP', '连接已关闭');
|
||||
});
|
||||
|
||||
this.imap.connect();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect() {
|
||||
if (this.imap && this.connected) {
|
||||
this.imap.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新的邮件
|
||||
* @param {number} count - 获取数量
|
||||
* @param {number} sinceDays - 获取几天内的邮件
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async getLatestEmails(count = 10, sinceDays = 1) {
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.imap.openBox('INBOX', false, (err, box) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算日期范围
|
||||
const sinceDate = new Date();
|
||||
sinceDate.setDate(sinceDate.getDate() - sinceDays);
|
||||
|
||||
// 搜索条件:最近N天的邮件
|
||||
this.imap.search(['UNSEEN', ['SINCE', sinceDate]], async (err, results) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 只取最新的N封
|
||||
const uids = results.slice(-count);
|
||||
const emails = [];
|
||||
|
||||
const fetch = this.imap.fetch(uids, {
|
||||
bodies: '',
|
||||
markSeen: false
|
||||
});
|
||||
|
||||
fetch.on('message', (msg) => {
|
||||
let buffer = '';
|
||||
|
||||
msg.on('body', (stream) => {
|
||||
stream.on('data', (chunk) => {
|
||||
buffer += chunk.toString('utf8');
|
||||
});
|
||||
});
|
||||
|
||||
msg.once('end', async () => {
|
||||
try {
|
||||
const parsed = await simpleParser(buffer);
|
||||
emails.push({
|
||||
uid: msg.uid,
|
||||
from: parsed.from?.text || '',
|
||||
to: parsed.to?.text || '',
|
||||
subject: parsed.subject || '',
|
||||
date: parsed.date,
|
||||
text: parsed.text || '',
|
||||
html: parsed.html || '',
|
||||
headers: parsed.headers
|
||||
});
|
||||
} catch (e) {
|
||||
logger.warn('IMAP', `解析邮件失败: ${e.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
fetch.once('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
fetch.once('end', () => {
|
||||
resolve(emails);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索包含特定关键词的邮件
|
||||
* @param {string} subject - 主题关键词
|
||||
* @param {number} sinceDays - 几天内
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async searchBySubject(subject, sinceDays = 1) {
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.imap.openBox('INBOX', false, (err, box) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const sinceDate = new Date();
|
||||
sinceDate.setDate(sinceDate.getDate() - sinceDays);
|
||||
|
||||
this.imap.search([['SINCE', sinceDate], ['SUBJECT', subject]], async (err, results) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const emails = [];
|
||||
const fetch = this.imap.fetch(results, {
|
||||
bodies: '',
|
||||
markSeen: false
|
||||
});
|
||||
|
||||
fetch.on('message', (msg) => {
|
||||
let buffer = '';
|
||||
|
||||
msg.on('body', (stream) => {
|
||||
stream.on('data', (chunk) => {
|
||||
buffer += chunk.toString('utf8');
|
||||
});
|
||||
});
|
||||
|
||||
msg.once('end', async () => {
|
||||
try {
|
||||
const parsed = await simpleParser(buffer);
|
||||
emails.push({
|
||||
uid: msg.uid,
|
||||
from: parsed.from?.text || '',
|
||||
to: parsed.to?.text || '',
|
||||
subject: parsed.subject || '',
|
||||
date: parsed.date,
|
||||
text: parsed.text || '',
|
||||
html: parsed.html || '',
|
||||
headers: parsed.headers
|
||||
});
|
||||
} catch (e) {
|
||||
logger.warn('IMAP', `解析邮件失败: ${e.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
fetch.once('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
fetch.once('end', () => {
|
||||
resolve(emails);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记邮件为已读
|
||||
* @param {number} uid - 邮件UID
|
||||
*/
|
||||
async markAsRead(uid) {
|
||||
if (!this.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.imap.addFlags(uid, ['\\Seen'], (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ImapConnector;
|
||||
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Email Configuration
|
||||
* 邮箱配置
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// 主邮箱配置(QQ邮箱 - 接收所有验证码)
|
||||
primary: {
|
||||
type: 'imap',
|
||||
host: 'imap.qq.com',
|
||||
port: 993,
|
||||
user: '1695551@qq.com',
|
||||
password: 'iogmboamejdsbjdh', // QQ邮箱授权码
|
||||
tls: true,
|
||||
tlsOptions: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
},
|
||||
|
||||
// 可以配置备用邮箱(未来扩展)
|
||||
backup: {
|
||||
// type: 'imap',
|
||||
// host: '...',
|
||||
// ...
|
||||
},
|
||||
|
||||
// 邮件搜索配置
|
||||
search: {
|
||||
maxWaitTime: 60, // 最长等待时间(秒)
|
||||
checkInterval: 3, // 检查间隔(秒)
|
||||
maxEmails: 10 // 每次获取的邮件数量
|
||||
}
|
||||
};
|
||||
161
src/tools/account-register/email-verification/index.js
Normal file
161
src/tools/account-register/email-verification/index.js
Normal file
@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Email Verification Service
|
||||
* 邮箱验证码服务 - 统一入口
|
||||
*/
|
||||
|
||||
const ImapConnector = require('./connectors/imap-connector');
|
||||
const WindsurfParser = require('./parsers/windsurf-parser');
|
||||
const emailConfig = require('./email-config');
|
||||
const logger = require('../../../shared/logger');
|
||||
|
||||
class EmailVerificationService {
|
||||
constructor(config = null) {
|
||||
this.config = config || emailConfig.primary;
|
||||
this.connector = null;
|
||||
this.parsers = [
|
||||
new WindsurfParser()
|
||||
// 未来添加更多解析器
|
||||
// new GitHubParser(),
|
||||
// new TwitterParser(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
* @param {string} siteName - 网站名称(如 'windsurf')
|
||||
* @param {string} recipientEmail - 接收验证码的邮箱地址
|
||||
* @param {number} timeout - 超时时间(秒)
|
||||
* @returns {Promise<string>} - 验证码
|
||||
*/
|
||||
async getVerificationCode(siteName, recipientEmail, timeout = 60) {
|
||||
logger.info('EmailVerification', `开始获取 ${siteName} 的验证码...`);
|
||||
logger.info('EmailVerification', `接收邮箱: ${recipientEmail}`);
|
||||
|
||||
try {
|
||||
// 1. 连接邮箱
|
||||
this.connector = new ImapConnector(this.config);
|
||||
await this.connector.connect();
|
||||
|
||||
// 2. 等待验证码邮件
|
||||
const startTime = Date.now();
|
||||
const checkInterval = emailConfig.search.checkInterval * 1000; // 转换为毫秒
|
||||
let attempts = 0;
|
||||
|
||||
while (Date.now() - startTime < timeout * 1000) {
|
||||
attempts++;
|
||||
logger.info('EmailVerification', `第 ${attempts} 次检查邮件...`);
|
||||
|
||||
// 获取最新邮件
|
||||
const emails = await this.connector.getLatestEmails(10, 1);
|
||||
|
||||
if (emails && emails.length > 0) {
|
||||
// 3. 查找匹配的邮件并提取验证码
|
||||
for (const email of emails.reverse()) { // 从最新的开始
|
||||
for (const parser of this.parsers) {
|
||||
if (parser.canParse(email)) {
|
||||
logger.success('EmailVerification', `找到匹配的邮件: ${email.subject}`);
|
||||
|
||||
const code = parser.extractCode(email);
|
||||
if (code) {
|
||||
logger.success('EmailVerification', `成功提取验证码: ${code}`);
|
||||
|
||||
// 标记为已读
|
||||
try {
|
||||
await this.connector.markAsRead(email.uid);
|
||||
} catch (e) {
|
||||
// 忽略标记失败
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
this.connector.disconnect();
|
||||
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 等待一段时间后再检查
|
||||
logger.info('EmailVerification', `等待 ${emailConfig.search.checkInterval} 秒后重试...`);
|
||||
await this.sleep(checkInterval);
|
||||
}
|
||||
|
||||
// 超时
|
||||
this.connector.disconnect();
|
||||
throw new Error(`获取验证码超时(${timeout}秒内未收到邮件)`);
|
||||
|
||||
} catch (error) {
|
||||
if (this.connector) {
|
||||
this.connector.disconnect();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索特定主题的邮件并提取验证码
|
||||
* @param {string} subject - 邮件主题关键词
|
||||
* @param {number} timeout - 超时时间(秒)
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async getCodeBySubject(subject, timeout = 60) {
|
||||
logger.info('EmailVerification', `搜索主题包含 "${subject}" 的邮件...`);
|
||||
|
||||
try {
|
||||
this.connector = new ImapConnector(this.config);
|
||||
await this.connector.connect();
|
||||
|
||||
const startTime = Date.now();
|
||||
const checkInterval = emailConfig.search.checkInterval * 1000;
|
||||
|
||||
while (Date.now() - startTime < timeout * 1000) {
|
||||
const emails = await this.connector.searchBySubject(subject, 1);
|
||||
|
||||
if (emails && emails.length > 0) {
|
||||
for (const email of emails.reverse()) {
|
||||
for (const parser of this.parsers) {
|
||||
if (parser.canParse(email)) {
|
||||
const code = parser.extractCode(email);
|
||||
if (code) {
|
||||
logger.success('EmailVerification', `提取到验证码: ${code}`);
|
||||
this.connector.disconnect();
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.sleep(checkInterval);
|
||||
}
|
||||
|
||||
this.connector.disconnect();
|
||||
throw new Error(`获取验证码超时`);
|
||||
|
||||
} catch (error) {
|
||||
if (this.connector) {
|
||||
this.connector.disconnect();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新的解析器
|
||||
* @param {BaseParser} parser - 解析器实例
|
||||
*/
|
||||
addParser(parser) {
|
||||
this.parsers.push(parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 休眠
|
||||
* @param {number} ms - 毫秒
|
||||
*/
|
||||
sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EmailVerificationService;
|
||||
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Base Parser - 邮件解析器基类
|
||||
* 所有网站的邮件解析器都继承此类
|
||||
*/
|
||||
|
||||
class BaseParser {
|
||||
constructor(siteName) {
|
||||
this.siteName = siteName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否能解析此邮件
|
||||
* @param {Object} email - 邮件对象
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canParse(email) {
|
||||
throw new Error('Must implement canParse() method');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从邮件中提取验证码
|
||||
* @param {Object} email - 邮件对象
|
||||
* @returns {string|null} - 验证码或null
|
||||
*/
|
||||
extractCode(email) {
|
||||
throw new Error('Must implement extractCode() method');
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的验证码提取方法
|
||||
* @param {string} content - 邮件内容
|
||||
* @param {RegExp} pattern - 正则表达式
|
||||
* @returns {string|null}
|
||||
*/
|
||||
extractByRegex(content, pattern) {
|
||||
if (!content) return null;
|
||||
|
||||
const match = content.match(pattern);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从HTML中提取文本
|
||||
* @param {string} html - HTML内容
|
||||
* @returns {string}
|
||||
*/
|
||||
stripHtml(html) {
|
||||
if (!html) return '';
|
||||
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseParser;
|
||||
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Windsurf Parser - Windsurf邮件解析器
|
||||
* 用于解析Windsurf发送的验证码邮件
|
||||
*/
|
||||
|
||||
const BaseParser = require('./base-parser');
|
||||
|
||||
class WindsurfParser extends BaseParser {
|
||||
constructor() {
|
||||
super('Windsurf');
|
||||
|
||||
// Windsurf邮件的特征
|
||||
this.senderKeywords = ['windsurf', 'codeium', 'exafunction'];
|
||||
this.subjectKeywords = ['verify', 'verification', 'code', '验证', 'welcome'];
|
||||
|
||||
// 验证码的正则表达式(根据实际邮件调整)
|
||||
this.codePatterns = [
|
||||
// HTML格式: <h1 class="code_xxx">866172</h1>
|
||||
/<h1[^>]*class="code[^"]*"[^>]*>(\d{6})<\/h1>/i,
|
||||
// 常见格式
|
||||
/6 digit code[^0-9]*(\d{6})/i,
|
||||
/verification code[^0-9]*(\d{6})/i,
|
||||
/verify.*code:?\s*(\d{6})/i,
|
||||
// 纯6位数字(最后尝试)
|
||||
/\b(\d{6})\b/
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是Windsurf的验证码邮件
|
||||
*/
|
||||
canParse(email) {
|
||||
if (!email) return false;
|
||||
|
||||
const from = (email.from || '').toLowerCase();
|
||||
const subject = (email.subject || '').toLowerCase();
|
||||
|
||||
// 检查发件人
|
||||
const hasSender = this.senderKeywords.some(keyword =>
|
||||
from.includes(keyword)
|
||||
);
|
||||
|
||||
// 检查主题
|
||||
const hasSubject = this.subjectKeywords.some(keyword =>
|
||||
subject.includes(keyword)
|
||||
);
|
||||
|
||||
return hasSender || hasSubject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从邮件中提取验证码
|
||||
*/
|
||||
extractCode(email) {
|
||||
if (!email) return null;
|
||||
|
||||
// 优先从HTML提取
|
||||
let code = this.extractFromHtml(email.html);
|
||||
if (code) return code;
|
||||
|
||||
// 其次从纯文本提取
|
||||
code = this.extractFromText(email.text);
|
||||
if (code) return code;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从HTML内容提取验证码
|
||||
*/
|
||||
extractFromHtml(html) {
|
||||
if (!html) return null;
|
||||
|
||||
// 先尝试直接从HTML提取(保留HTML标签)
|
||||
for (const pattern of this.codePatterns) {
|
||||
const code = this.extractByRegex(html, pattern);
|
||||
if (code && this.validateCode(code)) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果HTML提取失败,再去除标签后尝试
|
||||
const text = this.stripHtml(html);
|
||||
return this.extractFromText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文本内容提取验证码
|
||||
*/
|
||||
extractFromText(text) {
|
||||
if (!text) return null;
|
||||
|
||||
// 尝试所有正则表达式
|
||||
for (const pattern of this.codePatterns) {
|
||||
const code = this.extractByRegex(text, pattern);
|
||||
if (code && this.validateCode(code)) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证提取的验证码是否合理
|
||||
*/
|
||||
validateCode(code) {
|
||||
if (!code) return false;
|
||||
|
||||
// Windsurf验证码是6位数字
|
||||
if (code.length !== 6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 应该是纯数字
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WindsurfParser;
|
||||
@ -12,6 +12,7 @@
|
||||
|
||||
const AccountDataGenerator = require('../generator');
|
||||
const HumanBehavior = require('../utils/human-behavior');
|
||||
const EmailVerificationService = require('../email-verification');
|
||||
const logger = require('../../../shared/logger');
|
||||
|
||||
class WindsurfRegister {
|
||||
@ -20,6 +21,7 @@ class WindsurfRegister {
|
||||
this.siteUrl = 'https://windsurf.com/account/register';
|
||||
this.dataGen = new AccountDataGenerator();
|
||||
this.human = new HumanBehavior();
|
||||
this.emailService = new EmailVerificationService();
|
||||
this.browser = null;
|
||||
this.page = null;
|
||||
this.currentStep = 0;
|
||||
@ -66,7 +68,10 @@ class WindsurfRegister {
|
||||
name: options.name,
|
||||
email: options.email,
|
||||
username: options.username,
|
||||
password: options.password,
|
||||
password: {
|
||||
strategy: options.passwordStrategy || 'email',
|
||||
...options.password
|
||||
},
|
||||
includePhone: false // Windsurf第一步不需要手机号
|
||||
});
|
||||
|
||||
@ -181,38 +186,44 @@ class WindsurfRegister {
|
||||
// 点击Continue按钮(使用人类行为)
|
||||
logger.info(this.siteName, ' → 点击Continue按钮...');
|
||||
|
||||
// 尝试查找按钮
|
||||
let buttonSelector = null;
|
||||
const possibleSelectors = [
|
||||
'button:has-text("Continue")',
|
||||
'button[type="submit"]',
|
||||
'button.continue-button',
|
||||
'button[class*="continue"]'
|
||||
];
|
||||
|
||||
for (const selector of possibleSelectors) {
|
||||
try {
|
||||
const button = await this.page.$(selector);
|
||||
if (button) {
|
||||
buttonSelector = selector;
|
||||
break;
|
||||
// 等待按钮变为可点击状态(不再disabled)
|
||||
try {
|
||||
await this.page.waitForFunction(
|
||||
() => {
|
||||
const button = document.querySelector('button');
|
||||
if (!button) return false;
|
||||
const text = button.textContent.trim();
|
||||
return text === 'Continue' && !button.disabled;
|
||||
},
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
|
||||
logger.info(this.siteName, ' → 按钮已激活');
|
||||
|
||||
// 点击未禁用的Continue按钮
|
||||
const button = await this.page.$('button:not([disabled])');
|
||||
if (button) {
|
||||
const text = await this.page.evaluate(el => el.textContent.trim(), button);
|
||||
if (text === 'Continue') {
|
||||
// 同时等待导航和点击
|
||||
await Promise.all([
|
||||
this.page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 }).catch(() => {}),
|
||||
this.human.humanClick(this.page, 'button:not([disabled])')
|
||||
]);
|
||||
logger.success(this.siteName, ' → 已点击Continue按钮并等待跳转');
|
||||
}
|
||||
} catch (e) {
|
||||
// 继续尝试下一个
|
||||
}
|
||||
}
|
||||
|
||||
if (buttonSelector) {
|
||||
await this.human.visualSearch(this.page, buttonSelector);
|
||||
await this.human.humanClick(this.page, buttonSelector);
|
||||
} else {
|
||||
logger.warn(this.siteName, ' → 未找到Continue按钮,尝试按Enter键');
|
||||
} catch (error) {
|
||||
logger.warn(this.siteName, ' → 按钮等待超时,尝试按Enter键');
|
||||
await this.human.randomDelay(500, 1000);
|
||||
await this.page.keyboard.press('Enter');
|
||||
await Promise.all([
|
||||
this.page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 }).catch(() => {}),
|
||||
this.page.keyboard.press('Enter')
|
||||
]);
|
||||
}
|
||||
|
||||
// 等待跳转或响应
|
||||
await this.human.randomDelay(2000, 4000);
|
||||
// 额外等待确保页面稳定
|
||||
await this.human.randomDelay(1000, 2000);
|
||||
|
||||
this.currentStep = 1;
|
||||
logger.success(this.siteName, `步骤 1 完成`);
|
||||
@ -230,63 +241,185 @@ class WindsurfRegister {
|
||||
// 填写密码
|
||||
logger.info(this.siteName, ' → 填写密码...');
|
||||
await this.page.waitForSelector('#password', { timeout: 10000 });
|
||||
|
||||
// 先清空密码框
|
||||
await this.page.evaluate(() => {
|
||||
const elem = document.querySelector('#password');
|
||||
if (elem) elem.value = '';
|
||||
});
|
||||
|
||||
await this.human.humanType(this.page, '#password', this.accountData.password);
|
||||
|
||||
// 填写确认密码
|
||||
logger.info(this.siteName, ' → 填写确认密码...');
|
||||
await this.page.waitForSelector('#passwordConfirmation', { timeout: 10000 });
|
||||
|
||||
// 先清空确认密码框(防止有残留)
|
||||
await this.page.evaluate(() => {
|
||||
const elem = document.querySelector('#passwordConfirmation');
|
||||
if (elem) elem.value = '';
|
||||
});
|
||||
|
||||
await this.human.humanType(this.page, '#passwordConfirmation', this.accountData.password);
|
||||
|
||||
// 等待验证通过
|
||||
logger.info(this.siteName, ' → 等待密码验证...');
|
||||
await this.human.randomDelay(1000, 2000);
|
||||
|
||||
// 查找并点击Continue按钮
|
||||
logger.info(this.siteName, ' → 点击Continue按钮...');
|
||||
|
||||
const possibleSelectors = [
|
||||
'button:has-text("Continue")',
|
||||
'button[type="submit"]',
|
||||
'button.continue-button',
|
||||
'button[class*="continue"]'
|
||||
];
|
||||
|
||||
let buttonSelector = null;
|
||||
for (const selector of possibleSelectors) {
|
||||
try {
|
||||
const button = await this.page.$(selector);
|
||||
if (button) {
|
||||
buttonSelector = selector;
|
||||
break;
|
||||
// 等待按钮变为可点击状态(不再disabled)
|
||||
try {
|
||||
await this.page.waitForFunction(
|
||||
() => {
|
||||
const button = document.querySelector('button');
|
||||
if (!button) return false;
|
||||
const text = button.textContent.trim();
|
||||
return text === 'Continue' && !button.disabled;
|
||||
},
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
|
||||
logger.info(this.siteName, ' → 按钮已激活');
|
||||
|
||||
// 使用更精确的选择器
|
||||
const button = await this.page.$('button:not([disabled])');
|
||||
if (button) {
|
||||
const text = await this.page.evaluate(el => el.textContent.trim(), button);
|
||||
if (text === 'Continue') {
|
||||
// 同时等待导航和点击
|
||||
await Promise.all([
|
||||
this.page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 }).catch(() => {}),
|
||||
this.human.humanClick(this.page, 'button:not([disabled])')
|
||||
]);
|
||||
logger.success(this.siteName, ' → 已点击Continue按钮并等待跳转');
|
||||
}
|
||||
} catch (e) {
|
||||
// 继续尝试
|
||||
}
|
||||
}
|
||||
|
||||
if (buttonSelector) {
|
||||
await this.human.visualSearch(this.page, buttonSelector);
|
||||
await this.human.humanClick(this.page, buttonSelector);
|
||||
} else {
|
||||
logger.warn(this.siteName, ' → 未找到Continue按钮,尝试按Enter键');
|
||||
} catch (error) {
|
||||
logger.warn(this.siteName, ' → 按钮等待超时,尝试按Enter键');
|
||||
await this.human.randomDelay(500, 1000);
|
||||
await this.page.keyboard.press('Enter');
|
||||
await Promise.all([
|
||||
this.page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 }).catch(() => {}),
|
||||
this.page.keyboard.press('Enter')
|
||||
]);
|
||||
}
|
||||
|
||||
// 等待跳转或响应
|
||||
await this.human.randomDelay(2000, 4000);
|
||||
// 额外等待确保页面稳定
|
||||
await this.human.randomDelay(1000, 2000);
|
||||
|
||||
this.currentStep = 2;
|
||||
logger.success(this.siteName, `步骤 2 完成`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 步骤3: 邮箱验证(待实现)
|
||||
* 步骤3: 邮箱验证
|
||||
*/
|
||||
async step3_emailVerification() {
|
||||
logger.info(this.siteName, `[步骤 3/${this.getTotalSteps()}] 邮箱验证`);
|
||||
|
||||
// 邮箱验证逻辑
|
||||
// ...
|
||||
|
||||
this.currentStep = 3;
|
||||
logger.warn(this.siteName, '步骤 3 待实现(需要邮箱验证码)');
|
||||
try {
|
||||
// 等待验证码页面加载
|
||||
await this.human.readPage(1, 2);
|
||||
|
||||
// 获取验证码(从邮箱)
|
||||
logger.info(this.siteName, ' → 正在从邮箱获取验证码...');
|
||||
logger.info(this.siteName, ` → 接收邮箱: ${this.accountData.email}`);
|
||||
|
||||
const code = await this.emailService.getVerificationCode(
|
||||
'windsurf',
|
||||
this.accountData.email,
|
||||
90 // 90秒超时
|
||||
);
|
||||
|
||||
logger.success(this.siteName, ` → 验证码: ${code}`);
|
||||
|
||||
// 等待验证码输入框
|
||||
await this.human.randomDelay(1000, 2000);
|
||||
|
||||
// 查找验证码输入框(可能有多种选择器)
|
||||
const possibleSelectors = [
|
||||
'#code',
|
||||
'#verificationCode',
|
||||
'input[name="code"]',
|
||||
'input[name="verificationCode"]',
|
||||
'input[type="text"][placeholder*="code" i]',
|
||||
'input[type="text"][placeholder*="验证" i]'
|
||||
];
|
||||
|
||||
let codeInputSelector = null;
|
||||
for (const selector of possibleSelectors) {
|
||||
try {
|
||||
const input = await this.page.$(selector);
|
||||
if (input) {
|
||||
codeInputSelector = selector;
|
||||
logger.info(this.siteName, ` → 找到验证码输入框: ${selector}`);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// 继续尝试
|
||||
}
|
||||
}
|
||||
|
||||
if (codeInputSelector) {
|
||||
// 填写验证码
|
||||
logger.info(this.siteName, ' → 填写验证码...');
|
||||
await this.human.humanType(this.page, codeInputSelector, code);
|
||||
|
||||
// 查找并点击提交按钮
|
||||
logger.info(this.siteName, ' → 点击提交按钮...');
|
||||
|
||||
const buttonSelectors = [
|
||||
'button:has-text("Verify")',
|
||||
'button:has-text("Continue")',
|
||||
'button:has-text("Submit")',
|
||||
'button[type="submit"]',
|
||||
'button.verify-button',
|
||||
'button[class*="verify"]'
|
||||
];
|
||||
|
||||
let buttonSelector = null;
|
||||
for (const selector of buttonSelectors) {
|
||||
try {
|
||||
const button = await this.page.$(selector);
|
||||
if (button) {
|
||||
buttonSelector = selector;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// 继续尝试
|
||||
}
|
||||
}
|
||||
|
||||
if (buttonSelector) {
|
||||
await this.human.visualSearch(this.page, buttonSelector);
|
||||
await this.human.humanClick(this.page, buttonSelector);
|
||||
} else {
|
||||
logger.warn(this.siteName, ' → 未找到提交按钮,尝试按Enter键');
|
||||
await this.human.randomDelay(500, 1000);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
// 等待验证完成
|
||||
await this.human.randomDelay(3000, 5000);
|
||||
|
||||
this.currentStep = 3;
|
||||
logger.success(this.siteName, `步骤 3 完成`);
|
||||
|
||||
} else {
|
||||
logger.error(this.siteName, ' → 未找到验证码输入框!');
|
||||
logger.warn(this.siteName, ' → 请手动输入验证码: ' + code);
|
||||
|
||||
// 等待用户手动输入
|
||||
await this.human.randomDelay(30000, 30000);
|
||||
|
||||
this.currentStep = 3;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error(this.siteName, `邮箱验证失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -27,23 +27,36 @@ class HumanBehavior {
|
||||
if (element) element.value = '';
|
||||
}, selector);
|
||||
|
||||
// 逐字输入,随机延迟
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
|
||||
// 每个字符的输入延迟
|
||||
const delay = this.randomInt(80, 180);
|
||||
await page.type(selector, char, { delay });
|
||||
|
||||
// 偶尔停顿(模拟思考或查看)
|
||||
if (Math.random() < 0.15) {
|
||||
await this.randomDelay(300, 900);
|
||||
}
|
||||
|
||||
// 偶尔有短暂的快速输入(模拟熟悉的词语)
|
||||
if (Math.random() < 0.1 && i < text.length - 3) {
|
||||
i += 2; // 跳过几个字符,快速输入
|
||||
// 使用更可靠的方式:直接设置value然后触发input事件
|
||||
await page.evaluate((sel, value) => {
|
||||
const element = document.querySelector(sel);
|
||||
if (element) {
|
||||
element.value = value;
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}, selector, text);
|
||||
|
||||
// 模拟输入延迟
|
||||
await this.randomDelay(text.length * 100, text.length * 150);
|
||||
|
||||
// 验证输入是否正确
|
||||
const actualValue = await page.evaluate((sel) => {
|
||||
const element = document.querySelector(sel);
|
||||
return element ? element.value : '';
|
||||
}, selector);
|
||||
|
||||
if (actualValue !== text) {
|
||||
console.warn(`输入验证失败: 期望 "${text}", 实际 "${actualValue}"`);
|
||||
// 重试一次
|
||||
await page.evaluate((sel, value) => {
|
||||
const element = document.querySelector(sel);
|
||||
if (element) {
|
||||
element.value = value;
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}, selector, text);
|
||||
}
|
||||
|
||||
// 输入完成后的短暂停顿
|
||||
|
||||
Loading…
Reference in New Issue
Block a user