Files
FlowPilot/background/auto-run-controller.js
T

1130 lines
46 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function attachBackgroundAutoRunController(root, factory) {
root.MultiPageBackgroundAutoRunController = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundAutoRunControllerModule() {
function createAutoRunController(deps = {}) {
const {
addLog,
appendAccountRunRecord,
AUTO_RUN_MAX_RETRIES_PER_ROUND,
AUTO_RUN_RETRY_DELAY_MS,
AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
broadcastAutoRunStatus,
broadcastStopToContentScripts,
buildFreshAutoRunKeepState,
cancelPendingCommands,
clearStopRequest,
createAutoRunSessionId,
ensureHotmailMailboxReadyForAutoRunRound,
getAutoRunStatusPayload,
getErrorMessage,
getFirstUnfinishedNodeId,
getPendingAutoRunTimerPlan,
getRunningNodeIds,
getState,
hasSavedNodeProgress,
isAddPhoneAuthFailure,
isGpcTaskEndedFailure,
isPhoneSmsPlatformRateLimitFailure,
isPlusCheckoutNonFreeTrialFailure,
isRestartCurrentAttemptError,
isStep4Route405RecoveryLimitFailure,
isSignupUserAlreadyExistsFailure,
isStopError,
launchAutoRunTimerPlan,
normalizeAutoRunFallbackThreadIntervalMinutes,
persistAutoRunTimerPlan,
resetState,
runAutoSequenceFromNode,
runtime,
setState,
sleepWithStop,
throwIfAutoRunSessionStopped,
waitForRunningNodesToFinish,
} = deps;
function getRunningWorkflowNodes(state = {}) {
if (typeof getRunningNodeIds === 'function') {
return getRunningNodeIds(state.nodeStatuses || {}, state);
}
return [];
}
function getFirstUnfinishedWorkflowNode(state = {}) {
if (typeof getFirstUnfinishedNodeId === 'function') {
return getFirstUnfinishedNodeId(state.nodeStatuses || {}, state);
}
return null;
}
function hasSavedWorkflowProgress(state = {}) {
if (typeof hasSavedNodeProgress === 'function') {
return hasSavedNodeProgress(state.nodeStatuses || {}, state);
}
return false;
}
async function waitForRunningWorkflowNodesToFinish(payload = {}) {
if (typeof waitForRunningNodesToFinish === 'function') {
return waitForRunningNodesToFinish(payload);
}
return getState();
}
async function runAutoSequenceFromWorkflowNode(startNodeId, context = {}) {
if (typeof runAutoSequenceFromNode === 'function') {
return runAutoSequenceFromNode(startNodeId, context);
}
throw new Error('自动运行节点执行器未接入。');
}
function buildFreshStartStateSnapshot(state = {}) {
return {
...(state || {}),
currentNodeId: '',
nodeStatuses: {},
stepStatuses: {},
};
}
function resolveFreshStartNodeId(state = {}) {
const freshState = buildFreshStartStateSnapshot(state);
return String(getFirstUnfinishedWorkflowNode(freshState) || '').trim();
}
function buildFreshAttemptKeepState(state = {}, context = {}) {
if (typeof buildFreshAutoRunKeepState === 'function') {
const helperPatch = buildFreshAutoRunKeepState(state, context);
if (helperPatch && typeof helperPatch === 'object' && !Array.isArray(helperPatch)) {
return {
...helperPatch,
};
}
}
return {
activeFlowId: state.activeFlowId,
flowId: state.flowId || state.activeFlowId,
panelMode: state.panelMode,
kiroTargetId: state.kiroTargetId,
vpsUrl: state.vpsUrl,
vpsPassword: state.vpsPassword,
customPassword: state.customPassword,
plusModeEnabled: state.plusModeEnabled,
plusPaymentMethod: state.plusPaymentMethod,
phoneVerificationEnabled: state.phoneVerificationEnabled,
phoneSignupReloginAfterBindEmailEnabled: state.phoneSignupReloginAfterBindEmailEnabled,
paypalEmail: state.paypalEmail,
paypalPassword: state.paypalPassword,
kiroRsUrl: state.kiroRsUrl,
kiroRsKey: state.kiroRsKey,
autoRunSkipFailures: state.autoRunSkipFailures,
autoRunFallbackThreadIntervalMinutes: state.autoRunFallbackThreadIntervalMinutes,
autoRunDelayEnabled: state.autoRunDelayEnabled,
autoRunDelayMinutes: state.autoRunDelayMinutes,
autoStepDelaySeconds: state.autoStepDelaySeconds,
stepExecutionRangeByFlow: state.stepExecutionRangeByFlow,
signupMethod: state.signupMethod,
mailProvider: state.mailProvider,
emailGenerator: state.emailGenerator,
gmailBaseEmail: state.gmailBaseEmail,
mail2925BaseEmail: state.mail2925BaseEmail,
currentMail2925AccountId: state.currentMail2925AccountId,
emailPrefix: state.emailPrefix,
inbucketHost: state.inbucketHost,
inbucketMailbox: state.inbucketMailbox,
cloudflareDomain: state.cloudflareDomain,
cloudflareDomains: state.cloudflareDomains,
reusablePhoneActivation: state.reusablePhoneActivation,
};
}
function createAutoRunRoundSummary(round) {
return {
round,
status: 'pending',
attempts: 0,
failureReasons: [],
finalFailureReason: '',
};
}
function normalizeAutoRunRoundSummary(summary, round) {
const base = createAutoRunRoundSummary(round);
if (!summary || typeof summary !== 'object') {
return base;
}
const status = String(summary.status || '').trim().toLowerCase();
return {
round,
status: ['pending', 'success', 'failed'].includes(status) ? status : base.status,
attempts: Math.max(0, Math.floor(Number(summary.attempts) || 0)),
failureReasons: Array.isArray(summary.failureReasons)
? summary.failureReasons.map((item) => String(item || '').trim()).filter(Boolean)
: [],
finalFailureReason: String(summary.finalFailureReason || '').trim(),
};
}
function buildAutoRunRoundSummaries(totalRuns, rawSummaries = []) {
return Array.from({ length: totalRuns }, (_, index) => normalizeAutoRunRoundSummary(rawSummaries[index], index + 1));
}
function serializeAutoRunRoundSummaries(totalRuns, roundSummaries = []) {
return buildAutoRunRoundSummaries(totalRuns, roundSummaries).map((summary) => ({
...summary,
failureReasons: [...summary.failureReasons],
}));
}
function getAutoRunRoundRetryCount(summary) {
return Math.max(0, Number(summary?.attempts || 0) - 1);
}
function normalizeRecordNode(value = '') {
return String(value || '').trim();
}
function extractNodeFromRecordStatus(status = '') {
const match = String(status || '').trim().match(/^node:([^:]+):(failed|stopped)$/i);
return match ? normalizeRecordNode(match[1]) : '';
}
function getKnownNodeIdsFromState(state = {}) {
const ids = new Set();
for (const key of Object.keys(state?.nodeStatuses || {})) {
const nodeId = normalizeRecordNode(key);
if (nodeId) {
ids.add(nodeId);
}
}
const currentNodeId = normalizeRecordNode(state?.currentNodeId);
if (currentNodeId) {
ids.add(currentNodeId);
}
return Array.from(ids);
}
function inferRecordNodeFromState(state = {}, preferredStatuses = []) {
const statuses = state?.nodeStatuses || {};
const preferredStatusSet = new Set(preferredStatuses.map((item) => String(item || '').trim()).filter(Boolean));
const nodeIds = getKnownNodeIdsFromState(state);
const currentNodeId = normalizeRecordNode(state?.currentNodeId);
if (currentNodeId && preferredStatusSet.has(String(statuses[currentNodeId] || '').trim())) {
return currentNodeId;
}
const matchingNodes = nodeIds.filter((nodeId) => preferredStatusSet.has(String(statuses[nodeId] || '').trim()));
if (matchingNodes.length) {
return matchingNodes[matchingNodes.length - 1];
}
if (currentNodeId) {
const currentStatus = String(statuses[currentNodeId] || '').trim();
if (!['', 'pending', 'completed', 'manual_completed', 'skipped'].includes(currentStatus)) {
return currentNodeId;
}
}
return '';
}
function inferRecordNodeFromError(errorLike = null, state = {}) {
if (!errorLike || typeof errorLike !== 'object') {
return '';
}
return normalizeRecordNode(errorLike.failedNodeId)
|| normalizeRecordNode(errorLike.nodeId)
|| normalizeRecordNode(errorLike.currentNodeId);
}
function resolveAutoRunAccountRecordStatus(status, state = {}, errorLike = null) {
const normalizedStatus = String(status || '').trim().toLowerCase();
const explicitNode = extractNodeFromRecordStatus(status);
if (explicitNode) {
return `node:${explicitNode}:${normalizedStatus.endsWith(':stopped') ? 'stopped' : 'failed'}`;
}
if (normalizedStatus === 'failed') {
const failedNode = inferRecordNodeFromError(errorLike, state)
|| inferRecordNodeFromState(state, ['failed', 'running']);
return failedNode ? `node:${failedNode}:failed` : status;
}
if (normalizedStatus === 'stopped') {
const stoppedNode = inferRecordNodeFromError(errorLike, state)
|| inferRecordNodeFromState(state, ['stopped', 'running']);
return stoppedNode ? `node:${stoppedNode}:stopped` : status;
}
return status;
}
function formatAutoRunFailureReasons(reasons = []) {
if (!Array.isArray(reasons) || !reasons.length) {
return '未知错误';
}
const counts = new Map();
for (const reason of reasons) {
const normalized = String(reason || '').trim() || '未知错误';
counts.set(normalized, (counts.get(normalized) || 0) + 1);
}
return Array.from(counts.entries())
.map(([reason, count]) => (count > 1 ? `${reason}${count}次)` : reason))
.join('');
}
function isPhoneNumberSupplyExhaustedFailure(errorLike) {
const message = String(
typeof errorLike === 'string'
? errorLike
: (errorLike?.message || errorLike || '')
).trim();
if (!message) {
return false;
}
const hasGlobalNoSupplySignal = /Step\s*9:\s*all\s+provider\s+candidates\s+failed\s+to\s+acquire\s+number|(?:HeroSMS|5sim|NexSMS)\s+no\s+numbers\s+available\s+across|no\s+numbers\s+within\s+maxPrice|no\s+free\s+phones|numbers?\s+not\s+found/i.test(message);
if (!hasGlobalNoSupplySignal) {
return false;
}
const hasRecoverableStep9RotationSignal = /phone\s+verification\s+did\s+not\s+succeed\s+after\s+\d+\s+number\s+replacements|sms_timeout_after_|route_405_retry_loop|resend_throttled|activation_not_found|order\s+not\s+found/i.test(message);
if (hasRecoverableStep9RotationSignal) {
return false;
}
return true;
}
function shouldKeepCustomMailProviderPoolEmail(state = {}) {
return String(state?.mailProvider || '').trim().toLowerCase() === 'custom'
&& Array.isArray(state?.customMailProviderPool)
&& state.customMailProviderPool.length > 0;
}
function isPhoneNumberSupplyExhaustedFailure(error) {
const text = String(
typeof getErrorMessage === 'function'
? getErrorMessage(error)
: (error?.message || error || '')
).trim();
if (!text) {
return false;
}
return /no\s+numbers\s+available\s+across|all provider candidates failed to acquire number|no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+within\s+maxprice|countries\s+are\s+empty|均无可用号码|暂无可用号码|无可用号码|接码号池暂无|\bNO_NUMBERS\b/i.test(text);
}
async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) {
const summaries = buildAutoRunRoundSummaries(totalRuns, roundSummaries);
const successRounds = summaries.filter((item) => item.status === 'success');
const failedRounds = summaries.filter((item) => item.status === 'failed');
const pendingRounds = summaries.filter((item) => item.status === 'pending');
await addLog('=== 自动运行汇总 ===', failedRounds.length ? 'warn' : 'ok');
await addLog(
`总轮数:${totalRuns};成功:${successRounds.length};失败:${failedRounds.length};未完成:${pendingRounds.length}`,
failedRounds.length ? 'warn' : 'ok'
);
if (successRounds.length) {
await addLog(
`成功轮次:${successRounds
.map((item) => `${item.round} 轮(重试 ${getAutoRunRoundRetryCount(item)} 次)`)
.join('')}`,
'ok'
);
}
if (failedRounds.length) {
await addLog(
`失败轮次:${failedRounds
.map((item) => {
const retryCount = getAutoRunRoundRetryCount(item);
const finalReason = item.finalFailureReason || item.failureReasons[item.failureReasons.length - 1] || '未知错误';
const reasonSummary = formatAutoRunFailureReasons(item.failureReasons);
return !reasonSummary || reasonSummary === finalReason
? `${item.round} 轮(重试 ${retryCount} 次,最终原因:${finalReason}`
: `${item.round} 轮(重试 ${retryCount} 次,最终原因:${finalReason};失败记录:${reasonSummary}`;
})
.join('')}`,
'error'
);
}
if (pendingRounds.length) {
await addLog(
`未完成轮次:${pendingRounds.map((item) => `${item.round}`).join('')}`,
'warn'
);
}
}
async function skipAutoRunCountdown() {
const state = await getState();
const plan = getPendingAutoRunTimerPlan(state);
if (!plan || state.autoRunPhase !== 'waiting_interval') {
return false;
}
return launchAutoRunTimerPlan('manual', {
expectedKinds: [
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
],
});
}
async function waitBetweenAutoRunRounds(targetRun, totalRuns, roundSummary, options = {}) {
const { autoRunSkipFailures = false, roundSummaries = [] } = options;
if (totalRuns <= 1 || targetRun >= totalRuns) {
return false;
}
const fallbackThreadIntervalMinutes = normalizeAutoRunFallbackThreadIntervalMinutes(
(await getState()).autoRunFallbackThreadIntervalMinutes
);
if (fallbackThreadIntervalMinutes <= 0) {
return false;
}
const currentRuntime = runtime.get();
const statusLabel = roundSummary?.status === 'failed' ? '失败' : '完成';
await addLog(
`线程间隔:第 ${targetRun}/${totalRuns} 轮已${statusLabel},等待 ${fallbackThreadIntervalMinutes} 分钟后开始下一轮。`,
'info'
);
await persistAutoRunTimerPlan({
kind: AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
fireAt: Date.now() + fallbackThreadIntervalMinutes * 60 * 1000,
currentRun: targetRun,
totalRuns,
attemptRun: currentRuntime.autoRunAttemptRun,
autoRunSessionId: currentRuntime.autoRunSessionId,
autoRunSkipFailures,
roundSummaries,
countdownTitle: '线程间隔中',
countdownNote: `${Math.min(targetRun + 1, totalRuns)}/${totalRuns} 轮即将开始`,
}, {
autoRunSkipFailures,
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
runtime.set({ autoRunActive: false });
return true;
}
async function waitBeforeAutoRunRetry(targetRun, totalRuns, nextAttemptRun, options = {}) {
const { autoRunSkipFailures = false, roundSummaries = [] } = options;
const fallbackThreadIntervalMinutes = normalizeAutoRunFallbackThreadIntervalMinutes(
(await getState()).autoRunFallbackThreadIntervalMinutes
);
if (fallbackThreadIntervalMinutes <= 0) {
return false;
}
await addLog(
`线程间隔:等待 ${fallbackThreadIntervalMinutes} 分钟后开始第 ${targetRun}/${totalRuns} 轮第 ${nextAttemptRun} 次尝试。`,
'info'
);
await persistAutoRunTimerPlan({
kind: AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
fireAt: Date.now() + fallbackThreadIntervalMinutes * 60 * 1000,
currentRun: targetRun,
totalRuns,
attemptRun: nextAttemptRun,
autoRunSessionId: runtime.get().autoRunSessionId,
autoRunSkipFailures,
roundSummaries,
countdownTitle: '线程间隔中',
countdownNote: `${targetRun}/${totalRuns} 轮第 ${nextAttemptRun} 次尝试即将开始`,
}, {
autoRunSkipFailures,
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
runtime.set({ autoRunActive: false });
return true;
}
async function handleAutoRunLoopUnhandledError(error) {
const currentRuntime = runtime.get();
console.error('Auto run loop crashed:', error);
if (!isStopError(error)) {
await addLog(`自动运行异常终止:${getErrorMessage(error) || '未知错误'}`, 'error');
}
runtime.set({ autoRunActive: false, autoRunSessionId: 0 });
await broadcastAutoRunStatus('stopped', {
currentRun: currentRuntime.autoRunCurrentRun,
totalRuns: currentRuntime.autoRunTotalRuns,
attemptRun: currentRuntime.autoRunAttemptRun,
sessionId: 0,
}, {
autoRunSessionId: 0,
autoRunTimerPlan: null,
scheduledAutoRunPlan: null,
});
clearStopRequest();
}
function startAutoRunLoop(totalRuns, options = {}) {
autoRunLoop(totalRuns, options).catch((error) => {
handleAutoRunLoopUnhandledError(error).catch(() => {});
});
}
async function autoRunLoop(totalRuns, options = {}) {
let currentRuntime = runtime.get();
if (currentRuntime.autoRunActive) {
await addLog('自动运行已在进行中', 'warn');
return;
}
let sessionId = Number.isInteger(options.autoRunSessionId) && options.autoRunSessionId > 0
? options.autoRunSessionId
: 0;
if (sessionId) {
throwIfAutoRunSessionStopped(sessionId);
} else {
sessionId = createAutoRunSessionId();
}
clearStopRequest();
runtime.set({
autoRunActive: true,
autoRunTotalRuns: totalRuns,
autoRunCurrentRun: 0,
autoRunAttemptRun: 0,
autoRunSessionId: sessionId,
});
currentRuntime = runtime.get();
const autoRunSkipFailures = Boolean(options.autoRunSkipFailures);
const initialMode = options.mode === 'continue' ? 'continue' : 'restart';
const resumeCurrentRun = Number.isInteger(options.resumeCurrentRun) && options.resumeCurrentRun > 0
? Math.min(totalRuns, options.resumeCurrentRun)
: 1;
const resumeAttemptRun = Number.isInteger(options.resumeAttemptRun) && options.resumeAttemptRun > 0
? Math.min(AUTO_RUN_MAX_RETRIES_PER_ROUND + 1, options.resumeAttemptRun)
: 1;
let continueCurrentOnFirstAttempt = initialMode === 'continue';
let forceFreshTabsNextRun = false;
let stoppedEarly = false;
let parkedByTimer = false;
const roundSummaries = buildAutoRunRoundSummaries(totalRuns, options.resumeRoundSummaries);
if (continueCurrentOnFirstAttempt && resumeCurrentRun > 1) {
for (let round = 1; round < resumeCurrentRun; round += 1) {
const summary = roundSummaries[round - 1];
if (summary.status === 'pending') {
summary.status = 'success';
if (!summary.attempts) {
summary.attempts = 1;
}
}
}
}
let successfulRuns = roundSummaries.filter((item) => item.status === 'success').length;
const initialState = await getState();
const initialPhase = continueCurrentOnFirstAttempt && getRunningWorkflowNodes(initialState).length
? 'waiting_step'
: 'running';
const showResumePosition = continueCurrentOnFirstAttempt || resumeCurrentRun > 1 || resumeAttemptRun > 1;
await setState({
autoRunSessionId: sessionId,
autoRunSkipFailures,
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
...getAutoRunStatusPayload(initialPhase, {
currentRun: showResumePosition ? resumeCurrentRun : 0,
totalRuns,
attemptRun: showResumePosition ? resumeAttemptRun : 0,
sessionId,
}),
});
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;
const currentRoundState = await getState();
const keepSameEmailUntilAddPhone = autoRunSkipFailures && shouldKeepCustomMailProviderPoolEmail(currentRoundState);
const maxAttemptsForRound = autoRunSkipFailures
? (keepSameEmailUntilAddPhone ? Number.MAX_SAFE_INTEGER : AUTO_RUN_MAX_RETRIES_PER_ROUND + 1)
: Math.max(1, attemptRun);
while (attemptRun <= maxAttemptsForRound) {
runtime.set({
autoRunCurrentRun: targetRun,
autoRunAttemptRun: attemptRun,
});
roundSummary.attempts = attemptRun;
const attemptState = await getState();
const defaultStartNodeId = resolveFreshStartNodeId(attemptState);
let startNodeId = defaultStartNodeId;
let useExistingProgress = false;
if (reuseExistingProgress) {
let currentState = attemptState;
if (getRunningWorkflowNodes(currentState).length) {
currentState = await waitForRunningWorkflowNodesToFinish({
currentRun: targetRun,
totalRuns,
attemptRun,
});
}
const resumeNodeId = getFirstUnfinishedWorkflowNode(currentState);
if (resumeNodeId && hasSavedWorkflowProgress(currentState)) {
startNodeId = resumeNodeId;
useExistingProgress = true;
} else if (hasSavedWorkflowProgress(currentState)) {
await addLog('检测到当前流程已处理完成,本轮将改为从首个节点重新开始。', 'info');
}
}
if (!useExistingProgress) {
const prevState = attemptState;
const keepSettings = {
...buildFreshAttemptKeepState(prevState, {
targetRun,
totalRuns,
attemptRun,
sessionId,
}),
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
autoRunSessionId: sessionId,
tabRegistry: {},
sourceLastUrls: {},
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun, sessionId }),
};
await resetState();
await setState(keepSettings);
deps.chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => { });
await sleepWithStop(500);
} else {
await setState({
autoRunSessionId: sessionId,
autoRunSkipFailures,
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun, sessionId }),
});
}
if (forceFreshTabsNextRun) {
await addLog(`上一轮尝试已放弃,当前开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun} 次尝试。`, 'warn');
forceFreshTabsNextRun = false;
}
const appendRoundRecordIfNeeded = async (status, reason = '', errorLike = null) => {
if (roundRecordAppended) {
return;
}
if (typeof appendAccountRunRecord !== 'function') {
return;
}
const recordState = await getState();
const recordStatus = resolveAutoRunAccountRecordStatus(status, recordState, errorLike);
const record = await appendAccountRunRecord(recordStatus, recordState, reason);
if (record) {
roundRecordAppended = true;
}
};
try {
throwIfAutoRunSessionStopped(sessionId);
await broadcastAutoRunStatus('running', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId,
});
if (!useExistingProgress && startNodeId === defaultStartNodeId && typeof ensureHotmailMailboxReadyForAutoRunRound === 'function') {
await ensureHotmailMailboxReadyForAutoRunRound({
targetRun,
totalRuns,
attemptRun,
sessionId,
});
}
await runAutoSequenceFromWorkflowNode(startNodeId, {
targetRun,
totalRuns,
attemptRuns: attemptRun,
continued: useExistingProgress,
});
roundSummary.status = 'success';
roundSummary.finalFailureReason = '';
successfulRuns += 1;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await addLog(`=== 第 ${targetRun}/${totalRuns} 轮完成(第 ${attemptRun} 次尝试成功)===`, 'ok');
break;
} catch (err) {
if (isStopError(err)) {
stoppedEarly = true;
await appendRoundRecordIfNeeded('stopped', getErrorMessage(err), err);
await addLog(`${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
const reason = getErrorMessage(err);
roundSummary.failureReasons.push(reason);
const blockedByPhoneSmsRateLimit = typeof isPhoneSmsPlatformRateLimitFailure === 'function'
&& isPhoneSmsPlatformRateLimitFailure(err);
const blockedByPhoneNoSupply = !blockedByPhoneSmsRateLimit
&& isPhoneNumberSupplyExhaustedFailure(err);
const blockedByAddPhone = !blockedByPhoneSmsRateLimit
&& !blockedByPhoneNoSupply
&& typeof isAddPhoneAuthFailure === 'function'
&& isAddPhoneAuthFailure(err);
const blockedByPlusNonFreeTrial = typeof isPlusCheckoutNonFreeTrialFailure === 'function'
&& isPlusCheckoutNonFreeTrialFailure(err);
const blockedByGpcTaskEnded = typeof isGpcTaskEndedFailure === 'function'
? isGpcTaskEndedFailure(err)
: /GPC_TASK_ENDED::/i.test(err?.message || String(err || ''));
const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function'
&& !keepSameEmailUntilAddPhone
&& isSignupUserAlreadyExistsFailure(err);
const blockedByStep4Route405 = typeof isStep4Route405RecoveryLimitFailure === 'function'
&& isStep4Route405RecoveryLimitFailure(err);
const canRetry = !blockedByAddPhone
&& !blockedByPhoneNoSupply
&& !blockedByPlusNonFreeTrial
&& !blockedByGpcTaskEnded
&& !blockedBySignupUserAlreadyExists
&& autoRunSkipFailures
&& attemptRun < maxAttemptsForRound;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
if (blockedByAddPhone) {
roundSummary.status = 'failed';
roundSummary.finalFailureReason = reason;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因认证流程进入 add-phone 已终止。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
await addLog(
`${targetRun}/${totalRuns} 轮触发 add-phone/手机号页,自动重试未开启,当前自动运行将停止。`,
'warn'
);
stoppedEarly = true;
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
await addLog(`${targetRun}/${totalRuns} 轮触发 add-phone/手机号页,本轮将直接失败并跳过剩余重试。`, 'warn');
await addLog(
targetRun < totalRuns
? `${targetRun}/${totalRuns} 轮因 add-phone/手机号页提前结束,自动流程将继续下一轮。`
: `${targetRun}/${totalRuns} 轮因 add-phone/手机号页提前结束,已无后续轮次,本次自动运行结束。`,
'warn'
);
forceFreshTabsNextRun = true;
break;
}
if (blockedByPhoneNoSupply) {
roundSummary.status = 'failed';
roundSummary.finalFailureReason = reason;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因接码号池暂无可用号码已终止。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
await addLog(
`${targetRun}/${totalRuns} 轮接码号池暂无可用号码,自动重试未开启,当前自动运行将停止。`,
'warn'
);
stoppedEarly = true;
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
await addLog(`${targetRun}/${totalRuns} 轮接码号池暂无可用号码,本轮将直接失败并跳过剩余重试。`, 'warn');
await addLog(
targetRun < totalRuns
? `${targetRun}/${totalRuns} 轮因接码号池暂无可用号码提前结束,自动流程将继续下一轮。`
: `${targetRun}/${totalRuns} 轮因接码号池暂无可用号码提前结束,已无后续轮次,本次自动运行结束。`,
'warn'
);
forceFreshTabsNextRun = true;
break;
}
if (blockedByPlusNonFreeTrial) {
roundSummary.status = 'failed';
roundSummary.finalFailureReason = reason;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因 Plus 免费试用资格不可用已终止。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
await addLog(
`${targetRun}/${totalRuns} 轮检测到 Plus 今日应付金额非 0,自动重试未开启,当前自动运行将停止。`,
'warn'
);
stoppedEarly = true;
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
await addLog(`${targetRun}/${totalRuns} 轮没有 Plus 免费试用资格,本轮将直接失败并跳过剩余重试。`, 'warn');
await addLog(
targetRun < totalRuns
? `${targetRun}/${totalRuns} 轮因 Plus 今日应付金额非 0 提前结束,自动流程将继续下一轮。`
: `${targetRun}/${totalRuns} 轮因 Plus 今日应付金额非 0 提前结束,已无后续轮次,本次自动运行结束。`,
'warn'
);
forceFreshTabsNextRun = true;
break;
}
if (blockedByGpcTaskEnded) {
roundSummary.status = 'failed';
roundSummary.finalFailureReason = reason;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因 GPC 任务已结束。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
await addLog(
`${targetRun}/${totalRuns} 轮 GPC 任务已结束,自动重试未开启,当前自动运行将停止。`,
'warn'
);
stoppedEarly = true;
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
await addLog(`${targetRun}/${totalRuns} 轮 GPC 任务已结束,本轮将直接失败并跳过剩余重试。`, 'warn');
await addLog(
targetRun < totalRuns
? `${targetRun}/${totalRuns} 轮因 GPC 任务结束提前结束,自动流程将继续下一轮。`
: `${targetRun}/${totalRuns} 轮因 GPC 任务结束提前结束,已无后续轮次,本次自动运行结束。`,
'warn'
);
forceFreshTabsNextRun = true;
break;
}
if (blockedBySignupUserAlreadyExists) {
roundSummary.status = 'failed';
roundSummary.finalFailureReason = reason;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因 user_already_exists 已终止。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
await addLog(
`${targetRun}/${totalRuns} 轮触发 user_already_exists/用户已存在,自动重试未开启,当前自动运行将停止。`,
'warn'
);
stoppedEarly = true;
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
await addLog(`${targetRun}/${totalRuns} 轮触发 user_already_exists/用户已存在,本轮将直接失败并跳过剩余重试。`, 'warn');
await addLog(
targetRun < totalRuns
? `${targetRun}/${totalRuns} 轮因 user_already_exists/用户已存在提前结束,自动流程将继续下一轮。`
: `${targetRun}/${totalRuns} 轮因 user_already_exists/用户已存在提前结束,已无后续轮次,本次自动运行结束。`,
'warn'
);
forceFreshTabsNextRun = true;
break;
}
if (blockedByStep4Route405) {
roundSummary.status = 'failed';
roundSummary.finalFailureReason = reason;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因步骤 4 连续 405 错误已终止。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
await addLog(
`${targetRun}/${totalRuns} 轮步骤 4 连续 405 恢复失败,自动重试未开启,当前自动运行将停止。`,
'warn'
);
stoppedEarly = true;
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
await addLog(`${targetRun}/${totalRuns} 轮步骤 4 连续 405 恢复失败,本轮将直接失败并跳过剩余重试。`, 'warn');
await addLog(
targetRun < totalRuns
? `${targetRun}/${totalRuns} 轮因步骤 4 连续 405 提前结束,自动流程将继续下一轮。`
: `${targetRun}/${totalRuns} 轮因步骤 4 连续 405 提前结束,已无后续轮次,本次自动运行结束。`,
'warn'
);
forceFreshTabsNextRun = true;
break;
}
if (canRetry) {
const retryIndex = attemptRun;
if (isRestartCurrentAttemptError(err)) {
await addLog(`${targetRun}/${totalRuns} 轮第 ${attemptRun} 次尝试需要整轮重开:${reason}`, 'warn');
} else {
await addLog(`${targetRun}/${totalRuns} 轮第 ${attemptRun} 次尝试失败:${reason}`, 'error');
}
cancelPendingCommands('当前尝试已放弃。');
await broadcastStopToContentScripts();
await broadcastAutoRunStatus('retrying', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId,
});
forceFreshTabsNextRun = true;
await addLog(
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 {
await sleepWithStop(AUTO_RUN_RETRY_DELAY_MS);
} catch (sleepError) {
if (isStopError(sleepError)) {
stoppedEarly = true;
await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError), sleepError);
await addLog(`${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
throw sleepError;
}
try {
const parkedForRetry = await waitBeforeAutoRunRetry(targetRun, totalRuns, attemptRun + 1, {
autoRunSkipFailures,
roundSummaries,
});
if (parkedForRetry) {
parkedByTimer = true;
break;
}
} catch (sleepError) {
if (isStopError(sleepError)) {
stoppedEarly = true;
await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError), sleepError);
await addLog(`${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
throw sleepError;
}
attemptRun += 1;
reuseExistingProgress = false;
continue;
}
roundSummary.status = 'failed';
roundSummary.finalFailureReason = reason;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await appendRoundRecordIfNeeded('failed', reason, err);
if (!autoRunSkipFailures) {
cancelPendingCommands('当前轮执行失败。');
await broadcastStopToContentScripts();
await addLog('自动重试未开启,自动运行将在当前失败后停止。', 'warn');
stoppedEarly = true;
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
await addLog(`${targetRun}/${totalRuns} 轮最终失败:${reason}`, 'error');
await addLog(
targetRun < totalRuns
? `${targetRun}/${totalRuns} 轮已达到 ${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试上限,继续下一轮。`
: `${targetRun}/${totalRuns} 轮已达到 ${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试上限,本次自动运行结束。`,
'warn'
);
cancelPendingCommands('当前轮已达到重试上限。');
await broadcastStopToContentScripts();
forceFreshTabsNextRun = true;
break;
} finally {
reuseExistingProgress = false;
continueCurrentOnFirstAttempt = false;
}
}
if (stoppedEarly || parkedByTimer) {
break;
}
try {
const parkedForNextRound = await waitBetweenAutoRunRounds(targetRun, totalRuns, roundSummary, {
autoRunSkipFailures,
roundSummaries,
});
if (parkedForNextRound) {
parkedByTimer = true;
break;
}
} catch (sleepError) {
if (isStopError(sleepError)) {
stoppedEarly = true;
await addLog(`${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun: runtime.get().autoRunAttemptRun,
sessionId: 0,
});
break;
}
throw sleepError;
}
}
if (parkedByTimer) {
runtime.set({ autoRunActive: false });
clearStopRequest();
return;
}
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await logAutoRunFinalSummary(totalRuns, roundSummaries);
const finalRuntime = runtime.get();
if (deps.getStopRequested() || stoppedEarly) {
await addLog(`=== 已停止,完成 ${successfulRuns}/${finalRuntime.autoRunTotalRuns} 轮 ===`, 'warn');
await broadcastAutoRunStatus('stopped', {
currentRun: finalRuntime.autoRunCurrentRun,
totalRuns: finalRuntime.autoRunTotalRuns,
attemptRun: finalRuntime.autoRunAttemptRun,
sessionId: 0,
});
} else {
await addLog(`=== 全部 ${finalRuntime.autoRunTotalRuns} 轮已执行完成,成功 ${successfulRuns} 轮 ===`, 'ok');
await broadcastAutoRunStatus('complete', {
currentRun: finalRuntime.autoRunTotalRuns,
totalRuns: finalRuntime.autoRunTotalRuns,
attemptRun: finalRuntime.autoRunAttemptRun,
sessionId: 0,
});
}
runtime.set({ autoRunActive: false, autoRunSessionId: 0 });
const afterRuntime = runtime.get();
await setState({
autoRunSessionId: 0,
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
autoRunTimerPlan: null,
scheduledAutoRunPlan: null,
...getAutoRunStatusPayload(deps.getStopRequested() || stoppedEarly ? 'stopped' : 'complete', {
currentRun: deps.getStopRequested() || stoppedEarly ? afterRuntime.autoRunCurrentRun : afterRuntime.autoRunTotalRuns,
totalRuns: afterRuntime.autoRunTotalRuns,
attemptRun: afterRuntime.autoRunAttemptRun,
sessionId: 0,
}),
});
clearStopRequest();
}
return {
autoRunLoop,
buildAutoRunRoundSummaries,
createAutoRunRoundSummary,
formatAutoRunFailureReasons,
getAutoRunRoundRetryCount,
handleAutoRunLoopUnhandledError,
logAutoRunFinalSummary,
normalizeAutoRunRoundSummary,
resolveAutoRunAccountRecordStatus,
serializeAutoRunRoundSummaries,
skipAutoRunCountdown,
startAutoRunLoop,
waitBetweenAutoRunRounds,
waitBeforeAutoRunRetry,
};
}
return {
createAutoRunController,
};
});