feat: 修复停止逻辑
This commit is contained in:
+109
-1
@@ -308,6 +308,7 @@ const DEFAULT_STATE = {
|
||||
autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。
|
||||
autoRunTotalRuns: 1, // 自动运行计划总轮数。
|
||||
autoRunAttemptRun: 0, // 当前轮次的重试序号。
|
||||
autoRunSessionId: 0,
|
||||
autoRunRoundSummaries: [], // 自动运行轮次摘要。
|
||||
scheduledAutoRunAt: null, // 自动运行计划启动时间戳。
|
||||
autoRunTimerPlan: null, // 自动运行可恢复计时计划快照。
|
||||
@@ -426,6 +427,48 @@ function normalizeAutoRunTimerKind(value = '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeAutoRunSessionId(value) {
|
||||
const numeric = Math.floor(Number(value) || 0);
|
||||
return numeric > 0 ? numeric : 0;
|
||||
}
|
||||
|
||||
function createAutoRunSessionId() {
|
||||
autoRunSessionSeed = Math.max(autoRunSessionSeed + 1, Date.now());
|
||||
autoRunSessionId = autoRunSessionSeed;
|
||||
return autoRunSessionId;
|
||||
}
|
||||
|
||||
function setCurrentAutoRunSessionId(value) {
|
||||
autoRunSessionId = normalizeAutoRunSessionId(value);
|
||||
return autoRunSessionId;
|
||||
}
|
||||
|
||||
function clearCurrentAutoRunSessionId(expectedSessionId = null) {
|
||||
if (expectedSessionId === null) {
|
||||
autoRunSessionId = 0;
|
||||
return autoRunSessionId;
|
||||
}
|
||||
|
||||
const normalizedExpected = normalizeAutoRunSessionId(expectedSessionId);
|
||||
if (!normalizedExpected || normalizedExpected === autoRunSessionId) {
|
||||
autoRunSessionId = 0;
|
||||
}
|
||||
return autoRunSessionId;
|
||||
}
|
||||
|
||||
function isCurrentAutoRunSessionId(value) {
|
||||
const normalized = normalizeAutoRunSessionId(value);
|
||||
return normalized > 0 && normalized === autoRunSessionId;
|
||||
}
|
||||
|
||||
function throwIfAutoRunSessionStopped(sessionId) {
|
||||
const normalizedSessionId = normalizeAutoRunSessionId(sessionId);
|
||||
if (normalizedSessionId && !isCurrentAutoRunSessionId(normalizedSessionId)) {
|
||||
throw new Error(STOP_ERROR_MESSAGE);
|
||||
}
|
||||
throwIfStopped();
|
||||
}
|
||||
|
||||
function normalizeAutoRunTimerPlan(plan) {
|
||||
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
|
||||
return null;
|
||||
@@ -449,6 +492,7 @@ function normalizeAutoRunTimerPlan(plan) {
|
||||
0,
|
||||
Math.min(AUTO_RUN_MAX_RETRIES_PER_ROUND + 1, Math.floor(Number(plan.attemptRun) || 0))
|
||||
);
|
||||
const autoRunSessionId = normalizeAutoRunSessionId(plan.autoRunSessionId ?? plan.sessionId);
|
||||
const roundSummaries = serializeAutoRunRoundSummaries(totalRuns, plan.roundSummaries);
|
||||
const countdownTitle = String(plan.countdownTitle || '').trim();
|
||||
const countdownNote = String(plan.countdownNote || '').trim();
|
||||
@@ -462,6 +506,7 @@ function normalizeAutoRunTimerPlan(plan) {
|
||||
mode,
|
||||
currentRun: 0,
|
||||
attemptRun: 0,
|
||||
autoRunSessionId,
|
||||
roundSummaries: [],
|
||||
countdownTitle: countdownTitle || '已计划自动运行',
|
||||
countdownNote: countdownNote || `计划于 ${formatAutoRunScheduleTime(fireAt)} 开始`,
|
||||
@@ -479,6 +524,7 @@ function normalizeAutoRunTimerPlan(plan) {
|
||||
mode: 'restart',
|
||||
currentRun: normalizedCurrentRun,
|
||||
attemptRun: normalizedAttemptRun,
|
||||
autoRunSessionId,
|
||||
roundSummaries,
|
||||
countdownTitle: countdownTitle || '线程间隔中',
|
||||
countdownNote: countdownNote || `第 ${Math.min(normalizedCurrentRun + 1, totalRuns)}/${totalRuns} 轮即将开始`,
|
||||
@@ -495,6 +541,7 @@ function normalizeAutoRunTimerPlan(plan) {
|
||||
mode: 'restart',
|
||||
currentRun: normalizedCurrentRun,
|
||||
attemptRun: normalizedAttemptRun,
|
||||
autoRunSessionId,
|
||||
roundSummaries,
|
||||
countdownTitle: countdownTitle || '线程间隔中',
|
||||
countdownNote: countdownNote || `第 ${normalizedCurrentRun}/${totalRuns} 轮第 ${normalizedAttemptRun} 次尝试即将开始`,
|
||||
@@ -521,6 +568,7 @@ function normalizeAutoRunTimerPlanFromState(state = {}) {
|
||||
fireAt: legacyScheduledAt,
|
||||
totalRuns: state.scheduledAutoRunPlan?.totalRuns ?? state.autoRunTotalRuns,
|
||||
autoRunSkipFailures: state.scheduledAutoRunPlan?.autoRunSkipFailures ?? state.autoRunSkipFailures,
|
||||
autoRunSessionId: state.autoRunSessionId,
|
||||
mode: state.scheduledAutoRunPlan?.mode,
|
||||
});
|
||||
}
|
||||
@@ -541,6 +589,7 @@ function getAutoRunTimerStatusPayload(plan) {
|
||||
currentRun: normalizedPlan.currentRun,
|
||||
totalRuns: normalizedPlan.totalRuns,
|
||||
attemptRun: normalizedPlan.attemptRun,
|
||||
sessionId: normalizedPlan.autoRunSessionId,
|
||||
scheduledAt: phase === 'scheduled' ? normalizedPlan.fireAt : null,
|
||||
countdownAt: normalizedPlan.fireAt,
|
||||
countdownTitle: normalizedPlan.countdownTitle,
|
||||
@@ -3794,6 +3843,7 @@ const tabRuntime = self.MultiPageBackgroundTabRuntime?.createTabRuntime({
|
||||
LOG_PREFIX,
|
||||
matchesSourceUrlFamily,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
STOP_ERROR_MESSAGE,
|
||||
throwIfStopped,
|
||||
});
|
||||
@@ -3978,6 +4028,7 @@ function getAutoRunStatusPayload(phase, payload = {}) {
|
||||
currentRun: payload.currentRun ?? autoRunCurrentRun,
|
||||
totalRuns: payload.totalRuns ?? autoRunTotalRuns,
|
||||
attemptRun: payload.attemptRun ?? autoRunAttemptRun,
|
||||
sessionId: payload.sessionId ?? payload.autoRunSessionId ?? autoRunSessionId,
|
||||
};
|
||||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getAutoRunStatusPayload) {
|
||||
return loggingStatus.getAutoRunStatusPayload(phase, normalizedPayload);
|
||||
@@ -3993,6 +4044,7 @@ function getAutoRunStatusPayload(phase, payload = {}) {
|
||||
autoRunCurrentRun: normalizedPayload.currentRun ?? 0,
|
||||
autoRunTotalRuns: normalizedPayload.totalRuns ?? 1,
|
||||
autoRunAttemptRun: normalizedPayload.attemptRun ?? 0,
|
||||
autoRunSessionId: normalizeAutoRunSessionId(normalizedPayload.sessionId),
|
||||
scheduledAutoRunAt: Number.isFinite(Number(normalizedPayload.scheduledAt)) ? Number(normalizedPayload.scheduledAt) : null,
|
||||
autoRunCountdownAt: Number.isFinite(Number(normalizedPayload.countdownAt)) ? Number(normalizedPayload.countdownAt) : null,
|
||||
autoRunCountdownTitle: normalizedPayload.countdownTitle === undefined ? '' : String(normalizedPayload.countdownTitle || ''),
|
||||
@@ -4010,6 +4062,7 @@ async function broadcastAutoRunStatus(phase, payload = {}, extraState = {}) {
|
||||
currentRun: payload.currentRun ?? autoRunCurrentRun,
|
||||
totalRuns: payload.totalRuns ?? autoRunTotalRuns,
|
||||
attemptRun: payload.attemptRun ?? autoRunAttemptRun,
|
||||
sessionId: payload.sessionId ?? payload.autoRunSessionId ?? autoRunSessionId,
|
||||
scheduledAt: rawScheduledAt === null ? null : Number(rawScheduledAt),
|
||||
countdownAt: rawCountdownAt === null ? null : Number(rawCountdownAt),
|
||||
countdownTitle: payload.countdownTitle === undefined ? '' : String(payload.countdownTitle || ''),
|
||||
@@ -4119,6 +4172,7 @@ function getAutoRunTimerResumeOptions(plan) {
|
||||
if (normalizedPlan.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START) {
|
||||
return {
|
||||
loopOptions: {
|
||||
autoRunSessionId: normalizedPlan.autoRunSessionId,
|
||||
autoRunSkipFailures: normalizedPlan.autoRunSkipFailures,
|
||||
mode: normalizedPlan.mode,
|
||||
},
|
||||
@@ -4126,6 +4180,7 @@ function getAutoRunTimerResumeOptions(plan) {
|
||||
currentRun: 0,
|
||||
totalRuns: normalizedPlan.totalRuns,
|
||||
attemptRun: 0,
|
||||
sessionId: normalizedPlan.autoRunSessionId,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -4134,6 +4189,7 @@ function getAutoRunTimerResumeOptions(plan) {
|
||||
const nextRun = Math.min(normalizedPlan.currentRun + 1, normalizedPlan.totalRuns);
|
||||
return {
|
||||
loopOptions: {
|
||||
autoRunSessionId: normalizedPlan.autoRunSessionId,
|
||||
autoRunSkipFailures: normalizedPlan.autoRunSkipFailures,
|
||||
mode: 'restart',
|
||||
resumeCurrentRun: nextRun,
|
||||
@@ -4144,12 +4200,14 @@ function getAutoRunTimerResumeOptions(plan) {
|
||||
currentRun: nextRun,
|
||||
totalRuns: normalizedPlan.totalRuns,
|
||||
attemptRun: 1,
|
||||
sessionId: normalizedPlan.autoRunSessionId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
loopOptions: {
|
||||
autoRunSessionId: normalizedPlan.autoRunSessionId,
|
||||
autoRunSkipFailures: normalizedPlan.autoRunSkipFailures,
|
||||
mode: 'restart',
|
||||
resumeCurrentRun: normalizedPlan.currentRun,
|
||||
@@ -4160,6 +4218,7 @@ function getAutoRunTimerResumeOptions(plan) {
|
||||
currentRun: normalizedPlan.currentRun,
|
||||
totalRuns: normalizedPlan.totalRuns,
|
||||
attemptRun: normalizedPlan.attemptRun,
|
||||
sessionId: normalizedPlan.autoRunSessionId,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -4185,6 +4244,9 @@ async function launchAutoRunTimerPlan(trigger = 'alarm', options = {}) {
|
||||
if (autoRunActive) {
|
||||
return false;
|
||||
}
|
||||
if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const resumeOptions = getAutoRunTimerResumeOptions(plan);
|
||||
if (!resumeOptions) {
|
||||
@@ -4202,9 +4264,13 @@ async function launchAutoRunTimerPlan(trigger = 'alarm', options = {}) {
|
||||
}
|
||||
|
||||
await clearAutoRunTimerAlarm();
|
||||
if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) {
|
||||
return false;
|
||||
}
|
||||
autoRunCurrentRun = resumeOptions.statusPayload.currentRun;
|
||||
autoRunTotalRuns = plan.totalRuns;
|
||||
autoRunAttemptRun = resumeOptions.statusPayload.attemptRun;
|
||||
autoRunSessionId = normalizeAutoRunSessionId(plan.autoRunSessionId);
|
||||
if (plan.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START && trigger !== 'manual' && state.autoRunDelayEnabled) {
|
||||
await setAutoRunDelayEnabledState(false);
|
||||
}
|
||||
@@ -4219,6 +4285,9 @@ async function launchAutoRunTimerPlan(trigger = 'alarm', options = {}) {
|
||||
}
|
||||
);
|
||||
|
||||
if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) {
|
||||
return false;
|
||||
}
|
||||
clearStopRequest();
|
||||
let logMessage = '倒计时结束,自动运行开始执行。';
|
||||
if (plan.kind === AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS) {
|
||||
@@ -4233,6 +4302,9 @@ async function launchAutoRunTimerPlan(trigger = 'alarm', options = {}) {
|
||||
logMessage = '已手动跳过倒计时,自动运行立即开始。';
|
||||
}
|
||||
await addLog(logMessage, 'info');
|
||||
if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
startAutoRunLoop(plan.totalRuns, resumeOptions.loopOptions);
|
||||
return true;
|
||||
@@ -4251,17 +4323,20 @@ async function scheduleAutoRun(totalRuns, options = {}) {
|
||||
}
|
||||
|
||||
const delayMinutes = normalizeAutoRunDelayMinutes(options.delayMinutes);
|
||||
const sessionId = createAutoRunSessionId();
|
||||
const timerPlan = normalizeAutoRunTimerPlan({
|
||||
kind: AUTO_RUN_TIMER_KIND_SCHEDULED_START,
|
||||
fireAt: Date.now() + delayMinutes * 60 * 1000,
|
||||
totalRuns,
|
||||
autoRunSkipFailures: options.autoRunSkipFailures,
|
||||
autoRunSessionId: sessionId,
|
||||
mode: options.mode,
|
||||
});
|
||||
|
||||
autoRunCurrentRun = 0;
|
||||
autoRunTotalRuns = timerPlan.totalRuns;
|
||||
autoRunAttemptRun = 0;
|
||||
autoRunSessionId = sessionId;
|
||||
|
||||
await persistAutoRunTimerPlan(timerPlan, {
|
||||
autoRunSkipFailures: timerPlan.autoRunSkipFailures,
|
||||
@@ -4284,14 +4359,17 @@ async function cancelScheduledAutoRun(options = {}) {
|
||||
autoRunCurrentRun = 0;
|
||||
autoRunTotalRuns = plan.totalRuns;
|
||||
autoRunAttemptRun = 0;
|
||||
clearCurrentAutoRunSessionId(plan.autoRunSessionId);
|
||||
await broadcastAutoRunStatus(
|
||||
'idle',
|
||||
{
|
||||
currentRun: 0,
|
||||
totalRuns: plan.totalRuns,
|
||||
attemptRun: 0,
|
||||
sessionId: 0,
|
||||
},
|
||||
{
|
||||
autoRunSessionId: 0,
|
||||
autoRunRoundSummaries: [],
|
||||
autoRunTimerPlan: null,
|
||||
scheduledAutoRunPlan: null,
|
||||
@@ -4306,15 +4384,18 @@ async function cancelScheduledAutoRun(options = {}) {
|
||||
|
||||
async function restoreAutoRunTimerIfNeeded() {
|
||||
const state = await getState();
|
||||
const plan = getPendingAutoRunTimerPlan(state);
|
||||
let plan = getPendingAutoRunTimerPlan(state);
|
||||
if (!plan) {
|
||||
clearCurrentAutoRunSessionId();
|
||||
if (state.autoRunPhase === 'scheduled' || state.autoRunPhase === 'waiting_interval') {
|
||||
await clearAutoRunTimerAlarm();
|
||||
await broadcastAutoRunStatus('idle', {
|
||||
currentRun: 0,
|
||||
totalRuns: 1,
|
||||
attemptRun: 0,
|
||||
sessionId: 0,
|
||||
}, {
|
||||
autoRunSessionId: 0,
|
||||
autoRunRoundSummaries: [],
|
||||
autoRunTimerPlan: null,
|
||||
scheduledAutoRunPlan: null,
|
||||
@@ -4323,6 +4404,19 @@ async function restoreAutoRunTimerIfNeeded() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!plan.autoRunSessionId) {
|
||||
const restoredSessionId = createAutoRunSessionId();
|
||||
plan = await persistAutoRunTimerPlan({
|
||||
...plan,
|
||||
autoRunSessionId: restoredSessionId,
|
||||
}, {
|
||||
autoRunSkipFailures: plan.autoRunSkipFailures,
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(plan.totalRuns, plan.roundSummaries),
|
||||
});
|
||||
} else {
|
||||
setCurrentAutoRunSessionId(plan.autoRunSessionId);
|
||||
}
|
||||
|
||||
if (plan.fireAt <= Date.now()) {
|
||||
await launchAutoRunTimerPlan('restore');
|
||||
return;
|
||||
@@ -4333,6 +4427,7 @@ async function restoreAutoRunTimerIfNeeded() {
|
||||
statusPayload.phase,
|
||||
statusPayload,
|
||||
{
|
||||
autoRunSessionId: plan.autoRunSessionId,
|
||||
autoRunSkipFailures: plan.autoRunSkipFailures,
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(plan.totalRuns, plan.roundSummaries),
|
||||
autoRunTimerPlan: plan,
|
||||
@@ -4809,6 +4904,7 @@ async function requestStop(options = {}) {
|
||||
autoRunCurrentRun = timerPlan.currentRun;
|
||||
autoRunTotalRuns = timerPlan.totalRuns;
|
||||
autoRunAttemptRun = timerPlan.attemptRun;
|
||||
clearCurrentAutoRunSessionId(timerPlan.autoRunSessionId);
|
||||
if (options.logMessage !== false) {
|
||||
await addLog(options.logMessage || '已停止等待中的自动流程。', 'warn');
|
||||
}
|
||||
@@ -4816,7 +4912,9 @@ async function requestStop(options = {}) {
|
||||
currentRun: timerPlan.currentRun,
|
||||
totalRuns: timerPlan.totalRuns,
|
||||
attemptRun: timerPlan.attemptRun,
|
||||
sessionId: 0,
|
||||
}, {
|
||||
autoRunSessionId: 0,
|
||||
autoRunSkipFailures: timerPlan.autoRunSkipFailures,
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(timerPlan.totalRuns, timerPlan.roundSummaries),
|
||||
autoRunTimerPlan: null,
|
||||
@@ -4830,6 +4928,7 @@ async function requestStop(options = {}) {
|
||||
if (stopRequested) return;
|
||||
|
||||
stopRequested = true;
|
||||
clearCurrentAutoRunSessionId();
|
||||
cancelPendingCommands();
|
||||
cleanupStep8NavigationListeners();
|
||||
rejectPendingStep8(new Error(STOP_ERROR_MESSAGE));
|
||||
@@ -4853,7 +4952,9 @@ async function requestStop(options = {}) {
|
||||
currentRun: autoRunCurrentRun,
|
||||
totalRuns: autoRunTotalRuns,
|
||||
attemptRun: autoRunAttemptRun,
|
||||
sessionId: 0,
|
||||
}, {
|
||||
autoRunSessionId: 0,
|
||||
autoRunTimerPlan: null,
|
||||
scheduledAutoRunPlan: null,
|
||||
});
|
||||
@@ -5017,6 +5118,8 @@ let autoRunActive = false;
|
||||
let autoRunCurrentRun = 0;
|
||||
let autoRunTotalRuns = 1;
|
||||
let autoRunAttemptRun = 0;
|
||||
let autoRunSessionId = 0;
|
||||
let autoRunSessionSeed = 0;
|
||||
const EMAIL_FETCH_MAX_ATTEMPTS = 5;
|
||||
const VERIFICATION_POLL_MAX_ROUNDS = 5;
|
||||
const STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS = 25000;
|
||||
@@ -5088,6 +5191,7 @@ const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoR
|
||||
broadcastStopToContentScripts,
|
||||
cancelPendingCommands,
|
||||
clearStopRequest: () => clearStopRequest(),
|
||||
createAutoRunSessionId: () => createAutoRunSessionId(),
|
||||
getAutoRunStatusPayload,
|
||||
getErrorMessage,
|
||||
getFirstUnfinishedStep,
|
||||
@@ -5109,16 +5213,19 @@ const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoR
|
||||
autoRunCurrentRun,
|
||||
autoRunTotalRuns,
|
||||
autoRunAttemptRun,
|
||||
autoRunSessionId,
|
||||
}),
|
||||
set: (updates = {}) => {
|
||||
if (updates.autoRunActive !== undefined) autoRunActive = Boolean(updates.autoRunActive);
|
||||
if (updates.autoRunCurrentRun !== undefined) autoRunCurrentRun = Number(updates.autoRunCurrentRun) || 0;
|
||||
if (updates.autoRunTotalRuns !== undefined) autoRunTotalRuns = Number(updates.autoRunTotalRuns) || 0;
|
||||
if (updates.autoRunAttemptRun !== undefined) autoRunAttemptRun = Number(updates.autoRunAttemptRun) || 0;
|
||||
if (updates.autoRunSessionId !== undefined) autoRunSessionId = normalizeAutoRunSessionId(updates.autoRunSessionId);
|
||||
},
|
||||
},
|
||||
setState,
|
||||
sleepWithStop,
|
||||
throwIfAutoRunSessionStopped: (sessionId) => throwIfAutoRunSessionStopped(sessionId),
|
||||
waitForRunningStepsToFinish,
|
||||
throwIfStopped: () => throwIfStopped(),
|
||||
chrome,
|
||||
@@ -5558,6 +5665,7 @@ async function resumeAutoRun() {
|
||||
|
||||
await addLog('检测到自动流程暂停上下文已丢失,正在从当前进度恢复自动运行...', 'warn');
|
||||
startAutoRunLoop(totalRuns, {
|
||||
autoRunSessionId: normalizeAutoRunSessionId(state.autoRunSessionId),
|
||||
autoRunSkipFailures: Boolean(state.autoRunSkipFailures),
|
||||
mode: 'continue',
|
||||
resumeCurrentRun: currentRun,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
broadcastStopToContentScripts,
|
||||
cancelPendingCommands,
|
||||
clearStopRequest,
|
||||
createAutoRunSessionId,
|
||||
getAutoRunStatusPayload,
|
||||
getErrorMessage,
|
||||
getFirstUnfinishedStep,
|
||||
@@ -30,6 +31,7 @@
|
||||
runtime,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
throwIfAutoRunSessionStopped,
|
||||
waitForRunningStepsToFinish,
|
||||
} = deps;
|
||||
|
||||
@@ -175,6 +177,7 @@
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun: currentRuntime.autoRunAttemptRun,
|
||||
autoRunSessionId: currentRuntime.autoRunSessionId,
|
||||
autoRunSkipFailures,
|
||||
roundSummaries,
|
||||
countdownTitle: '线程间隔中',
|
||||
@@ -206,6 +209,7 @@
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun: nextAttemptRun,
|
||||
autoRunSessionId: runtime.get().autoRunSessionId,
|
||||
autoRunSkipFailures,
|
||||
roundSummaries,
|
||||
countdownTitle: '线程间隔中',
|
||||
@@ -225,12 +229,14 @@
|
||||
await addLog(`自动运行异常终止:${getErrorMessage(error) || '未知错误'}`, 'error');
|
||||
}
|
||||
|
||||
runtime.set({ autoRunActive: false });
|
||||
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,
|
||||
});
|
||||
@@ -250,12 +256,22 @@
|
||||
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();
|
||||
|
||||
@@ -293,12 +309,14 @@
|
||||
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,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -360,9 +378,10 @@
|
||||
cloudflareDomain: prevState.cloudflareDomain,
|
||||
cloudflareDomains: prevState.cloudflareDomains,
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
autoRunSessionId: sessionId,
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun }),
|
||||
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun, sessionId }),
|
||||
};
|
||||
await resetState();
|
||||
await setState(keepSettings);
|
||||
@@ -370,9 +389,10 @@
|
||||
await sleepWithStop(500);
|
||||
} else {
|
||||
await setState({
|
||||
autoRunSessionId: sessionId,
|
||||
autoRunSkipFailures,
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun }),
|
||||
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun, sessionId }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -397,11 +417,12 @@
|
||||
};
|
||||
|
||||
try {
|
||||
deps.throwIfStopped();
|
||||
throwIfAutoRunSessionStopped(sessionId);
|
||||
await broadcastAutoRunStatus('running', {
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
await runAutoSequenceFromStep(startStep, {
|
||||
@@ -428,6 +449,7 @@
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun,
|
||||
sessionId: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -453,6 +475,7 @@
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun,
|
||||
sessionId,
|
||||
});
|
||||
forceFreshTabsNextRun = true;
|
||||
await addLog(
|
||||
@@ -470,6 +493,7 @@
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun,
|
||||
sessionId: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -493,6 +517,7 @@
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun,
|
||||
sessionId: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -518,6 +543,7 @@
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun,
|
||||
sessionId: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -559,6 +585,7 @@
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun: runtime.get().autoRunAttemptRun,
|
||||
sessionId: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -584,6 +611,7 @@
|
||||
currentRun: finalRuntime.autoRunCurrentRun,
|
||||
totalRuns: finalRuntime.autoRunTotalRuns,
|
||||
attemptRun: finalRuntime.autoRunAttemptRun,
|
||||
sessionId: 0,
|
||||
});
|
||||
} else {
|
||||
await addLog(`=== 全部 ${finalRuntime.autoRunTotalRuns} 轮已执行完成,成功 ${successfulRuns} 轮 ===`, 'ok');
|
||||
@@ -591,11 +619,13 @@
|
||||
currentRun: finalRuntime.autoRunTotalRuns,
|
||||
totalRuns: finalRuntime.autoRunTotalRuns,
|
||||
attemptRun: finalRuntime.autoRunAttemptRun,
|
||||
sessionId: 0,
|
||||
});
|
||||
}
|
||||
runtime.set({ autoRunActive: false });
|
||||
runtime.set({ autoRunActive: false, autoRunSessionId: 0 });
|
||||
const afterRuntime = runtime.get();
|
||||
await setState({
|
||||
autoRunSessionId: 0,
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
autoRunTimerPlan: null,
|
||||
scheduledAutoRunPlan: null,
|
||||
@@ -603,6 +633,7 @@
|
||||
currentRun: deps.getStopRequested() || stoppedEarly ? afterRuntime.autoRunCurrentRun : afterRuntime.autoRunTotalRuns,
|
||||
totalRuns: afterRuntime.autoRunTotalRuns,
|
||||
attemptRun: afterRuntime.autoRunAttemptRun,
|
||||
sessionId: 0,
|
||||
}),
|
||||
});
|
||||
clearStopRequest();
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
autoRunCurrentRun: payload.currentRun ?? 0,
|
||||
autoRunTotalRuns: payload.totalRuns ?? 1,
|
||||
autoRunAttemptRun: payload.attemptRun ?? 0,
|
||||
autoRunSessionId: Math.max(0, Math.floor(Number(payload.sessionId ?? payload.autoRunSessionId) || 0)),
|
||||
scheduledAutoRunAt: Number.isFinite(Number(payload.scheduledAt)) ? Number(payload.scheduledAt) : null,
|
||||
autoRunCountdownAt: Number.isFinite(Number(payload.countdownAt)) ? Number(payload.countdownAt) : null,
|
||||
autoRunCountdownTitle: payload.countdownTitle === undefined ? '' : String(payload.countdownTitle || ''),
|
||||
|
||||
+72
-41
@@ -12,12 +12,73 @@
|
||||
LOG_PREFIX,
|
||||
matchesSourceUrlFamily,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
STOP_ERROR_MESSAGE,
|
||||
throwIfStopped,
|
||||
} = deps;
|
||||
|
||||
const pendingCommands = new Map();
|
||||
|
||||
async function sleepOrStop(ms) {
|
||||
if (typeof sleepWithStop === 'function') {
|
||||
await sleepWithStop(ms);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < ms) {
|
||||
throwIfStopped();
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.min(100, ms - (Date.now() - start))));
|
||||
}
|
||||
}
|
||||
|
||||
function waitForTabUpdateComplete(tabId, timeoutMs = 30000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
let stopTimer = null;
|
||||
|
||||
const cleanup = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
clearTimeout(stopTimer);
|
||||
chrome.tabs.onUpdated.removeListener(listener);
|
||||
};
|
||||
|
||||
const resolveSafely = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
|
||||
const rejectSafely = (error) => {
|
||||
cleanup();
|
||||
reject(error);
|
||||
};
|
||||
|
||||
const listener = (updatedTabId, info) => {
|
||||
if (updatedTabId === tabId && info.status === 'complete') {
|
||||
resolveSafely();
|
||||
}
|
||||
};
|
||||
|
||||
const timer = setTimeout(resolveSafely, timeoutMs);
|
||||
chrome.tabs.onUpdated.addListener(listener);
|
||||
|
||||
const pollStop = () => {
|
||||
if (settled) return;
|
||||
try {
|
||||
throwIfStopped();
|
||||
} catch (error) {
|
||||
rejectSafely(error);
|
||||
return;
|
||||
}
|
||||
stopTimer = setTimeout(pollStop, 100);
|
||||
};
|
||||
|
||||
pollStop();
|
||||
});
|
||||
}
|
||||
|
||||
async function getTabRegistry() {
|
||||
const state = await getState();
|
||||
return state.tabRegistry || {};
|
||||
@@ -179,7 +240,7 @@
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
||||
await sleepOrStop(retryDelayMs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -197,7 +258,7 @@
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
||||
await sleepOrStop(retryDelayMs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -215,7 +276,7 @@
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
||||
await sleepOrStop(retryDelayMs);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -296,7 +357,7 @@
|
||||
logged = true;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
||||
await sleepOrStop(retryDelayMs);
|
||||
}
|
||||
|
||||
throw lastError || new Error(`${getSourceLabel(source)} 内容脚本长时间未就绪。`);
|
||||
@@ -418,17 +479,7 @@
|
||||
if (registry[source]) registry[source].ready = false;
|
||||
await setState({ tabRegistry: registry });
|
||||
await chrome.tabs.reload(tabId);
|
||||
await new Promise((resolve) => {
|
||||
const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
|
||||
const listener = (tid, info) => {
|
||||
if (tid === tabId && info.status === 'complete') {
|
||||
chrome.tabs.onUpdated.removeListener(listener);
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
chrome.tabs.onUpdated.addListener(listener);
|
||||
});
|
||||
await waitForTabUpdateComplete(tabId);
|
||||
}
|
||||
|
||||
if (options.inject) {
|
||||
@@ -447,7 +498,7 @@
|
||||
target: { tabId },
|
||||
files: options.inject,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await sleepOrStop(500);
|
||||
}
|
||||
|
||||
await rememberSourceLastUrl(source, url);
|
||||
@@ -458,17 +509,7 @@
|
||||
await setState({ tabRegistry: registry });
|
||||
await chrome.tabs.update(tabId, { url, active: true });
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
|
||||
const listener = (tid, info) => {
|
||||
if (tid === tabId && info.status === 'complete') {
|
||||
chrome.tabs.onUpdated.removeListener(listener);
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
chrome.tabs.onUpdated.addListener(listener);
|
||||
});
|
||||
await waitForTabUpdateComplete(tabId);
|
||||
|
||||
if (options.inject) {
|
||||
if (options.injectSource) {
|
||||
@@ -486,7 +527,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await sleepOrStop(500);
|
||||
await rememberSourceLastUrl(source, url);
|
||||
return tabId;
|
||||
}
|
||||
@@ -495,17 +536,7 @@
|
||||
const tab = await chrome.tabs.create({ url, active: true });
|
||||
|
||||
if (options.inject) {
|
||||
await new Promise((resolve) => {
|
||||
const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
|
||||
const listener = (tabId, info) => {
|
||||
if (tabId === tab.id && info.status === 'complete') {
|
||||
chrome.tabs.onUpdated.removeListener(listener);
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
chrome.tabs.onUpdated.addListener(listener);
|
||||
});
|
||||
await waitForTabUpdateComplete(tab.id);
|
||||
if (options.injectSource) {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
@@ -580,7 +611,7 @@
|
||||
logged = true;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
||||
await sleepOrStop(retryDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,7 +659,7 @@
|
||||
injectSource: mail.injectSource,
|
||||
reloadIfSameUrl: true,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
await sleepOrStop(800);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -523,9 +523,12 @@ async function fillSignupEmailAndContinue(email, step) {
|
||||
log(`步骤 ${step}:邮箱已准备提交,正在前往密码页...`);
|
||||
window.setTimeout(() => {
|
||||
try {
|
||||
throwIfStopped();
|
||||
simulateClick(continueButton);
|
||||
} catch (error) {
|
||||
console.error('[MultiPage:signup-page] deferred signup email submit failed:', error?.message || error);
|
||||
if (!isStopError(error)) {
|
||||
console.error('[MultiPage:signup-page] deferred signup email submit failed:', error?.message || error);
|
||||
}
|
||||
}
|
||||
}, 120);
|
||||
|
||||
@@ -601,12 +604,15 @@ async function step3_fillEmailPassword(payload) {
|
||||
if (submitBtn) {
|
||||
window.setTimeout(async () => {
|
||||
try {
|
||||
throwIfStopped();
|
||||
await sleep(500);
|
||||
await humanPause(500, 1300);
|
||||
simulateClick(submitBtn);
|
||||
log('步骤 3:表单已提交');
|
||||
} catch (error) {
|
||||
console.error('[MultiPage:signup-page] deferred step 3 submit failed:', error?.message || error);
|
||||
if (!isStopError(error)) {
|
||||
console.error('[MultiPage:signup-page] deferred step 3 submit failed:', error?.message || error);
|
||||
}
|
||||
}
|
||||
}, 120);
|
||||
}
|
||||
@@ -2344,4 +2350,3 @@ async function step5_fillNameBirthday(payload) {
|
||||
log('步骤 5:已点击“完成帐户创建”,当前步骤直接完成,不再等待页面结果。');
|
||||
return completionPayload;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ function extractFunction(source, name) {
|
||||
|
||||
const helperBundle = [
|
||||
extractFunction(helperSource, 'clearStopRequest'),
|
||||
extractFunction(helperSource, 'normalizeAutoRunSessionId'),
|
||||
extractFunction(helperSource, 'throwIfStopped'),
|
||||
extractFunction(helperSource, 'isStopError'),
|
||||
extractFunction(helperSource, 'isStepDoneStatus'),
|
||||
@@ -88,6 +89,8 @@ const DEFAULT_STATE = {
|
||||
|
||||
let stopRequested = false;
|
||||
let runCalls = 0;
|
||||
let autoRunSessionId = 0;
|
||||
let autoRunSessionSeed = 1000;
|
||||
|
||||
const logs = [];
|
||||
const broadcasts = [];
|
||||
@@ -191,6 +194,17 @@ async function persistAutoRunTimerPlan() {}
|
||||
async function launchAutoRunTimerPlan() { return false; }
|
||||
function getPendingAutoRunTimerPlan() { return null; }
|
||||
function getErrorMessage(error) { return error?.message || String(error || ''); }
|
||||
function createAutoRunSessionId() {
|
||||
autoRunSessionSeed += 1;
|
||||
autoRunSessionId = autoRunSessionSeed;
|
||||
return autoRunSessionId;
|
||||
}
|
||||
function throwIfAutoRunSessionStopped(sessionId) {
|
||||
if (sessionId && sessionId !== autoRunSessionId) {
|
||||
throw new Error(STOP_ERROR_MESSAGE);
|
||||
}
|
||||
throwIfStopped();
|
||||
}
|
||||
const chrome = {
|
||||
runtime: {
|
||||
sendMessage() {
|
||||
@@ -242,6 +256,7 @@ const runtime = {
|
||||
autoRunCurrentRun: 0,
|
||||
autoRunTotalRuns: 1,
|
||||
autoRunAttemptRun: 0,
|
||||
autoRunSessionId: 0,
|
||||
},
|
||||
get() {
|
||||
return { ...this.state };
|
||||
@@ -261,6 +276,7 @@ const controller = self.MultiPageBackgroundAutoRunController.createAutoRunContro
|
||||
broadcastStopToContentScripts,
|
||||
cancelPendingCommands,
|
||||
clearStopRequest,
|
||||
createAutoRunSessionId,
|
||||
getAutoRunStatusPayload,
|
||||
getErrorMessage,
|
||||
getFirstUnfinishedStep,
|
||||
@@ -279,6 +295,7 @@ const controller = self.MultiPageBackgroundAutoRunController.createAutoRunContro
|
||||
runtime,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
throwIfAutoRunSessionStopped,
|
||||
waitForRunningStepsToFinish,
|
||||
throwIfStopped,
|
||||
chrome,
|
||||
@@ -309,6 +326,7 @@ return {
|
||||
assert.strictEqual(snapshot.currentState.autoRunPhase, 'complete', 'both runs should complete after reset');
|
||||
assert.strictEqual(snapshot.currentState.autoRunCurrentRun, 2, 'final run index should be recorded');
|
||||
assert.strictEqual(snapshot.autoRunActive, false, 'auto-run should exit active state after completion');
|
||||
assert.strictEqual(snapshot.currentState.autoRunSessionId, 0, 'session id should be cleared after completion');
|
||||
assert.strictEqual(snapshot.currentState.gmailBaseEmail, 'demo@gmail.com', 'gmail base email should survive fresh-attempt reset');
|
||||
assert.strictEqual(snapshot.currentState.mail2925BaseEmail, 'demo@2925.com', '2925 base email should survive fresh-attempt reset');
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const helperSource = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
function extractFunction(source, name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
const helperBundle = [
|
||||
extractFunction(helperSource, 'normalizeRunCount'),
|
||||
extractFunction(helperSource, 'normalizeAutoRunTimerKind'),
|
||||
extractFunction(helperSource, 'normalizeAutoRunSessionId'),
|
||||
extractFunction(helperSource, 'isCurrentAutoRunSessionId'),
|
||||
extractFunction(helperSource, 'normalizeAutoRunTimerPlan'),
|
||||
extractFunction(helperSource, 'getAutoRunTimerResumeOptions'),
|
||||
extractFunction(helperSource, 'launchAutoRunTimerPlan'),
|
||||
].join('\n');
|
||||
|
||||
test('launchAutoRunTimerPlan ignores stale timer plans after stop invalidates the session', async () => {
|
||||
const api = new Function(`
|
||||
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
|
||||
const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds';
|
||||
const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry';
|
||||
const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
|
||||
|
||||
let autoRunTimerLaunching = false;
|
||||
let autoRunActive = false;
|
||||
let autoRunCurrentRun = 0;
|
||||
let autoRunTotalRuns = 1;
|
||||
let autoRunAttemptRun = 0;
|
||||
let autoRunSessionId = 0;
|
||||
|
||||
const state = {
|
||||
autoRunDelayEnabled: false,
|
||||
autoRunTimerPlan: {
|
||||
kind: AUTO_RUN_TIMER_KIND_SCHEDULED_START,
|
||||
fireAt: Date.now() + 60_000,
|
||||
totalRuns: 2,
|
||||
autoRunSkipFailures: false,
|
||||
autoRunSessionId: 42,
|
||||
countdownTitle: '已计划自动运行',
|
||||
countdownNote: '等待启动',
|
||||
},
|
||||
};
|
||||
|
||||
let startCalls = 0;
|
||||
let clearStopCalls = 0;
|
||||
let clearAlarmCalls = 0;
|
||||
|
||||
async function getState() {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
function getPendingAutoRunTimerPlan() {
|
||||
return state.autoRunTimerPlan;
|
||||
}
|
||||
|
||||
async function clearAutoRunTimerAlarm() {
|
||||
clearAlarmCalls += 1;
|
||||
}
|
||||
|
||||
async function broadcastAutoRunStatus() {}
|
||||
async function addLog() {}
|
||||
async function setAutoRunDelayEnabledState() {}
|
||||
function serializeAutoRunRoundSummaries(totalRuns, summaries = []) {
|
||||
return Array.isArray(summaries) ? summaries : [];
|
||||
}
|
||||
function clearStopRequest() {
|
||||
clearStopCalls += 1;
|
||||
}
|
||||
function startAutoRunLoop() {
|
||||
startCalls += 1;
|
||||
}
|
||||
|
||||
${helperBundle}
|
||||
|
||||
return {
|
||||
launchAutoRunTimerPlan,
|
||||
snapshot() {
|
||||
return {
|
||||
startCalls,
|
||||
clearStopCalls,
|
||||
clearAlarmCalls,
|
||||
autoRunCurrentRun,
|
||||
autoRunTotalRuns,
|
||||
autoRunAttemptRun,
|
||||
};
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const started = await api.launchAutoRunTimerPlan('alarm');
|
||||
const snapshot = api.snapshot();
|
||||
|
||||
assert.equal(started, false);
|
||||
assert.equal(snapshot.startCalls, 0, 'stale timer plan should not restart auto-run');
|
||||
assert.equal(snapshot.clearStopCalls, 0, 'stale timer plan should not clear the stop flag for a cancelled run');
|
||||
assert.equal(snapshot.clearAlarmCalls, 0, 'stale timer plan should not clear a potentially newer alarm');
|
||||
assert.equal(snapshot.autoRunCurrentRun, 0);
|
||||
assert.equal(snapshot.autoRunTotalRuns, 1);
|
||||
assert.equal(snapshot.autoRunAttemptRun, 0);
|
||||
});
|
||||
@@ -47,6 +47,7 @@ test('tab runtime waitForTabComplete waits until tab status becomes complete', a
|
||||
registerTab: async () => {},
|
||||
setState: async () => {},
|
||||
shouldBypassStep9ForLocalCpa: () => false,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const result = await runtime.waitForTabComplete(9, {
|
||||
@@ -57,3 +58,43 @@ test('tab runtime waitForTabComplete waits until tab status becomes complete', a
|
||||
assert.equal(result?.status, 'complete');
|
||||
assert.equal(getCalls, 3);
|
||||
});
|
||||
|
||||
test('tab runtime waitForTabComplete aborts promptly when stop is requested', async () => {
|
||||
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope);
|
||||
|
||||
let throwCalls = 0;
|
||||
const runtime = api.createTabRuntime({
|
||||
LOG_PREFIX: '[test]',
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
get: async () => ({
|
||||
id: 9,
|
||||
url: 'https://example.com',
|
||||
status: 'loading',
|
||||
}),
|
||||
query: async () => [],
|
||||
},
|
||||
},
|
||||
getSourceLabel: (sourceName) => sourceName || 'unknown',
|
||||
getState: async () => ({ tabRegistry: {}, sourceLastUrls: {} }),
|
||||
matchesSourceUrlFamily: () => false,
|
||||
setState: async () => {},
|
||||
throwIfStopped: () => {
|
||||
throwCalls += 1;
|
||||
if (throwCalls >= 2) {
|
||||
throw new Error('Flow stopped.');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
runtime.waitForTabComplete(9, {
|
||||
timeoutMs: 2000,
|
||||
retryDelayMs: 1,
|
||||
}),
|
||||
/Flow stopped\./
|
||||
);
|
||||
});
|
||||
|
||||
@@ -98,6 +98,10 @@ function fillInput(input, value) {
|
||||
|
||||
async function humanPause() {}
|
||||
async function sleep() {}
|
||||
function throwIfStopped() {}
|
||||
function isStopError() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function log(message, level = 'info') {
|
||||
logs.push({ message, level });
|
||||
|
||||
@@ -52,6 +52,8 @@ function extractFunction(source, name) {
|
||||
}
|
||||
|
||||
const helperBundle = [
|
||||
extractFunction(helperSource, 'normalizeAutoRunSessionId'),
|
||||
extractFunction(helperSource, 'clearCurrentAutoRunSessionId'),
|
||||
extractFunction(helperSource, 'throwIfStopped'),
|
||||
extractFunction(helperSource, 'cleanupStep8NavigationListeners'),
|
||||
extractFunction(helperSource, 'rejectPendingStep8'),
|
||||
@@ -71,6 +73,7 @@ let autoRunActive = true;
|
||||
let autoRunCurrentRun = 2;
|
||||
let autoRunTotalRuns = 3;
|
||||
let autoRunAttemptRun = 4;
|
||||
let autoRunSessionId = 99;
|
||||
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
|
||||
const STEP8_CLICK_RETRY_DELAY_MS = 500;
|
||||
const STEP8_MAX_ROUNDS = 5;
|
||||
@@ -257,6 +260,7 @@ return {
|
||||
sentMessages,
|
||||
clickCount,
|
||||
autoRunActive,
|
||||
autoRunSessionId,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -288,6 +292,7 @@ return {
|
||||
assert.strictEqual(state.webNavCommittedListener, null, 'Stop 后 onCommitted 引用应为空');
|
||||
assert.strictEqual(state.step8TabUpdatedListener, null, 'Stop 后 tabs.onUpdated 引用应为空');
|
||||
assert.strictEqual(state.step8PendingReject, null, 'Stop 后不应保留 Step 8 挂起 reject');
|
||||
assert.strictEqual(state.autoRunSessionId, 0, 'Stop 后自动运行 session 应失效');
|
||||
|
||||
console.log('step8 stop cleanup tests passed');
|
||||
})().catch((error) => {
|
||||
|
||||
+13
-8
@@ -121,6 +121,7 @@
|
||||
- 当前邮箱 / 密码
|
||||
- localhost 回调地址
|
||||
- 自动运行轮次信息
|
||||
- 当前自动运行 session 标识 `autoRunSessionId`
|
||||
- 标签注册表
|
||||
- 最近打开的来源地址
|
||||
- LuckMail 当前运行时选择
|
||||
@@ -179,6 +180,7 @@
|
||||
|
||||
- 页面切换导致脚本暂时失联时,后台不会立刻误判彻底失败
|
||||
- 邮箱页或注册页能在注入恢复后继续执行
|
||||
- 用户点击 Stop 后,标签完成等待、注入后的短暂延迟和内容脚本重试等待会尽快中断,不会因为底层轮询还在 sleep 而继续往下恢复
|
||||
|
||||
## 6. 手动步骤完整链路
|
||||
|
||||
@@ -230,7 +232,8 @@
|
||||
4. 让内容脚本填写密码
|
||||
5. 内容脚本先上报 Step 3 完成信号
|
||||
6. 上报完成后再异步点击提交,避免页面跳转打断响应通道
|
||||
7. 后台在真正确认 Step 3 完成前,会额外检查提交后是否切换页面;如果出现认证页 `Try again / 重试` 页面,会先自动点击 `重试` 恢复,再继续后续链路
|
||||
7. 延迟提交真正触发前会再次检查 Stop 状态,避免用户已停止时页面仍继续自动提交
|
||||
8. 后台在真正确认 Step 3 完成前,会额外检查提交后是否切换页面;如果出现认证页 `Try again / 重试` 页面,会先自动点击 `重试` 恢复,再继续后续链路
|
||||
|
||||
### Step 4 / Step 7
|
||||
|
||||
@@ -440,20 +443,22 @@
|
||||
流程:
|
||||
|
||||
1. 读取总轮数与模式
|
||||
2. 计算是否从中断点继续
|
||||
3. 每轮执行前重置必要运行态
|
||||
4. 执行 `runAutoSequenceFromStep`
|
||||
2. 为本轮自动流程分配唯一 `autoRunSessionId`
|
||||
3. 计算是否从中断点继续
|
||||
4. 每轮执行前重置必要运行态
|
||||
5. 执行 `runAutoSequenceFromStep`
|
||||
- 步骤 7 内部仍保留登录态恢复的有限重试
|
||||
- 一旦进入步骤 7~10,遇到普通报错且认证流程未进入 `add-phone`,则自动回到步骤 7 无限重开
|
||||
- 如果是手动停止,则立即退出自动流程,不会再触发“回到步骤 7 重开”
|
||||
5. 如果失败,根据设置决定:
|
||||
6. 如果失败,根据设置决定:
|
||||
- 立即停止
|
||||
- 当前轮重试
|
||||
- 下一轮继续
|
||||
6. 当前轮最终失败时,写入邮箱记录,并在自动重试链路中累计重试次数
|
||||
7. 当前轮最终失败时,写入邮箱记录,并在自动重试链路中累计重试次数
|
||||
- 手动停止与自动停止会写入“停止”状态(若后续同邮箱成功/失败,会被覆盖为最新状态)
|
||||
7. 如果配置了线程间隔,则挂计时计划
|
||||
8. 所有轮次结束后输出汇总
|
||||
8. 如果配置了线程间隔,则挂计时计划;计时计划会带上当前 `autoRunSessionId`
|
||||
9. 旧 timer / 旧 alarm / 旧恢复入口只有在 session 仍有效时才允许恢复执行;Stop 会立即使当前 session 失效,防止“停止后旧倒计时又把流程重新拉起”
|
||||
10. 所有轮次结束后输出汇总,并清空当前 session 标识
|
||||
|
||||
## 9. 新增功能时最容易漏掉的地方
|
||||
|
||||
|
||||
+6
-5
@@ -41,14 +41,14 @@
|
||||
## `background/`
|
||||
|
||||
- `background/account-run-history.js`:邮箱记录模块,负责以邮箱为主键维护最新记录(同邮箱后续状态覆盖),在步骤 2 设定邮箱后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,支持清理记录,并在启用独立本地同步配置后把完整快照同步到本地 helper。
|
||||
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;fresh-attempt reset 时会额外保留 `gmailBaseEmail` 与 `mail2925BaseEmail`,避免自动流程重置后丢失别名基邮箱配置。
|
||||
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 `gmailBaseEmail` 与 `mail2925BaseEmail`,避免自动流程重置后丢失别名基邮箱配置。
|
||||
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口。
|
||||
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层。
|
||||
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息。
|
||||
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API 地址、步骤跳转相关判断。
|
||||
- `background/panel-bridge.js`:CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。
|
||||
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail / 2925 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成。
|
||||
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列。
|
||||
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
|
||||
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数。
|
||||
|
||||
## `background/steps/`
|
||||
@@ -76,7 +76,7 @@
|
||||
- `content/mail-163.js`:163 / 163 VIP 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。
|
||||
- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱收件轮询与验证码匹配。
|
||||
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
|
||||
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行。
|
||||
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击。
|
||||
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;当前承接步骤 10。
|
||||
- `content/utils.js`:内容脚本公共工具层,负责日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击。
|
||||
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做步骤 10 判定。
|
||||
@@ -123,6 +123,7 @@
|
||||
- `tests/activation-utils.test.js`:测试内容脚本激活策略与平台回调可恢复错误判断。
|
||||
- `tests/auth-page-recovery.test.js`:测试认证页共享恢复层的重试页识别与恢复行为。
|
||||
- `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文,并补充 `gmailBaseEmail` / `mail2925BaseEmail` 在 fresh reset 后仍被保留的回归验证。
|
||||
- `tests/auto-run-timer-session-guard.test.js`:测试自动运行的旧计时计划在 Stop 使 session 失效后,不能再把已停止的自动流程重新拉起。
|
||||
- `tests/auto-run-step6-restart.test.js`:测试自动运行在后半段授权链路遇错时会回到步骤 7 重开,并在命中 add-phone 时停止重开。
|
||||
- `tests/auto-step-random-delay.test.js`:测试自动运行步间延迟与旧配置键兼容解析。
|
||||
- `tests/background-auto-run-module.test.js`:测试自动运行控制器模块已接入且导出工厂。
|
||||
@@ -145,7 +146,7 @@
|
||||
- `tests/background-step6-retry-limit.test.js`:测试步骤 6 的 Cookie 清理,以及步骤 7 的有限重试上限。
|
||||
- `tests/background-step7-recovery.test.js`:测试步骤 8 获取登录验证码后直接提交(不再回放步骤 7),并为 2925 关闭重发间隔。
|
||||
- `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。
|
||||
- `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂。
|
||||
- `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成时的 Stop 中断行为。
|
||||
- `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。
|
||||
- `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑。
|
||||
- `tests/cloudflare-temp-email-utils.test.js`:测试 Cloudflare Temp Email 工具层的 URL、域名、邮件解析逻辑。
|
||||
@@ -165,7 +166,7 @@
|
||||
- `tests/signup-entry-diagnostics.test.js`:测试注册入口诊断快照输出。
|
||||
- `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
|
||||
- `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。
|
||||
- `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报。
|
||||
- `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报,并保留延迟提交回调的可执行性验证。
|
||||
- `tests/step5-direct-complete.test.js`:测试步骤 5 在资料页点击提交后立即完成当前步骤。
|
||||
- `tests/step6-login-state.test.js`:测试 OAuth 登录状态识别逻辑,当前对应步骤 7 链路。
|
||||
- `tests/step6-timeout-recovery.test.js`:测试 OAuth 登录超时报错页的可恢复结果构造,当前对应步骤 7 链路。
|
||||
|
||||
Reference in New Issue
Block a user