This commit is contained in:
dengqichen 2025-11-16 22:39:36 +08:00
parent 969f9cdb5d
commit af26fab852
3 changed files with 178 additions and 154 deletions

View File

@ -60,27 +60,23 @@ class ImapConnector {
/** /**
* 获取最新的邮件 * 获取最新的邮件
* @param {number} count - 获取数量 * @param {number} count - 获取数量
* @param {number} sinceDays - 获取几天内的邮件 * @param {string} folder - 邮箱文件夹名称默认'INBOX'
* @returns {Promise<Array>} * @returns {Promise<Array>}
*/ */
async getLatestEmails(count = 10, sinceDays = 1) { async getLatestEmails(count = 50, folder = 'INBOX') {
if (!this.connected) { if (!this.connected) {
await this.connect(); await this.connect();
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.imap.openBox('INBOX', false, (err, box) => { this.imap.openBox(folder, false, (err, box) => {
if (err) { if (err) {
reject(err); reject(err);
return; return;
} }
// 计算日期范围 // 搜索条件:只搜索未读邮件
const sinceDate = new Date(); this.imap.search(['UNSEEN'], (err, results) => {
sinceDate.setDate(sinceDate.getDate() - sinceDays);
// 搜索条件最近N天的所有邮件包括已读
this.imap.search([['SINCE', sinceDate]], async (err, results) => {
if (err) { if (err) {
reject(err); reject(err);
return; return;
@ -91,50 +87,50 @@ class ImapConnector {
return; return;
} }
logger.info('IMAP', `搜索到 ${results.length} 封未读邮件`);
// 只取最新的N封 // 只取最新的N封
const uids = results.slice(-count); const uids = results.slice(-count);
const emails = []; const emails = [];
let processedCount = 0;
const totalCount = uids.length;
const fetch = this.imap.fetch(uids, { const fetch = this.imap.fetch(uids, {
bodies: '', bodies: '',
markSeen: false markSeen: true
}); });
fetch.on('message', (msg) => { fetch.on('message', (msg) => {
let buffer = '';
msg.on('body', (stream) => { msg.on('body', (stream) => {
stream.on('data', (chunk) => { simpleParser(stream, (err, parsed) => {
buffer += chunk.toString('utf8'); if (err) {
logger.warn('IMAP', `解析邮件失败: ${err.message}`);
} else {
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
});
}
processedCount++;
// 所有邮件都处理完后才resolve
if (processedCount === totalCount) {
logger.info('IMAP', `成功解析 ${emails.length} 封邮件`);
resolve(emails);
}
}); });
}); });
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) => { fetch.once('error', (err) => {
reject(err); reject(err);
}); });
fetch.once('end', () => {
resolve(emails);
});
}); });
}); });
}); });
@ -161,7 +157,7 @@ class ImapConnector {
const sinceDate = new Date(); const sinceDate = new Date();
sinceDate.setDate(sinceDate.getDate() - sinceDays); sinceDate.setDate(sinceDate.getDate() - sinceDays);
this.imap.search([['SINCE', sinceDate], ['SUBJECT', subject]], async (err, results) => { this.imap.search([['SINCE', sinceDate], ['SUBJECT', subject]], (err, results) => {
if (err) { if (err) {
reject(err); reject(err);
return; return;
@ -175,21 +171,17 @@ class ImapConnector {
const emails = []; const emails = [];
const fetch = this.imap.fetch(results, { const fetch = this.imap.fetch(results, {
bodies: '', bodies: '',
markSeen: false markSeen: true
}); });
fetch.on('message', (msg) => { fetch.on('message', (msg) => {
let buffer = '';
msg.on('body', (stream) => { msg.on('body', (stream) => {
stream.on('data', (chunk) => { simpleParser(stream, (err, parsed) => {
buffer += chunk.toString('utf8'); if (err) {
}); logger.warn('IMAP', `解析邮件失败: ${err.message}`);
}); return;
}
msg.once('end', async () => {
try {
const parsed = await simpleParser(buffer);
emails.push({ emails.push({
uid: msg.uid, uid: msg.uid,
from: parsed.from?.text || '', from: parsed.from?.text || '',
@ -200,9 +192,7 @@ class ImapConnector {
html: parsed.html || '', html: parsed.html || '',
headers: parsed.headers headers: parsed.headers
}); });
} catch (e) { });
logger.warn('IMAP', `解析邮件失败: ${e.message}`);
}
}); });
}); });

View File

@ -27,40 +27,52 @@ class EmailVerificationService {
* @param {number} timeout - 超时时间 * @param {number} timeout - 超时时间
* @returns {Promise<string>} - 验证码 * @returns {Promise<string>} - 验证码
*/ */
async getVerificationCode(siteName, recipientEmail, timeout = 60) { async getVerificationCode(siteName, recipientEmail, timeout = 120) {
logger.info('EmailVerification', `开始获取 ${siteName} 的验证码...`); logger.info('EmailVerification', `开始获取 ${siteName} 的验证码...`);
logger.info('EmailVerification', `接收邮箱: ${recipientEmail}`); logger.info('EmailVerification', `接收邮箱: ${recipientEmail}`);
try { return new Promise((resolve, reject) => {
// 1. 初始化连接器
this.connector = new ImapConnector(this.config);
// 2. 等待验证码邮件
const startTime = Date.now(); const startTime = Date.now();
const checkInterval = emailConfig.search.checkInterval * 1000; // 转换为毫秒 const maxWaitTime = timeout * 1000;
let attempts = 0;
// 创建IMAP连接
this.connector = new ImapConnector(this.config);
let checkInterval;
let isResolved = false;
while (Date.now() - startTime < timeout * 1000) { const checkMail = async () => {
attempts++; if (Date.now() - startTime > maxWaitTime) {
logger.info('EmailVerification', `${attempts} 次检查邮件...`); clearInterval(checkInterval);
// 关键每次都重新连接以获取最新邮件QQ邮箱IMAP有延迟问题
if (attempts > 1) {
logger.info('EmailVerification', '断开并重新连接以刷新邮件...');
this.connector.disconnect(); this.connector.disconnect();
await this.sleep(1000); if (!isResolved) {
isResolved = true;
reject(new Error('获取验证码超时'));
}
return;
} }
await this.connector.connect();
// 获取最新邮件(倒序排列)
const emails = await this.connector.getLatestEmails(20, 1);
if (emails && emails.length > 0) { try {
logger.info('EmailVerification', `找到 ${emails.length} 封邮件`); // 获取最新邮件
logger.info('EmailVerification', '正在搜索邮件...');
const emails = await this.connector.getLatestEmails(50, 'INBOX');
// 打印最近5条邮件信息倒序最新的在前 if (!emails || emails.length === 0) {
const recentEmails = emails.slice(-5).reverse(); logger.info('EmailVerification', '暂无未读邮件');
return;
}
logger.info('EmailVerification', `✓ 找到 ${emails.length} 封未读邮件`);
// 按日期倒序排序(最新的在前)
emails.sort((a, b) => {
const dateA = a.date ? new Date(a.date).getTime() : 0;
const dateB = b.date ? new Date(b.date).getTime() : 0;
return dateB - dateA;
});
// 打印最近5条邮件信息
const recentEmails = emails.slice(0, 5);
logger.info('EmailVerification', '='.repeat(60)); logger.info('EmailVerification', '='.repeat(60));
logger.info('EmailVerification', '最近5条邮件'); logger.info('EmailVerification', '最近5条邮件');
recentEmails.forEach((email, index) => { recentEmails.forEach((email, index) => {
@ -72,20 +84,13 @@ class EmailVerificationService {
}); });
logger.info('EmailVerification', '='.repeat(60)); logger.info('EmailVerification', '='.repeat(60));
// 3. 查找匹配的邮件并提取验证码(从最新的开始) // 查找匹配的邮件并提取验证码
for (const email of emails.reverse()) { // 注意QQ邮箱转发邮件后收件人字段会被改写为QQ邮箱地址所以不能检查收件人
logger.info('EmailVerification', `检查邮件: 发件人="${email.from}", 主题="${email.subject}", 收件人="${email.to}"`); // 改为只检查发件人和主题,并按时间取最新的
for (const email of emails) {
if (isResolved) return;
// 关键:检查收件人是否匹配 logger.info('EmailVerification', `检查邮件: 发件人="${email.from}", 主题="${email.subject}", 时间="${email.date}"`);
const emailTo = (email.to || '').toLowerCase();
const isForRecipient = emailTo.includes(recipientEmail.toLowerCase());
if (!isForRecipient) {
logger.info('EmailVerification', ` ✗ 跳过:收件人不匹配(需要:${recipientEmail}`);
continue;
}
logger.info('EmailVerification', ` ✓ 收件人匹配!`);
for (const parser of this.parsers) { for (const parser of this.parsers) {
if (parser.canParse(email)) { if (parser.canParse(email)) {
@ -93,45 +98,40 @@ class EmailVerificationService {
const code = parser.extractCode(email); const code = parser.extractCode(email);
if (code) { if (code) {
logger.success('EmailVerification', ` ✓ 成功提取验证码: ${code}`); clearInterval(checkInterval);
// 标记为已读
try {
await this.connector.markAsRead(email.uid);
} catch (e) {
// 忽略标记失败
}
// 断开连接
this.connector.disconnect(); this.connector.disconnect();
if (!isResolved) {
return code; isResolved = true;
logger.success('EmailVerification', ` ✓ 成功提取验证码: ${code}`);
resolve(code);
}
return;
} else { } else {
logger.warn('EmailVerification', ` 邮件匹配但无法提取验证码`); logger.warn('EmailVerification', ` 邮件匹配但无法提取验证码`);
} }
} }
} }
} }
logger.warn('EmailVerification', `未找到匹配的Windsurf验证码邮件`);
} else { logger.warn('EmailVerification', `未找到匹配的验证码邮件`);
logger.info('EmailVerification', `没有邮件`);
} catch (err) {
logger.error('EmailVerification', `检查邮件失败: ${err.message}`);
} }
};
// 等待一段时间后再检查 // 连接成功后开始检查
logger.info('EmailVerification', `等待 ${emailConfig.search.checkInterval} 秒后重试...`); this.connector.connect().then(() => {
await this.sleep(checkInterval); logger.success('EmailVerification', 'IMAP连接成功开始监听验证码邮件...');
} checkMail();
checkInterval = setInterval(checkMail, emailConfig.search.checkInterval * 1000);
// 超时 }).catch((err) => {
this.connector.disconnect(); if (!isResolved) {
throw new Error(`获取验证码超时(${timeout}秒内未收到邮件)`); isResolved = true;
reject(new Error(`IMAP连接失败: ${err.message}`));
} catch (error) { }
if (this.connector) { });
this.connector.disconnect(); });
}
throw error;
}
} }
/** /**

View File

@ -60,6 +60,52 @@ class WindsurfRegister {
}; };
} }
/**
* 通用方法点击按钮并等待页面跳转
* @param {Function} checkPageChanged - 检查页面是否已跳转的函数返回Promise<boolean>
* @param {number} maxAttempts - 最多尝试次数
* @param {string} actionName - 操作名称用于日志
* @returns {Promise<boolean>} - 是否成功跳转
*/
async clickButtonAndWaitForPageChange(checkPageChanged, maxAttempts = 5, actionName = '点击按钮') {
let pageChanged = false;
let attempts = 0;
while (!pageChanged && attempts < maxAttempts) {
attempts++;
// 查找未禁用的按钮
const button = await this.page.$('button:not([disabled])');
if (button) {
const text = await this.page.evaluate(el => el.textContent.trim(), button);
logger.info(this.siteName, ` → 第${attempts}${actionName}"${text}"...`);
await button.click();
await this.human.randomDelay(2000, 3000);
// 使用自定义检查函数判断页面是否跳转
const changed = await checkPageChanged();
if (changed) {
pageChanged = true;
logger.success(this.siteName, ` → ✓ 页面已跳转`);
break;
}
logger.warn(this.siteName, ` → 页面未跳转,${attempts}/${maxAttempts}`);
await this.human.randomDelay(2000, 3000);
} else {
logger.warn(this.siteName, ` → 未找到可点击的按钮`);
break;
}
}
if (!pageChanged) {
logger.error(this.siteName, `${maxAttempts}次尝试后页面仍未跳转`);
}
return pageChanged;
}
/** /**
* 生成账号数据干运行模式 * 生成账号数据干运行模式
*/ */
@ -185,47 +231,31 @@ class WindsurfRegister {
logger.warn(this.siteName, ' → 未找到同意条款checkbox跳过'); logger.warn(this.siteName, ' → 未找到同意条款checkbox跳过');
} }
// 点击Continue按钮(使用人类行为) // 点击Continue按钮并等待跳转到密码页面
logger.info(this.siteName, ' → 点击Continue按钮...'); logger.info(this.siteName, ' → 点击Continue按钮...');
// 等待按钮变为可点击状态不再disabled // 等待按钮激活
try { await this.page.waitForSelector('button:not([disabled])', { timeout: 10000 });
await this.page.waitForFunction( logger.info(this.siteName, ' → 按钮已激活');
() => {
const button = document.querySelector('button'); // 使用通用方法处理页面跳转
if (!button) return false; const checkPageChanged = async () => {
const text = button.textContent.trim(); // 检查是否有密码输入框
return text === 'Continue' && !button.disabled; const hasPasswordField = await this.page.evaluate(() => {
}, return !!document.querySelector('#password');
{ timeout: 10000 } });
);
logger.info(this.siteName, ' → 按钮已激活'); // 检查URL是否改变
const currentUrl = this.page.url();
const urlChanged = currentUrl !== this.siteUrl;
// 点击未禁用的Continue按钮 return hasPasswordField || urlChanged;
const button = await this.page.$('button:not([disabled])'); };
if (button) {
const text = await this.page.evaluate(el => el.textContent.trim(), button); await this.clickButtonAndWaitForPageChange(checkPageChanged, 5, '点击Continue');
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); await this.human.randomDelay(2000, 3000);
this.currentStep = 1; this.currentStep = 1;
logger.success(this.siteName, `步骤 1 完成`); logger.success(this.siteName, `步骤 1 完成`);
@ -440,6 +470,10 @@ class WindsurfRegister {
// 等待验证码页面加载 // 等待验证码页面加载
await this.human.readPage(1, 2); await this.human.readPage(1, 2);
// 延迟15秒后再获取验证码让邮件有足够时间到达
logger.info(this.siteName, ' → 延迟15秒等待邮件到达...');
await new Promise(resolve => setTimeout(resolve, 15000));
// 获取验证码(从邮箱) // 获取验证码(从邮箱)
logger.info(this.siteName, ' → 正在从邮箱获取验证码...'); logger.info(this.siteName, ' → 正在从邮箱获取验证码...');
logger.info(this.siteName, ` → 接收邮箱: ${this.accountData.email}`); logger.info(this.siteName, ` → 接收邮箱: ${this.accountData.email}`);