diff --git a/background.js b/background.js index fdcd07d..e234edf 100644 --- a/background.js +++ b/background.js @@ -1712,23 +1712,27 @@ async function reuseOrCreateTab(source, url, options = {}) { // ============================================================ async function sendToContentScript(source, message, options = {}) { + throwIfStopped(); const { responseTimeoutMs = getContentScriptResponseTimeoutMs(message) } = options; const registry = await getTabRegistry(); const entry = registry[source]; if (!entry || !entry.ready) { + throwIfStopped(); console.log(LOG_PREFIX, `${source} not ready, queuing command`); return queueCommand(source, message); } // Verify tab is still alive const alive = await isTabAlive(source); + throwIfStopped(); if (!alive) { // Tab was closed — queue the command, it will be sent when tab is reopened console.log(LOG_PREFIX, `${source} tab was closed, queuing command`); return queueCommand(source, message); } + throwIfStopped(); console.log(LOG_PREFIX, `Sending to ${source} (tab ${entry.tabId}):`, message.type); return sendTabMessageWithTimeout(entry.tabId, source, message, responseTimeoutMs); } @@ -3810,13 +3814,17 @@ function getVerificationPollPayload(step, state, overrides = {}) { } async function requestVerificationCodeResend(step) { + throwIfStopped(); const signupTabId = await getTabId('signup-page'); if (!signupTabId) { throw new Error('认证页面标签页已关闭,无法重新请求验证码。'); } + throwIfStopped(); await chrome.tabs.update(signupTabId, { active: true }); + throwIfStopped(); await addLog(`步骤 ${step}:正在请求新的${getVerificationCodeLabel(step)}验证码...`, 'warn'); + throwIfStopped(); const result = await sendToContentScript('signup-page', { type: 'RESEND_VERIFICATION_CODE', @@ -3863,6 +3871,7 @@ async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) const maxRounds = pollOverrides.maxRounds || VERIFICATION_POLL_MAX_ROUNDS; for (let round = 1; round <= maxRounds; round++) { + throwIfStopped(); if (round > 1) { await requestVerificationCodeResend(step); } @@ -3902,6 +3911,9 @@ async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) return result; } catch (err) { + if (isStopError(err)) { + throw err; + } lastError = err; await addLog(`步骤 ${step}:${err.message}`, 'warn'); if (round < maxRounds) { @@ -3963,7 +3975,7 @@ async function resolveVerificationStep(step, state, mail, options = {}) { await requestVerificationCodeResend(step); await addLog(`步骤 ${step}:已先请求一封新的${getVerificationCodeLabel(step)}验证码,再开始轮询邮箱。`, 'warn'); } catch (err) { - if (step === 7 && isStep7RestartFromStep6Error(err)) { + if (isStopError(err) || (step === 7 && isStep7RestartFromStep6Error(err))) { throw err; } await addLog(`步骤 ${step}:首次重新获取验证码失败:${err.message},将继续使用当前时间窗口轮询。`, 'warn'); @@ -3984,7 +3996,9 @@ async function resolveVerificationStep(step, state, mail, options = {}) { filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined, }); + throwIfStopped(); await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`); + throwIfStopped(); const submitResult = await submitVerificationCode(step, result.code); if (submitResult.invalidCode) { @@ -4023,6 +4037,7 @@ async function executeStep4(state) { } await chrome.tabs.update(signupTabId, { active: true }); + throwIfStopped(); await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...'); const prepareResult = await sendToContentScriptResilient( 'signup-page', @@ -4050,6 +4065,7 @@ async function executeStep4(state) { return; } + throwIfStopped(); if (mail.provider === HOTMAIL_PROVIDER) { await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); } else { @@ -4161,6 +4177,7 @@ async function runStep7Attempt(state) { await reuseOrCreateTab('signup-page', state.oauthUrl); } + throwIfStopped(); await addLog('步骤 7:正在准备认证页,必要时切换到一次性验证码登录...'); const prepareResult = await sendToContentScript('signup-page', { type: 'PREPARE_LOGIN_CODE', @@ -4178,6 +4195,7 @@ async function runStep7Attempt(state) { throw new Error(prepareResult.error); } + throwIfStopped(); if (mail.provider === HOTMAIL_PROVIDER) { await addLog(`步骤 7:正在通过 ${mail.label} 轮询验证码...`); } else { diff --git a/tests/verification-stop-propagation.test.js b/tests/verification-stop-propagation.test.js new file mode 100644 index 0000000..1ecbea1 --- /dev/null +++ b/tests/verification-stop-propagation.test.js @@ -0,0 +1,186 @@ +const assert = require('assert'); +const fs = require('fs'); + +const source = fs.readFileSync('background.js', 'utf8'); + +function extractFunction(name) { + const markers = [`async function ${name}(`, `function ${name}(`]; + const start = markers + .map(marker => source.indexOf(marker)) + .find(index => index >= 0); + if (start < 0) { + throw new Error(`missing function ${name}`); + } + + let parenDepth = 0; + let signatureEnded = false; + let braceStart = -1; + for (let i = start; i < source.length; i += 1) { + const ch = source[i]; + if (ch === '(') { + parenDepth += 1; + } else if (ch === ')') { + parenDepth -= 1; + if (parenDepth === 0) { + signatureEnded = true; + } + } else if (ch === '{' && signatureEnded) { + braceStart = i; + break; + } + } + if (braceStart < 0) { + throw new Error(`missing body for function ${name}`); + } + + let depth = 0; + let end = braceStart; + for (; end < source.length; end += 1) { + const ch = source[end]; + if (ch === '{') depth += 1; + if (ch === '}') { + depth -= 1; + if (depth === 0) { + end += 1; + break; + } + } + } + + return source.slice(start, end); +} + +async function testPollFreshVerificationCodeRethrowsStop() { + const bundle = [ + extractFunction('isStopError'), + extractFunction('throwIfStopped'), + extractFunction('pollFreshVerificationCode'), + ].join('\n'); + + const api = new Function(` +let stopRequested = false; +const STOP_ERROR_MESSAGE = '流程已被用户停止。'; +const HOTMAIL_PROVIDER = 'hotmail-api'; +const VERIFICATION_POLL_MAX_ROUNDS = 5; +const logs = []; +let resendCalls = 0; + +function getHotmailVerificationPollConfig() { + return {}; +} +async function pollHotmailVerificationCode() { + throw new Error('hotmail path should not run in this test'); +} +function getVerificationCodeStateKey(step) { + return step === 4 ? 'lastSignupCode' : 'lastLoginCode'; +} +function getVerificationPollPayload(step, state, overrides = {}) { + return { + filterAfterTimestamp: 123, + ...overrides, + }; +} +async function sendToMailContentScriptResilient() { + throw new Error(STOP_ERROR_MESSAGE); +} +async function requestVerificationCodeResend() { + resendCalls += 1; +} +async function addLog(message, level) { + logs.push({ message, level }); +} + +${bundle} + +return { + pollFreshVerificationCode, + snapshot() { + return { logs, resendCalls }; + }, +}; +`)(); + + let error = null; + try { + await api.pollFreshVerificationCode(7, {}, { provider: 'qq' }, {}); + } catch (err) { + error = err; + } + + const state = api.snapshot(); + assert.strictEqual(error?.message, '流程已被用户停止。', 'Stop 错误应原样向上抛出'); + assert.strictEqual(state.resendCalls, 0, 'Stop 后不应继续请求新的验证码'); + assert.deepStrictEqual(state.logs, [], 'Stop 后不应再记录普通失败或重试日志'); +} + +async function testResolveVerificationStepRethrowsStopFromFreshRequest() { + const bundle = [ + extractFunction('isStopError'), + extractFunction('resolveVerificationStep'), + ].join('\n'); + + const api = new Function(` +const STOP_ERROR_MESSAGE = '流程已被用户停止。'; +const HOTMAIL_PROVIDER = 'hotmail-api'; +const logs = []; +let pollCalls = 0; + +function getVerificationCodeStateKey(step) { + return step === 4 ? 'lastSignupCode' : 'lastLoginCode'; +} +function getHotmailVerificationPollConfig() { + return {}; +} +function getVerificationCodeLabel(step) { + return step === 4 ? '注册' : '登录'; +} +function isStep7RestartFromStep6Error() { + return false; +} +async function requestVerificationCodeResend() { + throw new Error(STOP_ERROR_MESSAGE); +} +async function addLog(message, level) { + logs.push({ message, level }); +} +async function pollFreshVerificationCode() { + pollCalls += 1; + return { code: '123456', emailTimestamp: Date.now() }; +} +async function submitVerificationCode() { + throw new Error('submit should not run in this test'); +} +async function setState() {} +async function completeStepFromBackground() {} + +${bundle} + +return { + resolveVerificationStep, + snapshot() { + return { logs, pollCalls }; + }, +}; +`)(); + + let error = null; + try { + await api.resolveVerificationStep(7, {}, { provider: 'qq' }, { requestFreshCodeFirst: true }); + } catch (err) { + error = err; + } + + const state = api.snapshot(); + assert.strictEqual(error?.message, '流程已被用户停止。', '首次请求新验证码收到 Stop 后应立即终止'); + assert.strictEqual(state.pollCalls, 0, 'Stop 后不应继续进入邮箱轮询'); + assert.deepStrictEqual(state.logs, [], 'Stop 后不应追加降级日志'); +} + +(async () => { + await testPollFreshVerificationCodeRethrowsStop(); + await testResolveVerificationStepRethrowsStopFromFreshRequest(); + console.log('verification stop propagation tests passed'); +})().catch((error) => { + console.error(error); + process.exit(1); +});