feat: add account run history module and logging functionality
- Implemented `background/account-run-history.js` to persist account run results (success, failure, stop) in `chrome.storage.local` and log to a text file when the local Hotmail helper is enabled. - Enhanced `background/auto-run-controller.js` and `background/message-router.js` to utilize the new account run record functionality. - Updated steps in `background/steps/fetch-login-code.js`, `background/steps/fetch-signup-code.js`, and `background/steps/oauth-login.js` to handle retries and logging appropriately. - Introduced tests for account run history and step retry limits to ensure functionality and reliability. - Modified documentation to reflect new features and changes in behavior for specific providers (e.g., 2925).
This commit is contained in:
+46
-2
@@ -1,6 +1,7 @@
|
|||||||
// background.js — Service Worker: orchestration, state, tab management, message routing
|
// background.js — Service Worker: orchestration, state, tab management, message routing
|
||||||
|
|
||||||
importScripts(
|
importScripts(
|
||||||
|
'background/account-run-history.js',
|
||||||
'background/panel-bridge.js',
|
'background/panel-bridge.js',
|
||||||
'background/generated-email-helpers.js',
|
'background/generated-email-helpers.js',
|
||||||
'background/signup-flow-helpers.js',
|
'background/signup-flow-helpers.js',
|
||||||
@@ -123,6 +124,7 @@ const HOTMAIL_MAILBOXES = ['INBOX', 'Junk'];
|
|||||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||||
const HUMAN_STEP_DELAY_MIN = 700;
|
const HUMAN_STEP_DELAY_MIN = 700;
|
||||||
const HUMAN_STEP_DELAY_MAX = 2200;
|
const HUMAN_STEP_DELAY_MAX = 2200;
|
||||||
|
const STEP6_MAX_ATTEMPTS = 3;
|
||||||
const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8;
|
const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8;
|
||||||
const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000;
|
const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000;
|
||||||
const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000;
|
const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000;
|
||||||
@@ -154,6 +156,7 @@ const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
|
|||||||
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
|
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
|
||||||
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
|
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
|
||||||
const PERSISTENT_ALIAS_STATE_KEYS = ['manualAliasUsage', 'preservedAliases'];
|
const PERSISTENT_ALIAS_STATE_KEYS = ['manualAliasUsage', 'preservedAliases'];
|
||||||
|
const ACCOUNT_RUN_HISTORY_STORAGE_KEY = 'accountRunHistory';
|
||||||
|
|
||||||
initializeSessionStorageAccess();
|
initializeSessionStorageAccess();
|
||||||
setupDeclarativeNetRequestRules();
|
setupDeclarativeNetRequestRules();
|
||||||
@@ -4400,16 +4403,34 @@ function notifyStepError(step, error) {
|
|||||||
async function completeStepFromBackground(step, payload = {}) {
|
async function completeStepFromBackground(step, payload = {}) {
|
||||||
if (stopRequested) {
|
if (stopRequested) {
|
||||||
await setStepStatus(step, 'stopped');
|
await setStepStatus(step, 'stopped');
|
||||||
|
await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, null, STOP_ERROR_MESSAGE);
|
||||||
notifyStepError(step, STOP_ERROR_MESSAGE);
|
notifyStepError(step, STOP_ERROR_MESSAGE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const completionState = step === 9 ? await getState() : null;
|
||||||
await setStepStatus(step, 'completed');
|
await setStepStatus(step, 'completed');
|
||||||
await addLog(`步骤 ${step} 已完成`, 'ok');
|
await addLog(`步骤 ${step} 已完成`, 'ok');
|
||||||
await handleStepData(step, payload);
|
await handleStepData(step, payload);
|
||||||
|
if (step === 9 && accountRunHistoryHelpers?.appendAccountRunRecord) {
|
||||||
|
await accountRunHistoryHelpers.appendAccountRunRecord('success', completionState);
|
||||||
|
}
|
||||||
notifyStepComplete(step, payload);
|
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) {
|
async function finalizeDeferredStepExecutionError(step, error) {
|
||||||
const latestState = await getState();
|
const latestState = await getState();
|
||||||
const currentStatus = latestState.stepStatuses?.[step];
|
const currentStatus = latestState.stepStatuses?.[step];
|
||||||
@@ -4420,11 +4441,13 @@ async function finalizeDeferredStepExecutionError(step, error) {
|
|||||||
if (isStopError(error)) {
|
if (isStopError(error)) {
|
||||||
await setStepStatus(step, 'stopped');
|
await setStepStatus(step, 'stopped');
|
||||||
await addLog(`步骤 ${step} 已被用户停止`, 'warn');
|
await addLog(`步骤 ${step} 已被用户停止`, 'warn');
|
||||||
|
await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, latestState, getErrorMessage(error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await setStepStatus(step, 'failed');
|
await setStepStatus(step, 'failed');
|
||||||
await addLog(`步骤 ${step} 失败:${getErrorMessage(error)}`, 'error');
|
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) {
|
async function executeStepViaCompletionSignal(step, timeoutMs = AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS) {
|
||||||
@@ -4592,11 +4615,13 @@ async function executeStep(step, options = {}) {
|
|||||||
if (isStopError(err)) {
|
if (isStopError(err)) {
|
||||||
await setStepStatus(step, 'stopped');
|
await setStepStatus(step, 'stopped');
|
||||||
await addLog(`步骤 ${step} 已被用户停止`, 'warn');
|
await addLog(`步骤 ${step} 已被用户停止`, 'warn');
|
||||||
|
await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, state, getErrorMessage(err));
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step) && isRetryableContentScriptTransportError(err))) {
|
if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step) && isRetryableContentScriptTransportError(err))) {
|
||||||
await setStepStatus(step, 'failed');
|
await setStepStatus(step, 'failed');
|
||||||
await addLog(`步骤 ${step} 失败:${err.message}`, 'error');
|
await addLog(`步骤 ${step} 失败:${err.message}`, 'error');
|
||||||
|
await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, state, getErrorMessage(err));
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
LOG_PREFIX,
|
LOG_PREFIX,
|
||||||
@@ -4713,6 +4738,8 @@ let autoRunAttemptRun = 0;
|
|||||||
const EMAIL_FETCH_MAX_ATTEMPTS = 5;
|
const EMAIL_FETCH_MAX_ATTEMPTS = 5;
|
||||||
const VERIFICATION_POLL_MAX_ROUNDS = 5;
|
const VERIFICATION_POLL_MAX_ROUNDS = 5;
|
||||||
const STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS = 25000;
|
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 = {
|
const AUTO_STEP_DELAYS = {
|
||||||
1: 2000,
|
1: 2000,
|
||||||
2: 2000,
|
2: 2000,
|
||||||
@@ -4724,8 +4751,19 @@ const AUTO_STEP_DELAYS = {
|
|||||||
8: 2000,
|
8: 2000,
|
||||||
9: 1000,
|
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({
|
const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({
|
||||||
addLog,
|
addLog,
|
||||||
|
appendAccountRunRecord: (...args) => accountRunHistoryHelpers?.appendAccountRunRecord?.(...args),
|
||||||
AUTO_RUN_MAX_RETRIES_PER_ROUND,
|
AUTO_RUN_MAX_RETRIES_PER_ROUND,
|
||||||
AUTO_RUN_RETRY_DELAY_MS,
|
AUTO_RUN_RETRY_DELAY_MS,
|
||||||
AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
|
AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
|
||||||
@@ -5117,6 +5155,8 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create
|
|||||||
HOTMAIL_PROVIDER,
|
HOTMAIL_PROVIDER,
|
||||||
isStopError,
|
isStopError,
|
||||||
LUCKMAIL_PROVIDER,
|
LUCKMAIL_PROVIDER,
|
||||||
|
MAIL_2925_VERIFICATION_INTERVAL_MS,
|
||||||
|
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
|
||||||
pollCloudflareTempEmailVerificationCode,
|
pollCloudflareTempEmailVerificationCode,
|
||||||
pollHotmailVerificationCode,
|
pollHotmailVerificationCode,
|
||||||
pollLuckmailVerificationCode,
|
pollLuckmailVerificationCode,
|
||||||
@@ -5190,6 +5230,7 @@ const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({
|
|||||||
const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({
|
const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({
|
||||||
addLog,
|
addLog,
|
||||||
completeStepFromBackground,
|
completeStepFromBackground,
|
||||||
|
getErrorMessage,
|
||||||
getLoginAuthStateLabel,
|
getLoginAuthStateLabel,
|
||||||
getState,
|
getState,
|
||||||
isStep6RecoverableResult,
|
isStep6RecoverableResult,
|
||||||
@@ -5200,6 +5241,7 @@ const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({
|
|||||||
sendToContentScriptResilient,
|
sendToContentScriptResilient,
|
||||||
shouldSkipLoginVerificationForCpaCallback,
|
shouldSkipLoginVerificationForCpaCallback,
|
||||||
skipLoginVerificationStepsForCpaCallback,
|
skipLoginVerificationStepsForCpaCallback,
|
||||||
|
STEP6_MAX_ATTEMPTS,
|
||||||
throwIfStopped,
|
throwIfStopped,
|
||||||
});
|
});
|
||||||
const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
|
const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
|
||||||
@@ -5209,6 +5251,7 @@ const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
|
|||||||
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
|
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
|
||||||
ensureStep7VerificationPageReady,
|
ensureStep7VerificationPageReady,
|
||||||
executeStep6: (...args) => executeStep6(...args),
|
executeStep6: (...args) => executeStep6(...args),
|
||||||
|
getPanelMode,
|
||||||
getMailConfig,
|
getMailConfig,
|
||||||
getState,
|
getState,
|
||||||
getTabId,
|
getTabId,
|
||||||
@@ -5259,6 +5302,7 @@ const stepExecutorsByKey = {
|
|||||||
};
|
};
|
||||||
const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({
|
const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({
|
||||||
addLog,
|
addLog,
|
||||||
|
appendAccountRunRecord: (...args) => accountRunHistoryHelpers?.appendAccountRunRecord?.(...args),
|
||||||
batchUpdateLuckmailPurchases,
|
batchUpdateLuckmailPurchases,
|
||||||
buildLocalhostCleanupPrefix,
|
buildLocalhostCleanupPrefix,
|
||||||
buildLuckmailSessionSettingsPayload,
|
buildLuckmailSessionSettingsPayload,
|
||||||
@@ -5801,8 +5845,8 @@ async function skipLoginVerificationStepsForCpaCallback() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeStep6(state) {
|
async function executeStep6(state, options = {}) {
|
||||||
return step6Executor.executeStep6(state);
|
return step6Executor.executeStep6(state, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
function createAutoRunController(deps = {}) {
|
function createAutoRunController(deps = {}) {
|
||||||
const {
|
const {
|
||||||
addLog,
|
addLog,
|
||||||
|
appendAccountRunRecord,
|
||||||
AUTO_RUN_MAX_RETRIES_PER_ROUND,
|
AUTO_RUN_MAX_RETRIES_PER_ROUND,
|
||||||
AUTO_RUN_RETRY_DELAY_MS,
|
AUTO_RUN_RETRY_DELAY_MS,
|
||||||
AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
|
AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
|
||||||
@@ -303,6 +304,7 @@
|
|||||||
|
|
||||||
for (let targetRun = resumeCurrentRun; targetRun <= totalRuns; targetRun += 1) {
|
for (let targetRun = resumeCurrentRun; targetRun <= totalRuns; targetRun += 1) {
|
||||||
const roundSummary = roundSummaries[targetRun - 1];
|
const roundSummary = roundSummaries[targetRun - 1];
|
||||||
|
let roundRecordAppended = false;
|
||||||
const resumingCurrentRound = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun;
|
const resumingCurrentRound = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun;
|
||||||
let attemptRun = resumingCurrentRound ? resumeAttemptRun : 1;
|
let attemptRun = resumingCurrentRound ? resumeAttemptRun : 1;
|
||||||
let reuseExistingProgress = resumingCurrentRound;
|
let reuseExistingProgress = resumingCurrentRound;
|
||||||
@@ -377,6 +379,21 @@
|
|||||||
forceFreshTabsNextRun = false;
|
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 {
|
try {
|
||||||
deps.throwIfStopped();
|
deps.throwIfStopped();
|
||||||
await broadcastAutoRunStatus('running', {
|
await broadcastAutoRunStatus('running', {
|
||||||
@@ -403,6 +420,7 @@
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isStopError(err)) {
|
if (isStopError(err)) {
|
||||||
stoppedEarly = true;
|
stoppedEarly = true;
|
||||||
|
await appendRoundRecordIfNeeded('stopped', getErrorMessage(err));
|
||||||
await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
||||||
await broadcastAutoRunStatus('stopped', {
|
await broadcastAutoRunStatus('stopped', {
|
||||||
currentRun: targetRun,
|
currentRun: targetRun,
|
||||||
@@ -444,6 +462,7 @@
|
|||||||
} catch (sleepError) {
|
} catch (sleepError) {
|
||||||
if (isStopError(sleepError)) {
|
if (isStopError(sleepError)) {
|
||||||
stoppedEarly = true;
|
stoppedEarly = true;
|
||||||
|
await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError));
|
||||||
await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
||||||
await broadcastAutoRunStatus('stopped', {
|
await broadcastAutoRunStatus('stopped', {
|
||||||
currentRun: targetRun,
|
currentRun: targetRun,
|
||||||
@@ -466,6 +485,7 @@
|
|||||||
} catch (sleepError) {
|
} catch (sleepError) {
|
||||||
if (isStopError(sleepError)) {
|
if (isStopError(sleepError)) {
|
||||||
stoppedEarly = true;
|
stoppedEarly = true;
|
||||||
|
await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError));
|
||||||
await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
||||||
await broadcastAutoRunStatus('stopped', {
|
await broadcastAutoRunStatus('stopped', {
|
||||||
currentRun: targetRun,
|
currentRun: targetRun,
|
||||||
@@ -486,6 +506,7 @@
|
|||||||
await setState({
|
await setState({
|
||||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||||
});
|
});
|
||||||
|
await appendRoundRecordIfNeeded('failed', reason);
|
||||||
if (!autoRunSkipFailures) {
|
if (!autoRunSkipFailures) {
|
||||||
cancelPendingCommands('当前轮执行失败。');
|
cancelPendingCommands('当前轮执行失败。');
|
||||||
await broadcastStopToContentScripts();
|
await broadcastStopToContentScripts();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
function createMessageRouter(deps = {}) {
|
function createMessageRouter(deps = {}) {
|
||||||
const {
|
const {
|
||||||
addLog,
|
addLog,
|
||||||
|
appendAccountRunRecord,
|
||||||
batchUpdateLuckmailPurchases,
|
batchUpdateLuckmailPurchases,
|
||||||
buildLocalhostCleanupPrefix,
|
buildLocalhostCleanupPrefix,
|
||||||
buildLuckmailSessionSettingsPayload,
|
buildLuckmailSessionSettingsPayload,
|
||||||
@@ -78,6 +79,19 @@
|
|||||||
verifyHotmailAccount,
|
verifyHotmailAccount,
|
||||||
} = deps;
|
} = 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) {
|
async function handleStepData(step, payload) {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 1: {
|
case 1: {
|
||||||
@@ -187,12 +201,17 @@
|
|||||||
case 'STEP_COMPLETE': {
|
case 'STEP_COMPLETE': {
|
||||||
if (getStopRequested()) {
|
if (getStopRequested()) {
|
||||||
await setStepStatus(message.step, 'stopped');
|
await setStepStatus(message.step, 'stopped');
|
||||||
|
await appendManualAccountRunRecordIfNeeded(`step${message.step}_stopped`, null, '流程已被用户停止。');
|
||||||
notifyStepError(message.step, '流程已被用户停止。');
|
notifyStepError(message.step, '流程已被用户停止。');
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
const completionState = message.step === 9 ? await getState() : null;
|
||||||
await setStepStatus(message.step, 'completed');
|
await setStepStatus(message.step, 'completed');
|
||||||
await addLog(`步骤 ${message.step} 已完成`, 'ok');
|
await addLog(`步骤 ${message.step} 已完成`, 'ok');
|
||||||
await handleStepData(message.step, message.payload);
|
await handleStepData(message.step, message.payload);
|
||||||
|
if (message.step === 9 && typeof appendAccountRunRecord === 'function') {
|
||||||
|
await appendAccountRunRecord('success', completionState);
|
||||||
|
}
|
||||||
notifyStepComplete(message.step, message.payload);
|
notifyStepComplete(message.step, message.payload);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
@@ -201,10 +220,12 @@
|
|||||||
if (isStopError(message.error)) {
|
if (isStopError(message.error)) {
|
||||||
await setStepStatus(message.step, 'stopped');
|
await setStepStatus(message.step, 'stopped');
|
||||||
await addLog(`步骤 ${message.step} 已被用户停止`, 'warn');
|
await addLog(`步骤 ${message.step} 已被用户停止`, 'warn');
|
||||||
|
await appendManualAccountRunRecordIfNeeded(`step${message.step}_stopped`, null, message.error);
|
||||||
notifyStepError(message.step, message.error);
|
notifyStepError(message.step, message.error);
|
||||||
} else {
|
} else {
|
||||||
await setStepStatus(message.step, 'failed');
|
await setStepStatus(message.step, 'failed');
|
||||||
await addLog(`步骤 ${message.step} 失败:${message.error}`, 'error');
|
await addLog(`步骤 ${message.step} 失败:${message.error}`, 'error');
|
||||||
|
await appendManualAccountRunRecordIfNeeded(`step${message.step}_failed`, null, message.error);
|
||||||
notifyStepError(message.step, message.error);
|
notifyStepError(message.step, message.error);
|
||||||
}
|
}
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
confirmCustomVerificationStepBypass,
|
confirmCustomVerificationStepBypass,
|
||||||
ensureStep7VerificationPageReady,
|
ensureStep7VerificationPageReady,
|
||||||
executeStep6,
|
executeStep6,
|
||||||
|
getPanelMode,
|
||||||
getMailConfig,
|
getMailConfig,
|
||||||
getState,
|
getState,
|
||||||
getTabId,
|
getTabId,
|
||||||
@@ -77,18 +78,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldRefreshOAuthBeforeSubmit = getPanelMode(state) === 'cpa';
|
||||||
|
let step6ReplayCompleted = false;
|
||||||
|
|
||||||
await resolveVerificationStep(7, state, mail, {
|
await resolveVerificationStep(7, state, mail, {
|
||||||
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : Math.max(0, stepStartedAt - 60000),
|
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : Math.max(0, stepStartedAt - 60000),
|
||||||
requestFreshCodeFirst: false,
|
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();
|
const currentState = await getState();
|
||||||
await addLog('步骤 7:正在回到步骤 6,重新发起登录验证码流程...', 'warn');
|
await addLog(logMessage, 'warn');
|
||||||
await executeStep6(currentState);
|
await executeStep6(currentState, { skipPreLoginCleanup });
|
||||||
await sleepWithStop(3000);
|
if (postStepDelayMs > 0) {
|
||||||
|
await sleepWithStop(postStepDelayMs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeStep7(state) {
|
async function executeStep7(state) {
|
||||||
|
|||||||
@@ -89,7 +89,9 @@
|
|||||||
await resolveVerificationStep(4, state, mail, {
|
await resolveVerificationStep(4, state, mail, {
|
||||||
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt,
|
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt,
|
||||||
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
|
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
const {
|
const {
|
||||||
addLog,
|
addLog,
|
||||||
completeStepFromBackground,
|
completeStepFromBackground,
|
||||||
|
getErrorMessage,
|
||||||
getLoginAuthStateLabel,
|
getLoginAuthStateLabel,
|
||||||
getState,
|
getState,
|
||||||
isStep6RecoverableResult,
|
isStep6RecoverableResult,
|
||||||
@@ -15,10 +16,12 @@
|
|||||||
sendToContentScriptResilient,
|
sendToContentScriptResilient,
|
||||||
shouldSkipLoginVerificationForCpaCallback,
|
shouldSkipLoginVerificationForCpaCallback,
|
||||||
skipLoginVerificationStepsForCpaCallback,
|
skipLoginVerificationStepsForCpaCallback,
|
||||||
|
STEP6_MAX_ATTEMPTS,
|
||||||
throwIfStopped,
|
throwIfStopped,
|
||||||
} = deps;
|
} = deps;
|
||||||
|
|
||||||
async function executeStep6(state) {
|
async function executeStep6(state, options = {}) {
|
||||||
|
const { skipPreLoginCleanup = false } = options;
|
||||||
if (shouldSkipLoginVerificationForCpaCallback(state)) {
|
if (shouldSkipLoginVerificationForCpaCallback(state)) {
|
||||||
await skipLoginVerificationStepsForCpaCallback();
|
await skipLoginVerificationStepsForCpaCallback();
|
||||||
return;
|
return;
|
||||||
@@ -27,63 +30,77 @@
|
|||||||
throw new Error('缺少邮箱地址,请先完成步骤 3。');
|
throw new Error('缺少邮箱地址,请先完成步骤 3。');
|
||||||
}
|
}
|
||||||
|
|
||||||
await runPreStep6CookieCleanup();
|
if (!skipPreLoginCleanup) {
|
||||||
|
await runPreStep6CookieCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
while (true) {
|
while (attempt < STEP6_MAX_ATTEMPTS) {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
attempt += 1;
|
attempt += 1;
|
||||||
const currentState = attempt === 1 ? state : await getState();
|
try {
|
||||||
const password = currentState.password || currentState.customPassword || '';
|
const currentState = attempt === 1 ? state : await getState();
|
||||||
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
|
const password = currentState.password || currentState.customPassword || '';
|
||||||
|
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
|
||||||
|
|
||||||
if (attempt === 1) {
|
if (attempt === 1) {
|
||||||
await addLog('步骤 6:正在打开最新 OAuth 链接并登录...');
|
await addLog('步骤 6:正在打开最新 OAuth 链接并登录...');
|
||||||
} else {
|
} else {
|
||||||
await addLog(`步骤 6:上一轮登录未进入验证码页,正在重新发起第 ${attempt} 轮登录尝试...`, 'warn');
|
await addLog(`步骤 6:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, '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 (result?.error) {
|
await reuseOrCreateTab('signup-page', oauthUrl);
|
||||||
throw new Error(result.error);
|
|
||||||
|
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 };
|
return { executeStep6 };
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
HOTMAIL_PROVIDER,
|
HOTMAIL_PROVIDER,
|
||||||
isStopError,
|
isStopError,
|
||||||
LUCKMAIL_PROVIDER,
|
LUCKMAIL_PROVIDER,
|
||||||
|
MAIL_2925_VERIFICATION_INTERVAL_MS,
|
||||||
|
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
|
||||||
pollCloudflareTempEmailVerificationCode,
|
pollCloudflareTempEmailVerificationCode,
|
||||||
pollHotmailVerificationCode,
|
pollHotmailVerificationCode,
|
||||||
pollLuckmailVerificationCode,
|
pollLuckmailVerificationCode,
|
||||||
@@ -62,14 +64,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getVerificationPollPayload(step, state, overrides = {}) {
|
function getVerificationPollPayload(step, state, overrides = {}) {
|
||||||
|
const is2925Provider = state?.mailProvider === '2925';
|
||||||
if (step === 4) {
|
if (step === 4) {
|
||||||
return {
|
return {
|
||||||
filterAfterTimestamp: getHotmailVerificationRequestTimestamp(4, state),
|
filterAfterTimestamp: getHotmailVerificationRequestTimestamp(4, state),
|
||||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
||||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
|
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
|
||||||
targetEmail: state.email,
|
targetEmail: state.email,
|
||||||
maxAttempts: 5,
|
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
|
||||||
intervalMs: 3000,
|
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -79,8 +82,8 @@
|
|||||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
||||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
|
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
|
||||||
targetEmail: state.email,
|
targetEmail: state.email,
|
||||||
maxAttempts: 5,
|
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
|
||||||
intervalMs: 3000,
|
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -368,6 +371,9 @@
|
|||||||
const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER
|
const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER
|
||||||
? getHotmailVerificationPollConfig(step)
|
? getHotmailVerificationPollConfig(step)
|
||||||
: null;
|
: null;
|
||||||
|
const beforeSubmit = typeof options.beforeSubmit === 'function'
|
||||||
|
? options.beforeSubmit
|
||||||
|
: null;
|
||||||
const ignorePersistedLastCode = Boolean(hotmailPollConfig?.ignorePersistedLastCode);
|
const ignorePersistedLastCode = Boolean(hotmailPollConfig?.ignorePersistedLastCode);
|
||||||
if (state[stateKey] && !ignorePersistedLastCode) {
|
if (state[stateKey] && !ignorePersistedLastCode) {
|
||||||
rejectedCodes.add(state[stateKey]);
|
rejectedCodes.add(state[stateKey]);
|
||||||
@@ -431,6 +437,14 @@
|
|||||||
|
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`);
|
await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`);
|
||||||
|
if (beforeSubmit) {
|
||||||
|
await beforeSubmit(result, {
|
||||||
|
attempt,
|
||||||
|
rejectedCodes: new Set(rejectedCodes),
|
||||||
|
filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined,
|
||||||
|
lastResendAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
const submitResult = await submitVerificationCode(step, result.code);
|
const submitResult = await submitVerificationCode(step, result.code);
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import email
|
|||||||
import html
|
import html
|
||||||
import imaplib
|
import imaplib
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from email.header import decode_header
|
from email.header import decode_header
|
||||||
from email.utils import parseaddr, parsedate_to_datetime
|
from email.utils import parseaddr, parsedate_to_datetime
|
||||||
@@ -59,6 +62,9 @@ IMAP_HOST = "outlook.office365.com"
|
|||||||
IMAP_PORT = 993
|
IMAP_PORT = 993
|
||||||
REQUEST_TIMEOUT_SECONDS = 45
|
REQUEST_TIMEOUT_SECONDS = 45
|
||||||
FETCH_LIMIT_DEFAULT = 5
|
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):
|
def json_response(handler, status, payload):
|
||||||
@@ -113,6 +119,24 @@ def log_info(message):
|
|||||||
print(f"[HotmailHelper] {message}", flush=True)
|
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):
|
def try_refresh_access_token(endpoint, client_id, refresh_token):
|
||||||
request_data = {
|
request_data = {
|
||||||
"client_id": client_id,
|
"client_id": client_id,
|
||||||
@@ -600,6 +624,21 @@ class HotmailHelperHandler(BaseHTTPRequestHandler):
|
|||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
try:
|
try:
|
||||||
payload = read_json_payload(self)
|
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()
|
email_addr = str(payload.get("email") or "").strip()
|
||||||
client_id = str(payload.get("clientId") or "").strip()
|
client_id = str(payload.get("clientId") or "").strip()
|
||||||
refresh_token = str(payload.get("refreshToken") 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}"})
|
json_response(self, 404, {"ok": False, "error": f"Unsupported path: {self.path}"})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
traceback.print_exc()
|
||||||
json_response(self, 500, {"ok": False, "error": str(exc)})
|
json_response(self, 500, {"ok": False, "error": str(exc)})
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
server = ThreadingHTTPServer((HOST, PORT), HotmailHelperHandler)
|
server = ThreadingHTTPServer((HOST, PORT), HotmailHelperHandler)
|
||||||
print(f"Hotmail helper listening on http://{HOST}:{PORT}", flush=True)
|
print(f"Hotmail helper listening on http://{HOST}:{PORT}", flush=True)
|
||||||
|
print(f"Account log file: {ACCOUNT_LOG_PATH}", flush=True)
|
||||||
try:
|
try:
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -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 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -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'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
+19
-5
@@ -126,7 +126,7 @@
|
|||||||
|
|
||||||
### 4.2 `chrome.storage.local`
|
### 4.2 `chrome.storage.local`
|
||||||
|
|
||||||
保存持久配置:
|
保存持久配置与账号运行历史:
|
||||||
|
|
||||||
- CPA / SUB2API 配置
|
- CPA / SUB2API 配置
|
||||||
- 邮箱 provider 配置
|
- 邮箱 provider 配置
|
||||||
@@ -135,6 +135,9 @@
|
|||||||
- iCloud 相关偏好
|
- iCloud 相关偏好
|
||||||
- LuckMail API 配置
|
- 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 状态广播
|
### 4.3 状态广播
|
||||||
|
|
||||||
@@ -232,6 +235,11 @@
|
|||||||
5. 回填页面
|
5. 回填页面
|
||||||
6. 若页面拒绝,则重试或回退
|
6. 若页面拒绝,则重试或回退
|
||||||
|
|
||||||
|
补充行为:
|
||||||
|
|
||||||
|
- `2925` provider 会拉长单轮轮询窗口,并关闭 Step 4 / 7 的自动重发间隔,减少因邮件延迟过早判负。
|
||||||
|
- CPA 模式下,Step 7 在真正回填登录验证码前,会先刷新最新 OAuth 地址并快速重走一次 Step 6,降低验证码等待过久导致 OAuth 过期的概率。
|
||||||
|
|
||||||
### Step 5
|
### Step 5
|
||||||
|
|
||||||
文件:
|
文件:
|
||||||
@@ -257,7 +265,7 @@
|
|||||||
3. 打开最新 OAuth 链接
|
3. 打开最新 OAuth 链接
|
||||||
4. 登录
|
4. 登录
|
||||||
5. 确保真正进入验证码页
|
5. 确保真正进入验证码页
|
||||||
6. 如果未进入验证码页,则按可恢复逻辑重试
|
6. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次
|
||||||
|
|
||||||
### Step 8
|
### Step 8
|
||||||
|
|
||||||
@@ -285,7 +293,8 @@
|
|||||||
3. 打开相应后台
|
3. 打开相应后台
|
||||||
4. 提交回调地址
|
4. 提交回调地址
|
||||||
5. 完成平台侧验证
|
5. 完成平台侧验证
|
||||||
6. 做成功后的清理与标记
|
6. 追加账号运行历史成功记录
|
||||||
|
7. 做成功后的清理与标记
|
||||||
|
|
||||||
## 7. 邮箱与 provider 链路
|
## 7. 邮箱与 provider 链路
|
||||||
|
|
||||||
@@ -315,6 +324,10 @@
|
|||||||
- API 对接
|
- API 对接
|
||||||
- 本地 helper
|
- 本地 helper
|
||||||
|
|
||||||
|
补充:
|
||||||
|
|
||||||
|
- 本地 helper 除了收信与验证码读取,还提供账号运行历史文本追加接口。
|
||||||
|
|
||||||
### 7.3 LuckMail
|
### 7.3 LuckMail
|
||||||
|
|
||||||
组成:
|
组成:
|
||||||
@@ -345,8 +358,9 @@
|
|||||||
- 立即停止
|
- 立即停止
|
||||||
- 当前轮重试
|
- 当前轮重试
|
||||||
- 下一轮继续
|
- 下一轮继续
|
||||||
6. 如果配置了线程间隔,则挂计时计划
|
6. 当前轮最终失败或被停止时,追加账号运行历史记录
|
||||||
7. 所有轮次结束后输出汇总
|
7. 如果配置了线程间隔,则挂计时计划
|
||||||
|
8. 所有轮次结束后输出汇总
|
||||||
|
|
||||||
## 9. 新增功能时最容易漏掉的地方
|
## 9. 新增功能时最容易漏掉的地方
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -38,6 +38,7 @@
|
|||||||
|
|
||||||
## `background/`
|
## `background/`
|
||||||
|
|
||||||
|
- `background/account-run-history.js`:账号运行历史模块,负责将成功/失败/停止结果持久化到 `chrome.storage.local`,并在启用本地 Hotmail helper 时追加写入文本日志。
|
||||||
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑。
|
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑。
|
||||||
- `background/generated-email-helpers.js`:生成邮箱辅助层,封装 Duck、Cloudflare、Cloudflare Temp Email、iCloud 隐私邮箱的获取逻辑。
|
- `background/generated-email-helpers.js`:生成邮箱辅助层,封装 Duck、Cloudflare、Cloudflare Temp Email、iCloud 隐私邮箱的获取逻辑。
|
||||||
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层。
|
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层。
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
- `background/panel-bridge.js`:CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。
|
- `background/panel-bridge.js`:CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。
|
||||||
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱。
|
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱。
|
||||||
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列。
|
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列。
|
||||||
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退与自定义邮箱跳过逻辑。
|
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过以及 2925 长轮询参数。
|
||||||
|
|
||||||
## `background/steps/`
|
## `background/steps/`
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@
|
|||||||
|
|
||||||
## `scripts/`
|
## `scripts/`
|
||||||
|
|
||||||
- `scripts/hotmail_helper.py`:本地 Hotmail helper 服务,负责通过本地接口协助获取邮件和验证码。
|
- `scripts/hotmail_helper.py`:本地 Hotmail helper 服务,负责通过本地接口协助获取邮件和验证码,并可追加写入账号运行历史文本日志。
|
||||||
|
|
||||||
## `sidepanel/`
|
## `sidepanel/`
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@
|
|||||||
- `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文。
|
- `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文。
|
||||||
- `tests/auto-step-random-delay.test.js`:测试自动运行步间延迟与旧配置键兼容解析。
|
- `tests/auto-step-random-delay.test.js`:测试自动运行步间延迟与旧配置键兼容解析。
|
||||||
- `tests/background-auto-run-module.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-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂。
|
||||||
- `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。
|
- `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。
|
||||||
- `tests/background-icloud-mail-provider.test.js`:测试 iCloud 邮箱 provider 配置解析。
|
- `tests/background-icloud-mail-provider.test.js`:测试 iCloud 邮箱 provider 配置解析。
|
||||||
@@ -129,6 +131,8 @@
|
|||||||
- `tests/background-panel-bridge-module.test.js`:测试面板桥接模块已接入且导出工厂。
|
- `tests/background-panel-bridge-module.test.js`:测试面板桥接模块已接入且导出工厂。
|
||||||
- `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂。
|
- `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂。
|
||||||
- `tests/background-step-modules.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-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。
|
||||||
- `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂。
|
- `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂。
|
||||||
- `tests/background-verification-flow-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-cpa-mode.test.js`:测试本地 CPA Step 9 策略判断。
|
||||||
- `tests/step9-localhost-cleanup-scope.test.js`:测试 Step 9 仅清理精确命中的 localhost callback 和路径前缀残留页。
|
- `tests/step9-localhost-cleanup-scope.test.js`:测试 Step 9 仅清理精确命中的 localhost callback 和路径前缀残留页。
|
||||||
- `tests/verification-stop-propagation.test.js`:测试验证码流程在 Stop 场景下的错误传播与不中途降级。
|
- `tests/verification-stop-propagation.test.js`:测试验证码流程在 Stop 场景下的错误传播与不中途降级。
|
||||||
|
- `tests/verification-flow-polling.test.js`:测试 2925 长轮询参数,以及验证码提交流程中的 `beforeSubmit` 钩子执行顺序。
|
||||||
|
|||||||
Reference in New Issue
Block a user