diff --git a/background.js b/background.js index 65d93678..6f542114 100644 --- a/background.js +++ b/background.js @@ -4619,9 +4619,13 @@ async function setEmailState(email, options = {}) { await setEmailStateSilently(email, options); if (email) { const latestState = await getState(); - const recordStatus = shouldMarkAccountRunRecordRunning(latestState) ? 'running' : 'node:submit-signup-email:stopped'; - const recordReason = recordStatus === 'running' ? '正在运行' : '节点 submit-signup-email 已使用邮箱,流程尚未完成。'; - await appendManualAccountRunRecordIfNeeded(recordStatus, latestState, recordReason); + if (!shouldMarkAccountRunRecordRunning(latestState)) { + await appendManualAccountRunRecordIfNeeded( + 'node:submit-signup-email:stopped', + latestState, + '节点 submit-signup-email 已使用邮箱,流程尚未完成。' + ); + } await resumeAutoRunIfWaitingForEmail(); } } @@ -4700,9 +4704,13 @@ async function setSignupPhoneState(phoneNumber) { await setSignupPhoneStateSilently(phoneNumber); if (String(phoneNumber || '').trim()) { const latestState = await getState(); - const recordStatus = shouldMarkAccountRunRecordRunning(latestState) ? 'running' : 'node:submit-signup-email:stopped'; - const recordReason = recordStatus === 'running' ? '正在运行' : '节点 submit-signup-email 已使用手机号,流程尚未完成。'; - await appendManualAccountRunRecordIfNeeded(recordStatus, latestState, recordReason); + if (!shouldMarkAccountRunRecordRunning(latestState)) { + await appendManualAccountRunRecordIfNeeded( + 'node:submit-signup-email:stopped', + latestState, + '节点 submit-signup-email 已使用手机号,流程尚未完成。' + ); + } } } @@ -10985,6 +10993,27 @@ async function handleStepData(step, payload) { }), signupVerificationRequestedAt: null, }); + if (payload.skipProfileStep) { + const latestState = await getState(); + const step5NodeId = getNodeIdByStepForState(5, latestState); + const step5Status = step5NodeId ? latestState.nodeStatuses?.[step5NodeId] : ''; + if (step5NodeId && step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') { + await setNodeStatus(step5NodeId, 'skipped'); + if (payload.skipProfileStepReason === 'combined_verification_profile') { + await addLog('步骤 4:当前验证码页已内嵌完成注册资料提交,已自动跳过步骤 5。', 'warn'); + } else { + await addLog('步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。', 'warn'); + } + } + if (payload.skipRegistrationWaitStep) { + const step6NodeId = getNodeIdByStepForState(6, latestState); + const step6Status = step6NodeId ? latestState.nodeStatuses?.[step6NodeId] : ''; + if (step6NodeId && step6Status !== 'running' && step6Status !== 'completed' && step6Status !== 'manual_completed') { + await setNodeStatus(step6NodeId, 'skipped'); + await addLog('步骤 4:账号已进入 ChatGPT 已登录态,已自动跳过步骤 6,流程将直接进入步骤 7。', 'warn'); + } + } + } break; case 8: await setState({ @@ -11308,9 +11337,8 @@ async function completeNodeFromBackground(nodeId, payload = {}) { nodeId: normalizedNodeId, }); } + await runCompletedNodeSideEffects(normalizedNodeId, payload, completionState, lastNodeId); notifyNodeComplete(normalizedNodeId, payload); - void runCompletedNodeSideEffects(normalizedNodeId, payload, completionState, lastNodeId) - .catch((error) => reportCompletedNodeSideEffectError(normalizedNodeId, error)); return; } diff --git a/background/account-run-history.js b/background/account-run-history.js index c647eae0..dae17a93 100644 --- a/background/account-run-history.js +++ b/background/account-run-history.js @@ -32,9 +32,6 @@ if (normalized === 'success') { return 'success'; } - if (normalized === 'running' || /_running$/.test(normalized) || /^node:[^:]+:running$/.test(normalized)) { - return 'running'; - } if (normalized === 'failed' || /_failed$/.test(normalized) || /^node:[^:]+:failed$/.test(normalized)) { return 'failed'; } @@ -210,9 +207,6 @@ if (finalStatus === 'success') { return '流程完成'; } - if (finalStatus === 'running') { - return '正在运行'; - } if (finalStatus === 'stopped') { if (failedNodeId) { return `节点 ${getNodeDisplayName(failedNodeId, state)} 停止`; diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index c704504f..ec08a388 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -681,7 +681,6 @@ for (let targetRun = resumeCurrentRun; targetRun <= totalRuns; targetRun += 1) { const roundSummary = roundSummaries[targetRun - 1]; - let roundRecordAppended = false; const resumingCurrentRound = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun; let attemptRun = resumingCurrentRound ? resumeAttemptRun : 1; let reuseExistingProgress = resumingCurrentRound; @@ -756,21 +755,14 @@ forceFreshTabsNextRun = false; } - const appendRoundRecordIfNeeded = async (status, reason = '', errorLike = null) => { - if (roundRecordAppended) { - return; - } - + const appendRoundRecord = async (status, reason = '', errorLike = null, stateOverride = null) => { if (typeof appendAccountRunRecord !== 'function') { return; } - const recordState = await getState(); + const recordState = stateOverride || await getState(); const recordStatus = resolveAutoRunAccountRecordStatus(status, recordState, errorLike); - const record = await appendAccountRunRecord(recordStatus, recordState, reason); - if (record) { - roundRecordAppended = true; - } + return appendAccountRunRecord(recordStatus, recordState, reason); }; try { @@ -810,7 +802,7 @@ } catch (err) { if (isStopError(err)) { stoppedEarly = true; - await appendRoundRecordIfNeeded('stopped', getErrorMessage(err), err); + await appendRoundRecord('stopped', getErrorMessage(err), err); await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn'); await broadcastAutoRunStatus('stopped', { currentRun: targetRun, @@ -871,7 +863,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason, err); + await appendRoundRecord('failed', reason, err); cancelPendingCommands('当前轮因认证流程进入 add-phone 已终止。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { @@ -906,7 +898,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason, err); + await appendRoundRecord('failed', reason, err); cancelPendingCommands('当前轮因接码号池暂无可用号码已终止。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { @@ -941,7 +933,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason, err); + await appendRoundRecord('failed', reason, err); cancelPendingCommands('当前轮因 Plus 免费试用资格不可用已终止。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { @@ -976,7 +968,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason, err); + await appendRoundRecord('failed', reason, err); cancelPendingCommands('当前轮因 GPC 页面流程已结束。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { @@ -1011,7 +1003,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason, err); + await appendRoundRecord('failed', reason, err); cancelPendingCommands('当前轮因 user_already_exists 已终止。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { @@ -1046,7 +1038,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason, err); + await appendRoundRecord('failed', reason, err); cancelPendingCommands('当前轮因步骤 4 连续 405 错误已终止。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { @@ -1081,7 +1073,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason, err); + await appendRoundRecord('failed', reason, err); cancelPendingCommands('当前轮检测到 Kiro 代理异常页,已停止自动运行,等待用户切换代理。'); await broadcastStopToContentScripts(); await addLog(`第 ${targetRun}/${totalRuns} 轮检测到 Kiro 代理异常页:${reason}`, 'error'); @@ -1123,7 +1115,7 @@ } catch (sleepError) { if (isStopError(sleepError)) { stoppedEarly = true; - await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError), sleepError); + await appendRoundRecord('stopped', getErrorMessage(sleepError), sleepError); await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn'); await broadcastAutoRunStatus('stopped', { currentRun: targetRun, @@ -1147,7 +1139,7 @@ } catch (sleepError) { if (isStopError(sleepError)) { stoppedEarly = true; - await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError), sleepError); + await appendRoundRecord('stopped', getErrorMessage(sleepError), sleepError); await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn'); await broadcastAutoRunStatus('stopped', { currentRun: targetRun, @@ -1169,7 +1161,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason, err); + await appendRoundRecord('failed', reason, err); if (!autoRunSkipFailures) { cancelPendingCommands('当前轮执行失败。'); await broadcastStopToContentScripts(); diff --git a/background/message-router.js b/background/message-router.js index 9d1eecf1..587b9502 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -1014,6 +1014,13 @@ await addLog('步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。', 'warn'); } } + if (payload.skipRegistrationWaitStep) { + const step6Status = getNodeStatusByStep(6, latestState); + if (step6Status !== 'running' && step6Status !== 'completed' && step6Status !== 'manual_completed') { + await setNodeStatusByStep(6, 'skipped', latestState); + await addLog('步骤 4:账号已进入 ChatGPT 已登录态,已自动跳过步骤 6,流程将直接进入步骤 7。', 'warn'); + } + } } break; case 7: @@ -1141,10 +1148,10 @@ stepKey: nodeId, }); } - await handleStepData(resolvedStep, completionPayload); if (isFinalNode && typeof appendAccountRunRecord === 'function') { await appendAccountRunRecord('success', completionState); } + await handleStepData(resolvedStep, completionPayload); notifyNodeComplete(nodeId, completionPayload); return { ok: true }; } diff --git a/background/verification-flow.js b/background/verification-flow.js index 137adb35..02b9ff6a 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -78,12 +78,18 @@ : ''; } - const isRetryableVerificationTransportError = typeof deps.isRetryableContentScriptTransportError === 'function' + const baseRetryableVerificationTransportError = typeof deps.isRetryableContentScriptTransportError === 'function' ? deps.isRetryableContentScriptTransportError : ((error) => /back\/forward cache|message channel is closed|Receiving end does not exist|port closed before a response was received|A listener indicated an asynchronous response|内容脚本\s+\d+(?:\.\d+)?\s*秒内未响应|did not respond in \d+s/i.test( String(typeof error === 'string' ? error : error?.message || '') )); + function isRetryableVerificationTransportError(error) { + const message = String(typeof error === 'string' ? error : error?.message || ''); + return Boolean(baseRetryableVerificationTransportError(error)) + || /页面刚完成跳转或刷新,内容脚本还没有重新接回/i.test(message); + } + function getVerificationCodeStateKey(step) { return step === 4 ? 'lastSignupCode' : 'lastLoginCode'; } @@ -199,6 +205,7 @@ success: true, reason: 'chatgpt_home', skipProfileStep: true, + skipRegistrationWaitStep: true, url: currentUrl, }; } @@ -1176,6 +1183,7 @@ assumed: true, transportRecovered: true, skipProfileStep: Boolean(fallback.skipProfileStep), + skipRegistrationWaitStep: Boolean(fallback.skipRegistrationWaitStep), url: fallback.url, }; } @@ -1439,6 +1447,7 @@ code: result.code, phoneVerificationRequired: Boolean(submitResult.addPhonePage), ...(step === 4 && submitResult?.skipProfileStep ? { skipProfileStep: true } : {}), + ...(step === 4 && submitResult?.skipRegistrationWaitStep ? { skipRegistrationWaitStep: true } : {}), ...(step === 4 && submitResult?.skipProfileStepReason ? { skipProfileStepReason: submitResult.skipProfileStepReason } : {}), diff --git a/flows/openai/content/openai-auth.js b/flows/openai/content/openai-auth.js index 6c3d12f4..e0278e40 100644 --- a/flows/openai/content/openai-auth.js +++ b/flows/openai/content/openai-auth.js @@ -778,6 +778,7 @@ function inspectSignupEntryState() { return { state: 'logged_in_home', skipProfileStep: true, + skipRegistrationWaitStep: true, url: postVerificationState.url || location.href, }; } @@ -2707,6 +2708,7 @@ async function step3_fillEmailPassword(payload) { skippedPasswordPage: true, deferredSubmit: false, ...(snapshot.skipProfileStep ? { skipProfileStep: true } : {}), + ...(snapshot.skipRegistrationWaitStep ? { skipRegistrationWaitStep: true } : {}), }; log('步骤 3:当前页面已进入验证码或后续阶段,密码页按已跳过处理。', 'warn'); reportComplete(3, completionPayload); @@ -3065,6 +3067,7 @@ function getStep4PostVerificationState(options = {}) { return { state: 'logged_in_home', skipProfileStep: true, + skipRegistrationWaitStep: true, url: location.href, }; } @@ -5040,6 +5043,7 @@ function inspectSignupVerificationState() { return { state: 'logged_in_home', skipProfileStep: true, + skipRegistrationWaitStep: true, url: postVerificationState.url || location.href, }; } @@ -5200,11 +5204,12 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { } if (snapshot.state === 'logged_in_home') { - log(`${prepareLogLabel}:页面已直接进入 ChatGPT 已登录态,本步骤按已完成处理,并将跳过步骤 5。`, 'ok'); + log(`${prepareLogLabel}:页面已直接进入 ChatGPT 已登录态,本步骤按已完成处理,并将跳过步骤 5/6。`, 'ok'); return { ready: true, alreadyVerified: true, skipProfileStep: true, + skipRegistrationWaitStep: true, retried: recoveryRound, prepareSource, }; @@ -5219,6 +5224,7 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { skipLoginVerificationStep: true, directOAuthConsentPage: true, skipProfileStep: true, + skipRegistrationWaitStep: true, retried: recoveryRound, prepareSource, }; @@ -5341,6 +5347,7 @@ async function waitForVerificationSubmitOutcome(step, timeout, options = {}) { return { success: true, skipProfileStep: true, + skipRegistrationWaitStep: true, url: postVerificationState.url || location.href, }; } @@ -5384,6 +5391,7 @@ async function waitForVerificationSubmitOutcome(step, timeout, options = {}) { return { success: true, skipProfileStep: true, + skipRegistrationWaitStep: true, url: postVerificationState.url || location.href, }; } @@ -5521,6 +5529,7 @@ async function fillVerificationCode(step, payload) { assumed: true, alreadyAdvanced: true, skipProfileStep: true, + skipRegistrationWaitStep: true, url: postVerificationState.url || location.href, }; } diff --git a/sidepanel/account-records-manager.js b/sidepanel/account-records-manager.js index 33d1d920..b7a368b9 100644 --- a/sidepanel/account-records-manager.js +++ b/sidepanel/account-records-manager.js @@ -138,7 +138,8 @@ if (!isAutoRunRecordDisplayRunning(currentState)) { return record; } - if (getRecordDisplayStatus(record) === 'success') { + const persistedStatus = String(record.finalStatus || '').trim().toLowerCase(); + if (persistedStatus === 'success') { return record; } @@ -154,6 +155,72 @@ }; } + function buildRuntimeRunningRecord(currentState = {}) { + if (!isAutoRunRecordDisplayRunning(currentState)) { + return null; + } + + const recordId = buildCurrentAccountRecordId(currentState); + if (!recordId) { + return null; + } + + const accountIdentifierType = String(currentState.accountIdentifierType || '').trim().toLowerCase() === 'phone' + ? 'phone' + : 'email'; + 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(); + const retryCount = Math.max(0, Math.floor(Number(currentState.autoRunAttemptRun) || 0) - 1); + const sessionId = Number(currentState.autoRunSessionId) || 0; + const currentRun = Math.max(0, Math.floor(Number(currentState.autoRunCurrentRun) || 0)); + const totalRuns = Math.max(0, Math.floor(Number(currentState.autoRunTotalRuns) || 0)); + const attemptRun = Math.max(0, Math.floor(Number(currentState.autoRunAttemptRun) || 0)); + const phase = String(currentState.autoRunPhase || '').trim().toLowerCase(); + const phaseLabel = phase === 'waiting_step' + ? '等待步骤执行' + : (phase === 'waiting_email' + ? '等待邮箱' + : (phase === 'retrying' ? '等待重试' : '正在运行')); + const positionLabel = currentRun > 0 && totalRuns > 0 + ? `第 ${currentRun}/${totalRuns} 轮` + : ''; + const attemptLabel = attemptRun > 0 ? `第 ${attemptRun} 次尝试` : ''; + const summary = [phaseLabel, positionLabel, attemptLabel].filter(Boolean).join(' · '); + + return { + recordId, + runtimeRecord: true, + runtimeSessionId: sessionId, + accountIdentifierType, + accountIdentifier, + email, + phoneNumber, + finalStatus: 'running', + displayStatus: 'running', + displaySummary: summary || '正在运行', + failureLabel: '正在运行', + failureDetail: '', + finishedAt: new Date().toISOString(), + retryCount, + source: 'auto', + autoRunContext: { + currentRun, + totalRuns, + attemptRun, + }, + }; + } + function getRecordIdentifierType(record = {}) { const rawType = String(record.accountIdentifierType || '').trim().toLowerCase(); if (rawType === 'phone') { @@ -222,11 +289,20 @@ } function getAccountRunRecords(currentState = state.getLatestState()) { - return (Array.isArray(currentState?.accountRunHistory) ? currentState.accountRunHistory : []) + const persistedRecords = (Array.isArray(currentState?.accountRunHistory) ? currentState.accountRunHistory : []) .filter((item) => item && typeof item === 'object') .slice() .sort((left, right) => normalizeTimestamp(right.finishedAt) - normalizeTimestamp(left.finishedAt)) .map((record) => applyRunningDisplayState(record, currentState)); + const runtimeRecord = buildRuntimeRunningRecord(currentState); + if (!runtimeRecord) { + return persistedRecords; + } + + const runtimeRecordId = buildRecordId(runtimeRecord); + const filteredRecords = persistedRecords.filter((record) => buildRecordId(record) !== runtimeRecordId); + return [runtimeRecord, ...filteredRecords] + .sort((left, right) => normalizeTimestamp(right.finishedAt) - normalizeTimestamp(left.finishedAt)); } function summarizeAccountRunHistory(records = []) { @@ -642,6 +718,11 @@ const shouldSelect = forceSelected === null ? !selectedRecordIds.has(normalizedRecordId) : Boolean(forceSelected); + const records = getAccountRunRecords(); + const targetRecord = records.find((record) => buildRecordId(record) === normalizedRecordId); + if (targetRecord?.runtimeRecord) { + return; + } if (shouldSelect) { selectedRecordIds.add(normalizedRecordId); @@ -711,7 +792,10 @@ throw new Error(response.error); } - const existingRecords = getAccountRunRecords(); + const existingRecords = (Array.isArray(state.getLatestState()?.accountRunHistory) + ? state.getLatestState().accountRunHistory + : [] + ).filter((item) => item && typeof item === 'object'); const selectedIds = new Set(recordIds); const nextRecords = existingRecords.filter((record) => !selectedIds.has(buildRecordId(record))); diff --git a/tests/auto-run-add-phone-stop.test.js b/tests/auto-run-add-phone-stop.test.js index 74015ed3..61d079ba 100644 --- a/tests/auto-run-add-phone-stop.test.js +++ b/tests/auto-run-add-phone-stop.test.js @@ -1409,3 +1409,175 @@ test('auto-run controller retries 5sim rate limit failures instead of treating c assert.equal(runtime.state.autoRunActive, false); assert.equal(runtime.state.autoRunSessionId, 0); }); + +test('auto-run retryable failure keeps retry state out of persisted account history', async () => { + const events = { + logs: [], + broadcasts: [], + accountRecords: [], + runCalls: 0, + sleeps: [], + }; + let currentState = { + stepStatuses: {}, + tabRegistry: {}, + sourceLastUrls: {}, + autoRunning: false, + autoRunPhase: '', + autoRunCurrentRun: 0, + autoRunTotalRuns: 0, + autoRunAttemptRun: 0, + autoRunSessionId: 0, + accountIdentifierType: 'email', + accountIdentifier: 'retry@example.com', + email: 'retry@example.com', + }; + + const runtime = { + state: { + autoRunActive: false, + autoRunCurrentRun: 0, + autoRunTotalRuns: 1, + autoRunAttemptRun: 0, + autoRunSessionId: 0, + }, + get() { + return { ...this.state }; + }, + set(updates = {}) { + this.state = { ...this.state, ...updates }; + }, + }; + + let sessionSeed = 0; + + const controller = api.createAutoRunController({ + addLog: async (message, level = 'info') => { + events.logs.push({ message, level }); + }, + appendAccountRunRecord: async (status, state, reason) => { + events.accountRecords.push({ + status, + reason, + attemptRun: state?.autoRunAttemptRun ?? 0, + email: state?.email || '', + }); + return { status, reason }; + }, + AUTO_RUN_MAX_RETRIES_PER_ROUND: 3, + AUTO_RUN_RETRY_DELAY_MS: 3000, + AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry', + AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds', + broadcastAutoRunStatus: async (phase, payload = {}) => { + events.broadcasts.push({ phase, ...payload }); + currentState = { + ...currentState, + autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase), + autoRunPhase: phase, + autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun, + autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns, + autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun, + autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId, + }; + }, + broadcastStopToContentScripts: async () => {}, + cancelPendingCommands: () => {}, + clearStopRequest: () => {}, + createAutoRunSessionId: () => { + sessionSeed += 1; + return sessionSeed; + }, + getAutoRunStatusPayload: (phase, payload = {}) => ({ + autoRunning: ['running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase), + autoRunPhase: phase, + autoRunCurrentRun: payload.currentRun ?? 0, + autoRunTotalRuns: payload.totalRuns ?? 1, + autoRunAttemptRun: payload.attemptRun ?? 0, + autoRunSessionId: payload.sessionId ?? 0, + }), + getErrorMessage: (error) => error?.message || String(error || ''), + getFirstUnfinishedStep: () => 1, + getPendingAutoRunTimerPlan: () => null, + getRunningSteps: () => [], + getState: async () => ({ + ...currentState, + stepStatuses: { ...(currentState.stepStatuses || {}) }, + tabRegistry: { ...(currentState.tabRegistry || {}) }, + sourceLastUrls: { ...(currentState.sourceLastUrls || {}) }, + }), + getStopRequested: () => false, + hasSavedProgress: () => false, + isAddPhoneAuthFailure: () => false, + isPhoneSmsPlatformRateLimitFailure: () => false, + isRestartCurrentAttemptError: () => false, + isSignupUserAlreadyExistsFailure: () => false, + isStopError: () => false, + launchAutoRunTimerPlan: async () => false, + normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)), + persistAutoRunTimerPlan: async () => ({}), + resetState: async () => { + currentState = { + ...currentState, + stepStatuses: {}, + tabRegistry: {}, + sourceLastUrls: {}, + accountIdentifierType: 'email', + accountIdentifier: '', + email: null, + }; + }, + runAutoSequenceFromStep: async () => { + events.runCalls += 1; + if (events.runCalls === 1) { + currentState = { + ...currentState, + accountIdentifierType: 'email', + accountIdentifier: 'retry@example.com', + email: 'retry@example.com', + }; + throw new Error('temporary upstream failure'); + } + currentState = { + ...currentState, + accountIdentifierType: 'email', + accountIdentifier: 'retry-2@example.com', + email: 'retry-2@example.com', + }; + }, + runtime, + setState: async (updates = {}) => { + currentState = { + ...currentState, + ...updates, + stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses, + tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry, + sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls, + }; + }, + sleepWithStop: async (ms) => { + events.sleeps.push(ms); + }, + throwIfAutoRunSessionStopped: (sessionId) => { + if (sessionId && sessionId !== runtime.state.autoRunSessionId) { + throw new Error('stopped'); + } + }, + waitForRunningStepsToFinish: async () => currentState, + chrome: { + runtime: { + sendMessage() { + return Promise.resolve(); + }, + }, + }, + }); + + await controller.autoRunLoop(1, { + autoRunSkipFailures: true, + mode: 'restart', + }); + + assert.equal(events.runCalls, 2); + assert.equal(events.accountRecords.length, 0); + assert.equal(events.broadcasts.some(({ phase }) => phase === 'retrying'), true); +}); diff --git a/tests/background-account-run-history-module.test.js b/tests/background-account-run-history-module.test.js index ecdbb757..fac4211d 100644 --- a/tests/background-account-run-history-module.test.js +++ b/tests/background-account-run-history-module.test.js @@ -151,11 +151,7 @@ test('account run history helper upgrades old records, keeps stopped items and s 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'); + assert.equal(runningRecord, null); const normalizedStoppedRecord = helpers.normalizeAccountRunHistoryRecord({ recordId: 'legacy-stop@example.com', diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js index 7dff9088..9d8c6705 100644 --- a/tests/background-message-router-step2-skip.test.js +++ b/tests/background-message-router-step2-skip.test.js @@ -398,6 +398,25 @@ test('message router skips step 5 when step 4 reports already logged-in transiti assert.equal(events.logs[0]?.message, '步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。'); }); +test('message router skips steps 5 and 6 when step 4 reaches logged-in home', async () => { + const { router, events } = createRouter({ + state: { stepStatuses: { 5: 'pending', 6: 'pending' } }, + }); + + await router.handleStepData(4, { + emailTimestamp: 123, + skipProfileStep: true, + skipRegistrationWaitStep: true, + }); + + assert.deepStrictEqual(events.stepStatuses, [ + { step: 5, status: 'skipped' }, + { step: 6, status: 'skipped' }, + ]); + assert.equal(events.logs[0]?.message, '步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。'); + assert.equal(events.logs[1]?.message, '步骤 4:账号已进入 ChatGPT 已登录态,已自动跳过步骤 6,流程将直接进入步骤 7。'); +}); + test('message router skips login-code step when oauth login lands on consent page', async () => { const stepKeys = { 7: 'oauth-login', diff --git a/tests/sidepanel-account-records-manager.test.js b/tests/sidepanel-account-records-manager.test.js index 32802788..61929a05 100644 --- a/tests/sidepanel-account-records-manager.test.js +++ b/tests/sidepanel-account-records-manager.test.js @@ -286,6 +286,75 @@ test('account records manager shows full failure detail before short label', () assert.doesNotMatch(list.innerHTML, /account-record-item-summary">步骤 2 失败/); }); +test('account records manager renders runtime running entry from live state instead of persisted running history', () => { + 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: 'retrying', + autoRunCurrentRun: 2, + autoRunTotalRuns: 5, + autoRunAttemptRun: 3, + autoRunSessionId: 77, + 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', + finalStatus: 'stopped', + finishedAt: '2026-04-17T04:32:00.000Z', + retryCount: 0, + failureLabel: '姝ラ 2 鍋滄', + }, + { + recordId: 'failed@example.com', + accountIdentifierType: 'email', + accountIdentifier: 'failed@example.com', + email: 'failed@example.com', + finalStatus: 'failed', + finishedAt: '2026-04-17T04:30:00.000Z', + retryCount: 1, + failureLabel: '姝ラ 8 澶辫触', + }, + ], + }; + + 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, /is-running/); + assert.match(list.innerHTML, /等待重试/); + assert.match(list.innerHTML, /第 2\/5 轮/); + assert.match(list.innerHTML, /第 3 次尝试/); + assert.match(list.innerHTML, /failed@example\.com/); + assert.doesNotMatch(list.innerHTML, /姝ラ 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 = {}; diff --git a/tests/step4-submit-retry-recovery.test.js b/tests/step4-submit-retry-recovery.test.js index 0035e176..e2f473ba 100644 --- a/tests/step4-submit-retry-recovery.test.js +++ b/tests/step4-submit-retry-recovery.test.js @@ -195,6 +195,7 @@ return { assert.deepStrictEqual(result, { success: true, skipProfileStep: true, + skipRegistrationWaitStep: true, url: 'https://chatgpt.com/', }); }); diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js index 8e9d21c3..bc25dae7 100644 --- a/tests/verification-flow-polling.test.js +++ b/tests/verification-flow-polling.test.js @@ -1618,6 +1618,43 @@ test('verification flow uses resilient openai-auth transport when submitting ver assert.ok(resilientCalls[0].options.timeoutMs >= 30000); }); +test('verification flow treats step 4 resilient reconnect timeout as success when tab reached logged-in home', async () => { + const logs = []; + + const helpers = api.createVerificationFlowHelpers({ + addLog: async (message, level) => { + logs.push({ message, level }); + }, + chrome: { + tabs: { + update: async () => {}, + get: async () => ({ + id: 1, + url: 'https://chatgpt.com/', + }), + }, + }, + getTabId: async () => 1, + isRetryableContentScriptTransportError: (error) => /did not respond/i.test(String(error?.message || error || '')), + sendToContentScriptResilient: async () => { + throw new Error('认证页 页面刚完成跳转或刷新,内容脚本还没有重新接回;扩展已自动重试,但仍未恢复。请重试当前步骤。'); + }, + sleepWithStop: async () => {}, + }); + + const result = await helpers.submitVerificationCode(4, '654321'); + + assert.deepStrictEqual(result, { + success: true, + assumed: true, + transportRecovered: true, + skipProfileStep: true, + skipRegistrationWaitStep: true, + url: 'https://chatgpt.com/', + }); + assert.equal(logs.some(({ message }) => /ChatGPT 已登录首页/.test(message)), true); +}); + test('verification flow does not replay step 8 code submit after transient auth-page transport failure', async () => { const directMessages = []; const resilientMessages = [];