feat: 增强自动运行控制器以支持自定义邮箱池重试逻辑,并更新验证流程以处理手机号确认
This commit is contained in:
@@ -96,6 +96,12 @@
|
||||
.join(';');
|
||||
}
|
||||
|
||||
function shouldKeepCustomMailProviderPoolEmail(state = {}) {
|
||||
return String(state?.mailProvider || '').trim().toLowerCase() === 'custom'
|
||||
&& Array.isArray(state?.customMailProviderPool)
|
||||
&& state.customMailProviderPool.length > 0;
|
||||
}
|
||||
|
||||
async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) {
|
||||
const summaries = buildAutoRunRoundSummaries(totalRuns, roundSummaries);
|
||||
const successRounds = summaries.filter((item) => item.status === 'success');
|
||||
@@ -328,8 +334,10 @@
|
||||
const resumingCurrentRound = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun;
|
||||
let attemptRun = resumingCurrentRound ? resumeAttemptRun : 1;
|
||||
let reuseExistingProgress = resumingCurrentRound;
|
||||
const currentRoundState = await getState();
|
||||
const keepSameEmailUntilAddPhone = autoRunSkipFailures && shouldKeepCustomMailProviderPoolEmail(currentRoundState);
|
||||
const maxAttemptsForRound = autoRunSkipFailures
|
||||
? AUTO_RUN_MAX_RETRIES_PER_ROUND + 1
|
||||
? (keepSameEmailUntilAddPhone ? Number.MAX_SAFE_INTEGER : AUTO_RUN_MAX_RETRIES_PER_ROUND + 1)
|
||||
: Math.max(1, attemptRun);
|
||||
|
||||
while (attemptRun <= maxAttemptsForRound) {
|
||||
@@ -461,6 +469,7 @@
|
||||
roundSummary.failureReasons.push(reason);
|
||||
const blockedByAddPhone = typeof isAddPhoneAuthFailure === 'function' && isAddPhoneAuthFailure(err);
|
||||
const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function'
|
||||
&& !keepSameEmailUntilAddPhone
|
||||
&& isSignupUserAlreadyExistsFailure(err);
|
||||
const canRetry = !blockedByAddPhone && !blockedBySignupUserAlreadyExists && autoRunSkipFailures && attemptRun < maxAttemptsForRound;
|
||||
|
||||
@@ -555,7 +564,9 @@
|
||||
});
|
||||
forceFreshTabsNextRun = true;
|
||||
await addLog(
|
||||
`自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试(第 ${retryIndex}/${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试)。`,
|
||||
keepSameEmailUntilAddPhone
|
||||
? `自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后继续使用当前邮箱,开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试。`
|
||||
: `自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试(第 ${retryIndex}/${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试)。`,
|
||||
'warn'
|
||||
);
|
||||
try {
|
||||
|
||||
@@ -98,6 +98,9 @@
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (step === 8 && response?.addPhoneDetected) {
|
||||
throw new Error('步骤 8:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone');
|
||||
}
|
||||
if (!response?.confirmed) {
|
||||
throw new Error(`步骤 ${step}:已取消手动${verificationLabel}验证码确认。`);
|
||||
}
|
||||
|
||||
+38
-9
@@ -2785,9 +2785,45 @@ function getCustomVerificationPromptCopy(step) {
|
||||
text: `点击确认后会跳过步骤 ${step}。`,
|
||||
tone: 'danger',
|
||||
},
|
||||
...(step === 8 ? {
|
||||
phoneActionLabel: '出现手机号验证',
|
||||
phoneActionAlert: {
|
||||
text: '如果当前页面已经进入手机号验证,可直接标记为失败并继续下一个邮箱。',
|
||||
tone: 'danger',
|
||||
},
|
||||
} : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function openCustomVerificationConfirmDialog(step) {
|
||||
const promptCopy = getCustomVerificationPromptCopy(step);
|
||||
if (step === 8) {
|
||||
return openActionModal({
|
||||
title: promptCopy.title,
|
||||
message: promptCopy.message,
|
||||
alert: promptCopy.alert,
|
||||
actions: [
|
||||
{ id: null, label: '取消', variant: 'btn-ghost' },
|
||||
{ id: 'add_phone', label: promptCopy.phoneActionLabel || '出现手机号验证', variant: 'btn-outline' },
|
||||
{ id: 'confirm', label: '确认跳过', variant: 'btn-danger' },
|
||||
],
|
||||
buildResult: (choice) => ({
|
||||
confirmed: choice === 'confirm',
|
||||
addPhoneDetected: choice === 'add_phone',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const confirmed = await openConfirmModal({
|
||||
title: promptCopy.title,
|
||||
message: promptCopy.message,
|
||||
confirmLabel: '确认跳过',
|
||||
confirmVariant: 'btn-danger',
|
||||
alert: promptCopy.alert,
|
||||
});
|
||||
return { confirmed, addPhoneDetected: false };
|
||||
}
|
||||
|
||||
function getHotmailAccounts(state = latestState) {
|
||||
return Array.isArray(state?.hotmailAccounts) ? state.hotmailAccounts : [];
|
||||
}
|
||||
@@ -4950,15 +4986,8 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
case 'REQUEST_CUSTOM_VERIFICATION_BYPASS_CONFIRMATION': {
|
||||
(async () => {
|
||||
const step = Number(message.payload?.step);
|
||||
const promptCopy = getCustomVerificationPromptCopy(step);
|
||||
const confirmed = await openConfirmModal({
|
||||
title: promptCopy.title,
|
||||
message: promptCopy.message,
|
||||
confirmLabel: '确认跳过',
|
||||
confirmVariant: 'btn-danger',
|
||||
alert: promptCopy.alert,
|
||||
});
|
||||
sendResponse({ confirmed });
|
||||
const result = await openCustomVerificationConfirmDialog(step);
|
||||
sendResponse(result || { confirmed: false, addPhoneDetected: false });
|
||||
})().catch((err) => {
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
|
||||
@@ -331,3 +331,168 @@ test('auto-run controller skips user_already_exists failures to the next round i
|
||||
assert.equal(runtime.state.autoRunActive, false);
|
||||
assert.equal(runtime.state.autoRunSessionId, 0);
|
||||
});
|
||||
|
||||
test('auto-run controller keeps retrying the same custom mail provider pool email until success', async () => {
|
||||
const events = {
|
||||
logs: [],
|
||||
broadcasts: [],
|
||||
accountRecords: [],
|
||||
runCalls: 0,
|
||||
};
|
||||
|
||||
let currentState = {
|
||||
stepStatuses: {},
|
||||
vpsUrl: 'https://example.com/vps',
|
||||
vpsPassword: 'secret',
|
||||
customPassword: '',
|
||||
autoRunSkipFailures: true,
|
||||
autoRunFallbackThreadIntervalMinutes: 0,
|
||||
autoRunDelayEnabled: false,
|
||||
autoRunDelayMinutes: 30,
|
||||
autoStepDelaySeconds: null,
|
||||
mailProvider: 'custom',
|
||||
customMailProviderPool: ['first@example.com'],
|
||||
emailGenerator: 'duck',
|
||||
gmailBaseEmail: '',
|
||||
mail2925BaseEmail: '',
|
||||
emailPrefix: 'demo',
|
||||
inbucketHost: '',
|
||||
inbucketMailbox: '',
|
||||
cloudflareDomain: '',
|
||||
cloudflareDomains: [],
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
autoRunRoundSummaries: [],
|
||||
};
|
||||
|
||||
const runtime = {
|
||||
state: {
|
||||
autoRunActive: false,
|
||||
autoRunCurrentRun: 0,
|
||||
autoRunTotalRuns: 1,
|
||||
autoRunAttemptRun: 0,
|
||||
autoRunSessionId: 0,
|
||||
},
|
||||
get() {
|
||||
return { ...this.state };
|
||||
},
|
||||
set(updates = {}) {
|
||||
this.state = { ...this.state, ...updates };
|
||||
},
|
||||
};
|
||||
|
||||
let sessionSeed = 0;
|
||||
|
||||
const controller = api.createAutoRunController({
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.logs.push({ message, level });
|
||||
},
|
||||
appendAccountRunRecord: async (status, _state, reason) => {
|
||||
events.accountRecords.push({ status, reason });
|
||||
return { status, reason };
|
||||
},
|
||||
AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
|
||||
AUTO_RUN_RETRY_DELAY_MS: 3000,
|
||||
AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
|
||||
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
|
||||
broadcastAutoRunStatus: async (phase, payload = {}) => {
|
||||
events.broadcasts.push({ phase, ...payload });
|
||||
currentState = {
|
||||
...currentState,
|
||||
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
|
||||
autoRunPhase: phase,
|
||||
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
|
||||
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
|
||||
autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
|
||||
autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
|
||||
};
|
||||
},
|
||||
broadcastStopToContentScripts: async () => {},
|
||||
cancelPendingCommands: () => {},
|
||||
clearStopRequest: () => {},
|
||||
createAutoRunSessionId: () => {
|
||||
sessionSeed += 1;
|
||||
return sessionSeed;
|
||||
},
|
||||
getAutoRunStatusPayload: (phase, payload = {}) => ({
|
||||
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
|
||||
autoRunPhase: phase,
|
||||
autoRunCurrentRun: payload.currentRun ?? 0,
|
||||
autoRunTotalRuns: payload.totalRuns ?? 1,
|
||||
autoRunAttemptRun: payload.attemptRun ?? 0,
|
||||
autoRunSessionId: payload.sessionId ?? 0,
|
||||
}),
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getFirstUnfinishedStep: () => 1,
|
||||
getPendingAutoRunTimerPlan: () => null,
|
||||
getRunningSteps: () => [],
|
||||
getState: async () => ({
|
||||
...currentState,
|
||||
stepStatuses: { ...(currentState.stepStatuses || {}) },
|
||||
tabRegistry: { ...(currentState.tabRegistry || {}) },
|
||||
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
|
||||
customMailProviderPool: [...(currentState.customMailProviderPool || [])],
|
||||
}),
|
||||
getStopRequested: () => false,
|
||||
hasSavedProgress: () => false,
|
||||
isAddPhoneAuthFailure: (error) => /add-phone|手机号页面|手机号页|手机号码|手机号/i.test(error?.message || String(error || '')),
|
||||
isRestartCurrentAttemptError: () => false,
|
||||
isSignupUserAlreadyExistsFailure: (error) => /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(error?.message || String(error || '')),
|
||||
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
|
||||
launchAutoRunTimerPlan: async () => false,
|
||||
normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
|
||||
persistAutoRunTimerPlan: async () => ({}),
|
||||
resetState: async () => {
|
||||
currentState = {
|
||||
...currentState,
|
||||
stepStatuses: {},
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
};
|
||||
},
|
||||
runAutoSequenceFromStep: async () => {
|
||||
events.runCalls += 1;
|
||||
if (events.runCalls <= 2) {
|
||||
throw new Error('步骤 3:页面异常,当前尝试失败。');
|
||||
}
|
||||
},
|
||||
runtime,
|
||||
setState: async (updates = {}) => {
|
||||
currentState = {
|
||||
...currentState,
|
||||
...updates,
|
||||
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
|
||||
tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
|
||||
sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
|
||||
};
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfAutoRunSessionStopped: (sessionId) => {
|
||||
if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
|
||||
throw new Error('流程已被用户停止。');
|
||||
}
|
||||
},
|
||||
waitForRunningStepsToFinish: async () => currentState,
|
||||
chrome: {
|
||||
runtime: {
|
||||
sendMessage() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await controller.autoRunLoop(1, {
|
||||
autoRunSkipFailures: true,
|
||||
mode: 'restart',
|
||||
});
|
||||
|
||||
assert.equal(events.runCalls, 3, 'custom mail provider pool should keep retrying the same round until success');
|
||||
assert.equal(events.broadcasts.filter(({ phase }) => phase === 'retrying').length, 2);
|
||||
assert.ok(events.broadcasts.filter(({ phase }) => phase === 'retrying').every(({ currentRun }) => currentRun === 1));
|
||||
assert.ok(events.logs.some(({ message }) => /继续使用当前邮箱/.test(message)));
|
||||
assert.equal(events.logs.some(({ message }) => /达到 3 次重试上限,继续下一轮/.test(message)), false);
|
||||
assert.equal(events.accountRecords.length, 0);
|
||||
assert.equal(runtime.state.autoRunActive, false);
|
||||
assert.equal(runtime.state.autoRunSessionId, 0);
|
||||
});
|
||||
|
||||
@@ -174,3 +174,45 @@ return {
|
||||
assert.equal(api.getLockedRunCountFromEmailPool(), 3);
|
||||
assert.equal(api.getRunCountValue(), 3);
|
||||
});
|
||||
|
||||
test('sidepanel custom verification dialog exposes add-phone action for step 8', async () => {
|
||||
const bundle = [
|
||||
extractFunction('getCustomVerificationPromptCopy'),
|
||||
extractFunction('openCustomVerificationConfirmDialog'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let openActionModalPayload = null;
|
||||
|
||||
async function openActionModal(options) {
|
||||
openActionModalPayload = options;
|
||||
return options.buildResult('add_phone');
|
||||
}
|
||||
|
||||
async function openConfirmModal() {
|
||||
throw new Error('step 8 should use action modal');
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
getCustomVerificationPromptCopy,
|
||||
openCustomVerificationConfirmDialog,
|
||||
getOpenActionModalPayload: () => openActionModalPayload,
|
||||
};
|
||||
`)();
|
||||
|
||||
const prompt = api.getCustomVerificationPromptCopy(8);
|
||||
assert.equal(prompt.phoneActionLabel, '出现手机号验证');
|
||||
|
||||
const result = await api.openCustomVerificationConfirmDialog(8);
|
||||
assert.deepEqual(result, {
|
||||
confirmed: false,
|
||||
addPhoneDetected: true,
|
||||
});
|
||||
|
||||
const modalPayload = api.getOpenActionModalPayload();
|
||||
assert.equal(modalPayload.actions.length, 3);
|
||||
assert.equal(modalPayload.actions[1].id, 'add_phone');
|
||||
assert.equal(modalPayload.actions[1].label, '出现手机号验证');
|
||||
});
|
||||
|
||||
@@ -345,6 +345,51 @@ test('verification flow treats add-phone after login code submit as fatal instea
|
||||
]);
|
||||
});
|
||||
|
||||
test('verification flow treats manual step 8 add-phone confirmation as the same fatal add-phone error', async () => {
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {
|
||||
throw new Error('should not complete step 8');
|
||||
},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({
|
||||
confirmed: false,
|
||||
addPhoneDetected: 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 () => {
|
||||
throw new Error('should not mark step skipped when add-phone is chosen');
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => helpers.confirmCustomVerificationStepBypass(8),
|
||||
/验证码提交后页面进入手机号页面/
|
||||
);
|
||||
});
|
||||
|
||||
test('verification flow caps mail polling timeout to the remaining oauth budget', async () => {
|
||||
const mailPollCalls = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user