aaaaa
This commit is contained in:
parent
bf55bcee27
commit
c1d1381edb
@ -22,7 +22,9 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "^11.0.0",
|
"commander": "^11.0.0",
|
||||||
"puppeteer": "^21.11.0"
|
"puppeteer": "npm:rebrowser-puppeteer@^23.9.0",
|
||||||
|
"imap": "^0.8.19",
|
||||||
|
"mailparser": "^3.6.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
|
|||||||
@ -56,6 +56,7 @@ if (registerTool) {
|
|||||||
.option('-o, --output <file>', '保存账号数据到文件')
|
.option('-o, --output <file>', '保存账号数据到文件')
|
||||||
.option('--from-step <number>', '从第几步开始执行', '1')
|
.option('--from-step <number>', '从第几步开始执行', '1')
|
||||||
.option('--to-step <number>', '执行到第几步(不指定则执行全部)')
|
.option('--to-step <number>', '执行到第几步(不指定则执行全部)')
|
||||||
|
.option('--password-strategy <strategy>', '密码策略 (email=使用邮箱, random=随机)', 'email')
|
||||||
.option('--keep-browser-open', '保持浏览器打开', false)
|
.option('--keep-browser-open', '保持浏览器打开', false)
|
||||||
.action(async (options) => {
|
.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 AccountDataGenerator = require('../generator');
|
||||||
const HumanBehavior = require('../utils/human-behavior');
|
const HumanBehavior = require('../utils/human-behavior');
|
||||||
|
const EmailVerificationService = require('../email-verification');
|
||||||
const logger = require('../../../shared/logger');
|
const logger = require('../../../shared/logger');
|
||||||
|
|
||||||
class WindsurfRegister {
|
class WindsurfRegister {
|
||||||
@ -20,6 +21,7 @@ class WindsurfRegister {
|
|||||||
this.siteUrl = 'https://windsurf.com/account/register';
|
this.siteUrl = 'https://windsurf.com/account/register';
|
||||||
this.dataGen = new AccountDataGenerator();
|
this.dataGen = new AccountDataGenerator();
|
||||||
this.human = new HumanBehavior();
|
this.human = new HumanBehavior();
|
||||||
|
this.emailService = new EmailVerificationService();
|
||||||
this.browser = null;
|
this.browser = null;
|
||||||
this.page = null;
|
this.page = null;
|
||||||
this.currentStep = 0;
|
this.currentStep = 0;
|
||||||
@ -66,7 +68,10 @@ class WindsurfRegister {
|
|||||||
name: options.name,
|
name: options.name,
|
||||||
email: options.email,
|
email: options.email,
|
||||||
username: options.username,
|
username: options.username,
|
||||||
password: options.password,
|
password: {
|
||||||
|
strategy: options.passwordStrategy || 'email',
|
||||||
|
...options.password
|
||||||
|
},
|
||||||
includePhone: false // Windsurf第一步不需要手机号
|
includePhone: false // Windsurf第一步不需要手机号
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -181,38 +186,44 @@ class WindsurfRegister {
|
|||||||
// 点击Continue按钮(使用人类行为)
|
// 点击Continue按钮(使用人类行为)
|
||||||
logger.info(this.siteName, ' → 点击Continue按钮...');
|
logger.info(this.siteName, ' → 点击Continue按钮...');
|
||||||
|
|
||||||
// 尝试查找按钮
|
// 等待按钮变为可点击状态(不再disabled)
|
||||||
let buttonSelector = null;
|
|
||||||
const possibleSelectors = [
|
|
||||||
'button:has-text("Continue")',
|
|
||||||
'button[type="submit"]',
|
|
||||||
'button.continue-button',
|
|
||||||
'button[class*="continue"]'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const selector of possibleSelectors) {
|
|
||||||
try {
|
try {
|
||||||
const button = await this.page.$(selector);
|
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) {
|
if (button) {
|
||||||
buttonSelector = selector;
|
const text = await this.page.evaluate(el => el.textContent.trim(), button);
|
||||||
break;
|
if (text === 'Continue') {
|
||||||
}
|
// 同时等待导航和点击
|
||||||
} catch (e) {
|
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 (error) {
|
||||||
if (buttonSelector) {
|
logger.warn(this.siteName, ' → 按钮等待超时,尝试按Enter键');
|
||||||
await this.human.visualSearch(this.page, buttonSelector);
|
|
||||||
await this.human.humanClick(this.page, buttonSelector);
|
|
||||||
} else {
|
|
||||||
logger.warn(this.siteName, ' → 未找到Continue按钮,尝试按Enter键');
|
|
||||||
await this.human.randomDelay(500, 1000);
|
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;
|
this.currentStep = 1;
|
||||||
logger.success(this.siteName, `步骤 1 完成`);
|
logger.success(this.siteName, `步骤 1 完成`);
|
||||||
@ -230,25 +241,145 @@ class WindsurfRegister {
|
|||||||
// 填写密码
|
// 填写密码
|
||||||
logger.info(this.siteName, ' → 填写密码...');
|
logger.info(this.siteName, ' → 填写密码...');
|
||||||
await this.page.waitForSelector('#password', { timeout: 10000 });
|
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);
|
await this.human.humanType(this.page, '#password', this.accountData.password);
|
||||||
|
|
||||||
// 填写确认密码
|
// 填写确认密码
|
||||||
logger.info(this.siteName, ' → 填写确认密码...');
|
logger.info(this.siteName, ' → 填写确认密码...');
|
||||||
await this.page.waitForSelector('#passwordConfirmation', { timeout: 10000 });
|
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);
|
await this.human.humanType(this.page, '#passwordConfirmation', this.accountData.password);
|
||||||
|
|
||||||
|
// 等待验证通过
|
||||||
|
logger.info(this.siteName, ' → 等待密码验证...');
|
||||||
|
await this.human.randomDelay(1000, 2000);
|
||||||
|
|
||||||
// 查找并点击Continue按钮
|
// 查找并点击Continue按钮
|
||||||
logger.info(this.siteName, ' → 点击Continue按钮...');
|
logger.info(this.siteName, ' → 点击Continue按钮...');
|
||||||
|
|
||||||
|
// 等待按钮变为可点击状态(不再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 (error) {
|
||||||
|
logger.warn(this.siteName, ' → 按钮等待超时,尝试按Enter键');
|
||||||
|
await this.human.randomDelay(500, 1000);
|
||||||
|
await Promise.all([
|
||||||
|
this.page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 }).catch(() => {}),
|
||||||
|
this.page.keyboard.press('Enter')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 额外等待确保页面稳定
|
||||||
|
await this.human.randomDelay(1000, 2000);
|
||||||
|
|
||||||
|
this.currentStep = 2;
|
||||||
|
logger.success(this.siteName, `步骤 2 完成`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤3: 邮箱验证
|
||||||
|
*/
|
||||||
|
async step3_emailVerification() {
|
||||||
|
logger.info(this.siteName, `[步骤 3/${this.getTotalSteps()}] 邮箱验证`);
|
||||||
|
|
||||||
|
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 = [
|
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("Continue")',
|
||||||
|
'button:has-text("Submit")',
|
||||||
'button[type="submit"]',
|
'button[type="submit"]',
|
||||||
'button.continue-button',
|
'button.verify-button',
|
||||||
'button[class*="continue"]'
|
'button[class*="verify"]'
|
||||||
];
|
];
|
||||||
|
|
||||||
let buttonSelector = null;
|
let buttonSelector = null;
|
||||||
for (const selector of possibleSelectors) {
|
for (const selector of buttonSelectors) {
|
||||||
try {
|
try {
|
||||||
const button = await this.page.$(selector);
|
const button = await this.page.$(selector);
|
||||||
if (button) {
|
if (button) {
|
||||||
@ -264,29 +395,31 @@ class WindsurfRegister {
|
|||||||
await this.human.visualSearch(this.page, buttonSelector);
|
await this.human.visualSearch(this.page, buttonSelector);
|
||||||
await this.human.humanClick(this.page, buttonSelector);
|
await this.human.humanClick(this.page, buttonSelector);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(this.siteName, ' → 未找到Continue按钮,尝试按Enter键');
|
logger.warn(this.siteName, ' → 未找到提交按钮,尝试按Enter键');
|
||||||
await this.human.randomDelay(500, 1000);
|
await this.human.randomDelay(500, 1000);
|
||||||
await this.page.keyboard.press('Enter');
|
await this.page.keyboard.press('Enter');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待跳转或响应
|
// 等待验证完成
|
||||||
await this.human.randomDelay(2000, 4000);
|
await this.human.randomDelay(3000, 5000);
|
||||||
|
|
||||||
this.currentStep = 2;
|
|
||||||
logger.success(this.siteName, `步骤 2 完成`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 步骤3: 邮箱验证(待实现)
|
|
||||||
*/
|
|
||||||
async step3_emailVerification() {
|
|
||||||
logger.info(this.siteName, `[步骤 3/${this.getTotalSteps()}] 邮箱验证`);
|
|
||||||
|
|
||||||
// 邮箱验证逻辑
|
|
||||||
// ...
|
|
||||||
|
|
||||||
this.currentStep = 3;
|
this.currentStep = 3;
|
||||||
logger.warn(this.siteName, '步骤 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 = '';
|
if (element) element.value = '';
|
||||||
}, selector);
|
}, selector);
|
||||||
|
|
||||||
// 逐字输入,随机延迟
|
// 使用更可靠的方式:直接设置value然后触发input事件
|
||||||
for (let i = 0; i < text.length; i++) {
|
await page.evaluate((sel, value) => {
|
||||||
const char = text[i];
|
const element = document.querySelector(sel);
|
||||||
|
if (element) {
|
||||||
// 每个字符的输入延迟
|
element.value = value;
|
||||||
const delay = this.randomInt(80, 180);
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
await page.type(selector, char, { delay });
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
|
||||||
// 偶尔停顿(模拟思考或查看)
|
|
||||||
if (Math.random() < 0.15) {
|
|
||||||
await this.randomDelay(300, 900);
|
|
||||||
}
|
}
|
||||||
|
}, selector, text);
|
||||||
|
|
||||||
// 偶尔有短暂的快速输入(模拟熟悉的词语)
|
// 模拟输入延迟
|
||||||
if (Math.random() < 0.1 && i < text.length - 3) {
|
await this.randomDelay(text.length * 100, text.length * 150);
|
||||||
i += 2; // 跳过几个字符,快速输入
|
|
||||||
|
// 验证输入是否正确
|
||||||
|
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