From c3827d2cfc02a1e972fefcbcef1d514db5ae4662 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 10 May 2026 04:25:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E8=AE=B0=E5=BD=95=E7=8A=B6=E6=80=81=E5=A4=84?= =?UTF-8?q?=E7=90=86=EF=BC=8C=E7=A1=AE=E4=BF=9D=E7=9C=9F=E5=AE=9E=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E6=AD=A5=E9=AA=A4=E4=B8=8D=E8=A2=AB=E8=AF=AF=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E4=B8=BA=E6=8C=87=E5=AF=BC=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 28 ++++- background/account-run-history.js | 98 +++++++++++++-- background/auto-run-controller.js | 116 ++++++++++++++++-- ...kground-account-run-history-module.test.js | 48 ++++++++ tests/background-auto-run-module.test.js | 28 +++++ 5 files changed, 296 insertions(+), 22 deletions(-) diff --git a/background.js b/background.js index 574c2f4..b19f70a 100644 --- a/background.js +++ b/background.js @@ -10448,6 +10448,28 @@ async function runAutoSequenceFromStep(startStep, context = {}) { let currentStartStep = startStep; let continueCurrentAttempt = continued; const resolvedSignupMethod = await ensureResolvedSignupMethodForRun(); + const normalizePlusPaymentMethodForRun = typeof normalizePlusPaymentMethod === 'function' + ? normalizePlusPaymentMethod + : (value) => (String(value || '').trim().toLowerCase() === 'gpc-helper' ? 'gpc-helper' : String(value || '').trim().toLowerCase()); + const plusPaymentMethodGpcHelper = typeof PLUS_PAYMENT_METHOD_GPC_HELPER === 'string' + ? PLUS_PAYMENT_METHOD_GPC_HELPER + : 'gpc-helper'; + const attachFailedStep = (error, step) => { + const failedStep = Math.floor(Number(step) || 0); + if (!error || typeof error !== 'object' || failedStep <= 0) { + return error; + } + + if (!Number.isInteger(Number(error.failedStep)) || Number(error.failedStep) <= 0) { + try { + error.failedStep = failedStep; + } catch (_err) { + // Some host errors may be non-extensible; state-based inference still covers normal paths. + } + } + + return error; + }; while (true) { @@ -10487,6 +10509,7 @@ async function runAutoSequenceFromStep(startStep, context = {}) { try { await executeStepAndWait(3, AUTO_STEP_DELAYS[3]); } catch (err) { + attachFailedStep(err, 3); if (isStopError(err)) { throw err; } @@ -10533,6 +10556,7 @@ async function runAutoSequenceFromStep(startStep, context = {}) { await executeStepAndWait(step, AUTO_STEP_DELAYS[step]); step += 1; } catch (err) { + attachFailedStep(err, step); if (isStopError(err)) { throw err; } @@ -10540,8 +10564,8 @@ async function runAutoSequenceFromStep(startStep, context = {}) { const stepExecutionKey = typeof getStepExecutionKeyForState === 'function' ? getStepExecutionKeyForState(step, latestState) : ''; - const isGpcCheckoutStep = normalizePlusPaymentMethod(latestState?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GPC_HELPER - || String(latestState?.plusCheckoutSource || '').trim() === PLUS_PAYMENT_METHOD_GPC_HELPER; + const isGpcCheckoutStep = normalizePlusPaymentMethodForRun(latestState?.plusPaymentMethod) === plusPaymentMethodGpcHelper + || String(latestState?.plusCheckoutSource || '').trim() === plusPaymentMethodGpcHelper; if (isGpcCheckoutStep && (stepExecutionKey === 'plus-checkout-create' || stepExecutionKey === 'plus-checkout-billing') && isGpcCheckoutRestartRequiredFailure(err)) { diff --git a/background/account-run-history.js b/background/account-run-history.js index 0a73f29..e80908a 100644 --- a/background/account-run-history.js +++ b/background/account-run-history.js @@ -39,7 +39,7 @@ return ''; } - function extractRecordStep(status = '', detail = '') { + function extractRecordStepFromStatus(status = '') { const normalizedStatus = String(status || '').trim().toLowerCase(); const statusMatch = normalizedStatus.match(/^step(\d+)_(?:failed|stopped)$/); if (statusMatch) { @@ -47,6 +47,21 @@ return Number.isInteger(step) && step > 0 ? step : null; } + return null; + } + + function extractRecordStepFromDetailPrefix(detail = '') { + const text = String(detail || '').trim(); + const detailMatch = text.match(/^(?:Step\s+(\d+)|步骤\s*(\d+))\s*(?::|:|失败|停止|已|\b)/i); + if (!detailMatch) { + return null; + } + + const step = Number(detailMatch[1] || detailMatch[2]); + return Number.isInteger(step) && step > 0 ? step : null; + } + + function extractAnyRecordStepFromDetail(detail = '') { const text = String(detail || '').trim(); const detailMatch = text.match(/(?:Step\s+(\d+)|步骤\s*(\d+))/i); if (!detailMatch) { @@ -57,6 +72,76 @@ return Number.isInteger(step) && step > 0 ? step : null; } + function extractRecordStep(status = '', detail = '') { + return extractRecordStepFromStatus(status) || extractRecordStepFromDetailPrefix(detail); + } + + function parseFailureLabelStep(label = '') { + const match = String(label || '').trim().match(/^步骤\s*(\d+)\s*(?:失败|停止)$/); + if (!match) { + return null; + } + const step = Number(match[1]); + return Number.isInteger(step) && step > 0 ? step : null; + } + + function shouldIgnorePersistedFailedStepCandidate(candidate, failureDetail = '') { + if (!Number.isInteger(candidate) || candidate <= 0) { + return true; + } + + const text = String(failureDetail || '').trim(); + if (!text) { + return false; + } + + const leadingStep = extractRecordStepFromDetailPrefix(text); + if (Number.isInteger(leadingStep) && leadingStep > 0) { + return leadingStep !== candidate; + } + + const incidentalStep = extractAnyRecordStepFromDetail(text); + return incidentalStep === candidate; + } + + function resolveNormalizedFailedStep(record = {}, failureDetail = '') { + const explicitStatusStep = extractRecordStepFromStatus(record.finalStatus || record.status || ''); + if (Number.isInteger(explicitStatusStep) && explicitStatusStep > 0) { + return explicitStatusStep; + } + + const detailStep = extractRecordStepFromDetailPrefix(failureDetail); + if (Number.isInteger(detailStep) && detailStep > 0) { + return detailStep; + } + + const failedStepCandidate = Number(record.failedStep); + if (Number.isInteger(failedStepCandidate) + && failedStepCandidate > 0 + && !shouldIgnorePersistedFailedStepCandidate(failedStepCandidate, failureDetail)) { + return failedStepCandidate; + } + + return null; + } + + function resolveFailureLabel(finalStatus, rawFailureLabel = '', computedFailureLabel = '', failedStep = null) { + const rawLabel = String(rawFailureLabel || '').trim(); + if (finalStatus === 'stopped') { + return computedFailureLabel; + } + if (finalStatus !== 'failed') { + return rawLabel || computedFailureLabel; + } + + const rawStep = parseFailureLabelStep(rawLabel); + if (Number.isInteger(rawStep) && rawStep > 0) { + return rawStep === failedStep ? rawLabel : computedFailureLabel; + } + + return rawLabel || computedFailureLabel; + } + function isPhoneVerificationFailure(detail = '') { const text = String(detail || '').trim(); if (!text) { @@ -247,10 +332,9 @@ const failureDetail = finalStatus === 'failed' || finalStatus === 'stopped' ? String(record.failureDetail || record.reason || '').trim() : ''; - const failedStepCandidate = Number(record.failedStep); - const failedStep = Number.isInteger(failedStepCandidate) && failedStepCandidate > 0 - ? failedStepCandidate - : extractRecordStep(record.finalStatus || record.status || '', failureDetail); + const failedStep = finalStatus === 'failed' || finalStatus === 'stopped' + ? resolveNormalizedFailedStep(record, failureDetail) + : null; const autoRunContext = normalizeAutoRunContext(record.autoRunContext); const retryCount = normalizeRetryCount( record.retryCount !== undefined @@ -271,9 +355,7 @@ finalStatus, finishedAt, retryCount, - failureLabel: finalStatus === 'stopped' - ? computedFailureLabel - : (rawFailureLabel || computedFailureLabel), + failureLabel: resolveFailureLabel(finalStatus, rawFailureLabel, computedFailureLabel, failedStep), failureDetail, failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null, source, diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index 7c9e063..89a8b1f 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -85,6 +85,95 @@ return Math.max(0, Number(summary?.attempts || 0) - 1); } + function normalizeRecordStep(value) { + const step = Math.floor(Number(value) || 0); + return step > 0 ? step : null; + } + + function extractStepFromRecordStatus(status = '') { + const match = String(status || '').trim().toLowerCase().match(/^step(\d+)_(?:failed|stopped)$/); + if (!match) { + return null; + } + return normalizeRecordStep(match[1]); + } + + function getKnownStepIdsFromState(state = {}) { + const ids = new Set(); + for (const key of Object.keys(state?.stepStatuses || {})) { + const step = normalizeRecordStep(key); + if (step) { + ids.add(step); + } + } + + const currentStep = normalizeRecordStep(state?.currentStep); + if (currentStep) { + ids.add(currentStep); + } + + return Array.from(ids).sort((left, right) => left - right); + } + + function inferRecordStepFromState(state = {}, preferredStatuses = []) { + const statuses = state?.stepStatuses || {}; + const preferredStatusSet = new Set(preferredStatuses.map((item) => String(item || '').trim()).filter(Boolean)); + const stepIds = getKnownStepIdsFromState(state); + const currentStep = normalizeRecordStep(state?.currentStep); + + if (currentStep && preferredStatusSet.has(String(statuses[currentStep] || '').trim())) { + return currentStep; + } + + const matchingSteps = stepIds + .filter((step) => preferredStatusSet.has(String(statuses[step] || '').trim())) + .sort((left, right) => right - left); + if (matchingSteps.length) { + return matchingSteps[0]; + } + + if (currentStep) { + const currentStatus = String(statuses[currentStep] || '').trim(); + if (!['', 'pending', 'completed', 'manual_completed', 'skipped'].includes(currentStatus)) { + return currentStep; + } + } + + return null; + } + + function inferRecordStepFromError(errorLike = null) { + if (!errorLike || typeof errorLike !== 'object') { + return null; + } + + return normalizeRecordStep(errorLike.failedStep) + || normalizeRecordStep(errorLike.step) + || normalizeRecordStep(errorLike.currentStep); + } + + function resolveAutoRunAccountRecordStatus(status, state = {}, errorLike = null) { + const normalizedStatus = String(status || '').trim().toLowerCase(); + const explicitStep = extractStepFromRecordStatus(normalizedStatus); + if (explicitStep) { + return normalizedStatus; + } + + if (normalizedStatus === 'failed') { + const failedStep = inferRecordStepFromError(errorLike) + || inferRecordStepFromState(state, ['failed', 'running']); + return failedStep ? `step${failedStep}_failed` : status; + } + + if (normalizedStatus === 'stopped') { + const stoppedStep = inferRecordStepFromError(errorLike) + || inferRecordStepFromState(state, ['stopped', 'running']); + return stoppedStep ? `step${stoppedStep}_stopped` : status; + } + + return status; + } + function formatAutoRunFailureReasons(reasons = []) { if (!Array.isArray(reasons) || !reasons.length) { return '未知错误'; @@ -456,7 +545,7 @@ forceFreshTabsNextRun = false; } - const appendRoundRecordIfNeeded = async (status, reason = '') => { + const appendRoundRecordIfNeeded = async (status, reason = '', errorLike = null) => { if (roundRecordAppended) { return; } @@ -465,7 +554,9 @@ return; } - const record = await appendAccountRunRecord(status, null, reason); + const recordState = await getState(); + const recordStatus = resolveAutoRunAccountRecordStatus(status, recordState, errorLike); + const record = await appendAccountRunRecord(recordStatus, recordState, reason); if (record) { roundRecordAppended = true; } @@ -507,7 +598,7 @@ } catch (err) { if (isStopError(err)) { stoppedEarly = true; - await appendRoundRecordIfNeeded('stopped', getErrorMessage(err)); + await appendRoundRecordIfNeeded('stopped', getErrorMessage(err), err); await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn'); await broadcastAutoRunStatus('stopped', { currentRun: targetRun, @@ -556,7 +647,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason); + await appendRoundRecordIfNeeded('failed', reason, err); cancelPendingCommands('当前轮因认证流程进入 add-phone 已终止。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { @@ -591,7 +682,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason); + await appendRoundRecordIfNeeded('failed', reason, err); cancelPendingCommands('当前轮因接码号池暂无可用号码已终止。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { @@ -626,7 +717,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason); + await appendRoundRecordIfNeeded('failed', reason, err); cancelPendingCommands('当前轮因 Plus 免费试用资格不可用已终止。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { @@ -661,7 +752,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason); + await appendRoundRecordIfNeeded('failed', reason, err); cancelPendingCommands('当前轮因 GPC 任务已结束。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { @@ -696,7 +787,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason); + await appendRoundRecordIfNeeded('failed', reason, err); cancelPendingCommands('当前轮因 user_already_exists 已终止。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { @@ -731,7 +822,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason); + await appendRoundRecordIfNeeded('failed', reason, err); cancelPendingCommands('当前轮因步骤 4 连续 405 错误已终止。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { @@ -787,7 +878,7 @@ } catch (sleepError) { if (isStopError(sleepError)) { stoppedEarly = true; - await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError)); + await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError), sleepError); await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn'); await broadcastAutoRunStatus('stopped', { currentRun: targetRun, @@ -811,7 +902,7 @@ } catch (sleepError) { if (isStopError(sleepError)) { stoppedEarly = true; - await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError)); + await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError), sleepError); await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn'); await broadcastAutoRunStatus('stopped', { currentRun: targetRun, @@ -833,7 +924,7 @@ await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); - await appendRoundRecordIfNeeded('failed', reason); + await appendRoundRecordIfNeeded('failed', reason, err); if (!autoRunSkipFailures) { cancelPendingCommands('当前轮执行失败。'); await broadcastStopToContentScripts(); @@ -948,6 +1039,7 @@ handleAutoRunLoopUnhandledError, logAutoRunFinalSummary, normalizeAutoRunRoundSummary, + resolveAutoRunAccountRecordStatus, serializeAutoRunRoundSummaries, skipAutoRunCountdown, startAutoRunLoop, diff --git a/tests/background-account-run-history-module.test.js b/tests/background-account-run-history-module.test.js index d944d5f..c26d19c 100644 --- a/tests/background-account-run-history-module.test.js +++ b/tests/background-account-run-history-module.test.js @@ -198,6 +198,54 @@ test('account run history helper accepts phone-only records without forcing emai assert.equal(normalized.finalStatus, 'failed'); }); +test('account run history does not turn prerequisite guidance into a fake step 2 failure', () => { + const source = fs.readFileSync('background/account-run-history.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope); + + const helpers = api.createAccountRunHistoryHelpers({ + chrome: { storage: { local: { get: async () => ({}), set: async () => {} } } }, + getState: async () => ({}), + normalizeAccountRunHistoryHelperBaseUrl: (value) => String(value || '').trim(), + }); + + const genericFailedRecord = helpers.buildAccountRunHistoryRecord({ + email: 'late@example.com', + password: 'secret', + autoRunning: true, + autoRunCurrentRun: 1, + autoRunTotalRuns: 3, + autoRunAttemptRun: 2, + }, 'failed', '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。'); + + assert.equal(genericFailedRecord.failedStep, null); + assert.equal(genericFailedRecord.failureLabel, '流程失败'); + + const explicitFailedRecord = helpers.buildAccountRunHistoryRecord({ + email: 'late@example.com', + password: 'secret', + autoRunning: true, + autoRunCurrentRun: 1, + autoRunTotalRuns: 3, + autoRunAttemptRun: 2, + }, 'step10_failed', '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。'); + + assert.equal(explicitFailedRecord.failedStep, 10); + assert.equal(explicitFailedRecord.failureLabel, '步骤 10 失败'); + + const migratedOldRecord = helpers.normalizeAccountRunHistoryRecord({ + email: 'old@example.com', + password: 'secret', + finalStatus: 'failed', + failureLabel: '步骤 2 失败', + failureDetail: '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。', + failedStep: 2, + }); + + assert.equal(migratedOldRecord.failedStep, null); + assert.equal(migratedOldRecord.failureLabel, '流程失败'); +}); + test('account run history merges email and phone identities from the same run', async () => { const source = fs.readFileSync('background/account-run-history.js', 'utf8'); const globalScope = {}; diff --git a/tests/background-auto-run-module.test.js b/tests/background-auto-run-module.test.js index a293013..7152ccc 100644 --- a/tests/background-auto-run-module.test.js +++ b/tests/background-auto-run-module.test.js @@ -15,3 +15,31 @@ test('auto-run controller module exposes a factory', () => { assert.equal(typeof api?.createAutoRunController, 'function'); }); + +test('auto-run account record status preserves the real failed step instead of parsing guidance text', () => { + const source = fs.readFileSync('background/auto-run-controller.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope); + const controller = api.createAutoRunController({}); + + const state = { + currentStep: 11, + stepStatuses: { + 2: 'completed', + 10: 'completed', + 11: 'failed', + }, + }; + const error = new Error('缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。'); + + assert.equal( + controller.resolveAutoRunAccountRecordStatus('failed', state, error), + 'step11_failed' + ); + + error.failedStep = 13; + assert.equal( + controller.resolveAutoRunAccountRecordStatus('failed', state, error), + 'step13_failed' + ); +});