feat: 更新2925邮箱处理逻辑,增强验证码重发机制,调整测试用例

This commit is contained in:
QLHazyCoder
2026-04-20 13:15:39 +08:00
parent 8a16d81e01
commit ffecba6ab3
8 changed files with 108 additions and 48 deletions
+4 -1
View File
@@ -60,6 +60,7 @@
if (mail.error) throw new Error(mail.error);
const stepStartedAt = Date.now();
const verificationSessionKey = `8:${stepStartedAt}`;
const authTabId = await getTabId('signup-page');
if (authTabId) {
@@ -130,7 +131,9 @@
...state,
step8VerificationTargetEmail: displayedVerificationEmail || '',
}, mail, {
filterAfterTimestamp: stepStartedAt,
filterAfterTimestamp: mail.provider === '2925' ? 0 : stepStartedAt,
sessionKey: verificationSessionKey,
disableTimeBudgetCap: mail.provider === '2925',
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || ''),
requestFreshCodeFirst: false,
targetEmail: fixedTargetEmail,
+4 -1
View File
@@ -25,6 +25,7 @@
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const stepStartedAt = Date.now();
const verificationSessionKey = `4:${stepStartedAt}`;
const signupTabId = await getTabId('signup-page');
if (!signupTabId) {
throw new Error('认证页面标签页已关闭,无法继续步骤 4。');
@@ -91,7 +92,9 @@
}
await resolveVerificationStep(4, state, mail, {
filterAfterTimestamp: stepStartedAt,
filterAfterTimestamp: mail.provider === '2925' ? 0 : stepStartedAt,
sessionKey: verificationSessionKey,
disableTimeBudgetCap: mail.provider === '2925',
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0
+5 -3
View File
@@ -164,9 +164,10 @@
const nextPayload = { ...payload };
const intervalMs = Math.max(1, Number(nextPayload.intervalMs) || 3000);
const baseMaxAttempts = Math.max(1, Number(nextPayload.maxAttempts) || 1);
const disableTimeBudgetCap = Boolean(options.disableTimeBudgetCap);
const remainingMs = await getRemainingTimeBudgetMs(step, options, actionLabel);
if (remainingMs !== null) {
if (!disableTimeBudgetCap && remainingMs !== null) {
nextPayload.maxAttempts = Math.max(
1,
Math.min(baseMaxAttempts, Math.floor(Math.max(0, remainingMs - 1000) / intervalMs) + 1)
@@ -174,7 +175,7 @@
}
const defaultResponseTimeoutMs = Math.max(45000, nextPayload.maxAttempts * intervalMs + 25000);
const responseTimeoutMs = remainingMs === null
const responseTimeoutMs = disableTimeBudgetCap || remainingMs === null
? defaultResponseTimeoutMs
: Math.max(1000, Math.min(defaultResponseTimeoutMs, remainingMs));
@@ -564,7 +565,7 @@
getLegacyVerificationResendCountDefault(step, { requestFreshCodeFirst })
)
: getConfiguredVerificationResendCount(step, state, { requestFreshCodeFirst });
const maxSubmitAttempts = 7;
const maxSubmitAttempts = 15;
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
let lastResendAt = Number(options.lastResendAt) || 0;
@@ -609,6 +610,7 @@
for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) {
const pollOptions = {
excludeCodes: [...rejectedCodes],
disableTimeBudgetCap: Boolean(options.disableTimeBudgetCap),
getRemainingTimeMs: options.getRemainingTimeMs,
maxResendRequests: remainingAutomaticResendCount,
resendIntervalMs,
+5 -31
View File
@@ -53,6 +53,10 @@ async function persistSeenCodes() {
}
function buildSeenCodeSessionKey(step, payload = {}) {
const explicitSessionKey = String(payload?.sessionKey || '').trim();
if (explicitSessionKey) {
return explicitSessionKey;
}
const timestamp = Number(payload?.filterAfterTimestamp);
if (Number.isFinite(timestamp) && timestamp > 0) {
return `${step}:${timestamp}`;
@@ -551,11 +555,7 @@ async function handlePollEmail(step, payload) {
throw new Error('2925 邮箱列表未加载完成,请确认当前已打开收件箱。');
}
const knownMailIds = getCurrentMailIds(initialItems);
log(`步骤 ${step}:邮件列表已加载,共 ${initialItems.length} 封邮件`);
log(`步骤 ${step}:已记录当前 ${knownMailIds.size} 封旧邮件快照`);
const FALLBACK_AFTER = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
log(`步骤 ${step}:正在轮询 2925 邮箱,第 ${attempt}/${maxAttempts}`);
@@ -568,26 +568,10 @@ async function handlePollEmail(step, payload) {
const items = findMailItems();
if (items.length > 0) {
const useFallback = attempt > FALLBACK_AFTER;
const newMailIds = new Set();
items.forEach((item, index) => {
const itemId = getMailItemId(item, index);
if (!knownMailIds.has(itemId)) {
newMailIds.add(itemId);
}
});
for (let index = 0; index < items.length; index += 1) {
const item = items[index];
const itemId = getMailItemId(item, index);
const isNewMail = newMailIds.has(itemId);
const itemTimestamp = parseMailItemTimestamp(item);
if (!useFallback && !isNewMail) {
continue;
}
const previewText = getMailItemText(item);
if (!matchesMailFilters(previewText, senderFilters, subjectFilters)) {
continue;
@@ -613,21 +597,11 @@ async function handlePollEmail(step, payload) {
seenCodes.add(candidateCode);
persistSeenCodes();
const source = useFallback && !isNewMail
? (bodyCode ? '回退匹配邮件正文' : '回退匹配邮件')
: (bodyCode ? '新邮件正文' : '新邮件');
const source = bodyCode ? '邮件正文' : '邮件预览';
const timeLabel = itemTimestamp ? `,时间:${new Date(itemTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
log(`步骤 ${step}:已找到验证码:${candidateCode}(来源:${source}${timeLabel}`, 'ok');
return { ok: true, code: candidateCode, emailTimestamp: Date.now() };
}
items.forEach((item, index) => {
knownMailIds.add(getMailItemId(item, index));
});
}
if (attempt === FALLBACK_AFTER + 1) {
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
}
if (attempt < maxAttempts) {
+17 -5
View File
@@ -60,13 +60,22 @@ let refreshCalls = 0;
const clickOrder = [];
const readAndDeleteCalls = [];
const seenCodes = new Set();
const deletedMailIds = new Set();
const baselineMail = { id: 'baseline', text: 'OpenAI newsletter without code' };
const newMail = { id: 'new', text: 'OpenAI verification code 654321' };
function findMailItems() {
if (state === 'detail') return [];
if (state === 'baseline') return [baselineMail];
return [baselineMail, newMail];
const items = [];
if (state === 'detail') return items;
if (state === 'baseline' || state === 'with-new') {
if (!deletedMailIds.has('baseline')) {
items.push(baselineMail);
}
}
if (state === 'with-new' && !deletedMailIds.has('new')) {
items.push(newMail);
}
return items;
}
function getMailItemId(item) {
@@ -99,7 +108,9 @@ async function sleepRandom() {}
async function returnToInbox() {
clickOrder.push('inbox');
state = 'baseline';
if (state === 'detail') {
state = 'baseline';
}
return true;
}
@@ -113,6 +124,7 @@ async function refreshInbox() {
async function openMailAndDeleteAfterRead(item) {
readAndDeleteCalls.push(item.id);
deletedMailIds.add(item.id);
return item.id === 'new' ? 'Your ChatGPT code is 654321' : 'No code here';
}
@@ -142,7 +154,7 @@ return {
assert.equal(result.code, '654321');
assert.deepEqual(api.getClickOrder(), ['inbox', 'refresh', 'inbox', 'refresh']);
assert.deepEqual(api.getReadAndDeleteCalls(), ['new']);
assert.deepEqual(api.getReadAndDeleteCalls(), ['baseline', 'new']);
});
test('handlePollEmail ignores targetEmail and still tests any matching ChatGPT mail', async () => {
+67
View File
@@ -301,6 +301,73 @@ test('verification flow caps mail polling timeout to the remaining oauth budget'
assert.equal(mailPollCalls[0].payload.maxAttempts, 2);
});
test('verification flow keeps 2925 mailbox polling at 15 refresh attempts even when oauth budget is smaller', async () => {
const mailPollCalls = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'FILL_CODE') {
return {};
}
return {};
},
sendToMailContentScriptResilient: async (_mail, message, options) => {
mailPollCalls.push({
type: message.type,
payload: message.payload,
options,
});
return { code: '654321', emailTimestamp: 123 };
},
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await helpers.resolveVerificationStep(
8,
{
email: 'user@example.com',
mailProvider: '2925',
lastLoginCode: null,
},
{ provider: '2925', label: '2925 邮箱' },
{
getRemainingTimeMs: async () => 5000,
resendIntervalMs: 0,
disableTimeBudgetCap: true,
}
);
const pollCall = mailPollCalls.find((entry) => entry.type === 'POLL_EMAIL');
assert.ok(pollCall);
assert.equal(pollCall.payload.maxAttempts, 15);
assert.ok(pollCall.options.timeoutMs >= 250000);
});
test('verification flow keeps Hotmail request timestamp filtering on the first poll', async () => {
const pollPayloads = [];
+4 -5
View File
@@ -304,12 +304,11 @@
补充行为:
- `2925` provider 会拉长单轮轮询窗口,并关闭 Step 4 / 8 的自动重发间隔,减少因邮件延迟过早判负
- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925`依赖时间窗,而是以“当前收件箱快照 + 刷新后新增邮件”作为优先匹配依据
- `2925` provider 会关闭 Step 4 / 8 的自动重发间隔 25 秒节流;每次“重新发送验证码”之间,会在邮箱页内部执行一轮固定 15 次的刷新轮询,不再因 OAuth 剩余时间预算而缩短
- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 当前既不依赖时间窗,也不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中的匹配邮件
- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
- 验证码提交重试上限当前为 7 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
- `2925` 内容脚本会把每次成功读到的目标邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交。
- `2925` 在连续 3 轮未发现新增邮件后,会回退检查列表里现存的匹配邮件;每轮轮询之间只刷新 1 次收件箱列表。
- 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
- `2925` 内容脚本会把每一封实际打开检测的邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交;如果再次遇到相同验证码,对应邮件也会在读取后立即删除,避免后续反复打开
- `2925` 当前不再对邮件里的收件邮箱做比对,只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。
- 当验证码最终提交成功后,后台会异步向 2925 邮箱页发送 `DELETE_ALL_EMAILS`,执行“全选 + 删除”清理剩余邮件,不阻塞主流程。
+2 -2
View File
@@ -50,7 +50,7 @@
- `background/panel-bridge.js`CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail / 2925 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成。
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 7 次,步骤 8 提交登录验证码后若页面进入 `add-phone / 手机号页`,会直接抛出 fatal 错误而不是把当前步骤视为成功,并在 2925 提交成功后异步触发“删除全部邮件”的清理消息。
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询,步骤 8 提交登录验证码后若页面进入 `add-phone / 手机号页`,会直接抛出 fatal 错误而不是把当前步骤视为成功,并在 2925 提交成功后异步触发“删除全部邮件”的清理消息。
## `background/steps/`
@@ -75,7 +75,7 @@
- `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。
- `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。
- `content/mail-163.js`163 / 163 VIP 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。
- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱收件轮询、按步骤会话隔离“已试验证码”、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理。
- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱收件轮询、按步骤会话隔离“已试验证码”、在每次重发验证码之间执行一轮最多 15 次的邮箱刷新轮询、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理。
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击。
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;当前承接步骤 10。