feat: 自动流程支持按轮重试并输出汇总日志
This commit is contained in:
+230
-153
@@ -39,6 +39,8 @@ const DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback';
|
|||||||
const AUTO_RUN_ALARM_NAME = 'scheduled-auto-run';
|
const AUTO_RUN_ALARM_NAME = 'scheduled-auto-run';
|
||||||
const AUTO_RUN_DELAY_MIN_MINUTES = 1;
|
const AUTO_RUN_DELAY_MIN_MINUTES = 1;
|
||||||
const AUTO_RUN_DELAY_MAX_MINUTES = 1440;
|
const AUTO_RUN_DELAY_MAX_MINUTES = 1440;
|
||||||
|
const AUTO_RUN_RETRY_DELAY_MS = 3000;
|
||||||
|
const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
|
||||||
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
|
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
|
||||||
|
|
||||||
initializeSessionStorageAccess();
|
initializeSessionStorageAccess();
|
||||||
@@ -57,7 +59,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
sub2apiPassword: '', // SUB2API 登录密码。
|
sub2apiPassword: '', // SUB2API 登录密码。
|
||||||
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME, // SUB2API 创建账号时绑定的分组名。
|
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME, // SUB2API 创建账号时绑定的分组名。
|
||||||
customPassword: '', // 自定义账号密码;留空时由程序自动生成随机密码。
|
customPassword: '', // 自定义账号密码;留空时由程序自动生成随机密码。
|
||||||
autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
|
autoRunSkipFailures: true, // 保留旧字段兼容;自动运行遇错时默认重试当前轮。
|
||||||
autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
|
autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
|
||||||
autoRunDelayMinutes: 30, // 自动运行倒计时分钟数。
|
autoRunDelayMinutes: 30, // 自动运行倒计时分钟数。
|
||||||
mailProvider: '163', // 验证码邮箱来源(163 / 163-vip / qq / inbucket)。
|
mailProvider: '163', // 验证码邮箱来源(163 / 163-vip / qq / inbucket)。
|
||||||
@@ -67,6 +69,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
cloudflareDomain: '', // 仅当 emailGenerator=cloudflare 时填写自定义域名。
|
cloudflareDomain: '', // 仅当 emailGenerator=cloudflare 时填写自定义域名。
|
||||||
cloudflareDomains: [], // Cloudflare 可选域名列表。
|
cloudflareDomains: [], // Cloudflare 可选域名列表。
|
||||||
hotmailAccounts: [],
|
hotmailAccounts: [],
|
||||||
|
autoRunDelayEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||||||
@@ -3302,7 +3305,81 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Outer loop: keep retrying until the target number of successful runs is reached.
|
function createAutoRunRoundSummary(round) {
|
||||||
|
return {
|
||||||
|
round,
|
||||||
|
status: 'pending',
|
||||||
|
attempts: 0,
|
||||||
|
failureReasons: [],
|
||||||
|
finalFailureReason: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAutoRunRoundRetryCount(summary) {
|
||||||
|
return Math.max(0, Number(summary?.attempts || 0) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) {
|
||||||
|
const summaries = Array.from({ length: totalRuns }, (_, index) => {
|
||||||
|
return roundSummaries[index] || createAutoRunRoundSummary(index + 1);
|
||||||
|
});
|
||||||
|
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 `第 ${item.round} 轮(重试 ${retryCount} 次,最终原因:${finalReason};失败记录:${reasonSummary})`;
|
||||||
|
})
|
||||||
|
.join(';')}`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingRounds.length) {
|
||||||
|
await addLog(
|
||||||
|
`未完成轮次:${pendingRounds.map((item) => `第 ${item.round} 轮`).join(';')}`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function autoRunLoop(totalRuns, options = {}) {
|
async function autoRunLoop(totalRuns, options = {}) {
|
||||||
if (autoRunActive) {
|
if (autoRunActive) {
|
||||||
await addLog('自动运行已在进行中', 'warn');
|
await addLog('自动运行已在进行中', 'warn');
|
||||||
@@ -3314,17 +3391,19 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
autoRunTotalRuns = totalRuns;
|
autoRunTotalRuns = totalRuns;
|
||||||
autoRunCurrentRun = 0;
|
autoRunCurrentRun = 0;
|
||||||
autoRunAttemptRun = 0;
|
autoRunAttemptRun = 0;
|
||||||
const autoRunSkipFailures = Boolean(options.autoRunSkipFailures);
|
const autoRunSkipFailures = true;
|
||||||
const initialMode = options.mode === 'continue' ? 'continue' : 'restart';
|
const initialMode = options.mode === 'continue' ? 'continue' : 'restart';
|
||||||
const resumeCurrentRun = Number.isInteger(options.resumeCurrentRun) ? options.resumeCurrentRun : 0;
|
const resumeCurrentRun = Number.isInteger(options.resumeCurrentRun) && options.resumeCurrentRun > 0
|
||||||
const resumeSuccessfulRuns = Number.isInteger(options.resumeSuccessfulRuns) ? options.resumeSuccessfulRuns : 0;
|
? Math.min(totalRuns, options.resumeCurrentRun)
|
||||||
const resumeAttemptRunsProcessed = Number.isInteger(options.resumeAttemptRunsProcessed) ? options.resumeAttemptRunsProcessed : 0;
|
: 1;
|
||||||
let maxAttempts = autoRunSkipFailures ? Math.max(totalRuns * 10, totalRuns + 20) : totalRuns;
|
const resumeAttemptRun = Number.isInteger(options.resumeAttemptRun) && options.resumeAttemptRun > 0
|
||||||
const forcedRetryCap = Math.max(totalRuns * 10, totalRuns + 20);
|
? Math.min(AUTO_RUN_MAX_RETRIES_PER_ROUND + 1, options.resumeAttemptRun)
|
||||||
let successfulRuns = Math.max(0, resumeSuccessfulRuns);
|
: 1;
|
||||||
let attemptRuns = Math.max(0, resumeAttemptRunsProcessed);
|
let successfulRuns = 0;
|
||||||
let forceFreshTabsNextRun = false;
|
let forceFreshTabsNextRun = false;
|
||||||
let continueCurrentOnFirstAttempt = initialMode === 'continue';
|
let continueCurrentOnFirstAttempt = initialMode === 'continue';
|
||||||
|
let stoppedEarly = false;
|
||||||
|
const roundSummaries = Array.from({ length: totalRuns }, (_, index) => createAutoRunRoundSummary(index + 1));
|
||||||
const initialState = await getState();
|
const initialState = await getState();
|
||||||
const initialPhase = continueCurrentOnFirstAttempt && getRunningSteps(initialState.stepStatuses).length
|
const initialPhase = continueCurrentOnFirstAttempt && getRunningSteps(initialState.stepStatuses).length
|
||||||
? 'waiting_step'
|
? 'waiting_step'
|
||||||
@@ -3333,180 +3412,180 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
await setState({
|
await setState({
|
||||||
autoRunSkipFailures,
|
autoRunSkipFailures,
|
||||||
...getAutoRunStatusPayload(initialPhase, {
|
...getAutoRunStatusPayload(initialPhase, {
|
||||||
currentRun: resumeCurrentRun,
|
currentRun: continueCurrentOnFirstAttempt ? resumeCurrentRun : 0,
|
||||||
totalRuns,
|
totalRuns,
|
||||||
attemptRun: resumeAttemptRunsProcessed,
|
attemptRun: continueCurrentOnFirstAttempt ? resumeAttemptRun : 0,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
while (successfulRuns < totalRuns && attemptRuns < maxAttempts) {
|
for (let targetRun = resumeCurrentRun; targetRun <= totalRuns; targetRun++) {
|
||||||
attemptRuns += 1;
|
const roundSummary = roundSummaries[targetRun - 1];
|
||||||
const targetRun = successfulRuns + 1;
|
let attemptRun = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun ? resumeAttemptRun : 1;
|
||||||
autoRunCurrentRun = targetRun;
|
let reuseExistingProgress = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun;
|
||||||
autoRunAttemptRun = attemptRuns;
|
|
||||||
let startStep = 1;
|
|
||||||
let useExistingProgress = false;
|
|
||||||
|
|
||||||
if (continueCurrentOnFirstAttempt) {
|
while (attemptRun <= AUTO_RUN_MAX_RETRIES_PER_ROUND + 1) {
|
||||||
let currentState = await getState();
|
autoRunCurrentRun = targetRun;
|
||||||
if (getRunningSteps(currentState.stepStatuses).length) {
|
autoRunAttemptRun = attemptRun;
|
||||||
currentState = await waitForRunningStepsToFinish({
|
roundSummary.attempts = attemptRun;
|
||||||
currentRun: targetRun,
|
let startStep = 1;
|
||||||
totalRuns,
|
let useExistingProgress = false;
|
||||||
attemptRun: attemptRuns,
|
|
||||||
|
if (reuseExistingProgress) {
|
||||||
|
let currentState = await getState();
|
||||||
|
if (getRunningSteps(currentState.stepStatuses).length) {
|
||||||
|
currentState = await waitForRunningStepsToFinish({
|
||||||
|
currentRun: targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRun,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const resumeStep = getFirstUnfinishedStep(currentState.stepStatuses);
|
||||||
|
if (resumeStep && hasSavedProgress(currentState.stepStatuses)) {
|
||||||
|
startStep = resumeStep;
|
||||||
|
useExistingProgress = true;
|
||||||
|
} else if (hasSavedProgress(currentState.stepStatuses)) {
|
||||||
|
await addLog('检测到当前流程已处理完成,本轮将改为从步骤 1 重新开始。', 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!useExistingProgress) {
|
||||||
|
const prevState = await getState();
|
||||||
|
const keepSettings = {
|
||||||
|
vpsUrl: prevState.vpsUrl,
|
||||||
|
vpsPassword: prevState.vpsPassword,
|
||||||
|
customPassword: prevState.customPassword,
|
||||||
|
autoRunSkipFailures: prevState.autoRunSkipFailures,
|
||||||
|
autoRunDelayEnabled: prevState.autoRunDelayEnabled,
|
||||||
|
autoRunDelayMinutes: prevState.autoRunDelayMinutes,
|
||||||
|
mailProvider: prevState.mailProvider,
|
||||||
|
emailGenerator: prevState.emailGenerator,
|
||||||
|
inbucketHost: prevState.inbucketHost,
|
||||||
|
inbucketMailbox: prevState.inbucketMailbox,
|
||||||
|
cloudflareDomain: prevState.cloudflareDomain,
|
||||||
|
cloudflareDomains: prevState.cloudflareDomains,
|
||||||
|
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun }),
|
||||||
|
...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}),
|
||||||
|
};
|
||||||
|
await resetState();
|
||||||
|
await setState(keepSettings);
|
||||||
|
chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => { });
|
||||||
|
await sleepWithStop(500);
|
||||||
|
} else {
|
||||||
|
await setState({
|
||||||
|
autoRunSkipFailures,
|
||||||
|
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const resumeStep = getFirstUnfinishedStep(currentState.stepStatuses);
|
|
||||||
if (resumeStep && hasSavedProgress(currentState.stepStatuses)) {
|
if (forceFreshTabsNextRun) {
|
||||||
startStep = resumeStep;
|
await addLog(`上一轮尝试已放弃,当前开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun} 次尝试。`, 'warn');
|
||||||
useExistingProgress = true;
|
forceFreshTabsNextRun = false;
|
||||||
} else if (hasSavedProgress(currentState.stepStatuses)) {
|
|
||||||
await addLog('当前流程已全部处理,将按“重新开始”新开一轮自动运行。', 'info');
|
|
||||||
}
|
}
|
||||||
continueCurrentOnFirstAttempt = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!useExistingProgress) {
|
try {
|
||||||
// Reset everything at the start of each fresh attempt (keep user settings).
|
throwIfStopped();
|
||||||
const prevState = await getState();
|
await broadcastAutoRunStatus('running', {
|
||||||
const keepSettings = {
|
|
||||||
vpsUrl: prevState.vpsUrl,
|
|
||||||
vpsPassword: prevState.vpsPassword,
|
|
||||||
customPassword: prevState.customPassword,
|
|
||||||
autoRunSkipFailures: prevState.autoRunSkipFailures,
|
|
||||||
autoRunDelayEnabled: prevState.autoRunDelayEnabled,
|
|
||||||
autoRunDelayMinutes: prevState.autoRunDelayMinutes,
|
|
||||||
mailProvider: prevState.mailProvider,
|
|
||||||
emailGenerator: prevState.emailGenerator,
|
|
||||||
inbucketHost: prevState.inbucketHost,
|
|
||||||
inbucketMailbox: prevState.inbucketMailbox,
|
|
||||||
cloudflareDomain: prevState.cloudflareDomain,
|
|
||||||
cloudflareDomains: prevState.cloudflareDomains,
|
|
||||||
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }),
|
|
||||||
...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}),
|
|
||||||
};
|
|
||||||
await resetState();
|
|
||||||
await setState(keepSettings);
|
|
||||||
chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => { });
|
|
||||||
await sleepWithStop(500);
|
|
||||||
} else {
|
|
||||||
await setState({
|
|
||||||
autoRunSkipFailures,
|
|
||||||
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceFreshTabsNextRun) {
|
|
||||||
await addLog(`兜底模式:上一轮已放弃,当前开始第 ${attemptRuns} 次尝试,将使用新线程继续补足第 ${targetRun}/${totalRuns} 轮。`, 'warn');
|
|
||||||
forceFreshTabsNextRun = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
throwIfStopped();
|
|
||||||
await broadcastAutoRunStatus('running', {
|
|
||||||
currentRun: targetRun,
|
|
||||||
totalRuns,
|
|
||||||
attemptRun: attemptRuns,
|
|
||||||
});
|
|
||||||
|
|
||||||
await runAutoSequenceFromStep(startStep, {
|
|
||||||
targetRun,
|
|
||||||
totalRuns,
|
|
||||||
attemptRuns,
|
|
||||||
continued: useExistingProgress,
|
|
||||||
});
|
|
||||||
|
|
||||||
successfulRuns += 1;
|
|
||||||
autoRunCurrentRun = successfulRuns;
|
|
||||||
await addLog(`=== 目标 ${successfulRuns}/${totalRuns} 轮已完成(第 ${attemptRuns} 次尝试成功)===`, 'ok');
|
|
||||||
continue;
|
|
||||||
} catch (err) {
|
|
||||||
if (isStopError(err)) {
|
|
||||||
await addLog(`目标 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
|
||||||
await broadcastAutoRunStatus('stopped', {
|
|
||||||
currentRun: targetRun,
|
currentRun: targetRun,
|
||||||
totalRuns,
|
totalRuns,
|
||||||
attemptRun: attemptRuns,
|
attemptRun,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await runAutoSequenceFromStep(startStep, {
|
||||||
|
targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRuns: attemptRun,
|
||||||
|
continued: useExistingProgress,
|
||||||
|
});
|
||||||
|
|
||||||
|
roundSummary.status = 'success';
|
||||||
|
successfulRuns += 1;
|
||||||
|
await addLog(`=== 第 ${targetRun}/${totalRuns} 轮完成(第 ${attemptRun} 次尝试成功)===`, 'ok');
|
||||||
break;
|
break;
|
||||||
}
|
} catch (err) {
|
||||||
|
if (isStopError(err)) {
|
||||||
|
stoppedEarly = true;
|
||||||
|
await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
||||||
|
await broadcastAutoRunStatus('stopped', {
|
||||||
|
currentRun: targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRun,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (isRestartCurrentAttemptError(err)) {
|
const reason = getErrorMessage(err);
|
||||||
await addLog(`目标 ${targetRun}/${totalRuns} 轮检测到当前邮箱已存在,当前线程已放弃,将重新开始新一轮。`, 'warn');
|
roundSummary.failureReasons.push(reason);
|
||||||
cancelPendingCommands('当前线程因邮箱已存在而放弃。');
|
const canRetry = attemptRun <= AUTO_RUN_MAX_RETRIES_PER_ROUND;
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
forceFreshTabsNextRun = true;
|
||||||
|
await addLog(
|
||||||
|
`自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试(第 ${retryIndex}/${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试)。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
await sleepWithStop(AUTO_RUN_RETRY_DELAY_MS);
|
||||||
|
attemptRun += 1;
|
||||||
|
reuseExistingProgress = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
roundSummary.status = 'failed';
|
||||||
|
roundSummary.finalFailureReason = reason;
|
||||||
|
await addLog(`第 ${targetRun}/${totalRuns} 轮最终失败:${reason}`, 'error');
|
||||||
|
await addLog(`第 ${targetRun}/${totalRuns} 轮已达到 ${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试上限,继续下一轮。`, 'warn');
|
||||||
|
cancelPendingCommands('当前轮已达到重试上限。');
|
||||||
await broadcastStopToContentScripts();
|
await broadcastStopToContentScripts();
|
||||||
await broadcastAutoRunStatus('retrying', {
|
|
||||||
currentRun: targetRun,
|
|
||||||
totalRuns,
|
|
||||||
attemptRun: attemptRuns,
|
|
||||||
});
|
|
||||||
forceFreshTabsNextRun = true;
|
forceFreshTabsNextRun = true;
|
||||||
maxAttempts = Math.max(maxAttempts, Math.min(forcedRetryCap, attemptRuns + 1));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!autoRunSkipFailures) {
|
|
||||||
await addLog(`目标 ${targetRun}/${totalRuns} 轮失败:${err.message}`, 'error');
|
|
||||||
await broadcastAutoRunStatus('stopped', {
|
|
||||||
currentRun: targetRun,
|
|
||||||
totalRuns,
|
|
||||||
attemptRun: attemptRuns,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
|
} finally {
|
||||||
|
reuseExistingProgress = false;
|
||||||
|
continueCurrentOnFirstAttempt = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await addLog(`目标 ${targetRun}/${totalRuns} 轮的第 ${attemptRuns} 次尝试失败:${err.message}`, 'error');
|
if (stoppedEarly) {
|
||||||
await addLog('兜底开关已开启:将放弃当前线程,重新开一轮继续补足目标次数。', 'warn');
|
break;
|
||||||
cancelPendingCommands('当前尝试已放弃。');
|
|
||||||
await broadcastStopToContentScripts();
|
|
||||||
await broadcastAutoRunStatus('retrying', {
|
|
||||||
currentRun: targetRun,
|
|
||||||
totalRuns,
|
|
||||||
attemptRun: attemptRuns,
|
|
||||||
});
|
|
||||||
forceFreshTabsNextRun = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stopRequested && autoRunSkipFailures && successfulRuns < totalRuns && attemptRuns >= maxAttempts) {
|
await logAutoRunFinalSummary(totalRuns, roundSummaries);
|
||||||
await addLog(`已达到安全重试上限(${attemptRuns} 次尝试),当前仅完成 ${successfulRuns}/${totalRuns} 轮。`, 'error');
|
|
||||||
|
if (stopRequested || stoppedEarly) {
|
||||||
|
await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮 ===`, 'warn');
|
||||||
await broadcastAutoRunStatus('stopped', {
|
await broadcastAutoRunStatus('stopped', {
|
||||||
currentRun: successfulRuns,
|
currentRun: autoRunCurrentRun,
|
||||||
totalRuns: autoRunTotalRuns,
|
totalRuns: autoRunTotalRuns,
|
||||||
attemptRun: attemptRuns,
|
attemptRun: autoRunAttemptRun,
|
||||||
});
|
|
||||||
} else if (stopRequested) {
|
|
||||||
await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn');
|
|
||||||
await broadcastAutoRunStatus('stopped', {
|
|
||||||
currentRun: successfulRuns,
|
|
||||||
totalRuns: autoRunTotalRuns,
|
|
||||||
attemptRun: attemptRuns,
|
|
||||||
});
|
|
||||||
} else if (successfulRuns >= autoRunTotalRuns) {
|
|
||||||
await addLog(`=== 全部 ${autoRunTotalRuns} 轮均已成功完成,共尝试 ${attemptRuns} 次 ===`, 'ok');
|
|
||||||
await broadcastAutoRunStatus('complete', {
|
|
||||||
currentRun: successfulRuns,
|
|
||||||
totalRuns: autoRunTotalRuns,
|
|
||||||
attemptRun: attemptRuns,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn');
|
await addLog(`=== 全部 ${autoRunTotalRuns} 轮已执行完成,成功 ${successfulRuns} 轮 ===`, 'ok');
|
||||||
await broadcastAutoRunStatus('stopped', {
|
await broadcastAutoRunStatus('complete', {
|
||||||
currentRun: successfulRuns,
|
currentRun: autoRunTotalRuns,
|
||||||
totalRuns: autoRunTotalRuns,
|
totalRuns: autoRunTotalRuns,
|
||||||
attemptRun: attemptRuns,
|
attemptRun: autoRunAttemptRun,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
autoRunActive = false;
|
autoRunActive = false;
|
||||||
autoRunAttemptRun = attemptRuns;
|
await setState(getAutoRunStatusPayload(stopRequested || stoppedEarly ? 'stopped' : 'complete', {
|
||||||
await setState(getAutoRunStatusPayload(stopRequested ? 'stopped' : (successfulRuns >= autoRunTotalRuns ? 'complete' : 'stopped'), {
|
currentRun: stopRequested || stoppedEarly ? autoRunCurrentRun : autoRunTotalRuns,
|
||||||
currentRun: successfulRuns,
|
|
||||||
totalRuns: autoRunTotalRuns,
|
totalRuns: autoRunTotalRuns,
|
||||||
attemptRun: attemptRuns,
|
attemptRun: autoRunAttemptRun,
|
||||||
}));
|
}));
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForResume() {
|
async function waitForResume() {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
@@ -3544,15 +3623,13 @@ async function resumeAutoRun() {
|
|||||||
const totalRuns = state.autoRunTotalRuns || 1;
|
const totalRuns = state.autoRunTotalRuns || 1;
|
||||||
const currentRun = state.autoRunCurrentRun || 1;
|
const currentRun = state.autoRunCurrentRun || 1;
|
||||||
const attemptRun = state.autoRunAttemptRun || 1;
|
const attemptRun = state.autoRunAttemptRun || 1;
|
||||||
const successfulRuns = Math.max(0, currentRun - 1);
|
|
||||||
|
|
||||||
await addLog('检测到自动流程暂停上下文已丢失,正在从当前进度恢复自动运行...', 'warn');
|
await addLog('检测到自动流程暂停上下文已丢失,正在从当前进度恢复自动运行...', 'warn');
|
||||||
autoRunLoop(totalRuns, {
|
autoRunLoop(totalRuns, {
|
||||||
autoRunSkipFailures: Boolean(state.autoRunSkipFailures),
|
autoRunSkipFailures: Boolean(state.autoRunSkipFailures),
|
||||||
mode: 'continue',
|
mode: 'continue',
|
||||||
resumeCurrentRun: currentRun,
|
resumeCurrentRun: currentRun,
|
||||||
resumeSuccessfulRuns: successfulRuns,
|
resumeAttemptRun: attemptRun,
|
||||||
resumeAttemptRunsProcessed: Math.max(0, attemptRun - 1),
|
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,10 +172,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row data-check-row">
|
<div class="data-row data-check-row">
|
||||||
<span class="data-label">兜底</span>
|
<span class="data-label">自动重试</span>
|
||||||
<label class="data-check" for="input-auto-skip-failures">
|
<label class="data-check" for="input-auto-skip-failures">
|
||||||
<input type="checkbox" id="input-auto-skip-failures" />
|
<input type="checkbox" id="input-auto-skip-failures" checked />
|
||||||
<span>出现错误无法继续时,直接丢弃当前线程并新开一轮,直到补足目标运行次数</span>
|
<span>自动模式遇到单步错误时,会先显示报错,再等待几秒后从当前轮第 1 步重新开始</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
|
|||||||
@@ -614,7 +614,7 @@ function collectSettingsPayload() {
|
|||||||
inbucketMailbox: inputInbucketMailbox.value.trim(),
|
inbucketMailbox: inputInbucketMailbox.value.trim(),
|
||||||
cloudflareDomain: selectedCloudflareDomain,
|
cloudflareDomain: selectedCloudflareDomain,
|
||||||
cloudflareDomains: domains,
|
cloudflareDomains: domains,
|
||||||
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
autoRunSkipFailures: true,
|
||||||
autoRunDelayEnabled: inputAutoDelayEnabled.checked,
|
autoRunDelayEnabled: inputAutoDelayEnabled.checked,
|
||||||
autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
|
autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
|
||||||
};
|
};
|
||||||
@@ -757,7 +757,7 @@ function applyAutoRunStatus(payload = currentAutoRun) {
|
|||||||
btnAutoRun.disabled = currentAutoRun.autoRunning;
|
btnAutoRun.disabled = currentAutoRun.autoRunning;
|
||||||
btnFetchEmail.disabled = locked;
|
btnFetchEmail.disabled = locked;
|
||||||
inputEmail.disabled = locked;
|
inputEmail.disabled = locked;
|
||||||
inputAutoSkipFailures.disabled = scheduled;
|
inputAutoSkipFailures.disabled = true;
|
||||||
|
|
||||||
if (currentAutoRun.totalRuns > 0) {
|
if (currentAutoRun.totalRuns > 0) {
|
||||||
inputRunCount.value = String(currentAutoRun.totalRuns);
|
inputRunCount.value = String(currentAutoRun.totalRuns);
|
||||||
@@ -855,7 +855,7 @@ function applySettingsState(state) {
|
|||||||
inputInbucketMailbox.value = state?.inbucketMailbox || '';
|
inputInbucketMailbox.value = state?.inbucketMailbox || '';
|
||||||
renderCloudflareDomainOptions(state?.cloudflareDomain || '');
|
renderCloudflareDomainOptions(state?.cloudflareDomain || '');
|
||||||
setCloudflareDomainEditMode(false, { clearInput: true });
|
setCloudflareDomainEditMode(false, { clearInput: true });
|
||||||
inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
|
inputAutoSkipFailures.checked = true;
|
||||||
inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
|
inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
|
||||||
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes));
|
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes));
|
||||||
if (state?.autoRunTotalRuns) {
|
if (state?.autoRunTotalRuns) {
|
||||||
@@ -2113,7 +2113,7 @@ btnAutoRun.addEventListener('click', async () => {
|
|||||||
payload: {
|
payload: {
|
||||||
totalRuns,
|
totalRuns,
|
||||||
delayMinutes,
|
delayMinutes,
|
||||||
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
autoRunSkipFailures: true,
|
||||||
mode,
|
mode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user