diff --git a/background.js b/background.js index 93d42cc..ce02cc8 100644 --- a/background.js +++ b/background.js @@ -1,6 +1,7 @@ // background.js — Service Worker: orchestration, state, tab management, message routing importScripts( + 'background/account-run-history.js', 'background/panel-bridge.js', 'background/generated-email-helpers.js', 'background/signup-flow-helpers.js', @@ -123,6 +124,7 @@ const HOTMAIL_MAILBOXES = ['INBOX', 'Junk']; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; const HUMAN_STEP_DELAY_MIN = 700; const HUMAN_STEP_DELAY_MAX = 2200; +const STEP6_MAX_ATTEMPTS = 3; const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8; const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000; const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000; @@ -154,6 +156,7 @@ const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai'; const DISPLAY_TIMEZONE = 'Asia/Shanghai'; const MICROSOFT_TOKEN_DNR_RULE_ID = 1001; const PERSISTENT_ALIAS_STATE_KEYS = ['manualAliasUsage', 'preservedAliases']; +const ACCOUNT_RUN_HISTORY_STORAGE_KEY = 'accountRunHistory'; initializeSessionStorageAccess(); setupDeclarativeNetRequestRules(); @@ -4400,16 +4403,34 @@ function notifyStepError(step, error) { async function completeStepFromBackground(step, payload = {}) { if (stopRequested) { await setStepStatus(step, 'stopped'); + await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, null, STOP_ERROR_MESSAGE); notifyStepError(step, STOP_ERROR_MESSAGE); return; } + const completionState = step === 9 ? await getState() : null; await setStepStatus(step, 'completed'); await addLog(`步骤 ${step} 已完成`, 'ok'); await handleStepData(step, payload); + if (step === 9 && accountRunHistoryHelpers?.appendAccountRunRecord) { + await accountRunHistoryHelpers.appendAccountRunRecord('success', completionState); + } notifyStepComplete(step, payload); } +async function appendManualAccountRunRecordIfNeeded(status, stateOverride = null, reason = '') { + if (!accountRunHistoryHelpers?.appendAccountRunRecord) { + return null; + } + + const state = stateOverride || await getState(); + if (isAutoRunLockedState(state)) { + return null; + } + + return accountRunHistoryHelpers.appendAccountRunRecord(status, state, reason); +} + async function finalizeDeferredStepExecutionError(step, error) { const latestState = await getState(); const currentStatus = latestState.stepStatuses?.[step]; @@ -4420,11 +4441,13 @@ async function finalizeDeferredStepExecutionError(step, error) { if (isStopError(error)) { await setStepStatus(step, 'stopped'); await addLog(`步骤 ${step} 已被用户停止`, 'warn'); + await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, latestState, getErrorMessage(error)); return; } await setStepStatus(step, 'failed'); await addLog(`步骤 ${step} 失败:${getErrorMessage(error)}`, 'error'); + await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, latestState, getErrorMessage(error)); } async function executeStepViaCompletionSignal(step, timeoutMs = AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS) { @@ -4592,11 +4615,13 @@ async function executeStep(step, options = {}) { if (isStopError(err)) { await setStepStatus(step, 'stopped'); await addLog(`步骤 ${step} 已被用户停止`, 'warn'); + await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, state, getErrorMessage(err)); throw err; } if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step) && isRetryableContentScriptTransportError(err))) { await setStepStatus(step, 'failed'); await addLog(`步骤 ${step} 失败:${err.message}`, 'error'); + await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, state, getErrorMessage(err)); } else { console.warn( LOG_PREFIX, @@ -4713,6 +4738,8 @@ let autoRunAttemptRun = 0; const EMAIL_FETCH_MAX_ATTEMPTS = 5; const VERIFICATION_POLL_MAX_ROUNDS = 5; const STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS = 25000; +const MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15; +const MAIL_2925_VERIFICATION_INTERVAL_MS = 15000; const AUTO_STEP_DELAYS = { 1: 2000, 2: 2000, @@ -4724,8 +4751,19 @@ const AUTO_STEP_DELAYS = { 8: 2000, 9: 1000, }; +const accountRunHistoryHelpers = self.MultiPageBackgroundAccountRunHistory?.createAccountRunHistoryHelpers({ + ACCOUNT_RUN_HISTORY_STORAGE_KEY, + addLog, + buildHotmailLocalEndpoint, + chrome, + getErrorMessage, + getState, + HOTMAIL_SERVICE_MODE_LOCAL, + normalizeHotmailLocalBaseUrl, +}); const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({ addLog, + appendAccountRunRecord: (...args) => accountRunHistoryHelpers?.appendAccountRunRecord?.(...args), AUTO_RUN_MAX_RETRIES_PER_ROUND, AUTO_RUN_RETRY_DELAY_MS, AUTO_RUN_TIMER_KIND_BEFORE_RETRY, @@ -5117,6 +5155,8 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create HOTMAIL_PROVIDER, isStopError, LUCKMAIL_PROVIDER, + MAIL_2925_VERIFICATION_INTERVAL_MS, + MAIL_2925_VERIFICATION_MAX_ATTEMPTS, pollCloudflareTempEmailVerificationCode, pollHotmailVerificationCode, pollLuckmailVerificationCode, @@ -5190,6 +5230,7 @@ const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({ const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({ addLog, completeStepFromBackground, + getErrorMessage, getLoginAuthStateLabel, getState, isStep6RecoverableResult, @@ -5200,6 +5241,7 @@ const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({ sendToContentScriptResilient, shouldSkipLoginVerificationForCpaCallback, skipLoginVerificationStepsForCpaCallback, + STEP6_MAX_ATTEMPTS, throwIfStopped, }); const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({ @@ -5209,6 +5251,7 @@ const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({ confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, ensureStep7VerificationPageReady, executeStep6: (...args) => executeStep6(...args), + getPanelMode, getMailConfig, getState, getTabId, @@ -5259,6 +5302,7 @@ const stepExecutorsByKey = { }; const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({ addLog, + appendAccountRunRecord: (...args) => accountRunHistoryHelpers?.appendAccountRunRecord?.(...args), batchUpdateLuckmailPurchases, buildLocalhostCleanupPrefix, buildLuckmailSessionSettingsPayload, @@ -5801,8 +5845,8 @@ async function skipLoginVerificationStepsForCpaCallback() { } } -async function executeStep6(state) { - return step6Executor.executeStep6(state); +async function executeStep6(state, options = {}) { + return step6Executor.executeStep6(state, options); } // ============================================================ diff --git a/background/account-run-history.js b/background/account-run-history.js new file mode 100644 index 0000000..ba38cfa --- /dev/null +++ b/background/account-run-history.js @@ -0,0 +1,168 @@ +(function attachBackgroundAccountRunHistory(root, factory) { + root.MultiPageBackgroundAccountRunHistory = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundAccountRunHistoryModule() { + function createAccountRunHistoryHelpers(deps = {}) { + const { + ACCOUNT_RUN_HISTORY_STORAGE_KEY = 'accountRunHistory', + addLog, + buildHotmailLocalEndpoint, + chrome, + getErrorMessage, + getState, + HOTMAIL_SERVICE_MODE_LOCAL = 'local', + normalizeHotmailLocalBaseUrl, + } = deps; + + function normalizeAccountRunHistory(records) { + if (!Array.isArray(records)) { + return []; + } + + return records + .filter((item) => item && typeof item === 'object') + .map((item) => ({ + email: String(item.email || '').trim(), + password: String(item.password || '').trim(), + status: String(item.status || '').trim().toLowerCase(), + recordedAt: String(item.recordedAt || '').trim(), + reason: String(item.reason || '').trim(), + })) + .filter((item) => item.email && item.password && item.status); + } + + async function getPersistedAccountRunHistory() { + try { + const stored = await chrome.storage.local.get(ACCOUNT_RUN_HISTORY_STORAGE_KEY); + return normalizeAccountRunHistory(stored[ACCOUNT_RUN_HISTORY_STORAGE_KEY]); + } catch (err) { + console.warn('[MultiPage:account-run-history] Failed to read account run history:', err?.message || err); + return []; + } + } + + function buildAccountRunHistoryRecord(state = {}, status = '', reason = '') { + const email = String(state.email || '').trim(); + const password = String(state.password || state.customPassword || '').trim(); + const normalizedStatus = String(status || '').trim().toLowerCase(); + const normalizedReason = String(reason || '').trim(); + + if (!email || !password || !normalizedStatus) { + return null; + } + + return { + email, + password, + status: normalizedStatus, + recordedAt: new Date().toISOString(), + reason: normalizedReason, + }; + } + + async function appendAccountRunHistoryRecord(status, stateOverride = null, reason = '') { + const state = stateOverride || await getState(); + const record = buildAccountRunHistoryRecord(state, status, reason); + if (!record) { + return null; + } + + const history = await getPersistedAccountRunHistory(); + history.push(record); + await chrome.storage.local.set({ + [ACCOUNT_RUN_HISTORY_STORAGE_KEY]: history, + }); + return record; + } + + function shouldAppendAccountRunTextFile(state = {}) { + const serviceMode = String(state.hotmailServiceMode || '').trim().toLowerCase(); + if (serviceMode !== HOTMAIL_SERVICE_MODE_LOCAL) { + return false; + } + + const helperBaseUrl = normalizeHotmailLocalBaseUrl(state.hotmailLocalBaseUrl); + return Boolean(helperBaseUrl); + } + + async function appendAccountRunHistoryTextFile(record, stateOverride = null) { + const normalizedRecord = record && typeof record === 'object' + ? record + : buildAccountRunHistoryRecord(stateOverride || await getState(), ''); + if (!normalizedRecord?.email || !normalizedRecord?.password || !normalizedRecord?.status) { + return null; + } + + const state = stateOverride || await getState(); + if (!shouldAppendAccountRunTextFile(state)) { + return null; + } + + const helperBaseUrl = normalizeHotmailLocalBaseUrl(state.hotmailLocalBaseUrl); + let response; + try { + response = await fetch(buildHotmailLocalEndpoint(helperBaseUrl, '/append-account-log'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + email: normalizedRecord.email, + password: normalizedRecord.password, + status: normalizedRecord.status, + recordedAt: normalizedRecord.recordedAt, + reason: normalizedRecord.reason || '', + }), + }); + } catch (err) { + throw new Error(`账号文本记录写入失败:无法连接本地 helper(${getErrorMessage(err)})`); + } + + let payload = null; + try { + payload = await response.json(); + } catch (err) { + throw new Error(`账号文本记录写入失败:本地 helper 返回了无法解析的响应(${getErrorMessage(err)})`); + } + + if (!response.ok || payload?.ok === false) { + throw new Error(`账号文本记录写入失败:${payload?.error || `HTTP ${response.status}`}`); + } + + return payload?.filePath || ''; + } + + async function appendAccountRunRecord(status, stateOverride = null, reason = '') { + const state = stateOverride || await getState(); + const record = await appendAccountRunHistoryRecord(status, state, reason); + if (!record) { + return null; + } + + try { + const filePath = await appendAccountRunHistoryTextFile(record, state); + if (filePath) { + await addLog(`账号记录已追加到本地文本:${filePath}`, 'info'); + } + } catch (err) { + await addLog(getErrorMessage(err), 'warn'); + } + + return record; + } + + return { + appendAccountRunRecord, + appendAccountRunHistoryRecord, + appendAccountRunHistoryTextFile, + buildAccountRunHistoryRecord, + getPersistedAccountRunHistory, + normalizeAccountRunHistory, + shouldAppendAccountRunTextFile, + }; + } + + return { + createAccountRunHistoryHelpers, + }; +}); diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index 770eeeb..5105ccb 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -4,6 +4,7 @@ function createAutoRunController(deps = {}) { const { addLog, + appendAccountRunRecord, AUTO_RUN_MAX_RETRIES_PER_ROUND, AUTO_RUN_RETRY_DELAY_MS, AUTO_RUN_TIMER_KIND_BEFORE_RETRY, @@ -303,6 +304,7 @@ for (let targetRun = resumeCurrentRun; targetRun <= totalRuns; targetRun += 1) { const roundSummary = roundSummaries[targetRun - 1]; + let roundRecordAppended = false; const resumingCurrentRound = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun; let attemptRun = resumingCurrentRound ? resumeAttemptRun : 1; let reuseExistingProgress = resumingCurrentRound; @@ -377,6 +379,21 @@ forceFreshTabsNextRun = false; } + const appendRoundRecordIfNeeded = async (status, reason = '') => { + if (roundRecordAppended) { + return; + } + + if (typeof appendAccountRunRecord !== 'function') { + return; + } + + const record = await appendAccountRunRecord(status, null, reason); + if (record) { + roundRecordAppended = true; + } + }; + try { deps.throwIfStopped(); await broadcastAutoRunStatus('running', { @@ -403,6 +420,7 @@ } catch (err) { if (isStopError(err)) { stoppedEarly = true; + await appendRoundRecordIfNeeded('stopped', getErrorMessage(err)); await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn'); await broadcastAutoRunStatus('stopped', { currentRun: targetRun, @@ -444,6 +462,7 @@ } catch (sleepError) { if (isStopError(sleepError)) { stoppedEarly = true; + await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError)); await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn'); await broadcastAutoRunStatus('stopped', { currentRun: targetRun, @@ -466,6 +485,7 @@ } catch (sleepError) { if (isStopError(sleepError)) { stoppedEarly = true; + await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError)); await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn'); await broadcastAutoRunStatus('stopped', { currentRun: targetRun, @@ -486,6 +506,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); + await appendRoundRecordIfNeeded('failed', reason); if (!autoRunSkipFailures) { cancelPendingCommands('当前轮执行失败。'); await broadcastStopToContentScripts(); diff --git a/background/message-router.js b/background/message-router.js index 539de6c..4e2ac53 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -4,6 +4,7 @@ function createMessageRouter(deps = {}) { const { addLog, + appendAccountRunRecord, batchUpdateLuckmailPurchases, buildLocalhostCleanupPrefix, buildLuckmailSessionSettingsPayload, @@ -78,6 +79,19 @@ verifyHotmailAccount, } = deps; + async function appendManualAccountRunRecordIfNeeded(status, stateOverride = null, reason = '') { + if (typeof appendAccountRunRecord !== 'function') { + return null; + } + + const state = stateOverride || await getState(); + if (isAutoRunLockedState(state)) { + return null; + } + + return appendAccountRunRecord(status, state, reason); + } + async function handleStepData(step, payload) { switch (step) { case 1: { @@ -187,12 +201,17 @@ case 'STEP_COMPLETE': { if (getStopRequested()) { await setStepStatus(message.step, 'stopped'); + await appendManualAccountRunRecordIfNeeded(`step${message.step}_stopped`, null, '流程已被用户停止。'); notifyStepError(message.step, '流程已被用户停止。'); return { ok: true }; } + const completionState = message.step === 9 ? await getState() : null; await setStepStatus(message.step, 'completed'); await addLog(`步骤 ${message.step} 已完成`, 'ok'); await handleStepData(message.step, message.payload); + if (message.step === 9 && typeof appendAccountRunRecord === 'function') { + await appendAccountRunRecord('success', completionState); + } notifyStepComplete(message.step, message.payload); return { ok: true }; } @@ -201,10 +220,12 @@ if (isStopError(message.error)) { await setStepStatus(message.step, 'stopped'); await addLog(`步骤 ${message.step} 已被用户停止`, 'warn'); + await appendManualAccountRunRecordIfNeeded(`step${message.step}_stopped`, null, message.error); notifyStepError(message.step, message.error); } else { await setStepStatus(message.step, 'failed'); await addLog(`步骤 ${message.step} 失败:${message.error}`, 'error'); + await appendManualAccountRunRecordIfNeeded(`step${message.step}_failed`, null, message.error); notifyStepError(message.step, message.error); } return { ok: true }; diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 10100db..89d2d77 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -9,6 +9,7 @@ confirmCustomVerificationStepBypass, ensureStep7VerificationPageReady, executeStep6, + getPanelMode, getMailConfig, getState, getTabId, @@ -77,18 +78,45 @@ } } + const shouldRefreshOAuthBeforeSubmit = getPanelMode(state) === 'cpa'; + let step6ReplayCompleted = false; + await resolveVerificationStep(7, state, mail, { filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : Math.max(0, stepStartedAt - 60000), requestFreshCodeFirst: false, - resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, + resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') + ? 0 + : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, + beforeSubmit: shouldRefreshOAuthBeforeSubmit ? async (result) => { + if (step6ReplayCompleted) { + return; + } + + step6ReplayCompleted = true; + await addLog(`步骤 7:已拿到登录验证码 ${result.code},先刷新 CPA OAuth 链接并重走步骤 6,再回填验证码。`, 'warn'); + await rerunStep6ForStep7Recovery({ + logMessage: '步骤 7:正在重新获取最新 CPA OAuth 链接,并快速重走步骤 6...', + skipPreLoginCleanup: true, + postStepDelayMs: 1200, + }); + await ensureStep7VerificationPageReady(); + await addLog('步骤 7:登录验证码页面已重新就绪,开始回填刚才获取到的验证码。', 'info'); + } : undefined, }); } - async function rerunStep6ForStep7Recovery() { + async function rerunStep6ForStep7Recovery(options = {}) { + const { + logMessage = '步骤 7:正在回到步骤 6,重新发起登录验证码流程...', + skipPreLoginCleanup = false, + postStepDelayMs = 3000, + } = options; const currentState = await getState(); - await addLog('步骤 7:正在回到步骤 6,重新发起登录验证码流程...', 'warn'); - await executeStep6(currentState); - await sleepWithStop(3000); + await addLog(logMessage, 'warn'); + await executeStep6(currentState, { skipPreLoginCleanup }); + if (postStepDelayMs > 0) { + await sleepWithStop(postStepDelayMs); + } } async function executeStep7(state) { diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index a7ddc11..6a04aa7 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -89,7 +89,9 @@ await resolveVerificationStep(4, state, mail, { filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt, requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true, - resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, + resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') + ? 0 + : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, }); } diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index ce77fbc..845bc3f 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -5,6 +5,7 @@ const { addLog, completeStepFromBackground, + getErrorMessage, getLoginAuthStateLabel, getState, isStep6RecoverableResult, @@ -15,10 +16,12 @@ sendToContentScriptResilient, shouldSkipLoginVerificationForCpaCallback, skipLoginVerificationStepsForCpaCallback, + STEP6_MAX_ATTEMPTS, throwIfStopped, } = deps; - async function executeStep6(state) { + async function executeStep6(state, options = {}) { + const { skipPreLoginCleanup = false } = options; if (shouldSkipLoginVerificationForCpaCallback(state)) { await skipLoginVerificationStepsForCpaCallback(); return; @@ -27,63 +30,77 @@ throw new Error('缺少邮箱地址,请先完成步骤 3。'); } - await runPreStep6CookieCleanup(); + if (!skipPreLoginCleanup) { + await runPreStep6CookieCleanup(); + } let attempt = 0; + let lastError = null; - while (true) { + while (attempt < STEP6_MAX_ATTEMPTS) { throwIfStopped(); attempt += 1; - const currentState = attempt === 1 ? state : await getState(); - const password = currentState.password || currentState.customPassword || ''; - const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState); + try { + const currentState = attempt === 1 ? state : await getState(); + const password = currentState.password || currentState.customPassword || ''; + const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState); - if (attempt === 1) { - await addLog('步骤 6:正在打开最新 OAuth 链接并登录...'); - } else { - await addLog(`步骤 6:上一轮登录未进入验证码页,正在重新发起第 ${attempt} 轮登录尝试...`, 'warn'); - } - - await reuseOrCreateTab('signup-page', oauthUrl); - - const result = await sendToContentScriptResilient( - 'signup-page', - { - type: 'EXECUTE_STEP', - step: 6, - source: 'background', - payload: { - email: currentState.email, - password, - }, - }, - { - timeoutMs: 180000, - retryDelayMs: 700, - logMessage: '步骤 6:认证页正在切换,等待页面重新就绪后继续登录...', + if (attempt === 1) { + await addLog('步骤 6:正在打开最新 OAuth 链接并登录...'); + } else { + await addLog(`步骤 6:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn'); } - ); - if (result?.error) { - throw new Error(result.error); + await reuseOrCreateTab('signup-page', oauthUrl); + + const result = await sendToContentScriptResilient( + 'signup-page', + { + type: 'EXECUTE_STEP', + step: 6, + source: 'background', + payload: { + email: currentState.email, + password, + }, + }, + { + timeoutMs: 180000, + retryDelayMs: 700, + logMessage: '步骤 6:认证页正在切换,等待页面重新就绪后继续登录...', + } + ); + + if (result?.error) { + throw new Error(result.error); + } + + if (isStep6SuccessResult(result)) { + await completeStepFromBackground(6, { + loginVerificationRequestedAt: result.loginVerificationRequestedAt || null, + }); + return; + } + + if (isStep6RecoverableResult(result)) { + const reasonMessage = result.message + || `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 6。`; + throw new Error(reasonMessage); + } + + throw new Error('步骤 6:认证页未返回可识别的登录结果。'); + } catch (err) { + throwIfStopped(err); + lastError = err; + if (attempt >= STEP6_MAX_ATTEMPTS) { + break; + } + + await addLog(`步骤 6:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn'); } - - if (isStep6SuccessResult(result)) { - await completeStepFromBackground(6, { - loginVerificationRequestedAt: result.loginVerificationRequestedAt || null, - }); - return; - } - - if (isStep6RecoverableResult(result)) { - const reasonMessage = result.message - || `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 6。`; - await addLog(`步骤 6:${reasonMessage}`, 'warn'); - continue; - } - - throw new Error('步骤 6:认证页未返回可识别的登录结果。'); } + + throw new Error(`步骤 6:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`); } return { executeStep6 }; diff --git a/background/verification-flow.js b/background/verification-flow.js index ad504a9..b630669 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -15,6 +15,8 @@ HOTMAIL_PROVIDER, isStopError, LUCKMAIL_PROVIDER, + MAIL_2925_VERIFICATION_INTERVAL_MS, + MAIL_2925_VERIFICATION_MAX_ATTEMPTS, pollCloudflareTempEmailVerificationCode, pollHotmailVerificationCode, pollLuckmailVerificationCode, @@ -62,14 +64,15 @@ } function getVerificationPollPayload(step, state, overrides = {}) { + const is2925Provider = state?.mailProvider === '2925'; if (step === 4) { return { filterAfterTimestamp: getHotmailVerificationRequestTimestamp(4, state), senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'], subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'], targetEmail: state.email, - maxAttempts: 5, - intervalMs: 3000, + maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5, + intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000, ...overrides, }; } @@ -79,8 +82,8 @@ senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'], subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'], targetEmail: state.email, - maxAttempts: 5, - intervalMs: 3000, + maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5, + intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000, ...overrides, }; } @@ -368,6 +371,9 @@ const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER ? getHotmailVerificationPollConfig(step) : null; + const beforeSubmit = typeof options.beforeSubmit === 'function' + ? options.beforeSubmit + : null; const ignorePersistedLastCode = Boolean(hotmailPollConfig?.ignorePersistedLastCode); if (state[stateKey] && !ignorePersistedLastCode) { rejectedCodes.add(state[stateKey]); @@ -431,6 +437,14 @@ throwIfStopped(); await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`); + if (beforeSubmit) { + await beforeSubmit(result, { + attempt, + rejectedCodes: new Set(rejectedCodes), + filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined, + lastResendAt, + }); + } throwIfStopped(); const submitResult = await submitVerificationCode(step, result.code); diff --git a/scripts/hotmail_helper.py b/scripts/hotmail_helper.py index 8d218fa..4804af0 100644 --- a/scripts/hotmail_helper.py +++ b/scripts/hotmail_helper.py @@ -2,8 +2,11 @@ import email import html import imaplib import json +import os import re +import threading import time +import traceback from datetime import datetime, timezone from email.header import decode_header from email.utils import parseaddr, parsedate_to_datetime @@ -59,6 +62,9 @@ IMAP_HOST = "outlook.office365.com" IMAP_PORT = 993 REQUEST_TIMEOUT_SECONDS = 45 FETCH_LIMIT_DEFAULT = 5 +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +ACCOUNT_LOG_PATH = os.path.join(BASE_DIR, "data", "account-run-history.txt") +ACCOUNT_LOG_LOCK = threading.Lock() def json_response(handler, status, payload): @@ -113,6 +119,24 @@ def log_info(message): print(f"[HotmailHelper] {message}", flush=True) +def append_account_log(email_addr, password, status, recorded_at="", reason=""): + normalized_email = str(email_addr or "").strip() + normalized_password = str(password or "").strip() + normalized_status = str(status or "").strip().lower() + normalized_recorded_at = str(recorded_at or "").strip() or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + normalized_reason = str(reason or "").strip().replace("\r", " ").replace("\n", " ") + + if not normalized_email or not normalized_password or not normalized_status: + raise RuntimeError("Missing email/password/status for account log append") + + os.makedirs(os.path.dirname(ACCOUNT_LOG_PATH), exist_ok=True) + line = f"{normalized_recorded_at}\t{normalized_email}\t{normalized_password}\t{normalized_status}\t{normalized_reason}\n" + with ACCOUNT_LOG_LOCK: + with open(ACCOUNT_LOG_PATH, "a", encoding="utf-8") as handle: + handle.write(line) + return ACCOUNT_LOG_PATH + + def try_refresh_access_token(endpoint, client_id, refresh_token): request_data = { "client_id": client_id, @@ -600,6 +624,21 @@ class HotmailHelperHandler(BaseHTTPRequestHandler): def do_POST(self): try: payload = read_json_payload(self) + + if self.path == "/append-account-log": + file_path = append_account_log( + payload.get("email"), + payload.get("password"), + payload.get("status"), + payload.get("recordedAt"), + payload.get("reason"), + ) + json_response(self, 200, { + "ok": True, + "filePath": file_path, + }) + return + email_addr = str(payload.get("email") or "").strip() client_id = str(payload.get("clientId") or "").strip() refresh_token = str(payload.get("refreshToken") or "").strip() @@ -643,12 +682,14 @@ class HotmailHelperHandler(BaseHTTPRequestHandler): json_response(self, 404, {"ok": False, "error": f"Unsupported path: {self.path}"}) except Exception as exc: + traceback.print_exc() json_response(self, 500, {"ok": False, "error": str(exc)}) def main(): server = ThreadingHTTPServer((HOST, PORT), HotmailHelperHandler) print(f"Hotmail helper listening on http://{HOST}:{PORT}", flush=True) + print(f"Account log file: {ACCOUNT_LOG_PATH}", flush=True) try: server.serve_forever() except KeyboardInterrupt: diff --git a/tests/background-account-run-history-module.test.js b/tests/background-account-run-history-module.test.js new file mode 100644 index 0000000..9342b9f --- /dev/null +++ b/tests/background-account-run-history-module.test.js @@ -0,0 +1,76 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +test('background imports account run history module', () => { + const source = fs.readFileSync('background.js', 'utf8'); + assert.match(source, /background\/account-run-history\.js/); +}); + +test('account run history module exposes a factory', () => { + const source = fs.readFileSync('background/account-run-history.js', 'utf8'); + const globalScope = {}; + + const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope); + + assert.equal(typeof api?.createAccountRunHistoryHelpers, 'function'); +}); + +test('account run history helper normalizes records and persists without helper upload when local helper is disabled', async () => { + const source = fs.readFileSync('background/account-run-history.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope); + + let storedHistory = [{ email: 'old@example.com', password: 'old-pass', status: 'success', recordedAt: '2026-04-17T00:00:00.000Z' }]; + let fetchCalled = false; + global.fetch = async () => { + fetchCalled = true; + throw new Error('should not call fetch'); + }; + + const helpers = api.createAccountRunHistoryHelpers({ + ACCOUNT_RUN_HISTORY_STORAGE_KEY: 'accountRunHistory', + addLog: async () => {}, + buildHotmailLocalEndpoint: (baseUrl, path) => `${baseUrl}${path}`, + chrome: { + storage: { + local: { + get: async () => ({ accountRunHistory: storedHistory }), + set: async (payload) => { + storedHistory = payload.accountRunHistory; + }, + }, + }, + }, + getErrorMessage: (error) => error?.message || String(error || ''), + getState: async () => ({ + email: ' latest@example.com ', + password: ' secret ', + hotmailServiceMode: 'remote', + hotmailLocalBaseUrl: '', + }), + HOTMAIL_SERVICE_MODE_LOCAL: 'local', + normalizeHotmailLocalBaseUrl: (value) => String(value || '').trim(), + }); + + const record = helpers.buildAccountRunHistoryRecord( + { email: ' latest@example.com ', password: ' secret ' }, + ' FAILED ', + ' reason ' + ); + assert.deepStrictEqual(record, { + email: 'latest@example.com', + password: 'secret', + status: 'failed', + recordedAt: record.recordedAt, + reason: 'reason', + }); + + const appended = await helpers.appendAccountRunRecord('failed', null, 'boom'); + assert.equal(appended.email, 'latest@example.com'); + assert.equal(appended.status, 'failed'); + assert.equal(storedHistory.length, 2); + assert.equal(storedHistory[1].reason, 'boom'); + assert.equal(fetchCalled, false); + assert.equal(helpers.shouldAppendAccountRunTextFile({ hotmailServiceMode: 'remote', hotmailLocalBaseUrl: 'http://127.0.0.1:17373' }), false); +}); diff --git a/tests/background-step6-retry-limit.test.js b/tests/background-step6-retry-limit.test.js new file mode 100644 index 0000000..ee3a5ad --- /dev/null +++ b/tests/background-step6-retry-limit.test.js @@ -0,0 +1,103 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); +const globalScope = {}; +const api = new Function('self', `${source}; return self.MultiPageBackgroundStep6;`)(globalScope); + +test('step 6 retries up to configured limit and then fails', async () => { + const events = { + cleanupCalls: 0, + refreshCalls: 0, + sendCalls: 0, + completed: 0, + }; + + const executor = api.createStep6Executor({ + addLog: async () => {}, + completeStepFromBackground: async () => { + events.completed += 1; + }, + getErrorMessage: (error) => error?.message || String(error || ''), + getLoginAuthStateLabel: (state) => state || 'unknown', + getState: async () => ({ email: 'user@example.com', password: 'secret' }), + isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable', + isStep6SuccessResult: (result) => result?.step6Outcome === 'success', + refreshOAuthUrlBeforeStep6: async () => { + events.refreshCalls += 1; + return `https://oauth.example/${events.refreshCalls}`; + }, + reuseOrCreateTab: async () => {}, + runPreStep6CookieCleanup: async () => { + events.cleanupCalls += 1; + }, + sendToContentScriptResilient: async () => { + events.sendCalls += 1; + return { + step6Outcome: 'recoverable', + state: 'email_page', + message: '当前仍停留在邮箱页', + }; + }, + shouldSkipLoginVerificationForCpaCallback: () => false, + skipLoginVerificationStepsForCpaCallback: async () => {}, + STEP6_MAX_ATTEMPTS: 3, + throwIfStopped: () => {}, + }); + + await assert.rejects( + () => executor.executeStep6({ email: 'user@example.com', password: 'secret' }), + /已重试 2 次,仍未成功/ + ); + + assert.equal(events.cleanupCalls, 1); + assert.equal(events.refreshCalls, 3); + assert.equal(events.sendCalls, 3); + assert.equal(events.completed, 0); +}); + +test('step 6 can skip pre-login cleanup during step 7 recovery replay', async () => { + const events = { + cleanupCalls: 0, + completedPayloads: [], + }; + + const executor = api.createStep6Executor({ + addLog: async () => {}, + completeStepFromBackground: async (step, payload) => { + events.completedPayloads.push({ step, payload }); + }, + getErrorMessage: (error) => error?.message || String(error || ''), + getLoginAuthStateLabel: (state) => state || 'unknown', + getState: async () => ({ email: 'user@example.com', password: 'secret' }), + isStep6RecoverableResult: () => false, + isStep6SuccessResult: (result) => result?.step6Outcome === 'success', + refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest', + reuseOrCreateTab: async () => {}, + runPreStep6CookieCleanup: async () => { + events.cleanupCalls += 1; + }, + sendToContentScriptResilient: async () => ({ + step6Outcome: 'success', + loginVerificationRequestedAt: 123, + }), + shouldSkipLoginVerificationForCpaCallback: () => false, + skipLoginVerificationStepsForCpaCallback: async () => {}, + STEP6_MAX_ATTEMPTS: 3, + throwIfStopped: () => {}, + }); + + await executor.executeStep6( + { email: 'user@example.com', password: 'secret' }, + { skipPreLoginCleanup: true } + ); + + assert.equal(events.cleanupCalls, 0); + assert.deepStrictEqual(events.completedPayloads, [ + { + step: 6, + payload: { loginVerificationRequestedAt: 123 }, + }, + ]); +}); diff --git a/tests/background-step7-recovery.test.js b/tests/background-step7-recovery.test.js new file mode 100644 index 0000000..baeb408 --- /dev/null +++ b/tests/background-step7-recovery.test.js @@ -0,0 +1,127 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('background/steps/fetch-login-code.js', 'utf8'); +const globalScope = {}; +const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); + +test('step 7 refreshes CPA oauth via step 6 replay before submitting verification code', async () => { + const calls = { + ensureReady: 0, + executeStep6: [], + sleep: [], + resolveOptions: null, + }; + + const executor = api.createStep7Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + confirmCustomVerificationStepBypass: async () => {}, + ensureStep7VerificationPageReady: async () => { + calls.ensureReady += 1; + return { state: 'verification_page' }; + }, + executeStep6: async (_state, options = {}) => { + calls.executeStep6.push(options); + }, + getMailConfig: () => ({ + provider: 'qq', + label: 'QQ 邮箱', + source: 'mail-qq', + url: 'https://mail.qq.com', + navigateOnReuse: false, + }), + getPanelMode: () => 'cpa', + getState: async () => ({ email: 'user@example.com', password: 'secret' }), + getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2), + HOTMAIL_PROVIDER: 'hotmail-api', + isTabAlive: async () => true, + isVerificationMailPollingError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + resolveVerificationStep: async (_step, _state, _mail, options) => { + calls.resolveOptions = options; + await options.beforeSubmit({ code: '654321' }); + }, + reuseOrCreateTab: async () => {}, + setState: async () => {}, + setStepStatus: async () => {}, + shouldSkipLoginVerificationForCpaCallback: () => false, + shouldUseCustomRegistrationEmail: () => false, + sleepWithStop: async (ms) => { + calls.sleep.push(ms); + }, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, + STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8, + throwIfStopped: () => {}, + }); + + await executor.executeStep7({ + email: 'user@example.com', + password: 'secret', + oauthUrl: 'https://oauth.example/latest', + }); + + assert.equal(typeof calls.resolveOptions.beforeSubmit, 'function'); + assert.equal(calls.ensureReady, 2); + assert.deepStrictEqual(calls.executeStep6, [{ skipPreLoginCleanup: true }]); + assert.deepStrictEqual(calls.sleep, [1200]); + assert.equal(calls.resolveOptions.resendIntervalMs, 25000); +}); + +test('step 7 disables resend interval for 2925 mailbox polling', async () => { + let capturedOptions = null; + + const executor = api.createStep7Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + confirmCustomVerificationStepBypass: async () => {}, + ensureStep7VerificationPageReady: async () => ({ state: 'verification_page' }), + executeStep6: async () => {}, + getMailConfig: () => ({ + provider: '2925', + label: '2925 邮箱', + source: 'mail-2925', + url: 'https://2925.com', + navigateOnReuse: false, + }), + getPanelMode: () => 'sub2api', + getState: async () => ({ email: 'user@example.com', password: 'secret' }), + getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2), + HOTMAIL_PROVIDER: 'hotmail-api', + isTabAlive: async () => true, + isVerificationMailPollingError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + resolveVerificationStep: async (_step, _state, _mail, options) => { + capturedOptions = options; + }, + reuseOrCreateTab: async () => {}, + setState: async () => {}, + setStepStatus: async () => {}, + shouldSkipLoginVerificationForCpaCallback: () => false, + shouldUseCustomRegistrationEmail: () => false, + sleepWithStop: async () => {}, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, + STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8, + throwIfStopped: () => {}, + }); + + await executor.executeStep7({ + email: 'user@example.com', + password: 'secret', + oauthUrl: 'https://oauth.example/latest', + }); + + assert.equal(capturedOptions.resendIntervalMs, 0); + assert.equal(capturedOptions.beforeSubmit, undefined); +}); diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js new file mode 100644 index 0000000..37f104e --- /dev/null +++ b/tests/verification-flow-polling.test.js @@ -0,0 +1,110 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('background/verification-flow.js', 'utf8'); +const globalScope = {}; +const api = new Function('self', `${source}; return self.MultiPageBackgroundVerificationFlow;`)(globalScope); + +test('verification flow extends 2925 polling window', () => { + 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 () => ({}), + sendToMailContentScriptResilient: async () => ({}), + setState: async () => {}, + setStepStatus: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + VERIFICATION_POLL_MAX_ROUNDS: 5, + }); + + const step4Payload = helpers.getVerificationPollPayload(4, { email: 'user@example.com', mailProvider: '2925' }); + const step7Payload = helpers.getVerificationPollPayload(7, { email: 'user@example.com', mailProvider: '2925' }); + + assert.equal(step4Payload.maxAttempts, 15); + assert.equal(step4Payload.intervalMs, 15000); + assert.equal(step7Payload.maxAttempts, 15); + assert.equal(step7Payload.intervalMs, 15000); +}); + +test('verification flow runs beforeSubmit hook before filling the code', async () => { + const events = []; + + const helpers = api.createVerificationFlowHelpers({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + completeStepFromBackground: async (_step, payload) => { + events.push(['complete', payload.code]); + }, + 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') { + events.push(['submit', message.payload.code]); + return {}; + } + return {}; + }, + sendToMailContentScriptResilient: async () => ({ + code: '654321', + emailTimestamp: 123, + }), + setState: async (payload) => { + events.push(['state', payload.lastLoginCode || payload.lastSignupCode]); + }, + setStepStatus: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + VERIFICATION_POLL_MAX_ROUNDS: 5, + }); + + await helpers.resolveVerificationStep( + 7, + { email: 'user@example.com', lastLoginCode: null }, + { provider: 'qq', label: 'QQ 邮箱' }, + { + beforeSubmit: async (result) => { + events.push(['beforeSubmit', result.code]); + }, + } + ); + + assert.deepStrictEqual(events, [ + ['beforeSubmit', '654321'], + ['submit', '654321'], + ['state', '654321'], + ['complete', '654321'], + ]); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 45a5fb3..1c1d7c3 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -126,7 +126,7 @@ ### 4.2 `chrome.storage.local` -保存持久配置: +保存持久配置与账号运行历史: - CPA / SUB2API 配置 - 邮箱 provider 配置 @@ -135,6 +135,9 @@ - iCloud 相关偏好 - LuckMail API 配置 - 自动运行默认配置 +- 账号运行历史 `accountRunHistory` + +当启用了本地 Hotmail helper 时,账号运行历史还会通过 [scripts/hotmail_helper.py](c:/Users/projectf/Downloads/codex注册扩展/scripts/hotmail_helper.py) 追加写入 `data/account-run-history.txt` 文本文件,便于离线留档。 ### 4.3 状态广播 @@ -232,6 +235,11 @@ 5. 回填页面 6. 若页面拒绝,则重试或回退 +补充行为: + +- `2925` provider 会拉长单轮轮询窗口,并关闭 Step 4 / 7 的自动重发间隔,减少因邮件延迟过早判负。 +- CPA 模式下,Step 7 在真正回填登录验证码前,会先刷新最新 OAuth 地址并快速重走一次 Step 6,降低验证码等待过久导致 OAuth 过期的概率。 + ### Step 5 文件: @@ -257,7 +265,7 @@ 3. 打开最新 OAuth 链接 4. 登录 5. 确保真正进入验证码页 -6. 如果未进入验证码页,则按可恢复逻辑重试 +6. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次 ### Step 8 @@ -285,7 +293,8 @@ 3. 打开相应后台 4. 提交回调地址 5. 完成平台侧验证 -6. 做成功后的清理与标记 +6. 追加账号运行历史成功记录 +7. 做成功后的清理与标记 ## 7. 邮箱与 provider 链路 @@ -315,6 +324,10 @@ - API 对接 - 本地 helper +补充: + +- 本地 helper 除了收信与验证码读取,还提供账号运行历史文本追加接口。 + ### 7.3 LuckMail 组成: @@ -345,8 +358,9 @@ - 立即停止 - 当前轮重试 - 下一轮继续 -6. 如果配置了线程间隔,则挂计时计划 -7. 所有轮次结束后输出汇总 +6. 当前轮最终失败或被停止时,追加账号运行历史记录 +7. 如果配置了线程间隔,则挂计时计划 +8. 所有轮次结束后输出汇总 ## 9. 新增功能时最容易漏掉的地方 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 35a9722..dab1fcc 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -38,6 +38,7 @@ ## `background/` +- `background/account-run-history.js`:账号运行历史模块,负责将成功/失败/停止结果持久化到 `chrome.storage.local`,并在启用本地 Hotmail helper 时追加写入文本日志。 - `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑。 - `background/generated-email-helpers.js`:生成邮箱辅助层,封装 Duck、Cloudflare、Cloudflare Temp Email、iCloud 隐私邮箱的获取逻辑。 - `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层。 @@ -46,7 +47,7 @@ - `background/panel-bridge.js`:CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。 - `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱。 - `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列。 -- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退与自定义邮箱跳过逻辑。 +- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过以及 2925 长轮询参数。 ## `background/steps/` @@ -101,7 +102,7 @@ ## `scripts/` -- `scripts/hotmail_helper.py`:本地 Hotmail helper 服务,负责通过本地接口协助获取邮件和验证码。 +- `scripts/hotmail_helper.py`:本地 Hotmail helper 服务,负责通过本地接口协助获取邮件和验证码,并可追加写入账号运行历史文本日志。 ## `sidepanel/` @@ -119,6 +120,7 @@ - `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文。 - `tests/auto-step-random-delay.test.js`:测试自动运行步间延迟与旧配置键兼容解析。 - `tests/background-auto-run-module.test.js`:测试自动运行控制器模块已接入且导出工厂。 +- `tests/background-account-run-history-module.test.js`:测试账号运行历史模块已接入、导出工厂,并能在不启用本地 helper 时正确持久化记录。 - `tests/background-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂。 - `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。 - `tests/background-icloud-mail-provider.test.js`:测试 iCloud 邮箱 provider 配置解析。 @@ -129,6 +131,8 @@ - `tests/background-panel-bridge-module.test.js`:测试面板桥接模块已接入且导出工厂。 - `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂。 - `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。 +- `tests/background-step6-retry-limit.test.js`:测试 Step 6 的有限重试上限,以及 Step 7 回放时可跳过登录前 Cookie 清理。 +- `tests/background-step7-recovery.test.js`:测试 Step 7 在 CPA 模式下会先回放 Step 6 再提交登录验证码,并为 2925 关闭重发间隔。 - `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。 - `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂。 - `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。 @@ -157,3 +161,4 @@ - `tests/step9-cpa-mode.test.js`:测试本地 CPA Step 9 策略判断。 - `tests/step9-localhost-cleanup-scope.test.js`:测试 Step 9 仅清理精确命中的 localhost callback 和路径前缀残留页。 - `tests/verification-stop-propagation.test.js`:测试验证码流程在 Stop 场景下的错误传播与不中途降级。 +- `tests/verification-flow-polling.test.js`:测试 2925 长轮询参数,以及验证码提交流程中的 `beforeSubmit` 钩子执行顺序。