feat: add support for running status in account run history and UI

- Enhanced account run history to include 'running' status with appropriate translations and summaries.
- Updated logging status to handle new error message format.
- Introduced new properties for task progress in create-plus-checkout step.
- Modified fill-plus-checkout step to handle additional error details.
- Updated account records manager to display running status and summary correctly.
- Added CSS styles for running status in account records.
- Implemented logic to refresh account run history based on auto-run state changes.
- Added tests for running status and idle log restart functionality in auto-run steps.
- Improved account records manager tests to validate running state display and failure details.
This commit is contained in:
QLHazyCoder
2026-05-10 18:36:34 +08:00
parent 6bd00dbd5b
commit 48d3d5fc12
14 changed files with 727 additions and 35 deletions
+216 -11
View File
@@ -667,6 +667,9 @@ const PERSISTED_SETTING_DEFAULTS = {
gopayHelperFailureStage: '',
gopayHelperFailureDetail: '',
gopayHelperTaskPayload: null,
gopayHelperTaskProgressSignature: '',
gopayHelperTaskProgressAt: 0,
gopayHelperTaskProgressTaskId: '',
gopayHelperBalance: '',
gopayHelperBalancePayload: null,
gopayHelperBalanceUpdatedAt: 0,
@@ -836,6 +839,9 @@ const DEFAULT_STATE = {
gopayHelperFailureDetail: '',
gopayHelperTaskPayload: null,
gopayHelperOrderCreatedAt: 0,
gopayHelperTaskProgressSignature: '',
gopayHelperTaskProgressAt: 0,
gopayHelperTaskProgressTaskId: '',
gopayHelperPinPayload: null,
gopayHelperResolvedOtp: '',
gopayHelperOtpRequestId: '',
@@ -2939,7 +2945,10 @@ async function setEmailStateSilently(email) {
async function setEmailState(email) {
await setEmailStateSilently(email);
if (email) {
await appendManualAccountRunRecordIfNeeded('step2_stopped', null, '步骤 2 已使用邮箱,流程尚未完成。');
const latestState = await getState();
const recordStatus = shouldMarkAccountRunRecordRunning(latestState) ? 'running' : 'step2_stopped';
const recordReason = recordStatus === 'running' ? '正在运行' : '步骤 2 已使用邮箱,流程尚未完成。';
await appendManualAccountRunRecordIfNeeded(recordStatus, latestState, recordReason);
await resumeAutoRunIfWaitingForEmail();
}
}
@@ -2979,10 +2988,19 @@ async function setSignupPhoneStateSilently(phoneNumber) {
async function setSignupPhoneState(phoneNumber) {
await setSignupPhoneStateSilently(phoneNumber);
if (String(phoneNumber || '').trim()) {
await appendManualAccountRunRecordIfNeeded('step2_stopped', null, '步骤 2 已使用手机号,流程尚未完成。');
const latestState = await getState();
const recordStatus = shouldMarkAccountRunRecordRunning(latestState) ? 'running' : 'step2_stopped';
const recordReason = recordStatus === 'running' ? '正在运行' : '步骤 2 已使用手机号,流程尚未完成。';
await appendManualAccountRunRecordIfNeeded(recordStatus, latestState, recordReason);
}
}
function shouldMarkAccountRunRecordRunning(state = {}) {
const phase = String(state.autoRunPhase || '').trim().toLowerCase();
return Boolean(state.autoRunning)
&& ['running', 'waiting_step', 'waiting_email', 'retrying'].includes(phase);
}
async function setPasswordState(password) {
await setState({ password });
broadcastDataUpdate({ password });
@@ -7523,7 +7541,8 @@ function getErrorMessage(error) {
return loggingStatus.getErrorMessage(error);
}
return String(typeof error === 'string' ? error : error?.message || '')
.replace(/^GPC_TASK_ENDED::/i, '');
.replace(/^GPC_TASK_ENDED::/i, '')
.replace(/^AUTO_RUN_STEP_IDLE_RESTART::/i, '');
}
function isCloudflareSecurityBlockedError(error) {
@@ -7843,6 +7862,9 @@ function getDownstreamStateResets(step, state = {}) {
gopayHelperFailureDetail: '',
gopayHelperTaskPayload: null,
gopayHelperOrderCreatedAt: 0,
gopayHelperTaskProgressSignature: '',
gopayHelperTaskProgressAt: 0,
gopayHelperTaskProgressTaskId: '',
gopayHelperPinPayload: null,
gopayHelperResolvedOtp: '',
gopayHelperOtpRequestId: '',
@@ -8839,6 +8861,10 @@ async function handleStepData(step, payload) {
const stepWaiters = new Map();
let resumeWaiter = null;
const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000;
const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = 5 * 60 * 1000;
const AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS = 5000;
const AUTO_RUN_STEP_IDLE_RESTART_MAX_ATTEMPTS = 3;
const AUTO_RUN_STEP_IDLE_RESTART_ERROR_PREFIX = 'AUTO_RUN_STEP_IDLE_RESTART::';
const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8, 9]);
const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 10, 12]);
const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([
@@ -9078,6 +9104,120 @@ async function executeStepViaCompletionSignal(step, timeoutMs = 0) {
throw completionResult.error;
}
function getLatestLogTimestamp(logs = [], fallback = 0) {
if (!Array.isArray(logs) || !logs.length) {
return Number.isFinite(Number(fallback)) ? Number(fallback) : 0;
}
return logs.reduce((latest, entry) => {
const timestamp = Number(entry?.timestamp);
return Number.isFinite(timestamp) && timestamp > latest ? timestamp : latest;
}, Number.isFinite(Number(fallback)) ? Number(fallback) : 0);
}
function buildAutoRunStepIdleRestartError(step, idleMs = AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS) {
const seconds = Math.max(1, Math.round((Number(idleMs) || AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS) / 1000));
const error = new Error(`${AUTO_RUN_STEP_IDLE_RESTART_ERROR_PREFIX}步骤 ${step} 已连续 ${seconds} 秒没有新日志,准备重新开始当前步骤。`);
error.autoRunStepIdleRestart = true;
error.failedStep = Math.floor(Number(step) || 0);
return error;
}
function isAutoRunStepIdleRestartError(error) {
const message = String(typeof error === 'string' ? error : error?.message || '');
return Boolean(error?.autoRunStepIdleRestart) || message.startsWith(AUTO_RUN_STEP_IDLE_RESTART_ERROR_PREFIX);
}
function startAutoRunStepIdleLogWatchdog(step, options = {}) {
const idleTimeoutMs = Math.max(1000, Math.floor(Number(options.idleTimeoutMs) || AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS));
const checkIntervalMs = Math.max(250, Math.min(idleTimeoutMs, Math.floor(Number(options.checkIntervalMs) || AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS)));
let cancelled = false;
let timer = null;
let lastActivityAt = Date.now();
const promise = new Promise((_, reject) => {
const schedule = () => {
if (cancelled) {
return;
}
const idleForMs = Math.max(0, Date.now() - lastActivityAt);
const delayMs = Math.max(50, Math.min(checkIntervalMs, idleTimeoutMs - idleForMs));
timer = setTimeout(check, delayMs);
};
const check = async () => {
if (cancelled) {
return;
}
try {
const state = await getState();
if (state?.plusManualConfirmationPending) {
lastActivityAt = Date.now();
schedule();
return;
}
const latestLogAt = getLatestLogTimestamp(state?.logs || [], lastActivityAt);
if (latestLogAt > lastActivityAt) {
lastActivityAt = latestLogAt;
}
const idleForMs = Date.now() - lastActivityAt;
if (idleForMs >= idleTimeoutMs) {
reject(buildAutoRunStepIdleRestartError(step, idleForMs));
return;
}
} catch (_err) {
// Watchdog read failures should not break the real step; retry the check.
}
schedule();
};
schedule();
});
return {
promise,
cancel() {
cancelled = true;
if (timer) {
clearTimeout(timer);
}
},
};
}
async function runAutoStepActionWithIdleLogWatchdog(step, action, options = {}) {
const executionPromise = Promise.resolve().then(action);
const watchdog = startAutoRunStepIdleLogWatchdog(step, options);
try {
return await Promise.race([
executionPromise,
watchdog.promise,
]);
} catch (error) {
if (isAutoRunStepIdleRestartError(error)) {
void executionPromise.catch((lateError) => {
const lateMessage = getErrorMessage(lateError);
if (!lateMessage || isStopError(lateError) || isAutoRunStepIdleRestartError(lateError)) {
return;
}
addLog(`步骤 ${step}:无日志重开后收到原执行失败:${lateMessage}`, 'warn').catch(() => {});
});
}
throw error;
} finally {
watchdog.cancel();
}
}
async function executeStepAndWaitWithAutoRunIdleLogWatchdog(step, delayAfter = 2000, options = {}) {
return runAutoStepActionWithIdleLogWatchdog(
step,
() => executeStepAndWait(step, delayAfter),
options
);
}
async function waitForRunningStepsToFinish(payload = {}) {
let currentState = await getState();
let runningSteps = getRunningSteps(currentState.stepStatuses, currentState);
@@ -10451,6 +10591,7 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
let goPayCheckoutRestartCount = 0;
let gpcCheckoutRestartCount = 0;
let step4RestartCount = 0;
const stepIdleRestartCounts = new Map();
let currentStartStep = startStep;
let continueCurrentAttempt = continued;
const resolvedSignupMethod = await ensureResolvedSignupMethodForRun();
@@ -10476,6 +10617,39 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
return error;
};
const restartCurrentStepAfterIdle = async (step, error) => {
if (!isAutoRunStepIdleRestartError(error)) {
return false;
}
const idleRestartCount = (stepIdleRestartCounts.get(step) || 0) + 1;
stepIdleRestartCounts.set(step, idleRestartCount);
if (idleRestartCount > AUTO_RUN_STEP_IDLE_RESTART_MAX_ATTEMPTS) {
await addLog(
`步骤 ${step}:已连续 ${AUTO_RUN_STEP_IDLE_RESTART_MAX_ATTEMPTS} 次因 5 分钟无新日志而重开,停止自动重试。原因:${getErrorMessage(error)}`,
'error'
);
throw error;
}
const reason = getErrorMessage(error);
if (typeof cancelPendingCommands === 'function') {
cancelPendingCommands(`步骤 ${step} 5 分钟没有新日志,准备重开当前步骤。`);
}
if (typeof broadcastStopToContentScripts === 'function') {
await broadcastStopToContentScripts();
}
await addLog(
`步骤 ${step}:5 分钟没有新日志,准备重新开始当前步骤(第 ${idleRestartCount}/${AUTO_RUN_STEP_IDLE_RESTART_MAX_ATTEMPTS} 次)。原因:${reason}`,
'warn'
);
await invalidateDownstreamAfterStepRestart(Math.max(0, step - 1), {
logLabel: `步骤 ${step} 因 5 分钟无新日志准备重开(第 ${idleRestartCount}/${AUTO_RUN_STEP_IDLE_RESTART_MAX_ATTEMPTS} 次)`,
});
currentStartStep = step;
continueCurrentAttempt = true;
return true;
};
while (true) {
@@ -10486,16 +10660,40 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
}
if (currentStartStep <= 1) {
await executeStepAndWait(1, AUTO_STEP_DELAYS[1]);
try {
await executeStepAndWaitWithAutoRunIdleLogWatchdog(1, AUTO_STEP_DELAYS[1]);
} catch (err) {
attachFailedStep(err, 1);
if (isStopError(err)) {
throw err;
}
if (await restartCurrentStepAfterIdle(1, err)) {
continue;
}
throw err;
}
}
if (currentStartStep <= 2) {
if (resolvedSignupMethod === SIGNUP_METHOD_PHONE) {
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:本轮注册方式为手机号注册,将跳过邮箱预获取 ===`, 'info');
} else {
await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns);
try {
await runAutoStepActionWithIdleLogWatchdog(2, async () => {
if (resolvedSignupMethod === SIGNUP_METHOD_PHONE) {
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:本轮注册方式为手机号注册,将跳过邮箱预获取 ===`, 'info');
} else {
await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns);
}
await executeStepAndWait(2, AUTO_STEP_DELAYS[2]);
});
} catch (err) {
attachFailedStep(err, 2);
if (isStopError(err)) {
throw err;
}
if (await restartCurrentStepAfterIdle(2, err)) {
continue;
}
throw err;
}
await executeStepAndWait(2, AUTO_STEP_DELAYS[2]);
}
let restartFromStep1WithCurrentEmail = false;
@@ -10513,12 +10711,15 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
await addLog(`自动运行:步骤 3 当前状态为 ${step3Status},将直接继续后续流程。`, 'info');
} else {
try {
await executeStepAndWait(3, AUTO_STEP_DELAYS[3]);
await executeStepAndWaitWithAutoRunIdleLogWatchdog(3, AUTO_STEP_DELAYS[3]);
} catch (err) {
attachFailedStep(err, 3);
if (isStopError(err)) {
throw err;
}
if (await restartCurrentStepAfterIdle(3, err)) {
continue;
}
if (isSignupPhonePasswordMismatchFailure(err)) {
step4RestartCount += 1;
await restartSignupPhonePasswordMismatchAttemptFromStep(3, step4RestartCount, err);
@@ -10559,7 +10760,7 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
continue;
}
try {
await executeStepAndWait(step, AUTO_STEP_DELAYS[step]);
await executeStepAndWaitWithAutoRunIdleLogWatchdog(step, AUTO_STEP_DELAYS[step]);
step += 1;
} catch (err) {
attachFailedStep(err, step);
@@ -10567,6 +10768,10 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
throw err;
}
if (await restartCurrentStepAfterIdle(step, err)) {
continue;
}
const stepExecutionKey = typeof getStepExecutionKeyForState === 'function'
? getStepExecutionKeyForState(step, latestState)
: '';