aaaaa
This commit is contained in:
parent
969f9cdb5d
commit
af26fab852
@ -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}`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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}`);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user