Merge remote-tracking branch 'origin/dev' into fix/herosms-page-state-deadline
This commit is contained in:
+256
-19
@@ -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) {
|
||||
@@ -7757,7 +7776,22 @@ function isGpcCheckoutRestartRequiredFailure(error) {
|
||||
if (/GPC_TASK_ENDED::/i.test(rawMessage)) {
|
||||
return true;
|
||||
}
|
||||
return /GPC\s*API\s*请求超时|GPC\s*任务状态超过\s*\d+\s*秒无进展|GPC[\s\S]*请重新创建任务|步骤\s*[67][\s\S]*GPC[\s\S]*(?:任务轮询超时|请求超时|超时|timeout|timed\s*out|卡死|无响应)|account\s+already\s+linked|GOPAY已经绑了订阅|(?:账号|账户|GoPay|GOPAY)[\s\S]*(?:已绑定|已经绑定|已绑|绑了订阅|绑定了订阅)|创建\s*GPC\s*订单失败[\s\S]*(?:任务已结束|任务结束|failed|expired|discarded|请求超时|timeout|timed\s*out)/i.test(message);
|
||||
return /GPC\s*API\s*请求超时|GPC\s*任务状态超过\s*\d+\s*秒无进展|GPC[\s\S]*请重新创建任务|步骤\s*[67][\s\S]*GPC[\s\S]*(?:access\s*token|accessToken|任务轮询超时|请求超时|超时|timeout|timed\s*out|卡死|无响应|失败)|account\s+already\s+linked|GOPAY已经绑了订阅|(?:账号|账户|GoPay|GOPAY)[\s\S]*(?:已绑定|已经绑定|已绑|绑了订阅|绑定了订阅)|创建\s*GPC\s*订单失败[\s\S]*(?:任务已结束|任务结束|failed|expired|discarded|请求超时|timeout|timed\s*out)/i.test(message);
|
||||
}
|
||||
|
||||
function isPlusCheckoutRestartStep(step, stepExecutionKey = '', state = {}) {
|
||||
const normalizedKey = String(stepExecutionKey || '').trim();
|
||||
if (normalizedKey === 'plus-checkout-create'
|
||||
|| normalizedKey === 'plus-checkout-billing'
|
||||
|| normalizedKey === 'gopay-subscription-confirm') {
|
||||
return true;
|
||||
}
|
||||
const numericStep = Number(step);
|
||||
return Boolean(state?.plusModeEnabled) && (numericStep === 6 || numericStep === 7);
|
||||
}
|
||||
|
||||
function isPlusCheckoutRestartRequiredFailure(error) {
|
||||
return !isPlusCheckoutNonFreeTrialFailure(error);
|
||||
}
|
||||
|
||||
function isGoPayCheckoutRestartRequiredFailure(error) {
|
||||
@@ -7843,6 +7877,9 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
gopayHelperFailureDetail: '',
|
||||
gopayHelperTaskPayload: null,
|
||||
gopayHelperOrderCreatedAt: 0,
|
||||
gopayHelperTaskProgressSignature: '',
|
||||
gopayHelperTaskProgressAt: 0,
|
||||
gopayHelperTaskProgressTaskId: '',
|
||||
gopayHelperPinPayload: null,
|
||||
gopayHelperResolvedOtp: '',
|
||||
gopayHelperOtpRequestId: '',
|
||||
@@ -8839,6 +8876,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 +9119,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);
|
||||
@@ -10450,7 +10605,9 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
|
||||
let postStep7RestartCount = 0;
|
||||
let goPayCheckoutRestartCount = 0;
|
||||
let gpcCheckoutRestartCount = 0;
|
||||
let plusCheckoutRestartCount = 0;
|
||||
let step4RestartCount = 0;
|
||||
const stepIdleRestartCounts = new Map();
|
||||
let currentStartStep = startStep;
|
||||
let continueCurrentAttempt = continued;
|
||||
const resolvedSignupMethod = await ensureResolvedSignupMethodForRun();
|
||||
@@ -10476,6 +10633,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 +10676,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 +10727,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 +10776,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,21 +10784,41 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (await restartCurrentStepAfterIdle(step, err)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const stepExecutionKey = typeof getStepExecutionKeyForState === 'function'
|
||||
? getStepExecutionKeyForState(step, latestState)
|
||||
: '';
|
||||
const isGpcCheckoutStep = normalizePlusPaymentMethodForRun(latestState?.plusPaymentMethod) === plusPaymentMethodGpcHelper
|
||||
|| String(latestState?.plusCheckoutSource || '').trim() === plusPaymentMethodGpcHelper;
|
||||
if (isGpcCheckoutStep
|
||||
&& (stepExecutionKey === 'plus-checkout-create' || stepExecutionKey === 'plus-checkout-billing')
|
||||
&& isGpcCheckoutRestartRequiredFailure(err)) {
|
||||
gpcCheckoutRestartCount += 1;
|
||||
if (isPlusCheckoutRestartStep(step, stepExecutionKey, latestState)
|
||||
&& isPlusCheckoutRestartRequiredFailure(err)) {
|
||||
const isGoPayCheckoutStep = stepExecutionKey === 'gopay-subscription-confirm'
|
||||
|| normalizePlusPaymentMethodForRun(latestState?.plusPaymentMethod) === 'gopay';
|
||||
if (isGpcCheckoutStep) {
|
||||
gpcCheckoutRestartCount += 1;
|
||||
} else if (isGoPayCheckoutStep) {
|
||||
goPayCheckoutRestartCount += 1;
|
||||
} else {
|
||||
plusCheckoutRestartCount += 1;
|
||||
}
|
||||
const checkoutRestartCount = isGpcCheckoutStep
|
||||
? gpcCheckoutRestartCount
|
||||
: (isGoPayCheckoutStep ? goPayCheckoutRestartCount : plusCheckoutRestartCount);
|
||||
const checkoutLabel = isGpcCheckoutStep
|
||||
? 'GPC 任务'
|
||||
: (isGoPayCheckoutStep ? 'GoPay 订阅' : 'Plus Checkout');
|
||||
const recreateLabel = isGpcCheckoutStep
|
||||
? '重新创建 GPC 任务'
|
||||
: (isGoPayCheckoutStep ? '重新创建 GoPay 订阅' : '重新创建 Plus Checkout');
|
||||
await addLog(
|
||||
`步骤 ${step}:检测到 GPC 任务失败/卡住,准备回到步骤 6 重新创建 GPC 任务(第 ${gpcCheckoutRestartCount} 次)。原因:${getErrorMessage(err)}`,
|
||||
`步骤 ${step}:检测到 ${checkoutLabel} 失败/卡住,准备回到步骤 6 ${recreateLabel}(第 ${checkoutRestartCount} 次)。原因:${getErrorMessage(err)}`,
|
||||
'warn'
|
||||
);
|
||||
await invalidateDownstreamAfterStepRestart(5, {
|
||||
logLabel: `步骤 ${step} GPC 任务失败后准备回到步骤 6 重试(第 ${gpcCheckoutRestartCount} 次)`,
|
||||
logLabel: `步骤 ${step} ${checkoutLabel}失败后准备回到步骤 6 重试(第 ${checkoutRestartCount} 次)`,
|
||||
});
|
||||
step = 6;
|
||||
continue;
|
||||
@@ -11769,7 +12006,7 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
||||
if (isPhoneSmsPlatformRateLimitFailure(normalizedMessage)) {
|
||||
return false;
|
||||
}
|
||||
return /HeroSMS|phone verification did not succeed|number replacements|sms_timeout_after_resend|phone number is already linked|add-phone keeps rejecting current number|接码|手机号|手机验证码|步骤\s*9.*(?:手机号|验证码)|Step\s*9.*phone verification/i.test(normalizedMessage);
|
||||
return /HeroSMS|phone verification did not succeed|number replacements|sms_timeout_after(?:_[a-z0-9_]+)?|phone number is already linked|add-phone keeps rejecting current number|手机验证码|短信验证码|接码|步骤\s*9[::][\s\S]*(?:手机号验证|手机验证码|接码|没有可用手机号|无可用手机号)|(?:手机号验证|手机号码验证|手机号接码|手机号码接码)[\s\S]*(?:失败|超时|未成功|不可用|拒绝)|(?:手机号|手机号码)[\s\S]*(?:已绑定|被占用|不可用|拒绝|失败|超时|没有可用|无可用)|Step\s*9.*phone verification/i.test(normalizedMessage);
|
||||
};
|
||||
|
||||
const normalizedStep = Number(step);
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
if (normalized === 'success') {
|
||||
return 'success';
|
||||
}
|
||||
if (normalized === 'running' || /_running$/.test(normalized)) {
|
||||
return 'running';
|
||||
}
|
||||
if (normalized === 'failed' || /_failed$/.test(normalized)) {
|
||||
return 'failed';
|
||||
}
|
||||
@@ -157,6 +160,9 @@
|
||||
if (finalStatus === 'success') {
|
||||
return '流程完成';
|
||||
}
|
||||
if (finalStatus === 'running') {
|
||||
return '正在运行';
|
||||
}
|
||||
if (finalStatus === 'stopped') {
|
||||
if (Number.isInteger(failedStep) && failedStep > 0) {
|
||||
return `步骤 ${failedStep} 停止`;
|
||||
@@ -519,6 +525,8 @@
|
||||
summary.total += 1;
|
||||
if (record.finalStatus === 'success') {
|
||||
summary.success += 1;
|
||||
} else if (record.finalStatus === 'running') {
|
||||
summary.running += 1;
|
||||
} else if (record.finalStatus === 'failed') {
|
||||
summary.failed += 1;
|
||||
} else if (record.finalStatus === 'stopped') {
|
||||
@@ -529,6 +537,7 @@
|
||||
}, {
|
||||
total: 0,
|
||||
success: 0,
|
||||
running: 0,
|
||||
failed: 0,
|
||||
stopped: 0,
|
||||
retryTotal: 0,
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
|
||||
function 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 isVerificationMailPollingError(error) {
|
||||
|
||||
@@ -35,6 +35,23 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof chrome?.tabs?.get === 'function' && typeof chrome?.windows?.update === 'function') {
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
const windowId = Number(tab?.windowId);
|
||||
if (Number.isInteger(windowId) && windowId >= 0) {
|
||||
await chrome.windows.update(windowId, { state: 'normal', focused: true }).catch(() => {});
|
||||
await chrome.windows.update(windowId, {
|
||||
focused: true,
|
||||
width: 1200,
|
||||
height: 900,
|
||||
}).catch(() => {});
|
||||
}
|
||||
} catch {
|
||||
// Best-effort only. Step 2 still has content-side entry retries.
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(
|
||||
`步骤 ${step}:注册页已打开,正在等待页面加载完成并额外稳定 3 秒...`,
|
||||
|
||||
@@ -413,6 +413,9 @@
|
||||
gopayHelperRemoteStage: result.remoteStage,
|
||||
gopayHelperPhoneMode: result.phoneMode || normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode),
|
||||
gopayHelperTaskPayload: result.responsePayload,
|
||||
gopayHelperTaskProgressSignature: '',
|
||||
gopayHelperTaskProgressAt: 0,
|
||||
gopayHelperTaskProgressTaskId: result.taskId,
|
||||
gopayHelperReferenceId: '',
|
||||
gopayHelperGoPayGuid: '',
|
||||
gopayHelperRedirectUrl: '',
|
||||
|
||||
@@ -795,11 +795,9 @@
|
||||
task?.api_waiting_for,
|
||||
task?.last_input_error,
|
||||
task?.otp_invalid_count,
|
||||
task?.reference_id || task?.referenceId,
|
||||
task?.redirect_url || task?.redirectUrl,
|
||||
task?.flow_id || task?.flowId,
|
||||
task?.challenge_id || task?.challengeId,
|
||||
task?.gopay_guid || task?.gopayGuid,
|
||||
task?.failure_stage || task?.failureStage,
|
||||
task?.failure_detail || task?.failureDetail,
|
||||
task?.error_message || task?.errorMessage,
|
||||
].map((value) => String(value ?? '').trim()).join('|');
|
||||
}
|
||||
|
||||
@@ -898,6 +896,9 @@
|
||||
gopayHelperFailureStage: '',
|
||||
gopayHelperFailureDetail: '',
|
||||
gopayHelperTaskPayload: null,
|
||||
gopayHelperTaskProgressSignature: '',
|
||||
gopayHelperTaskProgressAt: 0,
|
||||
gopayHelperTaskProgressTaskId: '',
|
||||
gopayHelperPinPayload: null,
|
||||
gopayHelperResolvedOtp: '',
|
||||
gopayHelperOtpRequestId: '',
|
||||
@@ -971,8 +972,14 @@
|
||||
let lastSubmittedOtp = '';
|
||||
let pinSubmitted = false;
|
||||
let terminalReached = false;
|
||||
let lastProgressSignature = '';
|
||||
let lastProgressAt = Date.now();
|
||||
let lastProgressSignature = String(state?.gopayHelperTaskProgressSignature || '').trim();
|
||||
let lastProgressAt = normalizeEpochMilliseconds(state?.gopayHelperTaskProgressAt || 0) || Date.now();
|
||||
let lastProgressTaskId = String(state?.gopayHelperTaskProgressTaskId || '').trim();
|
||||
if (lastProgressTaskId !== taskId) {
|
||||
lastProgressSignature = '';
|
||||
lastProgressAt = Date.now();
|
||||
lastProgressTaskId = '';
|
||||
}
|
||||
const staleStatusTimeoutMs = getGpcTaskStaleStatusTimeoutMs(state);
|
||||
|
||||
if (!taskId) {
|
||||
@@ -1025,15 +1032,27 @@
|
||||
if (shouldWatchGpcTaskProgress(task, state)) {
|
||||
const progressSignature = buildGpcTaskProgressSignature(task);
|
||||
const now = Date.now();
|
||||
if (progressSignature && progressSignature !== lastProgressSignature) {
|
||||
if (progressSignature && (progressSignature !== lastProgressSignature || lastProgressTaskId !== taskId)) {
|
||||
lastProgressSignature = progressSignature;
|
||||
lastProgressAt = now;
|
||||
lastProgressTaskId = taskId;
|
||||
await setState({
|
||||
gopayHelperTaskProgressSignature: progressSignature,
|
||||
gopayHelperTaskProgressAt: now,
|
||||
gopayHelperTaskProgressTaskId: taskId,
|
||||
});
|
||||
} else if (progressSignature && now - lastProgressAt >= staleStatusTimeoutMs) {
|
||||
throw buildGpcTaskStaleStatusError(task, staleStatusTimeoutMs);
|
||||
}
|
||||
} else {
|
||||
lastProgressSignature = '';
|
||||
lastProgressAt = Date.now();
|
||||
lastProgressTaskId = '';
|
||||
await setState({
|
||||
gopayHelperTaskProgressSignature: '',
|
||||
gopayHelperTaskProgressAt: 0,
|
||||
gopayHelperTaskProgressTaskId: taskId,
|
||||
});
|
||||
}
|
||||
|
||||
if (isGpcTaskOtpWait(task, state)) {
|
||||
|
||||
@@ -185,6 +185,33 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function normalizeSignupTabWindowForStep2(tabId) {
|
||||
if (
|
||||
!Number.isInteger(tabId)
|
||||
|| typeof chrome?.tabs?.get !== 'function'
|
||||
|| typeof chrome?.windows?.update !== 'function'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
const windowId = Number(tab?.windowId);
|
||||
if (!Number.isInteger(windowId) || windowId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await chrome.windows.update(windowId, { state: 'normal', focused: true }).catch(() => {});
|
||||
await chrome.windows.update(windowId, {
|
||||
focused: true,
|
||||
width: 1200,
|
||||
height: 900,
|
||||
}).catch(() => {});
|
||||
} catch {
|
||||
// Best-effort only: content-side retries still handle layout recovery.
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureSignupPhoneEntryReady(tabId) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
throw new Error('步骤 2:未找到可用的注册页标签,无法切换到手机号注册入口。');
|
||||
@@ -230,6 +257,7 @@
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
} else {
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await normalizeSignupTabWindowForStep2(signupTabId);
|
||||
await waitForStep2SignupTabToSettle(
|
||||
signupTabId,
|
||||
'步骤 2:已切换到注册页标签,正在等待页面加载完成并额外稳定 3 秒...'
|
||||
|
||||
+42
-7
@@ -602,12 +602,38 @@ function getSignupEmailContinueButton({ allowDisabled = false } = {}) {
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findSignupEntryTrigger() {
|
||||
function findSignupEntryTrigger(options = {}) {
|
||||
const { allowHiddenFallback = true } = options || {};
|
||||
const candidates = document.querySelectorAll('a, button, [role="button"], [role="link"]');
|
||||
return Array.from(candidates).find((el) => {
|
||||
if (!isVisibleElement(el) || !isActionEnabled(el)) return false;
|
||||
return SIGNUP_ENTRY_TRIGGER_PATTERN.test(getActionText(el));
|
||||
}) || null;
|
||||
let hiddenSignupTrigger = null;
|
||||
|
||||
for (const el of Array.from(candidates)) {
|
||||
if (!isActionEnabled(el)) continue;
|
||||
if (!SIGNUP_ENTRY_TRIGGER_PATTERN.test(getActionText(el))) continue;
|
||||
if (isVisibleElement(el)) {
|
||||
return el;
|
||||
}
|
||||
if (!hiddenSignupTrigger) {
|
||||
hiddenSignupTrigger = el;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allowHiddenFallback || !hiddenSignupTrigger) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const view = typeof window !== 'undefined' ? window : globalThis;
|
||||
const collapsedViewport = Boolean(
|
||||
Math.round(Number(view?.innerWidth) || 0) < 240
|
||||
|| Math.round(Number(view?.innerHeight) || 0) < 160
|
||||
|| Math.round(Number(view?.outerWidth) || 0) < 320
|
||||
|| Math.round(Number(view?.outerHeight) || 0) < 180
|
||||
);
|
||||
const pageText = typeof getPageTextSnapshot === 'function'
|
||||
? getPageTextSnapshot()
|
||||
: '';
|
||||
const looksLikeLoggedOutHome = /登录|登入|log\s*in|sign\s*in/i.test(pageText);
|
||||
return collapsedViewport || looksLikeLoggedOutHome ? hiddenSignupTrigger : null;
|
||||
}
|
||||
|
||||
function getSignupPasswordDisplayedEmail() {
|
||||
@@ -719,6 +745,7 @@ function getSignupEntryStateSummary(snapshot = inspectSignupEntryState()) {
|
||||
summary.signupTrigger = {
|
||||
tag: (snapshot.signupTrigger.tagName || '').toLowerCase(),
|
||||
text: getActionText(snapshot.signupTrigger).slice(0, 80),
|
||||
visible: isVisibleElement(snapshot.signupTrigger),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1085,9 +1112,13 @@ async function waitForSignupEntryState(options = {}) {
|
||||
: `步骤 ${step}:已找到官网注册入口,等待 3 秒后点击...`);
|
||||
await sleep(3000);
|
||||
throwIfStopped();
|
||||
const clickTarget = findSignupEntryTrigger({ allowHiddenFallback: false }) || snapshot.signupTrigger;
|
||||
if (!isVisibleElement(clickTarget)) {
|
||||
log(`步骤 ${step}:注册入口仍处于不可见状态,继续按入口重试节奏尝试恢复点击...`, 'warn');
|
||||
}
|
||||
await humanPause(350, 900);
|
||||
await performOperationWithDelay({ stepKey: 'signup-entry', kind: 'click', label: 'open-signup-entry' }, async () => {
|
||||
simulateClick(snapshot.signupTrigger);
|
||||
simulateClick(clickTarget);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2380,9 +2411,13 @@ async function waitForSignupPhoneEntryState(options = {}) {
|
||||
: `步骤 ${step}:已找到官网注册入口,等待 3 秒后点击...`);
|
||||
await sleep(3000);
|
||||
throwIfStopped();
|
||||
const clickTarget = findSignupEntryTrigger({ allowHiddenFallback: false }) || snapshot.signupTrigger;
|
||||
if (!isVisibleElement(clickTarget)) {
|
||||
log(`步骤 ${step}:注册入口仍处于不可见状态,继续按入口重试节奏尝试恢复点击...`, 'warn');
|
||||
}
|
||||
await humanPause(350, 900);
|
||||
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'click', label: 'open-signup-entry' }, async () => {
|
||||
simulateClick(snapshot.signupTrigger);
|
||||
simulateClick(clickTarget);
|
||||
});
|
||||
}
|
||||
await sleep(250);
|
||||
|
||||
@@ -21,19 +21,25 @@
|
||||
success: {
|
||||
label: '成',
|
||||
className: 'is-success',
|
||||
matches: (record) => record.finalStatus === 'success',
|
||||
matches: (record) => getRecordDisplayStatus(record) === 'success',
|
||||
metaLabel: '成功',
|
||||
},
|
||||
running: {
|
||||
label: '运行',
|
||||
className: 'is-running',
|
||||
matches: (record) => getRecordDisplayStatus(record) === 'running',
|
||||
metaLabel: '运行中',
|
||||
},
|
||||
failed: {
|
||||
label: '失',
|
||||
className: 'is-failed',
|
||||
matches: (record) => record.finalStatus === 'failed',
|
||||
matches: (record) => getRecordDisplayStatus(record) === 'failed',
|
||||
metaLabel: '失败',
|
||||
},
|
||||
stopped: {
|
||||
label: '停',
|
||||
className: 'is-stopped',
|
||||
matches: (record) => record.finalStatus === 'stopped',
|
||||
matches: (record) => getRecordDisplayStatus(record) === 'stopped',
|
||||
metaLabel: '停止',
|
||||
},
|
||||
retry: {
|
||||
@@ -96,6 +102,58 @@
|
||||
: identifier.toLowerCase();
|
||||
}
|
||||
|
||||
function getRecordDisplayStatus(record = {}) {
|
||||
return String(record.displayStatus || record.finalStatus || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isAutoRunRecordDisplayRunning(currentState = {}) {
|
||||
const phase = String(currentState.autoRunPhase || '').trim().toLowerCase();
|
||||
return Boolean(currentState.autoRunning)
|
||||
&& ['running', 'waiting_step', 'waiting_email', 'retrying'].includes(phase);
|
||||
}
|
||||
|
||||
function buildCurrentAccountRecordId(currentState = {}) {
|
||||
const accountIdentifierType = String(currentState.accountIdentifierType || '').trim().toLowerCase();
|
||||
const email = String(currentState.email || '').trim();
|
||||
const phoneNumber = String(
|
||||
currentState.signupPhoneNumber
|
||||
|| currentState.phoneNumber
|
||||
|| currentState.phone
|
||||
|| ''
|
||||
).trim();
|
||||
const accountIdentifier = String(
|
||||
currentState.accountIdentifier
|
||||
|| (accountIdentifierType === 'phone' ? phoneNumber : email)
|
||||
|| ''
|
||||
).trim();
|
||||
return buildRecordId({
|
||||
accountIdentifierType,
|
||||
accountIdentifier,
|
||||
email,
|
||||
phoneNumber,
|
||||
});
|
||||
}
|
||||
|
||||
function applyRunningDisplayState(record = {}, currentState = {}) {
|
||||
if (!isAutoRunRecordDisplayRunning(currentState)) {
|
||||
return record;
|
||||
}
|
||||
if (getRecordDisplayStatus(record) === 'success') {
|
||||
return record;
|
||||
}
|
||||
|
||||
const currentRecordId = buildCurrentAccountRecordId(currentState);
|
||||
if (!currentRecordId || buildRecordId(record) !== currentRecordId) {
|
||||
return record;
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
displayStatus: 'running',
|
||||
displaySummary: '正在运行',
|
||||
};
|
||||
}
|
||||
|
||||
function getRecordIdentifierType(record = {}) {
|
||||
const rawType = String(record.accountIdentifierType || '').trim().toLowerCase();
|
||||
if (rawType === 'phone') {
|
||||
@@ -167,18 +225,22 @@
|
||||
return (Array.isArray(currentState?.accountRunHistory) ? currentState.accountRunHistory : [])
|
||||
.filter((item) => item && typeof item === 'object')
|
||||
.slice()
|
||||
.sort((left, right) => normalizeTimestamp(right.finishedAt) - normalizeTimestamp(left.finishedAt));
|
||||
.sort((left, right) => normalizeTimestamp(right.finishedAt) - normalizeTimestamp(left.finishedAt))
|
||||
.map((record) => applyRunningDisplayState(record, currentState));
|
||||
}
|
||||
|
||||
function summarizeAccountRunHistory(records = []) {
|
||||
return records.reduce((summary, record) => {
|
||||
const retryCount = normalizeRetryCount(record.retryCount);
|
||||
const status = getRecordDisplayStatus(record);
|
||||
summary.total += 1;
|
||||
if (record.finalStatus === 'success') {
|
||||
if (status === 'success') {
|
||||
summary.success += 1;
|
||||
} else if (record.finalStatus === 'failed') {
|
||||
} else if (status === 'running') {
|
||||
summary.running += 1;
|
||||
} else if (status === 'failed') {
|
||||
summary.failed += 1;
|
||||
} else if (record.finalStatus === 'stopped') {
|
||||
} else if (status === 'stopped') {
|
||||
summary.stopped += 1;
|
||||
}
|
||||
if (retryCount > 0) {
|
||||
@@ -189,6 +251,7 @@
|
||||
}, {
|
||||
total: 0,
|
||||
success: 0,
|
||||
running: 0,
|
||||
failed: 0,
|
||||
stopped: 0,
|
||||
retryRecordCount: 0,
|
||||
@@ -227,21 +290,44 @@
|
||||
}
|
||||
|
||||
function getStatusMeta(record = {}) {
|
||||
if (record.finalStatus === 'success') {
|
||||
const status = getRecordDisplayStatus(record);
|
||||
if (status === 'success') {
|
||||
return { kind: 'success', label: '成功' };
|
||||
}
|
||||
if (record.finalStatus === 'stopped') {
|
||||
if (status === 'running') {
|
||||
return { kind: 'running', label: '正在运行' };
|
||||
}
|
||||
if (status === 'stopped') {
|
||||
return { kind: 'stopped', label: '停止' };
|
||||
}
|
||||
return { kind: 'failed', label: '失败' };
|
||||
}
|
||||
|
||||
function getRecordSummaryText(record = {}) {
|
||||
if (record.finalStatus === 'success') {
|
||||
const status = getRecordDisplayStatus(record);
|
||||
if (record.displaySummary) {
|
||||
return String(record.displaySummary || '').trim();
|
||||
}
|
||||
if (status === 'success') {
|
||||
return '流程完成';
|
||||
}
|
||||
if (status === 'running') {
|
||||
return '正在运行';
|
||||
}
|
||||
|
||||
return String(record.failureLabel || '').trim() || '流程失败';
|
||||
return String(record.failureDetail || record.reason || '').trim()
|
||||
|| String(record.failureLabel || '').trim()
|
||||
|| '流程失败';
|
||||
}
|
||||
|
||||
function getRecordTooltipText(record = {}, summaryText = '') {
|
||||
const recordTitle = getRecordTitle(record);
|
||||
const status = getRecordDisplayStatus(record);
|
||||
const detail = String(record.displaySummary || record.failureDetail || record.reason || '').trim();
|
||||
if (status === 'success' || status === 'running' || !detail || detail === recordTitle) {
|
||||
return recordTitle;
|
||||
}
|
||||
return `${recordTitle}\n${detail}`;
|
||||
}
|
||||
|
||||
function getFilterConfig(filterKey = activeFilter) {
|
||||
@@ -378,6 +464,7 @@
|
||||
const summary = summarizeAccountRunHistory(allRecords);
|
||||
dom.accountRecordsStats.innerHTML = [
|
||||
createStatChip('all', summary.total),
|
||||
createStatChip('running', summary.running),
|
||||
createStatChip('success', summary.success),
|
||||
createStatChip('failed', summary.failed),
|
||||
createStatChip('stopped', summary.stopped),
|
||||
@@ -449,9 +536,9 @@
|
||||
const recordId = buildRecordId(record);
|
||||
const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(空账号)';
|
||||
const secondaryIdentifier = getRecordSecondaryIdentifier(record);
|
||||
const recordTitle = getRecordTitle(record);
|
||||
const statusMeta = getStatusMeta(record);
|
||||
const summaryText = getRecordSummaryText(record);
|
||||
const recordTitle = getRecordTooltipText(record, summaryText);
|
||||
const retryCount = normalizeRetryCount(record.retryCount);
|
||||
const isSelected = selectedRecordIds.has(recordId);
|
||||
const itemClassNames = [
|
||||
|
||||
+20
-2
@@ -2985,6 +2985,12 @@ header {
|
||||
border-color: color-mix(in srgb, var(--green) 16%, var(--border));
|
||||
}
|
||||
|
||||
.account-records-stat.is-running {
|
||||
color: var(--blue);
|
||||
background: var(--blue-soft);
|
||||
border-color: color-mix(in srgb, var(--blue) 16%, var(--border));
|
||||
}
|
||||
|
||||
.account-records-stat.is-failed {
|
||||
color: var(--red);
|
||||
background: var(--red-soft);
|
||||
@@ -3158,8 +3164,11 @@ header {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
@@ -3186,6 +3195,15 @@ header {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.account-record-item.is-running {
|
||||
border-color: color-mix(in srgb, var(--blue) 14%, var(--border-subtle));
|
||||
}
|
||||
|
||||
.account-record-item.is-running .account-record-item-status {
|
||||
background: var(--blue-soft);
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.account-record-item.is-failed {
|
||||
border-color: color-mix(in srgb, var(--red) 14%, var(--border-subtle));
|
||||
}
|
||||
|
||||
@@ -2065,6 +2065,23 @@ function syncLatestState(nextState) {
|
||||
renderAccountRecords(latestState);
|
||||
}
|
||||
|
||||
let accountRunHistoryRefreshTimer = null;
|
||||
|
||||
function scheduleAccountRunHistoryRefresh(delayMs = 150) {
|
||||
if (accountRunHistoryRefreshTimer) {
|
||||
clearTimeout(accountRunHistoryRefreshTimer);
|
||||
}
|
||||
accountRunHistoryRefreshTimer = setTimeout(() => {
|
||||
accountRunHistoryRefreshTimer = null;
|
||||
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => {
|
||||
syncLatestState(state);
|
||||
syncAutoRunState(state);
|
||||
updateStatusDisplay(latestState);
|
||||
updateButtonStates();
|
||||
}).catch(() => { });
|
||||
}, Math.max(0, Number(delayMs) || 0));
|
||||
}
|
||||
|
||||
function normalizeOperationDelayEnabled(value) {
|
||||
return typeof value === 'boolean' ? value : true;
|
||||
}
|
||||
@@ -13380,6 +13397,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
appendLog(message.payload);
|
||||
if (message.payload.level === 'error') {
|
||||
showToast(message.payload.message, 'error');
|
||||
scheduleAccountRunHistoryRefresh();
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -14135,6 +14153,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
applyAutoRunStatus(message.payload);
|
||||
updateStatusDisplay(latestState);
|
||||
updateButtonStates();
|
||||
if (!['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(message.payload.phase)) {
|
||||
scheduleAccountRunHistoryRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +53,23 @@ function extractFunction(name) {
|
||||
}
|
||||
|
||||
const bundle = [
|
||||
'const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = 300000;',
|
||||
'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::';",
|
||||
extractFunction('isAddPhoneAuthUrl'),
|
||||
extractFunction('isAddPhoneAuthState'),
|
||||
extractFunction('isMail2925ThreadTerminatedError'),
|
||||
extractFunction('isSignupUserAlreadyExistsFailure'),
|
||||
extractFunction('isPlusCheckoutNonFreeTrialFailure'),
|
||||
extractFunction('isPlusCheckoutRestartStep'),
|
||||
extractFunction('isPlusCheckoutRestartRequiredFailure'),
|
||||
extractFunction('getLatestLogTimestamp'),
|
||||
extractFunction('buildAutoRunStepIdleRestartError'),
|
||||
extractFunction('isAutoRunStepIdleRestartError'),
|
||||
extractFunction('startAutoRunStepIdleLogWatchdog'),
|
||||
extractFunction('runAutoStepActionWithIdleLogWatchdog'),
|
||||
extractFunction('executeStepAndWaitWithAutoRunIdleLogWatchdog'),
|
||||
extractFunction('getPostStep6AutoRestartDecision'),
|
||||
extractFunction('runAutoSequenceFromStep'),
|
||||
].join('\n');
|
||||
|
||||
@@ -53,6 +53,10 @@ function extractFunction(name) {
|
||||
}
|
||||
|
||||
const bundle = [
|
||||
'const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = 300000;',
|
||||
'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::';",
|
||||
extractFunction('isAddPhoneAuthUrl'),
|
||||
extractFunction('isAddPhoneAuthState'),
|
||||
extractFunction('isMail2925ThreadTerminatedError'),
|
||||
@@ -60,6 +64,15 @@ const bundle = [
|
||||
extractFunction('getSignupPhonePasswordMismatchRestartPayload'),
|
||||
extractFunction('restartSignupPhonePasswordMismatchAttemptFromStep'),
|
||||
extractFunction('isSignupUserAlreadyExistsFailure'),
|
||||
extractFunction('isPlusCheckoutNonFreeTrialFailure'),
|
||||
extractFunction('isPlusCheckoutRestartStep'),
|
||||
extractFunction('isPlusCheckoutRestartRequiredFailure'),
|
||||
extractFunction('getLatestLogTimestamp'),
|
||||
extractFunction('buildAutoRunStepIdleRestartError'),
|
||||
extractFunction('isAutoRunStepIdleRestartError'),
|
||||
extractFunction('startAutoRunStepIdleLogWatchdog'),
|
||||
extractFunction('runAutoStepActionWithIdleLogWatchdog'),
|
||||
extractFunction('executeStepAndWaitWithAutoRunIdleLogWatchdog'),
|
||||
extractFunction('getPostStep6AutoRestartDecision'),
|
||||
extractFunction('runAutoSequenceFromStep'),
|
||||
].join('\n');
|
||||
|
||||
@@ -56,7 +56,16 @@ const bundle = [
|
||||
extractFunction('isAddPhoneAuthFailure'),
|
||||
extractFunction('isAddPhoneAuthUrl'),
|
||||
extractFunction('isAddPhoneAuthState'),
|
||||
extractFunction('isPlusCheckoutNonFreeTrialFailure'),
|
||||
extractFunction('isGpcCheckoutRestartRequiredFailure'),
|
||||
extractFunction('isPlusCheckoutRestartStep'),
|
||||
extractFunction('isPlusCheckoutRestartRequiredFailure'),
|
||||
extractFunction('getLatestLogTimestamp'),
|
||||
extractFunction('buildAutoRunStepIdleRestartError'),
|
||||
extractFunction('isAutoRunStepIdleRestartError'),
|
||||
extractFunction('startAutoRunStepIdleLogWatchdog'),
|
||||
extractFunction('runAutoStepActionWithIdleLogWatchdog'),
|
||||
extractFunction('executeStepAndWaitWithAutoRunIdleLogWatchdog'),
|
||||
extractFunction('getPostStep6AutoRestartDecision'),
|
||||
extractFunction('runAutoSequenceFromStep'),
|
||||
].join('\n');
|
||||
@@ -86,6 +95,10 @@ function createHarness(options = {}) {
|
||||
stepIds = Object.keys(stepDefinitions).map(Number).sort((a, b) => a - b),
|
||||
lastStepId = Math.max(...stepIds),
|
||||
finalOAuthChainStartStep = 7,
|
||||
idleLogTimeoutMs = 300000,
|
||||
idleLogCheckIntervalMs = 5000,
|
||||
hangStep = 0,
|
||||
hangBudget = 0,
|
||||
} = options;
|
||||
|
||||
return new Function(`
|
||||
@@ -94,6 +107,10 @@ const LAST_STEP_ID = ${JSON.stringify(lastStepId)};
|
||||
const FINAL_OAUTH_CHAIN_START_STEP = ${JSON.stringify(finalOAuthChainStartStep)};
|
||||
const SIGNUP_METHOD_PHONE = 'phone';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = ${JSON.stringify(idleLogTimeoutMs)};
|
||||
const AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS = ${JSON.stringify(idleLogCheckIntervalMs)};
|
||||
const AUTO_RUN_STEP_IDLE_RESTART_MAX_ATTEMPTS = 3;
|
||||
const AUTO_RUN_STEP_IDLE_RESTART_ERROR_PREFIX = 'AUTO_RUN_STEP_IDLE_RESTART::';
|
||||
const LOG_PREFIX = '[test]';
|
||||
const chrome = {
|
||||
tabs: {
|
||||
@@ -102,14 +119,17 @@ const chrome = {
|
||||
};
|
||||
|
||||
let remainingFailures = ${JSON.stringify(failureBudget)};
|
||||
let remainingHangs = ${JSON.stringify(hangBudget)};
|
||||
const events = {
|
||||
steps: [],
|
||||
logs: [],
|
||||
invalidations: [],
|
||||
cancellations: [],
|
||||
stopBroadcasts: 0,
|
||||
};
|
||||
|
||||
async function addLog(message, level = 'info') {
|
||||
events.logs.push({ message, level });
|
||||
events.logs.push({ message, level, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
async function ensureAutoEmailReady() {}
|
||||
@@ -119,6 +139,7 @@ async function getState() {
|
||||
return {
|
||||
stepStatuses: { 3: 'completed' },
|
||||
mailProvider: '163',
|
||||
logs: events.logs,
|
||||
...${JSON.stringify(customState)},
|
||||
};
|
||||
}
|
||||
@@ -140,6 +161,10 @@ function isStepDoneStatus(status) {
|
||||
}
|
||||
async function executeStepAndWait(step) {
|
||||
events.steps.push(step);
|
||||
if (step === ${JSON.stringify(hangStep)} && remainingHangs > 0) {
|
||||
remainingHangs -= 1;
|
||||
return new Promise(() => {});
|
||||
}
|
||||
if (step === ${JSON.stringify(failureStep)} && remainingFailures > 0) {
|
||||
remainingFailures -= 1;
|
||||
throw new Error(${JSON.stringify(failureMessage)});
|
||||
@@ -151,6 +176,12 @@ async function getTabId() {
|
||||
async function invalidateDownstreamAfterStepRestart(step, options = {}) {
|
||||
events.invalidations.push({ step, options });
|
||||
}
|
||||
function cancelPendingCommands(reason = '') {
|
||||
events.cancellations.push(reason);
|
||||
}
|
||||
async function broadcastStopToContentScripts() {
|
||||
events.stopBroadcasts += 1;
|
||||
}
|
||||
function getLoginAuthStateLabel(state) {
|
||||
return state || 'unknown';
|
||||
}
|
||||
@@ -224,6 +255,62 @@ test('auto-run keeps restarting from step 7 after post-login failures without a
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts the current step after five minutes without new logs', async () => {
|
||||
const harness = createHarness({
|
||||
startStep: 10,
|
||||
failureStep: 0,
|
||||
hangStep: 10,
|
||||
hangBudget: 1,
|
||||
idleLogTimeoutMs: 20,
|
||||
idleLogCheckIntervalMs: 5,
|
||||
});
|
||||
|
||||
const events = await harness.run();
|
||||
|
||||
assert.deepStrictEqual(events.steps, [10, 10]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [9]);
|
||||
assert.equal(events.cancellations.length, 1);
|
||||
assert.equal(events.stopBroadcasts, 1);
|
||||
assert.ok(events.logs.some(({ message }) => /5 分钟没有新日志,准备重新开始当前步骤/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run applies the idle-log restart watchdog to early steps too', async () => {
|
||||
const harness = createHarness({
|
||||
startStep: 2,
|
||||
failureStep: 0,
|
||||
hangStep: 2,
|
||||
hangBudget: 1,
|
||||
idleLogTimeoutMs: 20,
|
||||
idleLogCheckIntervalMs: 5,
|
||||
});
|
||||
|
||||
const events = await harness.run();
|
||||
|
||||
assert.deepStrictEqual(events.steps, [2, 2, 4, 5, 6, 7, 8, 9, 10]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [1]);
|
||||
assert.equal(events.cancellations.length, 1);
|
||||
assert.equal(events.stopBroadcasts, 1);
|
||||
});
|
||||
|
||||
test('auto-run stops current-step idle restarts after the retry cap', async () => {
|
||||
const harness = createHarness({
|
||||
startStep: 10,
|
||||
failureStep: 0,
|
||||
hangStep: 10,
|
||||
hangBudget: 4,
|
||||
idleLogTimeoutMs: 20,
|
||||
idleLogCheckIntervalMs: 5,
|
||||
});
|
||||
|
||||
const result = await harness.runAndCaptureError();
|
||||
|
||||
assert.ok(result?.error);
|
||||
assert.match(result.error.message, /AUTO_RUN_STEP_IDLE_RESTART::步骤 10/);
|
||||
assert.deepStrictEqual(result.events.steps, [10, 10, 10, 10]);
|
||||
assert.deepStrictEqual(result.events.invalidations.map((entry) => entry.step), [9, 9, 9]);
|
||||
assert.ok(result.events.logs.some(({ message }) => /已连续 3 次因 5 分钟无新日志而重开/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run stops restarting once add-phone is detected', async () => {
|
||||
const harness = createHarness({
|
||||
failureStep: 7,
|
||||
@@ -334,6 +421,182 @@ test('auto-run restarts from confirm-oauth step after transient step10 token_exc
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 9 重新开始授权流程/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts Plus/GPC oauth-login aggregate entry-open failure from step 10', async () => {
|
||||
const plusGpcSteps = {
|
||||
6: { key: 'plus-checkout-create' },
|
||||
7: { key: 'plus-checkout-billing' },
|
||||
10: { key: 'oauth-login' },
|
||||
11: { key: 'fetch-login-code' },
|
||||
12: { key: 'confirm-oauth' },
|
||||
13: { key: 'platform-verify' },
|
||||
};
|
||||
const harness = createHarness({
|
||||
startStep: 10,
|
||||
failureStep: 10,
|
||||
failureBudget: 1,
|
||||
failureMessage: '步骤 10:判断失败后已重试 2 次,仍未成功。最后原因:点击登录入口后仍未进入手机号/邮箱/密码/验证码页。',
|
||||
authState: { state: 'entry_page', url: 'https://auth.openai.com/log-in' },
|
||||
stepDefinitions: plusGpcSteps,
|
||||
finalOAuthChainStartStep: 10,
|
||||
customState: {
|
||||
stepStatuses: { 3: 'completed', 6: 'completed', 7: 'completed' },
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
plusCheckoutSource: 'gpc-helper',
|
||||
},
|
||||
});
|
||||
|
||||
const events = await harness.run();
|
||||
|
||||
assert.deepStrictEqual(events.steps, [10, 10, 11, 12, 13]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [9]);
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 10 重新开始授权流程/.test(message)));
|
||||
assert.ok(!events.logs.some(({ message }) => /停止自动回到步骤 10 重开/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts Plus/GPC fetch-code and confirm-oauth failures from oauth-login step 10', async () => {
|
||||
const plusGpcSteps = {
|
||||
6: { key: 'plus-checkout-create' },
|
||||
7: { key: 'plus-checkout-billing' },
|
||||
10: { key: 'oauth-login' },
|
||||
11: { key: 'fetch-login-code' },
|
||||
12: { key: 'confirm-oauth' },
|
||||
13: { key: 'platform-verify' },
|
||||
};
|
||||
|
||||
for (const scenario of [
|
||||
{
|
||||
failureStep: 11,
|
||||
failureMessage: '步骤 11:无法获取新的登录验证码。',
|
||||
expectedSteps: [10, 11, 10, 11, 12, 13],
|
||||
},
|
||||
{
|
||||
failureStep: 12,
|
||||
failureMessage: '步骤 12:长时间未进入 OAuth 同意页,无法定位“继续”按钮。',
|
||||
expectedSteps: [10, 11, 12, 10, 11, 12, 13],
|
||||
},
|
||||
]) {
|
||||
const harness = createHarness({
|
||||
startStep: 10,
|
||||
failureStep: scenario.failureStep,
|
||||
failureBudget: 1,
|
||||
failureMessage: scenario.failureMessage,
|
||||
authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' },
|
||||
stepDefinitions: plusGpcSteps,
|
||||
finalOAuthChainStartStep: 10,
|
||||
customState: {
|
||||
stepStatuses: { 3: 'completed', 6: 'completed', 7: 'completed' },
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
plusCheckoutSource: 'gpc-helper',
|
||||
},
|
||||
});
|
||||
|
||||
const events = await harness.run();
|
||||
|
||||
assert.deepStrictEqual(events.steps, scenario.expectedSteps);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [9]);
|
||||
assert.ok(events.logs.some(({ message }) => new RegExp(`步骤 ${scenario.failureStep}:.*回到步骤 10 重新开始授权流程`).test(message)));
|
||||
}
|
||||
});
|
||||
|
||||
test('auto-run restarts Plus/GPC transient platform verify exchange failures from confirm-oauth step 12', async () => {
|
||||
const plusGpcSteps = {
|
||||
6: { key: 'plus-checkout-create' },
|
||||
7: { key: 'plus-checkout-billing' },
|
||||
10: { key: 'oauth-login' },
|
||||
11: { key: 'fetch-login-code' },
|
||||
12: { key: 'confirm-oauth' },
|
||||
13: { key: 'platform-verify' },
|
||||
};
|
||||
const harness = createHarness({
|
||||
startStep: 10,
|
||||
failureStep: 13,
|
||||
failureBudget: 1,
|
||||
failureMessage: 'token exchange failed at https://auth.openai.com/oauth/token: token_exchange_user_error: Invalid request. Please try again later.',
|
||||
authState: { state: 'oauth_consent_page', url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent' },
|
||||
stepDefinitions: plusGpcSteps,
|
||||
finalOAuthChainStartStep: 10,
|
||||
customState: {
|
||||
stepStatuses: { 3: 'completed', 6: 'completed', 7: 'completed' },
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
plusCheckoutSource: 'gpc-helper',
|
||||
panelMode: 'sub2api',
|
||||
},
|
||||
});
|
||||
|
||||
const events = await harness.run();
|
||||
|
||||
assert.deepStrictEqual(events.steps, [10, 11, 12, 13, 12, 13]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [11]);
|
||||
assert.ok(events.logs.some(({ message }) => /步骤 13:.*回到步骤 12 重新开始授权流程/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts Plus checkout from step 6 when checkout creation fails', async () => {
|
||||
const plusPaypalSteps = {
|
||||
6: { key: 'plus-checkout-create' },
|
||||
7: { key: 'plus-checkout-billing' },
|
||||
8: { key: 'paypal-approve' },
|
||||
9: { key: 'plus-checkout-return' },
|
||||
10: { key: 'oauth-login' },
|
||||
11: { key: 'fetch-login-code' },
|
||||
12: { key: 'confirm-oauth' },
|
||||
13: { key: 'platform-verify' },
|
||||
};
|
||||
const harness = createHarness({
|
||||
startStep: 6,
|
||||
failureStep: 6,
|
||||
failureBudget: 1,
|
||||
failureMessage: '步骤 6:创建 Plus Checkout 失败:checkout request failed',
|
||||
stepDefinitions: plusPaypalSteps,
|
||||
finalOAuthChainStartStep: 10,
|
||||
customState: {
|
||||
stepStatuses: { 3: 'completed' },
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal',
|
||||
},
|
||||
});
|
||||
|
||||
const events = await harness.run();
|
||||
|
||||
assert.deepStrictEqual(events.steps, [6, 6, 7, 8, 9, 10, 11, 12, 13]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 Plus Checkout/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts Plus checkout from step 6 when billing fails for non-free-trial reasons', async () => {
|
||||
const plusPaypalSteps = {
|
||||
6: { key: 'plus-checkout-create' },
|
||||
7: { key: 'plus-checkout-billing' },
|
||||
8: { key: 'paypal-approve' },
|
||||
9: { key: 'plus-checkout-return' },
|
||||
10: { key: 'oauth-login' },
|
||||
11: { key: 'fetch-login-code' },
|
||||
12: { key: 'confirm-oauth' },
|
||||
13: { key: 'platform-verify' },
|
||||
};
|
||||
const harness = createHarness({
|
||||
startStep: 6,
|
||||
failureStep: 7,
|
||||
failureBudget: 1,
|
||||
failureMessage: '步骤 7:账单地址 iframe 无法注入,请重新创建 checkout。',
|
||||
stepDefinitions: plusPaypalSteps,
|
||||
finalOAuthChainStartStep: 10,
|
||||
customState: {
|
||||
stepStatuses: { 3: 'completed' },
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal',
|
||||
},
|
||||
});
|
||||
|
||||
const events = await harness.run();
|
||||
|
||||
assert.deepStrictEqual(events.steps, [6, 7, 6, 7, 8, 9, 10, 11, 12, 13]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 Plus Checkout/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls', async () => {
|
||||
const plusGpcSteps = {
|
||||
6: { key: 'plus-checkout-create' },
|
||||
@@ -399,6 +662,36 @@ test('auto-run treats GPC account binding as recoverable step 6 restart', async
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
|
||||
});
|
||||
|
||||
test('auto-run restarts GPC checkout from step 6 when accessToken cannot be read', async () => {
|
||||
const plusGpcSteps = {
|
||||
6: { key: 'plus-checkout-create' },
|
||||
7: { key: 'plus-checkout-billing' },
|
||||
10: { key: 'oauth-login' },
|
||||
11: { key: 'fetch-login-code' },
|
||||
12: { key: 'confirm-oauth' },
|
||||
13: { key: 'platform-verify' },
|
||||
};
|
||||
const harness = createHarness({
|
||||
startStep: 6,
|
||||
failureStep: 6,
|
||||
failureBudget: 1,
|
||||
failureMessage: '步骤 6:GPC 模式获取 accessToken 失败。',
|
||||
stepDefinitions: plusGpcSteps,
|
||||
finalOAuthChainStartStep: 10,
|
||||
customState: {
|
||||
stepStatuses: { 3: 'completed' },
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
plusCheckoutSource: 'gpc-helper',
|
||||
},
|
||||
});
|
||||
|
||||
const events = await harness.run();
|
||||
|
||||
assert.deepStrictEqual(events.steps, [6, 6, 7, 10, 11, 12, 13]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts GPC checkout from step 6 when task status has no progress', async () => {
|
||||
const plusGpcSteps = {
|
||||
6: { key: 'plus-checkout-create' },
|
||||
@@ -429,6 +722,39 @@ test('auto-run restarts GPC checkout from step 6 when task status has no progres
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run keeps rebuilding GPC checkout beyond three failures', async () => {
|
||||
const plusGpcSteps = {
|
||||
6: { key: 'plus-checkout-create' },
|
||||
7: { key: 'plus-checkout-billing' },
|
||||
10: { key: 'oauth-login' },
|
||||
11: { key: 'fetch-login-code' },
|
||||
12: { key: 'confirm-oauth' },
|
||||
13: { key: 'platform-verify' },
|
||||
};
|
||||
const harness = createHarness({
|
||||
startStep: 6,
|
||||
failureStep: 7,
|
||||
failureBudget: 4,
|
||||
failureMessage: 'GPC_TASK_ENDED::GPC task status stalled, recreate the task.',
|
||||
stepDefinitions: plusGpcSteps,
|
||||
finalOAuthChainStartStep: 10,
|
||||
customState: {
|
||||
stepStatuses: { 3: 'completed' },
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
plusCheckoutSource: 'gpc-helper',
|
||||
},
|
||||
});
|
||||
|
||||
const events = await harness.run();
|
||||
|
||||
assert.deepStrictEqual(
|
||||
events.steps,
|
||||
[6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 10, 11, 12, 13]
|
||||
);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5, 5, 5, 5]);
|
||||
assert.ok(events.logs.some(({ message }) => /第 4 次/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run does not restart GPC checkout when Plus account has no free-trial eligibility', async () => {
|
||||
const plusGpcSteps = {
|
||||
6: { key: 'plus-checkout-create' },
|
||||
|
||||
@@ -126,6 +126,20 @@ test('account run history helper upgrades old records, keeps stopped items and s
|
||||
assert.equal(genericStoppedRecord.failureLabel, '流程已停止');
|
||||
assert.equal(genericStoppedRecord.failedStep, null);
|
||||
|
||||
const runningRecord = helpers.buildAccountRunHistoryRecord({
|
||||
email: 'run@b.com',
|
||||
password: 'z',
|
||||
autoRunning: true,
|
||||
autoRunCurrentRun: 1,
|
||||
autoRunTotalRuns: 2,
|
||||
autoRunAttemptRun: 1,
|
||||
}, 'running', '正在运行');
|
||||
assert.equal(runningRecord.finalStatus, 'running');
|
||||
assert.equal(runningRecord.failureLabel, '正在运行');
|
||||
assert.equal(runningRecord.failureDetail, '');
|
||||
assert.equal(runningRecord.failedStep, null);
|
||||
assert.equal(runningRecord.source, 'auto');
|
||||
|
||||
const normalizedStoppedRecord = helpers.normalizeAccountRunHistoryRecord({
|
||||
recordId: 'legacy-stop@example.com',
|
||||
email: 'legacy-stop@example.com',
|
||||
@@ -471,6 +485,7 @@ test('account run history helper clears persisted records and syncs full snapsho
|
||||
assert.deepStrictEqual(payload.summary, {
|
||||
total: 1,
|
||||
success: 0,
|
||||
running: 0,
|
||||
failed: 1,
|
||||
stopped: 0,
|
||||
retryTotal: 1,
|
||||
@@ -486,6 +501,7 @@ test('account run history helper clears persisted records and syncs full snapsho
|
||||
summary: {
|
||||
total: 0,
|
||||
success: 0,
|
||||
running: 0,
|
||||
failed: 0,
|
||||
stopped: 0,
|
||||
retryTotal: 0,
|
||||
@@ -586,6 +602,7 @@ test('account run history helper deletes selected records and syncs remaining sn
|
||||
summary: {
|
||||
total: 1,
|
||||
success: 1,
|
||||
running: 0,
|
||||
failed: 0,
|
||||
stopped: 0,
|
||||
retryTotal: 0,
|
||||
|
||||
@@ -1081,6 +1081,75 @@ test('GPC billing fails repeated checkout stage as stale so auto-run can recreat
|
||||
assert.equal(events.logs.some((entry) => entry.message === '步骤 7:GPC 任务状态:创建订单'), true);
|
||||
});
|
||||
|
||||
test('GPC billing fails unchanged visible created status even when hidden ids change', async () => {
|
||||
const originalNow = Date.now;
|
||||
let now = 1710000000000;
|
||||
let queryCount = 0;
|
||||
const fetchCalls = [];
|
||||
const { events, executor } = createExecutorHarness({
|
||||
frames: [],
|
||||
stateByFrame: {},
|
||||
sleepWithStop: async (ms) => {
|
||||
events.sleeps.push(ms);
|
||||
now += ms;
|
||||
},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_created') {
|
||||
queryCount += 1;
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => createGpcTaskResponse({
|
||||
task_id: 'task_created',
|
||||
phone_mode: 'auto',
|
||||
status: 'created',
|
||||
status_text: '已创建',
|
||||
remote_stage: '',
|
||||
api_waiting_for: '',
|
||||
reference_id: `ref_${queryCount}`,
|
||||
flow_id: `flow_${queryCount}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (url.endsWith('/api/gp/tasks/task_created/stop')) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => createGpcTaskResponse({
|
||||
task_id: 'task_created',
|
||||
status: 'discarded',
|
||||
status_text: '已停止',
|
||||
}),
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected url: ${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
Date.now = () => now;
|
||||
try {
|
||||
await assert.rejects(
|
||||
() => executor.executePlusCheckoutBilling({
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
plusCheckoutSource: 'gpc-helper',
|
||||
gopayHelperTaskId: 'task_created',
|
||||
gopayHelperPhoneMode: 'auto',
|
||||
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
|
||||
gopayHelperApiKey: 'gpc_auto',
|
||||
gopayHelperTaskStaleSeconds: 15,
|
||||
}),
|
||||
/GPC_TASK_ENDED::GPC 任务状态超过 15 秒无进展(已创建),请重新创建任务。/
|
||||
);
|
||||
} finally {
|
||||
Date.now = originalNow;
|
||||
}
|
||||
|
||||
assert.equal(queryCount > 1, true);
|
||||
assert.equal(fetchCalls.some((call) => call.url.endsWith('/api/gp/tasks/task_created/stop')), true);
|
||||
assert.equal(events.logs.some((entry) => entry.message === '步骤 7:GPC 任务状态:已创建'), true);
|
||||
});
|
||||
|
||||
test('GPC billing reads SMS OTP from local helper for sms_otp_wait', async () => {
|
||||
const fetchCalls = [];
|
||||
let pollCount = 0;
|
||||
|
||||
@@ -180,6 +180,112 @@ return { normalizeAccountRunHistoryHelperBaseUrlValue };
|
||||
);
|
||||
});
|
||||
|
||||
test('account records manager shows current auto-run account as running', () => {
|
||||
const source = fs.readFileSync('sidepanel/account-records-manager.js', 'utf8');
|
||||
const windowObject = {};
|
||||
const api = new Function('window', `${source}; return window.SidepanelAccountRecordsManager;`)(windowObject);
|
||||
|
||||
const latestState = {
|
||||
autoRunning: true,
|
||||
autoRunPhase: 'running',
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: 'running@example.com',
|
||||
email: 'running@example.com',
|
||||
accountRunHistory: [
|
||||
{
|
||||
recordId: 'running@example.com',
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: 'running@example.com',
|
||||
email: 'running@example.com',
|
||||
password: 'secret',
|
||||
finalStatus: 'stopped',
|
||||
finishedAt: '2026-04-17T04:32:00.000Z',
|
||||
retryCount: 0,
|
||||
failureLabel: '步骤 2 停止',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const list = createNode();
|
||||
const stats = createNode();
|
||||
const meta = createNode();
|
||||
const pageLabel = createNode();
|
||||
const manager = api.createAccountRecordsManager({
|
||||
state: {
|
||||
getLatestState: () => latestState,
|
||||
syncLatestState() {},
|
||||
},
|
||||
dom: {
|
||||
accountRecordsList: list,
|
||||
accountRecordsMeta: meta,
|
||||
accountRecordsPageLabel: pageLabel,
|
||||
accountRecordsStats: stats,
|
||||
},
|
||||
helpers: {
|
||||
escapeHtml: (value) => String(value || ''),
|
||||
},
|
||||
runtime: {
|
||||
sendMessage: async () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
manager.render();
|
||||
|
||||
assert.match(list.innerHTML, /is-running/);
|
||||
assert.match(list.innerHTML, /正在运行/);
|
||||
assert.doesNotMatch(list.innerHTML, /步骤 2 停止/);
|
||||
assert.match(stats.innerHTML, /data-account-record-filter="running"/);
|
||||
assert.match(stats.innerHTML, /<strong>1<\/strong>运行/);
|
||||
});
|
||||
|
||||
test('account records manager shows full failure detail before short label', () => {
|
||||
const source = fs.readFileSync('sidepanel/account-records-manager.js', 'utf8');
|
||||
const windowObject = {};
|
||||
const api = new Function('window', `${source}; return window.SidepanelAccountRecordsManager;`)(windowObject);
|
||||
|
||||
const latestState = {
|
||||
accountRunHistory: [
|
||||
{
|
||||
recordId: 'failed-detail@example.com',
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: 'failed-detail@example.com',
|
||||
email: 'failed-detail@example.com',
|
||||
finalStatus: 'failed',
|
||||
finishedAt: '2026-04-17T04:32:00.000Z',
|
||||
retryCount: 0,
|
||||
failureLabel: '步骤 2 失败',
|
||||
failureDetail: 'Duck 邮箱自动获取失败:未找到生成新 Duck 地址按钮。',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const list = createNode();
|
||||
const manager = api.createAccountRecordsManager({
|
||||
state: {
|
||||
getLatestState: () => latestState,
|
||||
syncLatestState() {},
|
||||
},
|
||||
dom: {
|
||||
accountRecordsList: list,
|
||||
accountRecordsMeta: createNode(),
|
||||
accountRecordsPageLabel: createNode(),
|
||||
accountRecordsStats: createNode(),
|
||||
},
|
||||
helpers: {
|
||||
escapeHtml: (value) => String(value || ''),
|
||||
},
|
||||
runtime: {
|
||||
sendMessage: async () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
manager.render();
|
||||
|
||||
assert.match(list.innerHTML, /Duck 邮箱自动获取失败/);
|
||||
assert.match(list.innerHTML, /title="failed-detail@example\.com\nDuck 邮箱自动获取失败/);
|
||||
assert.doesNotMatch(list.innerHTML, /account-record-item-summary">步骤 2 失败/);
|
||||
});
|
||||
|
||||
test('account records manager supports filter chips and partial multi-select delete', async () => {
|
||||
const source = fs.readFileSync('sidepanel/account-records-manager.js', 'utf8');
|
||||
const windowObject = {};
|
||||
|
||||
@@ -561,6 +561,139 @@ return {
|
||||
assert.equal(api.getLogs().some(({ message, level }) => level === 'warn' && /已完成 5 次重试/.test(message)), true);
|
||||
});
|
||||
|
||||
test('waitForSignupEntryState treats hidden signup action on collapsed logged-out home as retryable entry', async () => {
|
||||
const api = new Function(`
|
||||
const logs = [];
|
||||
const clicks = [];
|
||||
let now = 0;
|
||||
|
||||
const signupButton = {
|
||||
textContent: '免费注册',
|
||||
value: '',
|
||||
disabled: false,
|
||||
getAttribute(name) {
|
||||
if (name === 'type') return 'button';
|
||||
return '';
|
||||
},
|
||||
getBoundingClientRect() {
|
||||
return { width: 0, height: 0 };
|
||||
},
|
||||
};
|
||||
|
||||
const document = {
|
||||
querySelector() {
|
||||
return null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'a, button, [role="button"], [role="link"]') {
|
||||
return [signupButton];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
const window = {
|
||||
innerWidth: 0,
|
||||
innerHeight: 0,
|
||||
outerWidth: 159,
|
||||
outerHeight: 27,
|
||||
};
|
||||
|
||||
const location = {
|
||||
href: 'https://chatgpt.com/',
|
||||
};
|
||||
|
||||
const Date = {
|
||||
now() {
|
||||
return now;
|
||||
},
|
||||
};
|
||||
|
||||
${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')}
|
||||
${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')}
|
||||
${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
|
||||
|
||||
function isVisibleElement(el) {
|
||||
const rect = el?.getBoundingClientRect?.() || { width: 0, height: 0 };
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}
|
||||
|
||||
function isActionEnabled(el) {
|
||||
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
|
||||
}
|
||||
|
||||
function getActionText(el) {
|
||||
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.replace(/\\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getPageTextSnapshot() {
|
||||
return '跳至内容 ChatGPT 登录 我们先从哪里开始呢?';
|
||||
}
|
||||
|
||||
function getSignupPasswordInput() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function isSignupPasswordPage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getSignupPasswordSubmitButton() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSignupPasswordDisplayedEmail() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function throwIfStopped() {}
|
||||
|
||||
function log(message, level = 'info') {
|
||||
logs.push({ message, level });
|
||||
}
|
||||
|
||||
async function humanPause() {}
|
||||
|
||||
function simulateClick(target) {
|
||||
clicks.push(getActionText(target));
|
||||
}
|
||||
|
||||
async function sleep(ms) {
|
||||
now += ms;
|
||||
}
|
||||
|
||||
${extractFunction('getSignupEmailInput')}
|
||||
${extractFunction('getSignupPhoneInput')}
|
||||
${extractFunction('getSignupEmailContinueButton')}
|
||||
${extractFunction('findSignupEntryTrigger')}
|
||||
${extractFunction('inspectSignupEntryState')}
|
||||
${extractFunction('waitForSignupEntryState')}
|
||||
|
||||
return {
|
||||
async run() {
|
||||
return waitForSignupEntryState({ timeout: 30000, autoOpenEntry: true, step: 2 });
|
||||
},
|
||||
getClicks() {
|
||||
return clicks.slice();
|
||||
},
|
||||
getLogs() {
|
||||
return logs.slice();
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const snapshot = await api.run();
|
||||
|
||||
assert.equal(snapshot.state, 'entry_home');
|
||||
assert.equal(api.getClicks().length, 6);
|
||||
assert.equal(api.getLogs().some(({ message }) => message.includes('注册入口仍处于不可见状态')), true);
|
||||
assert.equal(api.getLogs().some(({ message, level }) => level === 'warn' && /已完成 5 次重试/.test(message)), true);
|
||||
});
|
||||
|
||||
test('ensureSignupPhoneEntryReady opens free signup before switching to the phone entry', async () => {
|
||||
const api = new Function(`
|
||||
const logs = [];
|
||||
|
||||
Reference in New Issue
Block a user