fix: stabilize signup verification recovery

- merge the latest dev branch into PR #130 before applying local fixes
- detect visible split OTP inputs even when the page exposes numeric single-character fields
- stop assuming verification success after repeated auth retry-page recovery failures
This commit is contained in:
QLHazyCoder
2026-04-21 21:48:25 +08:00
30 changed files with 2043 additions and 427 deletions
+150 -195
View File
@@ -6,16 +6,16 @@
addLog,
broadcastDataUpdate,
chrome,
ensureContentScriptReadyOnTab,
findMail2925Account,
getMail2925AccountStatus,
getState,
isAutoRunLockedState,
isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account,
normalizeMail2925Accounts,
pickMail2925AccountForRun,
getState,
isAutoRunLockedState,
ensureContentScriptReadyOnTab,
requestStop,
reuseOrCreateTab,
sendToContentScriptResilient,
@@ -87,7 +87,8 @@
function isMail2925LimitReachedError(error) {
const message = getErrorMessage(error);
return message.startsWith(MAIL2925_LIMIT_ERROR_PREFIX)
|| /子邮箱.{0,12}已达上限|已达上限邮箱|子邮箱上限|邮箱已达上限/i.test(message);
|| message.includes('子邮箱已达上限')
|| message.includes('已达上限邮箱');
}
function isMail2925ThreadTerminatedError(error) {
@@ -135,7 +136,7 @@
try {
const state = await getState();
const tabId = Number(state?.tabRegistry?.[MAIL2925_SOURCE]?.tabId || 0);
if (!Number.isInteger(tabId) || tabId <= 0) {
if (!Number.isInteger(tabId) || tabId <= 0 || typeof chrome.tabs?.get !== 'function') {
return '';
}
const tab = await chrome.tabs.get(tabId);
@@ -145,6 +146,28 @@
}
}
async function getMail2925TabUrlById(tabId) {
try {
if (!Number.isInteger(Number(tabId)) || Number(tabId) <= 0 || typeof chrome.tabs?.get !== 'function') {
return '';
}
const tab = await chrome.tabs.get(Number(tabId));
return String(tab?.url || '').trim();
} catch {
return '';
}
}
function isMail2925LoginUrl(rawUrl = '') {
try {
const parsed = new URL(String(rawUrl || ''));
return (parsed.hostname === '2925.com' || parsed.hostname === 'www.2925.com')
&& /^\/login\/?$/.test(parsed.pathname);
} catch {
return false;
}
}
async function setCurrentMail2925Account(accountId, options = {}) {
const { logMessage = '', updateLastUsedAt = false } = options;
const state = await getState();
@@ -361,7 +384,7 @@
origins: MAIL2925_COOKIE_ORIGINS,
});
} catch (_) {
// Best-effort cleanup only.
// Best effort cleanup only.
}
}
@@ -373,170 +396,15 @@
accountId = null,
forceRelogin = false,
actionLabel = '确保 2925 邮箱登录态',
allowLoginWhenOnLoginPage = true,
} = options;
const account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
});
let account = null;
if (forceRelogin) {
const removedCount = await clearMail2925SessionCookies();
await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info');
}
if (forceRelogin && typeof sleepWithStop === 'function') {
await sleepWithStop(3000);
}
throwIfStopped();
const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL;
const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
});
if (forceRelogin && typeof sleepWithStop === 'function') {
await sleepWithStop(3000);
}
let result;
try {
result = await sendToMailContentScriptResilient(
getMail2925MailConfig(),
{
type: 'ENSURE_MAIL2925_SESSION',
step: 0,
source: 'background',
payload: {
email: account.email,
password: account.password,
forceLogin: forceRelogin,
},
},
{
timeoutMs: forceRelogin ? 30000 : 25000,
responseTimeoutMs: forceRelogin ? 30000 : 25000,
maxRecoveryAttempts: 2,
}
);
} catch (err) {
const failedUrl = await getMail2925CurrentTabUrl();
await addLog(`2925ENSURE_MAIL2925_SESSION 通信失败,当前地址=${failedUrl || 'unknown'};原因=${getErrorMessage(err) || 'unknown'}`, 'warn');
const stopped = await stopAutoRunForMail2925LoginFailure(
`2925${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'}),已按手动停止逻辑暂停自动流程。`
);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw err;
}
if (!result?.loggedIn) {
const notLoggedInUrl = await getMail2925CurrentTabUrl();
await addLog(`2925:20 秒登录等待结束但仍未进入收件箱,当前地址=${notLoggedInUrl || 'unknown'}`, 'warn');
const stopped = await stopAutoRunForMail2925LoginFailure(
`2925${actionLabel}失败(20 秒内未进入收件箱),已按手动停止逻辑暂停自动流程。`
);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw new Error(`2925${actionLabel}失败,当前页面仍未进入收件箱。`);
}
if (result?.error) {
const resultErrorUrl = await getMail2925CurrentTabUrl();
await addLog(`2925:登录页返回业务错误,当前地址=${resultErrorUrl || 'unknown'};错误=${result.error}`, 'warn');
const stopped = await stopAutoRunForMail2925LoginFailure(
`2925${actionLabel}失败(${result.error}),已按手动停止逻辑暂停自动流程。`
);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw new Error(result.error);
}
if (result?.limitReached) {
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '2925 子邮箱已达上限邮箱'}`);
}
if (!result?.loggedIn) {
throw new Error(`2925${actionLabel}失败,当前页面仍未进入收件箱。`);
}
await patchMail2925Account(account.id, {
lastLoginAt: Date.now(),
lastError: '',
});
await setState({ currentMail2925AccountId: account.id });
broadcastDataUpdate({ currentMail2925AccountId: account.id });
return {
account: await ensureMail2925AccountForFlow({
allowAllocate: false,
preferredAccountId: account.id,
}),
mail: getMail2925MailConfig(),
result,
};
}
// Override the earlier version with a simpler login-page-only flow.
async function ensureMail2925MailboxSession(options = {}) {
const {
accountId = null,
forceRelogin = false,
actionLabel = '确保 2925 邮箱登录态',
} = options;
const account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
});
if (forceRelogin) {
const removedCount = await clearMail2925SessionCookies();
await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info');
if (typeof sleepWithStop === 'function') {
await addLog('2925:清理 cookie 后等待 3 秒,再打开登录页...', 'info');
await sleepWithStop(3000);
}
}
throwIfStopped();
await addLog(`2925:准备打开登录页 ${MAIL2925_LOGIN_URL}forceRelogin=${forceRelogin ? 'true' : 'false'}`, 'info');
const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL;
const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
});
const openedUrl = await getMail2925CurrentTabUrl();
await addLog(`2925:打开页后当前标签地址:${openedUrl || 'unknown'}`, 'info');
if (forceRelogin && typeof waitForTabUrlMatch === 'function') {
const matchedLoginTab = await waitForTabUrlMatch(
tabId,
(url) => {
try {
const parsed = new URL(String(url || ''));
return (parsed.hostname === '2925.com' || parsed.hostname === 'www.2925.com')
&& /^\/login\/?$/.test(parsed.pathname);
} catch {
return false;
}
},
{ timeoutMs: 15000, retryDelayMs: 300 }
);
await addLog(`2925:等待最终落到登录页结果:${matchedLoginTab?.url || 'timeout'}`, matchedLoginTab ? 'info' : 'warn');
if (matchedLoginTab && typeof ensureContentScriptReadyOnTab === 'function') {
await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, tabId, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
timeoutMs: 20000,
retryDelayMs: 800,
logMessage: '步骤 0:2925 登录页内容脚本未就绪,正在等待页面稳定后继续登录...',
});
}
}
if (forceRelogin && typeof sleepWithStop === 'function') {
await addLog('2925:登录页已打开,等待 3 秒后开始检查输入框并执行登录...', 'info');
await sleepWithStop(3000);
account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
});
}
const sendLoginMessage = typeof sendToContentScriptResilient === 'function'
@@ -551,9 +419,98 @@
}
);
const buildSuccessPayload = () => ({
account,
mail: getMail2925MailConfig(),
result: {
loggedIn: true,
currentView: 'mailbox',
usedExistingSession: true,
},
});
const failMailboxSession = async (message) => {
const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw new Error(message);
};
if (forceRelogin) {
const removedCount = await clearMail2925SessionCookies();
await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info');
if (typeof sleepWithStop === 'function') {
await addLog('2925:清理 cookie 后等待 3 秒,再打开登录页...', 'info');
await sleepWithStop(3000);
}
}
throwIfStopped();
const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL;
await addLog(
forceRelogin
? `2925:准备打开登录页 ${MAIL2925_LOGIN_URL}(强制重登录)`
: `2925:准备打开邮箱页 ${MAIL2925_URL}(登录页自动登录=${allowLoginWhenOnLoginPage ? '开启' : '关闭'}`,
'info'
);
const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
});
let openedUrl = await getMail2925TabUrlById(tabId);
if (!openedUrl) {
openedUrl = await getMail2925CurrentTabUrl();
}
await addLog(`2925:打开页后当前标签地址:${openedUrl || 'unknown'}`, 'info');
if (forceRelogin && typeof waitForTabUrlMatch === 'function') {
const matchedLoginTab = await waitForTabUrlMatch(
tabId,
(url) => isMail2925LoginUrl(url),
{ timeoutMs: 15000, retryDelayMs: 300 }
);
await addLog(`2925:等待最终落到登录页结果:${matchedLoginTab?.url || '超时'}`, matchedLoginTab ? 'info' : 'warn');
if (matchedLoginTab?.url) {
openedUrl = String(matchedLoginTab.url || '').trim();
}
}
if (!forceRelogin && !isMail2925LoginUrl(openedUrl)) {
await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info');
return buildSuccessPayload();
}
if (!forceRelogin && !allowLoginWhenOnLoginPage) {
await failMailboxSession(`2925${actionLabel}失败,当前页面已跳转到登录页,且当前未启用 2925 账号池,不执行自动登录。`);
}
if (!account) {
account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
});
}
if (typeof ensureContentScriptReadyOnTab === 'function') {
await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, tabId, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
timeoutMs: 20000,
retryDelayMs: 800,
logMessage: '步骤 0:2925 登录页内容脚本未就绪,正在等待页面稳定后继续登录...',
});
}
if (forceRelogin && typeof sleepWithStop === 'function') {
await addLog('2925:登录页已打开,等待 3 秒后开始检查输入框并执行登录...', 'info');
await sleepWithStop(3000);
}
let result;
try {
const beforeSendUrl = await getMail2925CurrentTabUrl();
const beforeSendUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl();
await addLog(`2925:发送 ENSURE_MAIL2925_SESSION 前当前地址:${beforeSendUrl || 'unknown'}`, 'info');
result = await sendLoginMessage(
MAIL2925_SOURCE,
@@ -571,13 +528,12 @@
timeoutMs: forceRelogin ? 30000 : 25000,
retryDelayMs: 800,
responseTimeoutMs: forceRelogin ? 30000 : 25000,
logMessage: `步骤 0:2925 登录页通信异常,正在等待当前页面重新就绪后继续确认登录态...`,
logMessage: '步骤 0:2925 登录页通信异常,正在等待页面恢复...',
}
);
} catch (err) {
const stopped = await stopAutoRunForMail2925LoginFailure(
`2925${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'}),已按手动停止逻辑暂停自动流程。`
);
const message = `2925${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'})。`;
const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`);
if (stopped) {
throw new Error('流程已被用户停止。');
}
@@ -585,27 +541,13 @@
}
if (result?.error) {
const stopped = await stopAutoRunForMail2925LoginFailure(
`2925${actionLabel}失败(${result.error}),已按手动停止逻辑暂停自动流程。`
);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw new Error(result.error);
await failMailboxSession(`2925${actionLabel}失败(${result.error})。`);
}
if (result?.limitReached) {
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '2925 子邮箱已达上限邮箱'}`);
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '子邮箱已达上限邮箱'}`);
}
if (!result?.loggedIn) {
const stopped = await stopAutoRunForMail2925LoginFailure(
`2925${actionLabel}失败(20 秒内未进入收件箱),已按手动停止逻辑暂停自动流程。`
);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw new Error(`2925${actionLabel}失败,当前页面仍未进入收件箱。`);
await failMailboxSession(`2925${actionLabel}失败,登录后仍未进入收件箱。`);
}
await patchMail2925Account(account.id, {
@@ -614,7 +556,8 @@
});
await setState({ currentMail2925AccountId: account.id });
broadcastDataUpdate({ currentMail2925AccountId: account.id });
const finalUrl = await getMail2925CurrentTabUrl();
const finalUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl();
await addLog(`2925:登录态确认成功,当前地址=${finalUrl || 'unknown'}`, 'ok');
return {
@@ -632,10 +575,21 @@
|| '子邮箱已达上限邮箱';
const state = await getState();
const currentAccount = getCurrentMail2925Account(state);
const poolEnabled = Boolean(state?.mail2925UseAccountPool);
if (!poolEnabled) {
if (typeof requestStop === 'function') {
await requestStop({
logMessage: `步骤 ${step}2925 检测到“${reason}”,当前未启用账号池,已按手动停止逻辑暂停自动流程。`,
});
}
return new Error('流程已被用户停止。');
}
if (!currentAccount) {
if (typeof requestStop === 'function') {
await requestStop({
logMessage: `步骤 ${step}2925 检测到“${reason}”,当前没有可识别账号,已按手动停止逻辑暂停流程`,
logMessage: `步骤 ${step}2925 检测到“${reason}”,当前没有可识别账号可供切换`,
});
}
return new Error('流程已被用户停止。');
@@ -648,7 +602,7 @@
lastError: reason,
});
await addLog(
`步骤 ${step}2925 账号 ${currentAccount.email} 命中“${reason}”,已禁用 24 小时,恢复时间 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })}。`,
`步骤 ${step}2925 账号 ${currentAccount.email} 命中“${reason}”,已禁用 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })}。`,
'warn'
);
@@ -663,7 +617,7 @@
broadcastDataUpdate({ currentMail2925AccountId: null });
if (typeof requestStop === 'function') {
await requestStop({
logMessage: `步骤 ${step}2925 账号 ${currentAccount.email} 已因${reason}禁用 24 小时,且当前没有可切换的下一个账号,已按手动停止逻辑暂停流程`,
logMessage: `步骤 ${step}2925 账号 ${currentAccount.email} 命中${reason},但当前没有可切换的下一个账号。`,
});
}
return new Error('流程已被用户停止。');
@@ -673,11 +627,12 @@
await ensureMail2925MailboxSession({
accountId: nextAccount.id,
forceRelogin: true,
allowLoginWhenOnLoginPage: true,
actionLabel: `步骤 ${step}:切换 2925 账号`,
});
await addLog(`步骤 ${step}2925 已自动切换到下一个账号 ${nextAccount.email} 并完成登录,当前尝试将直接结束`, 'warn');
await addLog(`步骤 ${step}2925 已切换到下一个账号 ${nextAccount.email}`, 'warn');
return buildMail2925ThreadTerminatedError(
`步骤 ${step}2925 账号 ${currentAccount.email} 命中“${reason}并已禁用 24 小时,已切换到 ${nextAccount.email},当前尝试结束,等待自动重试进入下一次尝试。`
`步骤 ${step}2925 账号 ${currentAccount.email} 命中“${reason}”,已切换到 ${nextAccount.email},当前尝试结束,等待下一轮重试。`
);
}
+25 -26
View File
@@ -9,7 +9,6 @@
chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
confirmCustomVerificationStepBypass,
ensureMail2925MailboxSession,
ensureStep8VerificationPageReady,
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
@@ -58,6 +57,28 @@
return String(value || '').trim().toLowerCase();
}
async function focusOrOpenMailTab(mail) {
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
await chrome.tabs.update(tabId, { active: true });
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
async function runStep8Attempt(state) {
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
@@ -111,33 +132,11 @@
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
) {
await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`);
} else if (mail.provider === '2925') {
if (state?.mail2925UseAccountPool && typeof ensureMail2925MailboxSession === 'function') {
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
actionLabel: '步骤 8:确认 2925 邮箱登录态',
});
}
await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`);
} else {
await addLog(`步骤 8:正在打开${mail.label}...`);
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
} else {
const tabId = await getTabId(mail.source);
await chrome.tabs.update(tabId, { active: true });
}
} else {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
await focusOrOpenMailTab(mail);
if (mail.provider === '2925') {
await addLog(`步骤 8:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
}
}
+38 -21
View File
@@ -24,15 +24,39 @@
throwIfStopped,
} = deps;
async function focusOrOpenMailTab(mail) {
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
await chrome.tabs.update(tabId, { active: true });
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
async function executeStep4(state) {
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const stepStartedAt = Date.now();
const verificationFilterAfterTimestamp = mail.provider === '2925'
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
const verificationSessionKey = `4:${stepStartedAt}`;
const signupTabId = await getTabId('signup-page');
if (!signupTabId) {
throw new Error('认证页面标签页已关闭,无法继续步骤 4。');
}
@@ -40,6 +64,7 @@
await chrome.tabs.update(signupTabId, { active: true });
throwIfStopped();
await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
const prepareResult = await sendToContentScriptResilient(
'signup-page',
{
@@ -73,36 +98,28 @@
}
throwIfStopped();
if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
if (
mail.provider === HOTMAIL_PROVIDER
|| mail.provider === LUCKMAIL_PROVIDER
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
) {
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
} else if (mail.provider === '2925') {
if (state?.mail2925UseAccountPool && typeof ensureMail2925MailboxSession === 'function') {
await addLog(`步骤 4:正在打开${mail.label}...`);
if (typeof ensureMail2925MailboxSession === 'function') {
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
} else {
await focusOrOpenMailTab(mail);
}
await addLog(`步骤 4正在通过 ${mail.label} 轮询验证码...`);
await addLog(`步骤 4将直接使用当前已登录的 ${mail.label} 轮询验证码`, 'info');
} else {
await addLog(`步骤 4:正在打开${mail.label}...`);
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
} else {
const tabId = await getTabId(mail.source);
await chrome.tabs.update(tabId, { active: true });
}
} else {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
await focusOrOpenMailTab(mail);
}
const shouldRequestFreshCodeFirst = ![