From 691ac39514bf67bb0f638d660bef98e4165792b3 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 3 May 2026 19:25:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=AD=A5=E9=AA=A4=E5=9C=A8?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E4=B8=AD=E7=9A=84=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 250 ++++++++++++------ background/logging-status.js | 22 +- background/message-router.js | 25 +- background/phone-verification-flow.js | 73 ++++- background/steps/confirm-oauth.js | 35 ++- background/steps/fetch-login-code.js | 40 ++- background/steps/oauth-login.js | 35 ++- background/steps/platform-verify.js | 70 +++-- background/tab-runtime.js | 21 +- background/verification-flow.js | 56 +++- content/signup-page.js | 161 ++++++----- content/sub2api-panel.js | 18 +- content/utils.js | 23 +- content/vps-panel.js | 37 +-- sidepanel/sidepanel.js | 4 +- tests/background-contribution-mode.test.js | 22 +- ...ckground-message-router-step2-skip.test.js | 6 +- ...ckground-platform-verify-codex2api.test.js | 8 +- ...background-platform-verify-cpa-api.test.js | 14 +- tests/phone-verification-flow.test.js | 17 +- tests/step6-passwordless-otp-login.test.js | 6 +- tests/step8-restart-step7-error.test.js | 4 +- 项目完整链路说明.md | 12 + 项目开发规范(AI协作).md | 9 + 项目文件结构说明.md | 20 +- 25 files changed, 691 insertions(+), 297 deletions(-) diff --git a/background.js b/background.js index 86a1012..b8b1af6 100644 --- a/background.js +++ b/background.js @@ -443,6 +443,18 @@ function getStepDefinitionForState(step, state = {}) { return getStepDefinitionsForState(state).find((definition) => Number(definition.id) === numericStep) || null; } +function getStepIdByKeyForState(stepKey, state = {}) { + const normalizedKey = String(stepKey || '').trim(); + if (!normalizedKey) return null; + const ids = getStepIdsForState(state); + for (const id of ids) { + if (String(getStepDefinitionForState(id, state)?.key || '').trim() === normalizedKey) { + return Number(id); + } + } + return null; +} + initializeSessionStorageAccess(); setupDeclarativeNetRequestRules(); @@ -6395,13 +6407,20 @@ async function sendToMailContentScriptResilient(mail, message, options = {}) { // Logging // ============================================================ -async function addLog(message, level = 'info') { +async function addLog(message, level = 'info', options = {}) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.addLog) { - return loggingStatus.addLog(message, level); + return loggingStatus.addLog(message, level, options); } const state = await getState(); const logs = state.logs || []; - const entry = { message, level, timestamp: Date.now() }; + const step = Math.floor(Number(options?.step) || 0); + const entry = { + message: String(message || ''), + level, + timestamp: Date.now(), + step: step > 0 ? step : null, + stepKey: String(options?.stepKey || '').trim(), + }; logs.push(entry); if (logs.length > 500) logs.splice(0, logs.length - 500); await setState({ logs }); @@ -7484,13 +7503,14 @@ async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY await sleepWithStop(duration); } -async function clickWithDebugger(tabId, rect) { +async function clickWithDebugger(tabId, rect, options = {}) { + const visibleStep = Math.floor(Number(options.visibleStep) || 0) || 9; throwIfStopped(); if (!tabId) { throw new Error('未找到用于调试点击的认证页面标签页。'); } if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) { - throw new Error('步骤 9 的调试器兜底点击需要有效的按钮坐标。'); + throw new Error(`步骤 ${visibleStep} 的调试器兜底点击需要有效的按钮坐标。`); } const target = { tabId }; @@ -7498,7 +7518,7 @@ async function clickWithDebugger(tabId, rect) { await chrome.debugger.attach(target, '1.3'); } catch (err) { throw new Error( - `步骤 9 的调试器兜底点击附加失败:${err.message}。` + + `步骤 ${visibleStep} 的调试器兜底点击附加失败:${err.message}。` + '如果认证页标签已打开 DevTools,请先关闭后重试。' ); } @@ -7843,7 +7863,7 @@ async function runCompletedStepSideEffects(step, payload, completionState, lastS async function reportCompletedStepSideEffectError(step, error) { const message = getErrorMessage(error); console.warn(LOG_PREFIX, `[completeStepFromBackground] step ${step} post-completion side effect failed:`, error); - await addLog(`步骤 ${step} 已完成,但完成后的收尾处理失败:${message}`, 'warn'); + await addLog(`已完成,但完成后的收尾处理失败:${message}`, 'warn', { step }); } async function completeStepFromBackground(step, payload = {}) { @@ -7860,7 +7880,7 @@ async function completeStepFromBackground(step, payload = {}) { : (typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10); const completionState = step === lastStepId ? latestState : null; await setStepStatus(step, 'completed'); - await addLog(`步骤 ${step} 已完成`, 'ok'); + await addLog('已完成', 'ok', { step }); if (step === lastStepId) { notifyStepComplete(step, payload); @@ -7891,13 +7911,13 @@ async function finalizeDeferredStepExecutionError(step, error) { if (isStopError(error)) { await setStepStatus(step, 'stopped'); - await addLog(`步骤 ${step} 已被用户停止`, 'warn'); + await addLog('已被用户停止', 'warn', { step }); await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, latestState, getErrorMessage(error)); return; } await setStepStatus(step, 'failed'); - await addLog(`步骤 ${step} 失败:${getErrorMessage(error)}`, 'error'); + await addLog(`失败:${getErrorMessage(error)}`, 'error', { step }); await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, latestState, getErrorMessage(error)); } @@ -8159,7 +8179,7 @@ async function executeStep(step, options = {}) { throwIfStopped(); try { await setStepStatus(step, 'running'); - await addLog(`步骤 ${step} 开始执行`); + await addLog('开始执行', 'info', { step }); await humanStepDelay(); const fetchRetryPolicy = typeof getStepFetchNetworkRetryPolicy === 'function' ? getStepFetchNetworkRetryPolicy(step) @@ -8222,7 +8242,7 @@ async function executeStep(step, options = {}) { const errorState = await getState(); if (isStopError(err)) { await setStepStatus(step, 'stopped'); - await addLog(`步骤 ${step} 已被用户停止`, 'warn'); + await addLog('已被用户停止', 'warn', { step }); await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, errorState, getErrorMessage(err)); throw err; } @@ -8236,7 +8256,7 @@ async function executeStep(step, options = {}) { } if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step, errorState) && isRetryableContentScriptTransportError(err))) { await setStepStatus(step, 'failed'); - await addLog(`步骤 ${step} 失败:${err.message}`, 'error'); + await addLog(`失败:${err.message}`, 'error', { step }); await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, errorState, getErrorMessage(err)); } else { console.warn( @@ -10209,7 +10229,10 @@ async function refreshOAuthUrlBeforeStep6(state, options = {}) { throw new Error(`步骤 ${visibleStep}:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。`); } if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) { - await addLog(`步骤 ${visibleStep}:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...`, 'info'); + await addLog('contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...', 'info', { + step: visibleStep, + stepKey: 'oauth-login', + }); const contributionState = await contributionOAuthManager.startContributionFlow({ nickname: state.contributionNickname || '', openAuthTab: false, @@ -10222,7 +10245,10 @@ async function refreshOAuthUrlBeforeStep6(state, options = {}) { await handleStepData(1, { oauthUrl }); return oauthUrl; } - await addLog(`步骤 ${visibleStep}:contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info'); + await addLog(`contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info', { + step: visibleStep, + stepKey: 'oauth-login', + }); console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel'); const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: `步骤 ${visibleStep}` }); await handleStepData(1, refreshResult); @@ -10490,7 +10516,9 @@ async function getPostStep6AutoRestartDecision(step, error) { } async function getLoginAuthStateFromContent(options = {}) { - const { logMessage = '步骤 8:认证页正在切换,等待页面重新就绪后继续确认验证码页状态...' } = options; + const visibleStep = Math.floor(Number(options.visibleStep || options.logStep || options.step) || 0); + const logStep = visibleStep > 0 ? visibleStep : null; + const { logMessage = '认证页正在切换,等待页面重新就绪后继续确认验证码页状态...' } = options; const result = await sendToContentScriptResilient( 'signup-page', { @@ -10503,6 +10531,8 @@ async function getLoginAuthStateFromContent(options = {}) { retryDelayMs: options.retryDelayMs ?? 600, responseTimeoutMs: options.responseTimeoutMs ?? (options.timeoutMs ?? 15000), logMessage, + logStep, + logStepKey: options.logStepKey || '', } ); @@ -10553,7 +10583,9 @@ async function ensureStep8VerificationPageReady(options = {}) { timeoutMs: recoverTimeoutMs, responseTimeoutMs: recoverTimeoutMs, retryDelayMs: 700, - logMessage: `步骤 ${visibleStep}:认证页进入重试/超时报错状态,正在尝试点击“重试”恢复...`, + logMessage: '认证页进入重试/超时报错状态,正在尝试点击“重试”恢复...', + logStep: visibleStep, + logStepKey: 'fetch-login-code', } ); } else if (typeof sendToContentScript === 'function') { @@ -10567,7 +10599,10 @@ async function ensureStep8VerificationPageReady(options = {}) { } recovered = Boolean(recoverResult?.recovered || Number(recoverResult?.clickCount) > 0); if (recovered && typeof addLog === 'function') { - await addLog(`步骤 ${visibleStep}:认证页已点击“重试”,正在重新确认验证码页状态...`, 'warn'); + await addLog('认证页已点击“重试”,正在重新确认验证码页状态...', 'warn', { + step: visibleStep, + stepKey: 'fetch-login-code', + }); } } catch (recoverError) { const recoverMessage = getErrorMessage(recoverError); @@ -10575,7 +10610,10 @@ async function ensureStep8VerificationPageReady(options = {}) { throw recoverError; } if (typeof addLog === 'function') { - await addLog(`步骤 ${visibleStep}:认证页“重试”恢复失败:${recoverMessage}`, 'warn'); + await addLog(`认证页“重试”恢复失败:${recoverMessage}`, 'warn', { + step: visibleStep, + stepKey: 'fetch-login-code', + }); } } @@ -10584,7 +10622,8 @@ async function ensureStep8VerificationPageReady(options = {}) { timeoutMs: 10000, responseTimeoutMs: 10000, retryDelayMs: 500, - logMessage: `步骤 ${visibleStep}:认证页恢复后,正在确认验证码页是否可继续...`, + logMessage: '认证页恢复后,正在确认验证码页是否可继续...', + logStepKey: 'fetch-login-code', }); if (pageState.state === 'verification_page' || pageState.state === 'oauth_consent_page') { return pageState; @@ -10614,7 +10653,9 @@ async function ensureStep8VerificationPageReady(options = {}) { async function rerunStep7ForStep8Recovery(options = {}) { const { - logMessage = '步骤 8:正在回到步骤 7,重新发起登录验证码流程...', + logMessage = '正在回到授权登录步骤,重新发起登录验证码流程...', + logStep = null, + logStepKey = 'fetch-login-code', postStepDelayMs = 3000, } = options; @@ -10623,9 +10664,12 @@ async function rerunStep7ForStep8Recovery(options = {}) { const authLoginStep = typeof getAuthChainStartStepId === 'function' ? getAuthChainStartStepId(initialState) : FINAL_OAUTH_CHAIN_START_STEP; - await addLog(logMessage, 'warn'); + await addLog(logMessage, 'warn', { + step: logStep, + stepKey: logStepKey, + }); await setStepStatus(authLoginStep, 'running'); - await addLog(`步骤 ${authLoginStep} 开始执行`); + await addLog('开始执行', 'info', { step: authLoginStep }); try { await step7Executor.executeStep7({ @@ -10636,7 +10680,7 @@ async function rerunStep7ForStep8Recovery(options = {}) { const latestState = await getState(); if (isStopError(err)) { await setStepStatus(authLoginStep, 'stopped'); - await addLog(`步骤 ${authLoginStep} 已被用户停止`, 'warn'); + await addLog('已被用户停止', 'warn', { step: authLoginStep }); await appendManualAccountRunRecordIfNeeded(`step${authLoginStep}_stopped`, latestState, getErrorMessage(err)); throw err; } @@ -10645,7 +10689,7 @@ async function rerunStep7ForStep8Recovery(options = {}) { throw new Error(STOP_ERROR_MESSAGE); } await setStepStatus(authLoginStep, 'failed'); - await addLog(`步骤 ${authLoginStep} 失败:${getErrorMessage(err)}`, 'error'); + await addLog(`失败:${getErrorMessage(err)}`, 'error', { step: authLoginStep }); await appendManualAccountRunRecordIfNeeded(`step${authLoginStep}_failed`, latestState, getErrorMessage(err)); throw err; } @@ -10752,21 +10796,24 @@ function throwIfStep8SettledOrStopped(isSettled = false) { } async function ensureStep8SignupPageReady(tabId, options = {}) { + const visibleStep = Math.floor(Number(options.visibleStep || options.logStep || options.step) || 0); await ensureContentScriptReadyOnTab('signup-page', tabId, { inject: SIGNUP_PAGE_INJECT_FILES, injectSource: 'signup-page', timeoutMs: options.timeoutMs ?? 15000, retryDelayMs: options.retryDelayMs ?? 600, logMessage: options.logMessage || '', + logStep: visibleStep > 0 ? visibleStep : null, + logStepKey: options.logStepKey || '', }); } -async function getStep8PageState(tabId, responseTimeoutMs = 1500) { +async function getStep8PageState(tabId, responseTimeoutMs = 1500, visibleStep = 9) { try { const result = await sendTabMessageWithTimeout(tabId, 'signup-page', { type: 'STEP8_GET_STATE', source: 'background', - payload: {}, + payload: { visibleStep }, }, responseTimeoutMs); if (result?.error) { throw new Error(result.error); @@ -10780,13 +10827,14 @@ async function getStep8PageState(tabId, responseTimeoutMs = 1500) { } } -async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) { +async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS, options = {}) { + const visibleStep = Math.floor(Number(options.visibleStep) || 0) || 9; const start = Date.now(); let recovered = false; while (Date.now() - start < timeoutMs) { throwIfStopped(); - const pageState = await getStep8PageState(tabId); + const pageState = await getStep8PageState(tabId, 1500, visibleStep); if (pageState?.maxCheckAttemptsBlocked) { throw new Error(`${CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX}${CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE}`); } @@ -10796,17 +10844,17 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) const urlPart = pageState?.url ? ` URL: ${pageState.url}` : ''; throw new Error( pageState?.phoneVerificationPage - ? `步骤 9:当前认证页进入手机验证码页,但未开启接码功能,无法继续自动授权。${urlPart}`.trim() - : `步骤 9:当前认证页进入手机号页面,但未开启接码功能,无法继续自动授权。${urlPart}`.trim() + ? `步骤 ${visibleStep}:当前认证页进入手机验证码页,但未开启接码功能,无法继续自动授权。${urlPart}`.trim() + : `步骤 ${visibleStep}:当前认证页进入手机号页面,但未开启接码功能,无法继续自动授权。${urlPart}`.trim() ); } - await phoneVerificationHelpers.completePhoneVerificationFlow(tabId, pageState); + await phoneVerificationHelpers.completePhoneVerificationFlow(tabId, pageState, { visibleStep }); recovered = false; await sleepWithStop(250); continue; } if (pageState?.addPhonePage) { - throw new Error('步骤 9:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。'); + throw new Error(`步骤 ${visibleStep}:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。`); } if (pageState?.retryPage) { const retryUrl = String(pageState?.url || '').trim(); @@ -10816,7 +10864,7 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) || /\/sign-in-with-chatgpt\/[^/?#]+\/consent(?:[/?#]|$)/i.test(retryUrl) ); if (!consentLikeRetry) { - throw new Error(`步骤 9:当前认证页已进入重试页,当前流程将直接报错。URL: ${pageState.url || 'unknown'}`); + throw new Error(`步骤 ${visibleStep}:当前认证页已进入重试页,当前流程将直接报错。URL: ${pageState.url || 'unknown'}`); } } if (pageState?.consentReady) { @@ -10826,7 +10874,9 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) recovered = true; await ensureStep8SignupPageReady(tabId, { timeoutMs: Math.min(10000, timeoutMs), - logMessage: '步骤 9:认证页内容脚本已失联,正在等待页面重新就绪...', + visibleStep, + logStepKey: 'confirm-oauth', + logMessage: '认证页内容脚本已失联,正在等待页面重新就绪...', }); continue; } @@ -10834,25 +10884,30 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) await sleepWithStop(250); } - throw new Error('步骤 9:长时间未进入 OAuth 同意页,无法定位“继续”按钮。'); + throw new Error(`步骤 ${visibleStep}:长时间未进入 OAuth 同意页,无法定位“继续”按钮。`); } async function prepareStep8DebuggerClick(tabId, options = {}) { const timeoutMs = options.timeoutMs ?? 15000; const responseTimeoutMs = options.responseTimeoutMs ?? timeoutMs; + const visibleStep = Math.floor(Number(options.visibleStep) || 0) || 9; await ensureStep8SignupPageReady(tabId, { timeoutMs, - logMessage: '步骤 9:认证页内容脚本已失联,正在恢复后继续定位按钮...', + visibleStep, + logStepKey: 'confirm-oauth', + logMessage: '认证页内容脚本已失联,正在恢复后继续定位按钮...', }); const result = await sendToContentScriptResilient('signup-page', { type: 'STEP8_FIND_AND_CLICK', source: 'background', - payload: {}, + payload: { visibleStep }, }, { timeoutMs, responseTimeoutMs, retryDelayMs: 600, - logMessage: '步骤 9:认证页正在切换,等待 OAuth 同意页按钮重新就绪...', + logMessage: '认证页正在切换,等待 OAuth 同意页按钮重新就绪...', + logStep: visibleStep, + logStepKey: 'confirm-oauth', }); if (result?.error) { @@ -10865,14 +10920,18 @@ async function prepareStep8DebuggerClick(tabId, options = {}) { async function triggerStep8ContentStrategy(tabId, strategy, options = {}) { const timeoutMs = options.timeoutMs ?? 15000; const responseTimeoutMs = options.responseTimeoutMs ?? timeoutMs; + const visibleStep = Math.floor(Number(options.visibleStep) || 0) || 9; await ensureStep8SignupPageReady(tabId, { timeoutMs, - logMessage: '步骤 9:认证页内容脚本已失联,正在恢复后继续点击“继续”按钮...', + visibleStep, + logStepKey: 'confirm-oauth', + logMessage: '认证页内容脚本已失联,正在恢复后继续点击“继续”按钮...', }); const result = await sendToContentScriptResilient('signup-page', { type: 'STEP8_TRIGGER_CONTINUE', source: 'background', payload: { + visibleStep, strategy, findTimeoutMs: 4000, enabledTimeoutMs: 3000, @@ -10881,7 +10940,9 @@ async function triggerStep8ContentStrategy(tabId, strategy, options = {}) { timeoutMs, responseTimeoutMs, retryDelayMs: 600, - logMessage: '步骤 9:认证页正在切换,等待“继续”按钮重新就绪...', + logMessage: '认证页正在切换,等待“继续”按钮重新就绪...', + logStep: visibleStep, + logStepKey: 'confirm-oauth', }); if (result?.error) { @@ -10895,10 +10956,13 @@ async function recoverAuthRetryPageOnTab(tabId, payload = {}, options = {}) { const readyTimeoutMs = options.readyTimeoutMs ?? 15000; const timeoutMs = options.timeoutMs ?? 15000; const responseTimeoutMs = options.responseTimeoutMs ?? timeoutMs; + const visibleStep = Math.floor(Number(options.visibleStep || payload?.visibleStep || payload?.step) || 0) || 9; await ensureStep8SignupPageReady(tabId, { timeoutMs: readyTimeoutMs, retryDelayMs: options.retryDelayMs ?? 600, - logMessage: options.readyLogMessage || '步骤 9:认证页内容脚本已失联,正在恢复后继续处理重试页...', + visibleStep, + logStepKey: 'confirm-oauth', + logMessage: options.readyLogMessage || '认证页内容脚本已失联,正在恢复后继续处理重试页...', }); const result = await sendToContentScriptResilient('signup-page', { type: 'RECOVER_AUTH_RETRY_PAGE', @@ -10908,7 +10972,9 @@ async function recoverAuthRetryPageOnTab(tabId, payload = {}, options = {}) { timeoutMs, responseTimeoutMs, retryDelayMs: options.retryDelayMs ?? 600, - logMessage: options.logMessage || '步骤 9:认证页正在切换,等待“重试”按钮重新就绪...', + logMessage: options.logMessage || '认证页正在切换,等待“重试”按钮重新就绪...', + logStep: visibleStep, + logStepKey: 'confirm-oauth', }); if (result?.error) { @@ -10918,9 +10984,10 @@ async function recoverAuthRetryPageOnTab(tabId, payload = {}, options = {}) { return result; } -async function reloadStep8ConsentPage(tabId, timeoutMs = 30000) { +async function reloadStep8ConsentPage(tabId, timeoutMs = 30000, options = {}) { + const visibleStep = Math.floor(Number(options.visibleStep) || 0) || 9; if (!Number.isInteger(tabId)) { - throw new Error('步骤 9:缺少有效的认证页标签页,无法刷新后重试。'); + throw new Error(`步骤 ${visibleStep}:缺少有效的认证页标签页,无法刷新后重试。`); } await chrome.tabs.update(tabId, { active: true }).catch(() => { }); @@ -10931,7 +10998,7 @@ async function reloadStep8ConsentPage(tabId, timeoutMs = 30000) { if (settled) return; settled = true; chrome.tabs.onUpdated.removeListener(listener); - reject(new Error('步骤 9:刷新认证页后等待页面完成加载超时。')); + reject(new Error(`步骤 ${visibleStep}:刷新认证页后等待页面完成加载超时。`)); }, timeoutMs); const listener = (updatedTabId, changeInfo) => { @@ -10956,11 +11023,14 @@ async function reloadStep8ConsentPage(tabId, timeoutMs = 30000) { await ensureStep8SignupPageReady(tabId, { timeoutMs: Math.min(15000, timeoutMs), - logMessage: '步骤 9:认证页刷新后内容脚本尚未就绪,正在等待页面恢复...', + visibleStep, + logStepKey: 'confirm-oauth', + logMessage: '认证页刷新后内容脚本尚未就绪,正在等待页面恢复...', }); } -async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLICK_EFFECT_TIMEOUT_MS) { +async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLICK_EFFECT_TIMEOUT_MS, options = {}) { + const visibleStep = Math.floor(Number(options.visibleStep) || 0) || 9; const start = Date.now(); let recovered = false; @@ -10969,19 +11039,19 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI const tab = await chrome.tabs.get(tabId).catch(() => null); if (!tab) { - throw new Error('步骤 9:认证页面标签页已关闭,无法继续自动授权。'); + throw new Error(`步骤 ${visibleStep}:认证页面标签页已关闭,无法继续自动授权。`); } if (baselineUrl && typeof tab.url === 'string' && tab.url !== baselineUrl) { return { progressed: true, reason: 'url_changed', url: tab.url }; } - const pageState = await getStep8PageState(tabId); + const pageState = await getStep8PageState(tabId, 1500, visibleStep); if (pageState?.maxCheckAttemptsBlocked) { throw new Error(`${CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX}${CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE}`); } if (pageState?.addPhonePage) { - throw new Error('步骤 9:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。'); + throw new Error(`步骤 ${visibleStep}:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。`); } if (pageState?.retryPage) { const retryUrl = String(pageState?.url || baselineUrl || '').trim(); @@ -10991,7 +11061,7 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI || /\/sign-in-with-chatgpt\/[^/?#]+\/consent(?:[/?#]|$)/i.test(retryUrl) ); if (!consentLikeRetry) { - throw new Error(`步骤 9:点击“继续”后页面进入认证页重试页,当前流程将直接报错。URL: ${pageState.url || baselineUrl || 'unknown'}`); + throw new Error(`步骤 ${visibleStep}:点击“继续”后页面进入认证页重试页,当前流程将直接报错。URL: ${pageState.url || baselineUrl || 'unknown'}`); } } if (pageState === null) { @@ -10999,7 +11069,9 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI recovered = true; await ensureStep8SignupPageReady(tabId, { timeoutMs: Math.max(1000, Math.min(8000, timeoutMs)), - logMessage: '步骤 9:点击后认证页正在重载,正在等待内容脚本重新就绪...', + visibleStep, + logStepKey: 'confirm-oauth', + logMessage: '点击后认证页正在重载,正在等待内容脚本重新就绪...', }).catch(() => null); continue; } @@ -11073,8 +11145,9 @@ async function recoverOAuthLocalhostTimeout(details = {}) { const loginCodeStep = Number(visibleStep) >= 12 ? 11 : 8; await addLog( - `步骤 ${visibleStep}:检测到 OAuth localhost 回调等待窗口已过期,正在复核认证页并回到步骤 ${authLoginStep} 重拉授权链路。`, - 'warn' + `检测到 OAuth localhost 回调等待窗口已过期,正在复核认证页并回到步骤 ${authLoginStep} 重拉授权链路。`, + 'warn', + { step: visibleStep, stepKey: 'confirm-oauth' } ); let authState = null; @@ -11082,26 +11155,31 @@ async function recoverOAuthLocalhostTimeout(details = {}) { authState = await getLoginAuthStateFromContent({ timeoutMs: 10000, responseTimeoutMs: 10000, - logMessage: `步骤 ${visibleStep}:正在复核认证页状态,确认是否可自动恢复 localhost 回调链路...`, + visibleStep, + logMessage: '正在复核认证页状态,确认是否可自动恢复 localhost 回调链路...', + logStepKey: 'confirm-oauth', }); } catch (inspectError) { await addLog( - `步骤 ${visibleStep}:复核认证页状态失败(${getErrorMessage(inspectError)}),将先尝试按步骤 ${loginCodeStep} 收尾恢复。`, - 'warn' + `复核认证页状态失败(${getErrorMessage(inspectError)}),将先尝试按步骤 ${loginCodeStep} 收尾恢复。`, + 'warn', + { step: visibleStep, stepKey: 'confirm-oauth' } ); } if (isAddPhoneAuthState(authState)) { const stateLabel = getLoginAuthStateLabel(authState.state); await addLog( - `步骤 ${visibleStep}:当前认证页为 ${stateLabel},将直接回到步骤 ${authLoginStep} 重新拉起授权链路,避免步骤 8/9 恢复冲突。`, - 'warn' + `当前认证页为 ${stateLabel},将直接回到步骤 ${authLoginStep} 重新拉起授权链路,避免验证码/OAuth 恢复冲突。`, + 'warn', + { step: visibleStep, stepKey: 'confirm-oauth' } ); } else if (authState && authState.state && !['verification_page', 'oauth_consent_page'].includes(authState.state)) { const stateLabel = getLoginAuthStateLabel(authState.state); await addLog( - `步骤 ${visibleStep}:当前认证页为 ${stateLabel},不满足快速恢复条件,将回到步骤 ${authLoginStep} 重开授权链路。`, - 'warn' + `当前认证页为 ${stateLabel},不满足快速恢复条件,将回到步骤 ${authLoginStep} 重开授权链路。`, + 'warn', + { step: visibleStep, stepKey: 'confirm-oauth' } ); } @@ -11111,8 +11189,9 @@ async function recoverOAuthLocalhostTimeout(details = {}) { } await addLog( - `步骤 ${visibleStep}:正在自动重开步骤 ${authLoginStep} -> ${loginCodeStep},恢复到可继续捕获 localhost 回调的状态。`, - 'warn' + `正在自动重开步骤 ${authLoginStep} -> ${loginCodeStep},恢复到可继续捕获 localhost 回调的状态。`, + 'warn', + { step: visibleStep, stepKey: 'confirm-oauth' } ); await step7Executor.executeStep7({ ...latestState, @@ -11139,8 +11218,9 @@ async function recoverOAuthLocalhostTimeout(details = {}) { }); await addLog( - `步骤 ${visibleStep}:已恢复到步骤 ${authLoginStep} -> ${loginCodeStep} 收尾状态,并刷新 OAuth localhost 回调等待窗口,准备重试当前步骤。`, - 'warn' + `已恢复到步骤 ${authLoginStep} -> ${loginCodeStep} 收尾状态,并刷新 OAuth localhost 回调等待窗口,准备重试当前步骤。`, + 'warn', + { step: visibleStep, stepKey: 'confirm-oauth' } ); return await getState(); } @@ -11189,20 +11269,32 @@ async function executeStep9(state) { // ============================================================ async function executeContributionStep10(state) { + const platformVerifyStep = typeof getStepIdByKeyForState === 'function' + ? (getStepIdByKeyForState('platform-verify', state) || 10) + : 10; + const confirmOauthStep = typeof getStepIdByKeyForState === 'function' + ? (getStepIdByKeyForState('confirm-oauth', state) || 9) + : 9; + const authLoginStep = typeof getStepIdByKeyForState === 'function' + ? (getStepIdByKeyForState('oauth-login', state) || 7) + : 7; if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { - throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。'); + throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}。`); } if (!state.localhostUrl) { - throw new Error('缺少 localhost 回调地址,请先完成步骤 9。'); + throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}。`); } if (!state.contributionSessionId) { - throw new Error('缺少贡献会话信息,请重新从步骤 7 开始。'); + throw new Error(`缺少贡献会话信息,请重新从步骤 ${authLoginStep} 开始。`); } if (!contributionOAuthManager?.pollContributionStatus) { - throw new Error('贡献 OAuth 流程尚未接入,无法完成贡献模式的步骤 10。'); + throw new Error(`贡献 OAuth 流程尚未接入,无法完成贡献模式的步骤 ${platformVerifyStep}。`); } - await addLog('步骤 10:贡献模式正在提交回调并等待最终结果...'); + await addLog('贡献模式正在提交回调并等待最终结果...', 'info', { + step: platformVerifyStep, + stepKey: 'platform-verify', + }); let latestState = await getState(); const callbackUrl = latestState.localhostUrl || state.localhostUrl; @@ -11220,7 +11312,7 @@ async function executeContributionStep10(state) { const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(120000, { - step: 10, + step: platformVerifyStep, actionLabel: '贡献流程最终结果', }) : 120000; @@ -11230,8 +11322,11 @@ async function executeContributionStep10(state) { const status = String(latestState.contributionStatus || '').trim().toLowerCase(); if (contributionOAuthManager?.isContributionFinalStatus?.(status)) { if (status === 'auto_approved') { - await addLog(`步骤 10:贡献流程已结束,最终状态:${latestState.contributionStatusMessage || status}`, 'ok'); - await completeStepFromBackground(10, { + await addLog(`贡献流程已结束,最终状态:${latestState.contributionStatusMessage || status}`, 'ok', { + step: platformVerifyStep, + stepKey: 'platform-verify', + }); + await completeStepFromBackground(platformVerifyStep, { contributionStatus: status, contributionStatusMessage: latestState.contributionStatusMessage || '', localhostUrl: callbackUrl, @@ -11248,12 +11343,15 @@ async function executeContributionStep10(state) { }); } - throw new Error('步骤 10:等待贡献流程最终结果超时。'); + throw new Error(`步骤 ${platformVerifyStep}:等待贡献流程最终结果超时。`); } async function executeStep10(state) { + const platformVerifyStep = typeof getStepIdByKeyForState === 'function' + ? (getStepIdByKeyForState('platform-verify', state || {}) || 10) + : 10; if (state?.contributionModeExpected && !state?.contributionMode) { - throw new Error('步骤 10:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 提交。请重新进入贡献模式后再点击自动。'); + throw new Error(`步骤 ${platformVerifyStep}:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 提交。请重新进入贡献模式后再点击自动。`); } if (state?.contributionMode) { return executeContributionStep10(state); diff --git a/background/logging-status.js b/background/logging-status.js index 21cf691..1a4578d 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -31,10 +31,28 @@ return labels[source] || source || '未知来源'; } - async function addLog(message, level = 'info') { + function normalizeLogStep(value) { + const step = Math.floor(Number(value) || 0); + return step > 0 ? step : null; + } + + function buildLogEntry(message, level = 'info', options = {}) { + const normalizedOptions = options && typeof options === 'object' ? options : {}; + const step = normalizeLogStep(normalizedOptions.step); + const stepKey = String(normalizedOptions.stepKey || '').trim(); + return { + message: String(message || ''), + level, + timestamp: Date.now(), + step, + stepKey, + }; + } + + async function addLog(message, level = 'info', options = {}) { const state = await getState(); const logs = state.logs || []; - const entry = { message, level, timestamp: Date.now() }; + const entry = buildLogEntry(message, level, options); logs.push(entry); if (logs.length > 500) logs.splice(0, logs.length - 500); await setState({ logs }); diff --git a/background/message-router.js b/background/message-router.js index ed32560..8f14c7f 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -253,7 +253,10 @@ const currentStatus = latestState.stepStatuses?.[loginCodeStep]; if (!isStepProtectedFromAutoSkip(currentStatus)) { await setStepStatus(loginCodeStep, 'skipped'); - await addLog(`步骤 ${step}:认证页已直接进入 OAuth 授权页,已自动跳过步骤 ${loginCodeStep} 的登录验证码。`, 'warn'); + await addLog(`认证页已直接进入 OAuth 授权页,已自动跳过步骤 ${loginCodeStep} 的登录验证码。`, 'warn', { + step, + stepKey: 'oauth-login', + }); } } } else if (payload.loginVerificationRequestedAt) { @@ -357,8 +360,16 @@ } case 'LOG': { - const { message: msg, level } = message.payload; - await addLog(`[${getSourceLabel(message.source)}] ${msg}`, level); + const { message: msg, level, step: payloadStep, stepKey } = message.payload; + const logStep = Math.floor(Number(message.step || payloadStep) || 0); + await addLog( + `[${getSourceLabel(message.source)}] ${msg}`, + level, + { + step: logStep > 0 ? logStep : null, + stepKey, + } + ); return { ok: true }; } @@ -383,7 +394,7 @@ } const errorMessage = error?.message || String(error || '步骤 3 提交后确认失败'); await setStepStatus(message.step, 'failed'); - await addLog(`步骤 ${message.step} 失败:${errorMessage}`, 'error'); + await addLog(`失败:${errorMessage}`, 'error', { step: message.step }); await appendManualAccountRunRecordIfNeeded(`step${message.step}_failed`, null, errorMessage); notifyStepError(message.step, errorMessage); return { ok: true, error: errorMessage }; @@ -395,7 +406,7 @@ : 10; const completionState = message.step === lastStepId ? completionStateCandidate : null; await setStepStatus(message.step, 'completed'); - await addLog(`步骤 ${message.step} 已完成`, 'ok'); + await addLog('已完成', 'ok', { step: message.step }); await handleStepData(message.step, message.payload); if (message.step === lastStepId && typeof appendAccountRunRecord === 'function') { await appendAccountRunRecord('success', completionState); @@ -414,12 +425,12 @@ } if (isStopError(message.error)) { await setStepStatus(message.step, 'stopped'); - await addLog(`步骤 ${message.step} 已被用户停止`, 'warn'); + await addLog('已被用户停止', 'warn', { step: message.step }); await appendManualAccountRunRecordIfNeeded(`step${message.step}_stopped`, null, message.error); notifyStepError(message.step, message.error); } else { await setStepStatus(message.step, 'failed'); - await addLog(`步骤 ${message.step} 失败:${message.error}`, 'error'); + await addLog(`失败:${message.error}`, 'error', { step: message.step }); await appendManualAccountRunRecordIfNeeded(`step${message.step}_failed`, null, message.error); notifyStepError(message.step, message.error); } diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index cb18f0c..35055cd 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -3,7 +3,7 @@ })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPhoneVerificationModule() { function createPhoneVerificationHelpers(deps = {}) { const { - addLog, + addLog: rawAddLog = async () => {}, ensureStep8SignupPageReady, fetchImpl = (...args) => fetch(...args), getOAuthFlowStepTimeoutMs, @@ -87,6 +87,35 @@ const PHONE_SMS_FAILURE_SKIP_THRESHOLD = 2; const MAX_ACTIVATION_PRICE_HINTS = 256; const activationPriceHintsByKey = new Map(); + let activePhoneVerificationLogStep = null; + + function normalizeLogStep(value) { + const step = Math.floor(Number(value) || 0); + return step > 0 ? step : null; + } + + function normalizePhoneVerificationLogMessage(message) { + return String(message || '') + .replace(/^Step\s+9\s+diagnostics\s*:\s*/i, 'diagnostics: ') + .replace(/^Step\s+9\s*[::]\s*/i, '') + .replace(/^步骤\s*9\s*[::]\s*/, '') + .replace(/\bstep\s+9\b/gi, 'current step') + .trim(); + } + + async function addLog(message, level = 'info', options = {}) { + const normalizedOptions = options && typeof options === 'object' ? { ...options } : {}; + const step = normalizeLogStep(normalizedOptions.step || normalizedOptions.visibleStep) + || normalizeLogStep(activePhoneVerificationLogStep); + if (step) { + normalizedOptions.step = step; + if (!normalizedOptions.stepKey) { + normalizedOptions.stepKey = 'phone-verification'; + } + } + delete normalizedOptions.visibleStep; + return rawAddLog(normalizePhoneVerificationLogMessage(message), level, normalizedOptions); + } function normalizeUrl(value, fallback = DEFAULT_HERO_SMS_BASE_URL) { const trimmed = String(value || '').trim(); @@ -3176,19 +3205,24 @@ } async function readPhonePageState(tabId, timeoutMs = 10000) { + const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; await ensureStep8SignupPageReady(tabId, { timeoutMs, - logMessage: 'Step 9: waiting for auth page content script to recover before phone verification.', + visibleStep, + logStepKey: 'phone-verification', + logMessage: 'waiting for auth page content script to recover before phone verification.', }); const result = await sendToContentScriptResilient('signup-page', { type: 'STEP8_GET_STATE', source: 'background', - payload: {}, + payload: { visibleStep }, }, { timeoutMs, responseTimeoutMs: timeoutMs, retryDelayMs: 600, - logMessage: 'Step 9: auth page is switching, waiting to inspect phone verification state again...', + logMessage: 'auth page is switching, waiting to inspect phone verification state again...', + logStep: visibleStep, + logStepKey: 'phone-verification', }); if (result?.error) { @@ -3270,8 +3304,9 @@ async function submitPhoneNumber(tabId, phoneNumber, activation = null) { const state = await getState(); const countryConfig = resolveCountryConfigFromActivation(activation, state); + const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' - ? await getOAuthFlowStepTimeoutMs(30000, { step: 9, actionLabel: 'submit add-phone number' }) + ? await getOAuthFlowStepTimeoutMs(30000, { step: visibleStep, actionLabel: 'submit add-phone number' }) : 30000; const result = await sendToContentScriptResilient('signup-page', { type: 'SUBMIT_PHONE_NUMBER', @@ -3285,7 +3320,9 @@ timeoutMs, responseTimeoutMs: timeoutMs, retryDelayMs: 600, - logMessage: 'Step 9: waiting for add-phone page to become ready...', + logMessage: 'waiting for add-phone page to become ready...', + logStep: visibleStep, + logStepKey: 'phone-verification', }); if (result?.error) { @@ -3295,8 +3332,9 @@ } async function submitPhoneVerificationCode(tabId, code) { + const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' - ? await getOAuthFlowStepTimeoutMs(45000, { step: 9, actionLabel: 'submit phone verification code' }) + ? await getOAuthFlowStepTimeoutMs(45000, { step: visibleStep, actionLabel: 'submit phone verification code' }) : 45000; const result = await sendToContentScriptResilient('signup-page', { type: 'SUBMIT_PHONE_VERIFICATION_CODE', @@ -3306,7 +3344,9 @@ timeoutMs, responseTimeoutMs: timeoutMs, retryDelayMs: 600, - logMessage: 'Step 9: waiting for phone verification page before filling the SMS code...', + logMessage: 'waiting for phone verification page before filling the SMS code...', + logStep: visibleStep, + logStepKey: 'phone-verification', }); if (result?.error) { @@ -3316,8 +3356,9 @@ } async function resendPhoneVerificationCode(tabId) { + const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' - ? await getOAuthFlowStepTimeoutMs(65000, { step: 9, actionLabel: 'resend phone verification code' }) + ? await getOAuthFlowStepTimeoutMs(65000, { step: visibleStep, actionLabel: 'resend phone verification code' }) : 65000; const request = { type: 'RESEND_PHONE_VERIFICATION_CODE', @@ -3332,7 +3373,9 @@ timeoutMs, responseTimeoutMs: timeoutMs, retryDelayMs: 600, - logMessage: 'Step 9: waiting for the phone verification resend button...', + logMessage: 'waiting for the phone verification resend button...', + logStep: visibleStep, + logStepKey: 'phone-verification', }); if (result?.error) { @@ -3342,8 +3385,9 @@ } async function returnToAddPhone(tabId) { + const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' - ? await getOAuthFlowStepTimeoutMs(30000, { step: 9, actionLabel: 'return to add-phone page' }) + ? await getOAuthFlowStepTimeoutMs(30000, { step: visibleStep, actionLabel: 'return to add-phone page' }) : 30000; const result = await sendToContentScriptResilient('signup-page', { type: 'RETURN_TO_ADD_PHONE', @@ -3353,7 +3397,9 @@ timeoutMs, responseTimeoutMs: timeoutMs, retryDelayMs: 600, - logMessage: 'Step 9: returning to add-phone page to replace the phone number...', + logMessage: 'returning to add-phone page to replace the phone number...', + logStep: visibleStep, + logStepKey: 'phone-verification', }); if (result?.error) { @@ -3846,7 +3892,8 @@ throw new Error('Phone verification did not complete successfully.'); } - async function completePhoneVerificationFlow(tabId, initialPageState = null) { + async function completePhoneVerificationFlow(tabId, initialPageState = null, options = {}) { + activePhoneVerificationLogStep = normalizeLogStep(options.visibleStep || options.step) || 9; let state = await getState(); let activation = normalizeActivation(state[PHONE_ACTIVATION_STATE_KEY]); let pageState = initialPageState || await readPhonePageState(tabId); diff --git a/background/steps/confirm-oauth.js b/background/steps/confirm-oauth.js index 37d9cd9..a2cff0c 100644 --- a/background/steps/confirm-oauth.js +++ b/background/steps/confirm-oauth.js @@ -47,6 +47,10 @@ return visibleStep >= 12 ? 10 : 7; } + function addStepLog(step, message, level = 'info') { + return addLog(message, level, { step, stepKey: 'confirm-oauth' }); + } + async function executeStep9(state) { const visibleStep = getVisibleStep(state, 9); let activeState = state; @@ -56,7 +60,7 @@ throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}。`); } - await addLog(`步骤 ${visibleStep}:正在监听 localhost 回调地址...`); + await addStepLog(visibleStep, '正在监听 localhost 回调地址...'); let callbackTimeoutMs = LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS; let timeoutRecoveryAttempted = false; @@ -115,7 +119,7 @@ resolved = true; cleanupListener(); - addLog(`步骤 ${visibleStep}:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => { + addStepLog(visibleStep, `已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => { return completeStepFromBackground(visibleStep, { localhostUrl: callbackUrl }); }).then(() => { resolve(); @@ -185,10 +189,10 @@ if (signupTabId && await isTabAlive('signup-page')) { await chrome.tabs.update(signupTabId, { active: true }); - await addLog(`步骤 ${visibleStep}:已切回认证页,正在准备调试器点击...`); + await addStepLog(visibleStep, '已切回认证页,正在准备调试器点击...'); } else { signupTabId = await reuseOrCreateTab('signup-page', activeState.oauthUrl); - await addLog(`步骤 ${visibleStep}:已重新打开认证页,正在准备调试器点击...`); + await addStepLog(visibleStep, '已重新打开认证页,正在准备调试器点击...'); } throwIfStep8SettledOrStopped(resolved); @@ -202,7 +206,9 @@ actionLabel: '等待 OAuth 同意页内容脚本就绪', }) : 15000, - logMessage: `步骤 ${visibleStep}:认证页内容脚本尚未就绪,正在等待页面恢复...`, + visibleStep, + logStepKey: 'confirm-oauth', + logMessage: '认证页内容脚本尚未就绪,正在等待页面恢复...', }); for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) { @@ -214,7 +220,8 @@ step: visibleStep, actionLabel: '等待 OAuth 同意页出现', }) - : STEP8_READY_WAIT_TIMEOUT_MS + : STEP8_READY_WAIT_TIMEOUT_MS, + { visibleStep } ); if (!pageState?.consentReady) { await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS); @@ -223,7 +230,7 @@ const strategy = STEP8_STRATEGIES[Math.min(round - 1, STEP8_STRATEGIES.length - 1)]; - await addLog(`步骤 ${visibleStep}:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`); + await addStepLog(visibleStep, `第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`); if (strategy.mode === 'debugger') { const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' @@ -235,9 +242,10 @@ const clickTarget = await prepareStep8DebuggerClick(signupTabId, { timeoutMs: clickActionTimeoutMs, responseTimeoutMs: clickActionTimeoutMs, + visibleStep, }); throwIfStep8SettledOrStopped(resolved); - await clickWithDebugger(signupTabId, clickTarget?.rect); + await clickWithDebugger(signupTabId, clickTarget?.rect, { visibleStep }); } else { const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(15000, { @@ -248,6 +256,7 @@ await triggerStep8ContentStrategy(signupTabId, strategy.strategy, { timeoutMs: clickActionTimeoutMs, responseTimeoutMs: clickActionTimeoutMs, + visibleStep, }); } @@ -263,14 +272,15 @@ step: visibleStep, actionLabel: '等待 OAuth 同意页点击生效', }) - : 15000 + : 15000, + { visibleStep } ); if (resolved) { return; } if (effect.progressed) { - await addLog(`步骤 ${visibleStep}:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info'); + await addStepLog(visibleStep, `检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info'); break; } @@ -278,7 +288,7 @@ throw new Error(`步骤 ${visibleStep}:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`); } - await addLog(`步骤 ${visibleStep}:${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn'); + await addStepLog(visibleStep, `${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn'); await reloadStep8ConsentPage( signupTabId, typeof getOAuthFlowStepTimeoutMs === 'function' @@ -286,7 +296,8 @@ step: visibleStep, actionLabel: '刷新 OAuth 同意页', }) - : 30000 + : 30000, + { visibleStep } ); await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS); } diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index b9bd3b0..d40cf2e 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -5,7 +5,7 @@ function createStep8Executor(deps = {}) { const { - addLog, + addLog: rawAddLog = async () => {}, chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, completeStepFromBackground, @@ -32,6 +32,33 @@ STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS, throwIfStopped, } = deps; + let activeFetchLoginCodeStep = null; + + function normalizeLogStep(value) { + const step = Math.floor(Number(value) || 0); + return step > 0 ? step : null; + } + + function normalizeStepLogMessage(message) { + return String(message || '') + .replace(/^步骤\s*\d+\s*[::]\s*/, '') + .replace(/^Step\s+\d+\s*[::]\s*/i, '') + .trim(); + } + + function addLog(message, level = 'info', options = {}) { + const normalizedOptions = options && typeof options === 'object' ? { ...options } : {}; + const step = normalizeLogStep(normalizedOptions.step || normalizedOptions.visibleStep) + || normalizeLogStep(activeFetchLoginCodeStep); + if (step) { + normalizedOptions.step = step; + if (!normalizedOptions.stepKey) { + normalizedOptions.stepKey = 'fetch-login-code'; + } + } + delete normalizedOptions.visibleStep; + return rawAddLog(normalizeStepLogMessage(message), level, normalizedOptions); + } function getVisibleStep(state, fallback = 8) { const visibleStep = Math.floor(Number(state?.visibleStep) || 0); @@ -181,6 +208,7 @@ async function runStep8Attempt(state, runtime = {}) { const visibleStep = getVisibleStep(state, 8); + activeFetchLoginCodeStep = visibleStep; const mail = getMailConfig(state); if (mail.error) throw new Error(mail.error); const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0; @@ -371,7 +399,9 @@ 'warn' ); await rerunStep7ForStep8Recovery({ - logMessage: `步骤 ${visibleStep}:邮箱通信异常持续未恢复,正在回到步骤 ${authLoginStep} 重新发起登录流程...`, + logMessage: `邮箱通信异常持续未恢复,正在回到步骤 ${authLoginStep} 重新发起登录流程...`, + logStep: visibleStep, + logStepKey: 'fetch-login-code', }); currentState = await getState(); retryWithoutStep7Streak = 0; @@ -415,8 +445,10 @@ ); await rerunStep7ForStep8Recovery({ logMessage: isStep8RestartStep7Error(currentError) - ? `步骤 ${visibleStep}:认证页进入重试/超时报错状态,正在回到步骤 ${authLoginStep} 重新发起登录流程...` - : `步骤 ${visibleStep}:正在回到步骤 ${authLoginStep},重新发起登录验证码流程...`, + ? `认证页进入重试/超时报错状态,正在回到步骤 ${authLoginStep} 重新发起登录流程...` + : `正在回到步骤 ${authLoginStep},重新发起登录验证码流程...`, + logStep: visibleStep, + logStepKey: 'fetch-login-code', }); currentState = await getState(); } diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index f24178e..4c9bad2 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -59,20 +59,26 @@ const password = currentState.password || currentState.customPassword || ''; const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState); if (typeof startOAuthFlowTimeoutWindow === 'function') { - await startOAuthFlowTimeoutWindow({ step: 7, oauthUrl }); + await startOAuthFlowTimeoutWindow({ step: completionStep, oauthUrl }); } const loginTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(180000, { - step: 7, + step: completionStep, actionLabel: 'OAuth 登录并进入验证码页', oauthUrl, }) : 180000; if (attempt === 1) { - await addLog('步骤 7:正在打开最新 OAuth 链接并登录...'); + await addLog('正在打开最新 OAuth 链接并登录...', 'info', { + step: completionStep, + stepKey: 'oauth-login', + }); } else { - await addLog(`步骤 7:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn'); + await addLog(`上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn', { + step: completionStep, + stepKey: 'oauth-login', + }); } await reuseOrCreateTab('signup-page', oauthUrl); @@ -86,13 +92,16 @@ payload: { email: currentState.email, password, + visibleStep: completionStep, }, }, { timeoutMs: loginTimeoutMs, responseTimeoutMs: loginTimeoutMs, retryDelayMs: 700, - logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续登录...', + logMessage: '认证页正在切换,等待页面重新就绪后继续登录...', + logStep: completionStep, + logStepKey: 'oauth-login', } ); @@ -117,11 +126,11 @@ if (isStep6RecoverableResult(result)) { const reasonMessage = result.message - || `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 7。`; + || `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 ${completionStep}。`; throw new Error(reasonMessage); } - throw new Error('步骤 7:认证页未返回可识别的登录结果。'); + throw new Error(`步骤 ${completionStep}:认证页未返回可识别的登录结果。`); } catch (err) { throwIfStopped(err); if (isAddPhoneAuthFailure(err)) { @@ -129,8 +138,9 @@ } if (isManagementSecretConfigError(err)) { await addLog( - `步骤 7:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`, - 'error' + `检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`, + 'error', + { step: completionStep, stepKey: 'oauth-login' } ); throw err; } @@ -139,11 +149,14 @@ break; } - await addLog(`步骤 7:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn'); + await addLog(`第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn', { + step: completionStep, + stepKey: 'oauth-login', + }); } } - throw new Error(`步骤 7:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`); + throw new Error(`步骤 ${completionStep}:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`); } return { executeStep7 }; diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js index af7023f..79be710 100644 --- a/background/steps/platform-verify.js +++ b/background/steps/platform-verify.js @@ -31,18 +31,31 @@ return visibleStep >= 10 ? visibleStep : 10; } - function parseLocalhostCallback(rawUrl) { + function resolveConfirmOauthStep(platformVerifyStep = 10) { + return Number(platformVerifyStep) >= 13 ? 12 : 9; + } + + function resolveAuthLoginStep(platformVerifyStep = 10) { + return Number(platformVerifyStep) >= 13 ? 10 : 7; + } + + function addStepLog(step, message, level = 'info') { + return addLog(message, level, { step, stepKey: 'platform-verify' }); + } + + function parseLocalhostCallback(rawUrl, platformVerifyStep = 10) { + const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep); let parsed; try { parsed = new URL(rawUrl); } catch { - throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 9。'); + throw new Error(`步骤 ${platformVerifyStep} 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 ${confirmOauthStep}。`); } const code = normalizeString(parsed.searchParams.get('code')); const state = normalizeString(parsed.searchParams.get('state')); if (!code || !state) { - throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 9。'); + throw new Error(`步骤 ${platformVerifyStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmOauthStep}。`); } return { @@ -206,18 +219,20 @@ async function executeCpaStep10(state) { const platformVerifyStep = resolvePlatformVerifyStep(state); + const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep); + const authLoginStep = resolveAuthLoginStep(platformVerifyStep); if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { - throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。'); + throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}。`); } if (!state.localhostUrl) { - throw new Error('缺少 localhost 回调地址,请先完成步骤 9。'); + throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}。`); } if (!state.vpsUrl) { throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。'); } if (shouldBypassStep9ForLocalCpa(state)) { - await addLog('步骤 10:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。', 'info'); + await addStepLog(platformVerifyStep, '检测到本地 CPA,且当前策略为“跳过平台回调验证”,本轮不再重复提交回调地址。', 'info'); await completeStepFromBackground(platformVerifyStep, { localhostUrl: state.localhostUrl, verifiedStatus: 'local-auto', @@ -225,17 +240,17 @@ return; } - const callback = parseLocalhostCallback(state.localhostUrl); + const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep); const expectedState = normalizeString(state.cpaOAuthState); if (expectedState && expectedState !== callback.state) { - throw new Error('CPA 回调 state 与当前授权会话不匹配,请重新执行步骤 7。'); + throw new Error(`CPA 回调 state 与当前授权会话不匹配,请重新执行步骤 ${authLoginStep}。`); } const managementKey = normalizeString(state.vpsPassword); if (!managementKey) { throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。'); } - await addLog('步骤 10:正在通过 CPA 管理接口提交回调地址...'); + await addStepLog(platformVerifyStep, '正在通过 CPA 管理接口提交回调地址...'); try { const origin = normalizeString(state.cpaManagementOrigin) || deriveCpaManagementOrigin(state.vpsUrl); const result = await fetchCpaManagementJson(origin, '/v0/management/oauth-callback', { @@ -250,43 +265,45 @@ const verifiedStatus = normalizeString(result?.message) || normalizeString(result?.status_message) || 'CPA 已通过接口提交回调'; - await addLog(`步骤 10:${verifiedStatus}`, 'ok'); + await addStepLog(platformVerifyStep, verifiedStatus, 'ok'); await completeStepFromBackground(platformVerifyStep, { localhostUrl: callback.url, verifiedStatus, }); } catch (error) { const reason = normalizeString(error?.message) || 'unknown error'; - await addLog(`步骤 10:CPA 接口提交失败:${reason}`, 'error'); + await addStepLog(platformVerifyStep, `CPA 接口提交失败:${reason}`, 'error'); throw error; } } async function executeCodex2ApiStep10(state) { const platformVerifyStep = resolvePlatformVerifyStep(state); + const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep); + const authLoginStep = resolveAuthLoginStep(platformVerifyStep); if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { - throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。'); + throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}。`); } if (!state.localhostUrl) { - throw new Error('缺少 localhost 回调地址,请先完成步骤 9。'); + throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}。`); } if (!state.codex2apiSessionId) { - throw new Error('缺少 Codex2API 会话信息,请重新执行步骤 7。'); + throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${authLoginStep}。`); } if (!normalizeString(state.codex2apiAdminKey)) { throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。'); } - const callback = parseLocalhostCallback(state.localhostUrl); + const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep); const expectedState = normalizeString(state.codex2apiOAuthState); if (expectedState && expectedState !== callback.state) { - throw new Error('Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 7。'); + throw new Error(`Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 ${authLoginStep}。`); } const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl); const origin = new URL(codex2apiUrl).origin; - await addLog('步骤 10:正在向 Codex2API 提交回调并创建账号...'); + await addStepLog(platformVerifyStep, '正在向 Codex2API 提交回调并创建账号...'); const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/exchange-code', { adminKey: state.codex2apiAdminKey, method: 'POST', @@ -298,7 +315,7 @@ }); const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功'; - await addLog(`步骤 10:${verifiedStatus}`, 'ok'); + await addStepLog(platformVerifyStep, verifiedStatus, 'ok'); await completeStepFromBackground(platformVerifyStep, { localhostUrl: callback.url, verifiedStatus, @@ -307,11 +324,12 @@ async function executeSub2ApiStep10(state) { const visibleStep = resolvePlatformVerifyStep(state); + const confirmOauthStep = resolveConfirmOauthStep(visibleStep); if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { - throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。'); + throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}。`); } if (!state.localhostUrl) { - throw new Error('缺少 localhost 回调地址,请先完成步骤 9。'); + throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}。`); } if (!state.sub2apiSessionId) { throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。'); @@ -326,7 +344,7 @@ const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl); const injectFiles = ['content/utils.js', 'content/sub2api-panel.js']; - await addLog('步骤 10:正在打开 SUB2API 后台...'); + await addStepLog(visibleStep, '正在打开 SUB2API 后台...'); let tabId = await getTabId('sub2api-panel'); const alive = tabId && await isTabAlive('sub2api-panel'); @@ -348,13 +366,14 @@ injectSource: 'sub2api-panel', }); - await addLog(`步骤 ${visibleStep}:正在向 SUB2API 提交回调并创建账号...`); + await addStepLog(visibleStep, '正在向 SUB2API 提交回调并创建账号...'); const requestMessage = { type: 'EXECUTE_STEP', - step: 10, + step: visibleStep, source: 'background', payload: { localhostUrl: state.localhostUrl, + visibleStep, sub2apiUrl, sub2apiEmail: state.sub2apiEmail, sub2apiPassword: state.sub2apiPassword, @@ -384,8 +403,9 @@ throw error; } await addLog( - `步骤 ${visibleStep}:SUB2API 回调交换出现临时网络波动(${error.message}),正在重试 ${attempt + 1}/${maxExchangeAttempts}...`, - 'warn' + `SUB2API 回调交换出现临时网络波动(${error.message}),正在重试 ${attempt + 1}/${maxExchangeAttempts}...`, + 'warn', + { step: visibleStep, stepKey: 'platform-verify' } ); await sleep(1200 * attempt); } diff --git a/background/tab-runtime.js b/background/tab-runtime.js index 17b3275..898eda6 100644 --- a/background/tab-runtime.js +++ b/background/tab-runtime.js @@ -339,6 +339,8 @@ timeoutMs = 30000, retryDelayMs = 700, logMessage = '', + logStep = null, + logStepKey = '', } = options; const start = Date.now(); @@ -399,7 +401,10 @@ if (logMessage && !logged) { console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ${source} tab=${tabId} still not ready after ${Date.now() - start}ms`); - await addLog(logMessage, 'warn'); + await addLog(logMessage, 'warn', { + step: logStep, + stepKey: logStepKey, + }); logged = true; } @@ -645,6 +650,8 @@ timeoutMs = 30000, retryDelayMs = 600, logMessage = '', + logStep = null, + logStepKey = '', responseTimeoutMs, } = options; const start = Date.now(); @@ -676,7 +683,10 @@ lastError = err; if (logMessage && !logged) { - await addLog(logMessage, 'warn'); + await addLog(logMessage, 'warn', { + step: logStep, + stepKey: logStepKey, + }); logged = true; } @@ -691,6 +701,8 @@ const { timeoutMs = 45000, maxRecoveryAttempts = 2, + logStep = null, + logStepKey = '', responseTimeoutMs, } = options; const start = Date.now(); @@ -720,7 +732,10 @@ lastError = err; if (!logged) { - await addLog(`步骤 ${message.step}:${mail.label} 页面通信异常,正在尝试让邮箱页重新就绪...`, 'warn'); + await addLog(`${mail.label} 页面通信异常,正在尝试让邮箱页重新就绪...`, 'warn', { + step: logStep, + stepKey: logStepKey, + }); logged = true; } diff --git a/background/verification-flow.js b/background/verification-flow.js index aa1d8bd..fe39705 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -6,7 +6,7 @@ function createVerificationFlowHelpers(deps = {}) { const { - addLog, + addLog: rawAddLog = async () => {}, chrome, closeConflictingTabsForSource, CLOUDFLARE_TEMP_EMAIL_PROVIDER, @@ -34,6 +34,33 @@ throwIfStopped, VERIFICATION_POLL_MAX_ROUNDS, } = deps; + let activeVerificationLogStep = null; + + function normalizeLogStep(value) { + const step = Math.floor(Number(value) || 0); + return step > 0 ? step : null; + } + + function normalizeVerificationLogMessage(message) { + return String(message || '') + .replace(/^步骤\s*\d+\s*[::]\s*/, '') + .replace(/^Step\s+\d+\s*[::]\s*/i, '') + .trim(); + } + + function addLog(message, level = 'info', options = {}) { + const normalizedOptions = options && typeof options === 'object' ? { ...options } : {}; + const step = normalizeLogStep(normalizedOptions.step || normalizedOptions.visibleStep) + || normalizeLogStep(activeVerificationLogStep); + if (step) { + normalizedOptions.step = step; + if (!normalizedOptions.stepKey) { + normalizedOptions.stepKey = step === 4 ? 'fetch-signup-code' : 'fetch-login-code'; + } + } + delete normalizedOptions.visibleStep; + return rawAddLog(normalizeVerificationLogMessage(message), level, normalizedOptions); + } const isRetryableVerificationTransportError = typeof deps.isRetryableContentScriptTransportError === 'function' ? deps.isRetryableContentScriptTransportError @@ -518,6 +545,8 @@ timeoutMs: responseTimeoutMs, responseTimeoutMs, maxRecoveryAttempts: 2, + logStep: activeVerificationLogStep, + logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code', } ); @@ -583,6 +612,8 @@ timeoutMs: 10000, responseTimeoutMs: 5000, maxRecoveryAttempts: 1, + logStep: activeVerificationLogStep, + logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code', } ); } catch (_) { @@ -693,6 +724,8 @@ timeoutMs: timeoutWindow.timeoutMs, maxRecoveryAttempts: 2, responseTimeoutMs: timeoutWindow.responseTimeoutMs, + logStep: activeVerificationLogStep, + logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code', } ); @@ -959,6 +992,8 @@ timeoutMs: timeoutWindow.timeoutMs, maxRecoveryAttempts: 2, responseTimeoutMs: timeoutWindow.responseTimeoutMs, + logStep: activeVerificationLogStep, + logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code', } ); @@ -1000,6 +1035,8 @@ } async function submitVerificationCode(step, code, options = {}) { + const completionStep = getCompletionStep(step, options); + const authLoginStep = completionStep >= 11 ? 10 : 7; const signupTabId = await getTabId('signup-page'); if (!signupTabId) { throw new Error('认证页面标签页已关闭,无法填写验证码。'); @@ -1028,7 +1065,9 @@ timeoutMs: Math.max(baseResponseTimeoutMs + 15000, 30000), retryDelayMs: 700, responseTimeoutMs: baseResponseTimeoutMs, - logMessage: `步骤 ${step}:认证页正在切换,等待页面重新就绪后继续确认验证码提交结果...`, + logMessage: '认证页正在切换,等待页面重新就绪后继续确认验证码提交结果...', + logStep: completionStep, + logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code', }); } catch (err) { if (step === 4 && isRetryableVerificationTransportError(err)) { @@ -1058,9 +1097,15 @@ }); if (fallback.success) { if (fallback.addPhonePage) { - await addLog('步骤 8:验证码提交后通信中断,但页面已进入手机号验证页,按提交成功继续。', 'warn'); + await addLog('验证码提交后通信中断,但页面已进入手机号验证页,按提交成功继续。', 'warn', { + step: completionStep, + stepKey: 'fetch-login-code', + }); } else { - await addLog('步骤 8:验证码提交后通信中断,但页面已进入 OAuth 授权页,按提交成功继续。', 'warn'); + await addLog('验证码提交后通信中断,但页面已进入 OAuth 授权页,按提交成功继续。', 'warn', { + step: completionStep, + stepKey: 'fetch-login-code', + }); } return { success: true, @@ -1072,7 +1117,7 @@ } if (fallback.restartStep7) { const urlPart = fallback.url ? ` URL: ${fallback.url}` : ''; - throw new Error(`STEP8_RESTART_STEP7::步骤 8:验证码提交后认证页进入登录超时报错页,请回到步骤 7 重新开始。${urlPart}`.trim()); + throw new Error(`STEP8_RESTART_STEP7::步骤 ${completionStep}:验证码提交后认证页进入登录超时报错页,请回到步骤 ${authLoginStep} 重新开始。${urlPart}`.trim()); } } throw err; @@ -1092,6 +1137,7 @@ async function resolveVerificationStep(step, state, mail, options = {}) { const completionStep = getCompletionStep(step, options); + activeVerificationLogStep = completionStep; const stateKey = getVerificationCodeStateKey(step); const rejectedCodes = new Set(); const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER diff --git a/content/signup-page.js b/content/signup-page.js index 2259f91..8f76b3a 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -42,7 +42,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1' } if (message.type === 'STEP8_FIND_AND_CLICK') { - log(`步骤 9:${err.message}`, 'error'); + log(err.message, 'error', { step: reportedStep || 9, stepKey: 'confirm-oauth' }); sendResponse({ error: err.message }); return; } @@ -94,7 +94,7 @@ async function handleCommand(message) { case 'ENSURE_SIGNUP_PASSWORD_PAGE_READY': return await ensureSignupPasswordPageReady(); case 'STEP8_FIND_AND_CLICK': - return await step8_findAndClick(); + return await step8_findAndClick(message.payload); case 'STEP8_GET_STATE': return getStep8State(); case 'STEP8_TRIGGER_CONTINUE': @@ -102,6 +102,15 @@ async function handleCommand(message) { } } +function resolveVisibleStep(payload = {}, fallback = 0) { + const step = Math.floor(Number(payload?.visibleStep) || 0); + return step > 0 ? step : fallback; +} + +function stepLog(step, message, level = 'info', stepKey = '') { + return log(message, level, { step, stepKey }); +} + const VERIFICATION_CODE_INPUT_SELECTOR = [ 'input[name="code"]', 'input[name="otp"]', @@ -224,7 +233,7 @@ function isEmailVerificationPage() { async function resendVerificationCode(step, timeout = 45000) { if (step === 8) { - await waitForLoginVerificationPageReady(); + await waitForLoginVerificationPageReady(10000, step); } const start = Date.now(); @@ -1915,7 +1924,11 @@ async function waitForKnownLoginAuthState(timeout = 15000) { return snapshot; } -async function waitForLoginVerificationPageReady(timeout = 10000) { +function getAuthLoginStepForLoginCodeStep(step = 8) { + return Number(step) >= 11 ? 10 : 7; +} + +async function waitForLoginVerificationPageReady(timeout = 10000, visibleStep = 8) { const start = Date.now(); let snapshot = inspectLoginAuthState(); @@ -1932,7 +1945,7 @@ async function waitForLoginVerificationPageReady(timeout = 10000) { } throw new Error( - `当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:${getLoginAuthStateLabel(snapshot)}。URL: ${snapshot?.url || location.href}` + `当前未进入登录验证码页面,请先重新完成步骤 ${Number(visibleStep) >= 11 ? 10 : 7}。当前状态:${getLoginAuthStateLabel(snapshot)}。URL: ${snapshot?.url || location.href}` ); } @@ -1979,6 +1992,7 @@ function createStep6RecoverableResult(reason, snapshot, options = {}) { async function createStep6LoginTimeoutRecoveryTransition(reason, snapshot, message, options = {}) { const { loginVerificationRequestedAt = null, + visibleStep = 7, via = 'login_timeout_recovered', } = options; let resolvedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); @@ -1987,19 +2001,19 @@ async function createStep6LoginTimeoutRecoveryTransition(reason, snapshot, messa try { const recoveryResult = await recoverCurrentAuthRetryPage({ flow: 'login', - logLabel: '步骤 7:检测到登录超时报错,正在点击“重试”恢复当前页面', - step: 7, + logLabel: `步骤 ${visibleStep}:检测到登录超时报错,正在点击“重试”恢复当前页面`, + step: visibleStep, timeoutMs: 12000, }); recovered = Boolean(recoveryResult?.recovered); if (recovered) { - log('步骤 7:登录超时报错页已点击“重试”,正在按恢复后的页面状态继续当前流程。', 'warn'); + log('登录超时报错页已点击“重试”,正在按恢复后的页面状态继续当前流程。', 'warn', { step: visibleStep, stepKey: 'oauth-login' }); } } catch (error) { if (/CF_SECURITY_BLOCKED::/i.test(String(error?.message || error || ''))) { throw error; } - log(`步骤 7:登录超时报错页自动点击“重试”失败:${error.message}`, 'warn'); + log(`登录超时报错页自动点击“重试”失败:${error.message}`, 'warn', { step: visibleStep, stepKey: 'oauth-login' }); } } @@ -2027,12 +2041,12 @@ async function createStep6LoginTimeoutRecoveryTransition(reason, snapshot, messa } if (resolvedSnapshot.state === 'password_page') { - log('步骤 7:登录超时报错页恢复后已进入密码页,继续当前登录流程。', 'warn'); + log('登录超时报错页恢复后已进入密码页,继续当前登录流程。', 'warn', { step: visibleStep, stepKey: 'oauth-login' }); return { action: 'password', snapshot: resolvedSnapshot }; } if (resolvedSnapshot.state === 'email_page') { - log('步骤 7:登录超时报错页恢复后已回到邮箱输入页,继续当前登录流程。', 'warn'); + log('登录超时报错页恢复后已回到邮箱输入页,继续当前登录流程。', 'warn', { step: visibleStep, stepKey: 'oauth-login' }); return { action: 'email', snapshot: resolvedSnapshot }; } @@ -2045,8 +2059,8 @@ async function createStep6LoginTimeoutRecoveryTransition(reason, snapshot, messa }; } -async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, message) { - const transition = await createStep6LoginTimeoutRecoveryTransition(reason, snapshot, message); +async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, message, options = {}) { + const transition = await createStep6LoginTimeoutRecoveryTransition(reason, snapshot, message, options); if (transition?.action === 'done' || transition?.action === 'recoverable') { return transition.result; } @@ -2058,7 +2072,8 @@ async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, messag async function finalizeStep6VerificationReady(options = {}) { const { - logLabel = '步骤 7 收尾', + visibleStep = 7, + logLabel = `步骤 ${visibleStep} 收尾`, loginVerificationRequestedAt = null, timeout = 12000, via = 'verification_page_ready', @@ -2071,14 +2086,14 @@ async function finalizeStep6VerificationReady(options = {}) { while (Date.now() - start < timeout && round < maxRounds) { throwIfStopped(); round += 1; - log(`${logLabel}:确认页面是否稳定停留在登录验证码阶段(第 ${round}/${maxRounds} 轮,先等待 3 秒)...`, 'info'); + log(`确认页面是否稳定停留在登录验证码阶段(第 ${round}/${maxRounds} 轮,先等待 3 秒)...`, 'info', { step: visibleStep, stepKey: 'oauth-login' }); await sleep(settleDelayMs); const rawSnapshot = inspectLoginAuthState(); const snapshot = normalizeStep6Snapshot(rawSnapshot); if (snapshot.state === 'verification_page') { - log(`${logLabel}:登录验证码页面已稳定就绪。`, 'ok'); + log('登录验证码页面已稳定就绪。', 'ok', { step: visibleStep, stepKey: 'oauth-login' }); return createStep6SuccessResult(snapshot, { via, loginVerificationRequestedAt, @@ -2086,24 +2101,25 @@ async function finalizeStep6VerificationReady(options = {}) { } if (snapshot.state === 'oauth_consent_page') { - log(`${logLabel}:认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。`, 'ok'); + log('认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。', 'ok', { step: visibleStep, stepKey: 'oauth-login' }); return createStep6OAuthConsentSuccessResult(snapshot, { via: `${via}_oauth_consent`, }); } if (snapshot.state === 'login_timeout_error_page') { - log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn'); + log(`页面进入登录超时报错页,准备自动恢复后重试步骤 ${visibleStep}。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' }); return createStep6LoginTimeoutRecoverableResult( 'login_timeout_error_page', snapshot, - '登录验证码页面准备就绪前进入登录超时报错页。' + '登录验证码页面准备就绪前进入登录超时报错页。', + { visibleStep } ); } if (snapshot.state === 'password_page' || snapshot.state === 'email_page') { return createStep6RecoverableResult('verification_page_unstable', snapshot, { - message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 7。`, + message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 ${visibleStep}。`, loginVerificationRequestedAt, }); } @@ -2116,35 +2132,36 @@ async function finalizeStep6VerificationReady(options = {}) { const rawSnapshot = inspectLoginAuthState(); const snapshot = normalizeStep6Snapshot(rawSnapshot); if (snapshot.state === 'verification_page') { - log(`${logLabel}:登录验证码页面已稳定就绪。`, 'ok'); + log('登录验证码页面已稳定就绪。', 'ok', { step: visibleStep, stepKey: 'oauth-login' }); return createStep6SuccessResult(snapshot, { via, loginVerificationRequestedAt, }); } if (snapshot.state === 'oauth_consent_page') { - log(`${logLabel}:认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。`, 'ok'); + log('认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。', 'ok', { step: visibleStep, stepKey: 'oauth-login' }); return createStep6OAuthConsentSuccessResult(snapshot, { via: `${via}_oauth_consent`, }); } if (snapshot.state === 'login_timeout_error_page') { - log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn'); + log(`页面进入登录超时报错页,准备自动恢复后重试步骤 ${visibleStep}。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' }); return createStep6LoginTimeoutRecoverableResult( 'login_timeout_error_page', snapshot, - '登录验证码页面准备就绪前进入登录超时报错页。' + '登录验证码页面准备就绪前进入登录超时报错页。', + { visibleStep } ); } if (snapshot.state === 'password_page' || snapshot.state === 'email_page') { return createStep6RecoverableResult('verification_page_unstable', snapshot, { - message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 7。`, + message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 ${visibleStep}。`, loginVerificationRequestedAt, }); } return createStep6RecoverableResult('verification_page_finalize_unknown', snapshot, { - message: '登录验证码页面状态在收尾确认阶段未稳定,准备重新执行步骤 7。', + message: `登录验证码页面状态在收尾确认阶段未稳定,准备重新执行步骤 ${visibleStep}。`, loginVerificationRequestedAt, }); } @@ -2153,13 +2170,13 @@ function normalizeStep6Snapshot(snapshot) { return snapshot; } -function throwForStep6FatalState(snapshot) { +function throwForStep6FatalState(snapshot, visibleStep = 7) { snapshot = normalizeStep6Snapshot(snapshot); switch (snapshot?.state) { case 'oauth_consent_page': return; case 'add_phone_page': - throw new Error(`当前页面已进入手机号页面,未经过登录验证码页,无法完成步骤 7。URL: ${snapshot.url}`); + throw new Error(`当前页面已进入手机号页面,未经过登录验证码页,无法完成步骤 ${visibleStep}。URL: ${snapshot.url}`); case 'unknown': throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`); default: @@ -2573,7 +2590,7 @@ async function fillVerificationCode(step, payload) { log(`步骤 ${step}:正在填写验证码:${code}`); if (step === 8) { - await waitForLoginVerificationPageReady(); + await waitForLoginVerificationPageReady(10000, step); } const combinedSignupProfilePage = step === 4 @@ -2731,6 +2748,7 @@ async function resolveStep6PostSubmitSnapshot(snapshot, options = {}) { allowFinalPasswordAction = false, allowFinalEmailAction = false, allowFinalSwitchAction = false, + visibleStep = 7, final = false, addPhoneMessage, } = options; @@ -2760,6 +2778,7 @@ async function resolveStep6PostSubmitSnapshot(snapshot, options = {}) { normalizedSnapshot, timeoutRecoveryMessage, { + visibleStep, loginVerificationRequestedAt, via: timeoutRecoveryVia, } @@ -2844,9 +2863,10 @@ async function waitForStep6PostSubmitTransition(options = {}) { }; } -async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000) { +async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000, options = {}) { return waitForStep6PostSubmitTransition({ timeout, + visibleStep: Math.floor(Number(options?.visibleStep) || 0) || 7, via: 'email_submit', oauthConsentVia: 'email_submit_oauth_consent', loginVerificationRequestedAt: emailSubmittedAt, @@ -2859,9 +2879,10 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120 }); } -async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout = 10000) { +async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout = 10000, options = {}) { return waitForStep6PostSubmitTransition({ timeout, + visibleStep: Math.floor(Number(options?.visibleStep) || 0) || 7, via: 'password_submit', oauthConsentVia: 'password_submit_oauth_consent', loginVerificationRequestedAt: passwordSubmittedAt, @@ -2874,9 +2895,10 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout }); } -async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeout = 10000) { +async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeout = 10000, options = {}) { const transition = await waitForStep6PostSubmitTransition({ timeout, + visibleStep: Math.floor(Number(options?.visibleStep) || 0) || 7, via: 'switch_to_one_time_code_login', oauthConsentVia: 'switch_to_one_time_code_oauth_consent', loginVerificationRequestedAt, @@ -2910,6 +2932,7 @@ async function waitForLoginEntryOpenTransition(timeout = 10000) { } async function step6OpenLoginEntry(payload, snapshot) { + const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7; const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); const trigger = currentSnapshot.loginEntryTrigger || findLoginEntryTrigger(); if (!trigger || !isActionEnabled(trigger)) { @@ -2918,7 +2941,7 @@ async function step6OpenLoginEntry(payload, snapshot) { }); } - log(`步骤 7:检测到登录入口页,正在点击 "${getActionText(trigger).slice(0, 80)}"...`); + log(`检测到登录入口页,正在点击 "${getActionText(trigger).slice(0, 80)}"...`, 'info', { step: visibleStep, stepKey: 'oauth-login' }); await humanPause(350, 900); simulateClick(trigger); const nextSnapshot = await waitForLoginEntryOpenTransition(); @@ -2931,7 +2954,7 @@ async function step6OpenLoginEntry(payload, snapshot) { } if (nextSnapshot.state === 'verification_page') { return finalizeStep6VerificationReady({ - logLabel: '步骤 7 收尾', + visibleStep, loginVerificationRequestedAt: null, via: 'entry_open_verification_page', }); @@ -2945,7 +2968,8 @@ async function step6OpenLoginEntry(payload, snapshot) { const transition = await createStep6LoginTimeoutRecoveryTransition( 'login_timeout_after_entry_open', nextSnapshot, - '点击登录入口后进入登录超时报错页。' + '点击登录入口后进入登录超时报错页。', + { visibleStep } ); if (transition.action === 'done') return transition.result; if (transition.action === 'email') return step6LoginFromEmailPage(payload, transition.snapshot); @@ -2959,6 +2983,7 @@ async function step6OpenLoginEntry(payload, snapshot) { } async function step6SwitchToOneTimeCodeLogin(payload, snapshot) { + const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7; const switchTrigger = snapshot?.switchTrigger || findOneTimeCodeLoginTrigger(); if (!switchTrigger || !isActionEnabled(switchTrigger)) { return createStep6RecoverableResult('missing_one_time_code_trigger', normalizeStep6Snapshot(inspectLoginAuthState()), { @@ -2966,19 +2991,19 @@ async function step6SwitchToOneTimeCodeLogin(payload, snapshot) { }); } - log('步骤 7:已检测到一次性验证码登录入口,准备切换...'); + log('已检测到一次性验证码登录入口,准备切换...', 'info', { step: visibleStep, stepKey: 'oauth-login' }); const loginVerificationRequestedAt = Date.now(); await humanPause(350, 900); simulateClick(switchTrigger); - log('步骤 7:已点击一次性验证码登录'); + log('已点击一次性验证码登录', 'info', { step: visibleStep, stepKey: 'oauth-login' }); await sleep(1200); - const result = await waitForStep6SwitchTransition(loginVerificationRequestedAt); + const result = await waitForStep6SwitchTransition(loginVerificationRequestedAt, 10000, { visibleStep }); if (result?.step6Outcome === 'success') { if (result.skipLoginVerificationStep) { return result; } return finalizeStep6VerificationReady({ - logLabel: '步骤 7 收尾', + visibleStep, loginVerificationRequestedAt: result.loginVerificationRequestedAt || loginVerificationRequestedAt, via: result.via || 'switch_to_one_time_code_login', }); @@ -2993,13 +3018,14 @@ async function step6SwitchToOneTimeCodeLogin(payload, snapshot) { } async function step6LoginFromPasswordPage(payload, snapshot) { + const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7; const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); const hasPassword = Boolean(String(payload?.password || '').trim()); if (currentSnapshot.passwordInput) { if (!hasPassword) { if (currentSnapshot.switchTrigger) { - log('步骤 7:当前未提供密码,改走一次性验证码登录。', 'warn'); + log('当前未提供密码,改走一次性验证码登录。', 'warn', { step: visibleStep, stepKey: 'oauth-login' }); return step6SwitchToOneTimeCodeLogin(payload, currentSnapshot); } @@ -3008,29 +3034,29 @@ async function step6LoginFromPasswordPage(payload, snapshot) { }); } - log('步骤 7:已进入密码页,准备填写密码...'); + log('已进入密码页,准备填写密码...', 'info', { step: visibleStep, stepKey: 'oauth-login' }); await humanPause(550, 1450); fillInput(currentSnapshot.passwordInput, payload.password); - log('步骤 7:已填写密码'); + log('已填写密码', 'info', { step: visibleStep, stepKey: 'oauth-login' }); await sleep(500); const passwordSubmittedAt = Date.now(); await triggerLoginSubmitAction(currentSnapshot.submitButton, currentSnapshot.passwordInput); - log('步骤 7:已提交密码'); + log('已提交密码', 'info', { step: visibleStep, stepKey: 'oauth-login' }); - const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt); + const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt, 10000, { visibleStep }); if (transition.action === 'done') { if (transition.result?.skipLoginVerificationStep) { return transition.result; } return finalizeStep6VerificationReady({ - logLabel: '步骤 7 收尾', + visibleStep, loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || passwordSubmittedAt, via: transition.result.via || 'password_submit', }); } if (transition.action === 'recoverable') { - log(`步骤 7:${transition.result.message || '提交密码后仍未进入登录验证码页面,准备重新执行步骤 7。'}`, 'warn'); + log(transition.result.message || `提交密码后仍未进入登录验证码页面,准备重新执行步骤 ${visibleStep}。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' }); return transition.result; } if (transition.action === 'password') { @@ -3058,6 +3084,7 @@ async function step6LoginFromPasswordPage(payload, snapshot) { } async function step6LoginFromEmailPage(payload, snapshot) { + const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7; const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); const emailInput = currentSnapshot.emailInput || getLoginEmailInput(); if (!emailInput) { @@ -3067,29 +3094,29 @@ async function step6LoginFromEmailPage(payload, snapshot) { if ((emailInput.value || '').trim() !== payload.email) { await humanPause(500, 1400); fillInput(emailInput, payload.email); - log('步骤 7:已填写邮箱'); + log('已填写邮箱', 'info', { step: visibleStep, stepKey: 'oauth-login' }); } else { - log('步骤 7:邮箱已在输入框中,准备提交...'); + log('邮箱已在输入框中,准备提交...', 'info', { step: visibleStep, stepKey: 'oauth-login' }); } await sleep(500); const emailSubmittedAt = Date.now(); await triggerLoginSubmitAction(currentSnapshot.submitButton, emailInput); - log('步骤 7:已提交邮箱'); + log('已提交邮箱', 'info', { step: visibleStep, stepKey: 'oauth-login' }); - const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt); + const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt, 12000, { visibleStep }); if (transition.action === 'done') { if (transition.result?.skipLoginVerificationStep) { return transition.result; } return finalizeStep6VerificationReady({ - logLabel: '步骤 7 收尾', + visibleStep, loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || emailSubmittedAt, via: transition.result.via || 'email_submit', }); } if (transition.action === 'recoverable') { - log(`步骤 7:${transition.result.message || '提交邮箱后仍未进入目标页面,准备重新执行步骤 7。'}`, 'warn'); + log(transition.result.message || `提交邮箱后仍未进入目标页面,准备重新执行步骤 ${visibleStep}。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' }); return transition.result; } if (transition.action === 'email') { @@ -3105,34 +3132,36 @@ async function step6LoginFromEmailPage(payload, snapshot) { } async function step6_login(payload) { + const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7; const { email } = payload; if (!email) throw new Error('登录时缺少邮箱地址。'); const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000)); if (snapshot.state === 'verification_page') { - log('步骤 7:认证页已在登录验证码页,开始确认页面是否稳定。'); + log('认证页已在登录验证码页,开始确认页面是否稳定。', 'info', { step: visibleStep, stepKey: 'oauth-login' }); return finalizeStep6VerificationReady({ - logLabel: '步骤 7 收尾', + visibleStep, loginVerificationRequestedAt: null, via: 'already_on_verification_page', }); } if (snapshot.state === 'oauth_consent_page') { - log('步骤 7:认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。', 'ok'); + log('认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。', 'ok', { step: visibleStep, stepKey: 'oauth-login' }); return createStep6OAuthConsentSuccessResult(snapshot, { via: 'already_on_oauth_consent_page', }); } if (snapshot.state === 'login_timeout_error_page') { - log('步骤 7:检测到登录超时报错页,先尝试恢复当前页面。', 'warn'); + log('检测到登录超时报错页,先尝试恢复当前页面。', 'warn', { step: visibleStep, stepKey: 'oauth-login' }); const transition = await createStep6LoginTimeoutRecoveryTransition( 'login_timeout_error_page', snapshot, '当前页面处于登录超时报错页。', { + visibleStep, loginVerificationRequestedAt: null, via: 'login_timeout_initial_recovered', } @@ -3142,7 +3171,7 @@ async function step6_login(payload) { return transition.result; } return finalizeStep6VerificationReady({ - logLabel: '步骤 7 收尾', + visibleStep, loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || null, via: transition.result.via || 'login_timeout_initial_recovered', }); @@ -3157,12 +3186,12 @@ async function step6_login(payload) { } if (snapshot.state === 'email_page') { - log(`步骤 7:正在使用 ${email} 登录...`); + log(`正在使用 ${email} 登录...`, 'info', { step: visibleStep, stepKey: 'oauth-login' }); return step6LoginFromEmailPage(payload, snapshot); } if (snapshot.state === 'password_page') { - log('步骤 7:认证页已在密码页,继续当前登录流程。'); + log('认证页已在密码页,继续当前登录流程。', 'info', { step: visibleStep, stepKey: 'oauth-login' }); return step6LoginFromPasswordPage(payload, snapshot); } @@ -3170,7 +3199,7 @@ async function step6_login(payload) { return step6OpenLoginEntry(payload, snapshot); } - throwForStep6FatalState(snapshot); + throwForStep6FatalState(snapshot, visibleStep); throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`); } @@ -3181,13 +3210,14 @@ async function step6_login(payload) { // "使用 ChatGPT 登录到 Codex" with a "继续" submit button. // Background performs the actual click through the debugger Input API. -async function step8_findAndClick() { - log('步骤 9:正在查找 OAuth 同意页的“继续”按钮...'); +async function step8_findAndClick(payload = {}) { + const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 9; + log('正在查找 OAuth 同意页的“继续”按钮...', 'info', { step: visibleStep, stepKey: 'confirm-oauth' }); const continueBtn = await prepareStep8ContinueButton(); const rect = getSerializableRect(continueBtn); - log('步骤 9:已找到“继续”按钮并准备好调试器点击坐标。'); + log('已找到“继续”按钮并准备好调试器点击坐标。', 'info', { step: visibleStep, stepKey: 'confirm-oauth' }); return { rect, buttonText: (continueBtn.textContent || '').trim(), @@ -3227,6 +3257,7 @@ function getStep8State() { } async function step8_triggerContinue(payload = {}) { + const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 9; const strategy = payload?.strategy || 'requestSubmit'; const continueBtn = await prepareStep8ContinueButton({ findTimeoutMs: payload?.findTimeoutMs, @@ -3248,10 +3279,10 @@ async function step8_triggerContinue(payload = {}) { simulateClick(continueBtn); break; default: - throw new Error(`未知的 Step 9 触发策略:${strategy}`); + throw new Error(`未知的 Step ${visibleStep} 触发策略:${strategy}`); } - log(`Step 9: continue button triggered via ${strategy}.`); + log(`continue button triggered via ${strategy}.`, 'info', { step: visibleStep, stepKey: 'confirm-oauth' }); return { strategy, ...getStep8State(), diff --git a/content/sub2api-panel.js b/content/sub2api-panel.js index dabfefb..854b7bd 100644 --- a/content/sub2api-panel.js +++ b/content/sub2api-panel.js @@ -24,7 +24,7 @@ if (document.documentElement.getAttribute(SUB2API_PANEL_LISTENER_SENTINEL) !== ' }).catch((err) => { if (isStopError(err)) { if (message.step) { - log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + log('已被用户停止。', 'warn', { step: message.step }); } sendResponse({ stopped: true, error: err.message }); return; @@ -352,7 +352,7 @@ function extractStateFromAuthUrl(authUrl) { } } -function parseLocalhostCallback(rawUrl) { +function parseLocalhostCallback(rawUrl, visibleStep = 10) { let parsed; try { parsed = new URL(rawUrl); @@ -364,7 +364,7 @@ function parseLocalhostCallback(rawUrl) { throw new Error('回调 URL 协议不正确。'); } if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) { - throw new Error('步骤 10 只接受 localhost / 127.0.0.1 回调地址。'); + throw new Error(`步骤 ${visibleStep} 只接受 localhost / 127.0.0.1 回调地址。`); } if (parsed.pathname !== '/auth/callback') { throw new Error('回调 URL 路径必须是 /auth/callback。'); @@ -504,7 +504,7 @@ async function step1_generateOpenAiAuthUrl(payload = {}, options = {}) { async function step9_submitOpenAiCallback(payload = {}) { const visibleStep = Number(payload?.visibleStep) || 10; - const callback = parseLocalhostCallback(payload.localhostUrl || ''); + const callback = parseLocalhostCallback(payload.localhostUrl || '', visibleStep); const backgroundState = await getBackgroundState(); const flowEmail = String(backgroundState.email || '').trim(); @@ -528,11 +528,11 @@ async function step9_submitOpenAiCallback(payload = {}) { throw new Error('本次 localhost 回调中的 state 与步骤 1 生成的 state 不一致,请重新执行步骤 1。'); } - log(`步骤 ${visibleStep}:正在向 SUB2API 交换 OpenAI 授权码...`); + log('正在向 SUB2API 交换 OpenAI 授权码...', 'info', { step: visibleStep, stepKey: 'platform-verify' }); if (proxy) { - log(`步骤 ${visibleStep}:使用 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`); + log(`使用 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`, 'info', { step: visibleStep, stepKey: 'platform-verify' }); } else { - log(`步骤 ${visibleStep}:未配置 SUB2API 默认代理,本次将不使用代理。`); + log('未配置 SUB2API 默认代理,本次将不使用代理。', 'info', { step: visibleStep, stepKey: 'platform-verify' }); } const exchangeRequestBody = { session_id: sessionId, @@ -579,7 +579,7 @@ async function step9_submitOpenAiCallback(payload = {}) { createPayload.extra = extra; } - log(`步骤 ${visibleStep}:授权码交换成功,正在创建 SUB2API 账号(名称:${accountName})...`); + log(`授权码交换成功,正在创建 SUB2API 账号(名称:${accountName})...`, 'info', { step: visibleStep, stepKey: 'platform-verify' }); const createdAccount = await requestJson(origin, '/api/v1/admin/accounts', { method: 'POST', token, @@ -587,7 +587,7 @@ async function step9_submitOpenAiCallback(payload = {}) { }); const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`; - log(`步骤 ${visibleStep}:${verifiedStatus}`, 'ok'); + log(verifiedStatus, 'ok', { step: visibleStep, stepKey: 'platform-verify' }); reportComplete(visibleStep, { localhostUrl: callback.url, verifiedStatus, diff --git a/content/utils.js b/content/utils.js index f208aca..0741e8f 100644 --- a/content/utils.js +++ b/content/utils.js @@ -262,17 +262,30 @@ function fillSelect(el, value) { log(`已选择 [${el.name || el.id || '未知'}] = ${value}`); } +function normalizeLogStep(value) { + const step = Math.floor(Number(value) || 0); + return step > 0 ? step : null; +} + /** * Send a log message to Side Panel via Background. * @param {string} message * @param {string} level - 'info' | 'ok' | 'warn' | 'error' + * @param {{ step?: number, stepKey?: string }} options */ -function log(message, level = 'info') { +function log(message, level = 'info', options = {}) { + const step = normalizeLogStep(options?.step); chrome.runtime.sendMessage({ type: 'LOG', source: getRuntimeScriptSource(), - step: null, - payload: { message, level, timestamp: Date.now() }, + step, + payload: { + message: String(message || ''), + level, + timestamp: Date.now(), + step, + stepKey: String(options?.stepKey || '').trim(), + }, error: null, }); } @@ -305,7 +318,7 @@ function reportReady() { */ function reportComplete(step, data = {}) { console.log(LOG_PREFIX, `步骤 ${step} 已完成`, data); - log(`步骤 ${step} 已成功完成`, 'ok'); + log('已成功完成', 'ok', { step }); const message = { type: 'STEP_COMPLETE', source: getRuntimeScriptSource(), @@ -336,7 +349,7 @@ function reportComplete(step, data = {}) { */ function reportError(step, errorMessage) { console.error(LOG_PREFIX, `步骤 ${step} 失败: ${errorMessage}`); - log(`步骤 ${step} 失败:${errorMessage}`, 'error'); + log(`失败:${errorMessage}`, 'error', { step }); const message = { type: 'STEP_ERROR', source: getRuntimeScriptSource(), diff --git a/content/vps-panel.js b/content/vps-panel.js index 60522a1..ddb4ce3 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -66,7 +66,7 @@ if (document.documentElement.getAttribute(VPS_PANEL_LISTENER_SENTINEL) !== '1') }); if (isStopError(err)) { if (message.step) { - log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + log('已被用户停止。', 'warn', { step: message.step }); } sendResponse({ stopped: true, error: err.message }); return; @@ -88,6 +88,7 @@ async function handleStep(step, payload) { case 1: return await step1_getOAuthLink(payload); case 10: case 12: + case 13: return await step9_vpsVerify({ ...(payload || {}), visibleStep: step }); default: throw new Error(`vps-panel.js 不处理步骤 ${step}`); @@ -618,7 +619,7 @@ function explainStep10Failure(statusText, sourceKind = 'unknown') { { code: 'oauth_state_mismatch', pattern: /state code error/i, - message: 'CPA 校验到回调里的 state 与当前 OAuth 会话不一致,通常是步骤 1 已刷新过新的授权链接,但步骤 10 仍提交旧回调。', + message: 'CPA 校验到回调里的 state 与当前 OAuth 会话不一致,通常是授权链接已刷新,但平台回调验证仍提交旧回调。', }, { code: 'oauth_code_exchange_failed', @@ -666,7 +667,7 @@ function explainStep10Failure(statusText, sourceKind = 'unknown') { }; } -async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS) { +async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS, visibleStep = 10) { const start = Date.now(); let lastDiagnosticsSignature = ''; let lastHeartbeatLoggedAt = 0; @@ -681,11 +682,11 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS if (diagnostics.signature !== lastDiagnosticsSignature) { lastDiagnosticsSignature = diagnostics.signature; lastHeartbeatLoggedAt = elapsed; - log(`步骤 10:认证状态检测中,${diagnostics.summary}`); + log(`认证状态检测中,${diagnostics.summary}`, 'info', { step: visibleStep, stepKey: 'platform-verify' }); console.log(LOG_PREFIX, '[Step 9] status badge diagnostics changed', diagnostics); } else if (elapsed - lastHeartbeatLoggedAt >= 10000) { lastHeartbeatLoggedAt = elapsed; - log(`步骤 10:仍在等待认证成功,${diagnostics.summary}`); + log(`仍在等待认证成功,${diagnostics.summary}`, 'info', { step: visibleStep, stepKey: 'platform-verify' }); console.log(LOG_PREFIX, '[Step 9] still waiting for success badge', diagnostics); } @@ -697,8 +698,9 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS if (callbackSubmittedSignature !== lastCallbackSubmittedSignature) { lastCallbackSubmittedSignature = callbackSubmittedSignature; log( - `步骤 10:CPA 已接受 localhost 回调,正在等待后台完成认证。回调提示=${formatStep10StatusSummaryValue(diagnostics.callbackStatusText)};主状态=${formatStep10StatusSummaryValue(diagnostics.mainStatusText)}`, - 'info' + `CPA 已接受 localhost 回调,正在等待后台完成认证。回调提示=${formatStep10StatusSummaryValue(diagnostics.callbackStatusText)};主状态=${formatStep10StatusSummaryValue(diagnostics.mainStatusText)}`, + 'info', + { step: visibleStep, stepKey: 'platform-verify' } ); console.info(LOG_PREFIX, '[Step 9] callback accepted and waiting for auth completion', diagnostics); } @@ -706,7 +708,7 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS if (isStep10BrowserSwitchRequiredConflict(diagnostics)) { const browserSwitchMessage = getStep10BrowserSwitchRequiredMessage(diagnostics); - log(`步骤 10:${browserSwitchMessage}`, 'error'); + log(browserSwitchMessage, 'error', { step: visibleStep, stepKey: 'platform-verify' }); console.error(LOG_PREFIX, '[Step 9] browser-switch conflict detected', diagnostics); throw new Error(`BROWSER_SWITCH_REQUIRED::${browserSwitchMessage}`); } @@ -723,8 +725,9 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS ? diagnostics.pageErrorSummary : diagnostics.failureSummary; log( - `步骤 10:同时检测到成功徽标和失败提示,本轮不判定成功。成功徽标:${diagnostics.exactSuccessSummary};失败提示:${failureSummary}`, - 'warn' + `同时检测到成功徽标和失败提示,本轮不判定成功。成功徽标:${diagnostics.exactSuccessSummary};失败提示:${failureSummary}`, + 'warn', + { step: visibleStep, stepKey: 'platform-verify' } ); console.warn(LOG_PREFIX, '[Step 9] success badge is blocked by visible failure', diagnostics); } @@ -1021,7 +1024,7 @@ async function step9_vpsVerify(payload) { throw new Error(`步骤 ${visibleStep} 只接受真实的 localhost OAuth 回调地址,请重新执行步骤 ${confirmStep}。`); } if (!localhostUrl) { - log(`步骤 ${visibleStep}:payload 中没有 localhostUrl,正在从状态中读取...`); + log('payload 中没有 localhostUrl,正在从状态中读取...', 'info', { step: visibleStep, stepKey: 'platform-verify' }); const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); localhostUrl = state.localhostUrl; if (localhostUrl && !isLocalhostOAuthCallbackUrl(localhostUrl)) { @@ -1031,9 +1034,9 @@ async function step9_vpsVerify(payload) { if (!localhostUrl) { throw new Error(`未找到 localhost 回调地址,请先完成步骤 ${confirmStep}。`); } - log(`步骤 ${visibleStep}:已获取 localhostUrl:${localhostUrl.slice(0, 60)}...`); + log(`已获取 localhostUrl:${localhostUrl.slice(0, 60)}...`, 'info', { step: visibleStep, stepKey: 'platform-verify' }); - log(`步骤 ${visibleStep}:正在查找回调地址输入框...`); + log('正在查找回调地址输入框...', 'info', { step: visibleStep, stepKey: 'platform-verify' }); // Find the callback URL input // Actual DOM: @@ -1050,7 +1053,7 @@ async function step9_vpsVerify(payload) { await humanPause(600, 1500); fillInput(urlInput, localhostUrl); - log(`步骤 ${visibleStep}:已填写回调地址:${localhostUrl.slice(0, 80)}...`); + log(`已填写回调地址:${localhostUrl.slice(0, 80)}...`, 'info', { step: visibleStep, stepKey: 'platform-verify' }); // Find and click the callback submit button in supported UI languages. const callbackSubmitPattern = /提交回调\s*URL|Submit\s+Callback\s+URL|Отправить\s+Callback\s+URL/i; @@ -1071,9 +1074,9 @@ async function step9_vpsVerify(payload) { await humanPause(450, 1200); simulateClick(submitBtn); - log(`步骤 ${visibleStep}:已点击回调提交按钮,正在等待认证结果...`); + log('已点击回调提交按钮,正在等待认证结果...', 'info', { step: visibleStep, stepKey: 'platform-verify' }); - const verifiedStatus = await waitForExactSuccessBadge(); - log(`步骤 ${visibleStep}:${verifiedStatus}`, 'ok'); + const verifiedStatus = await waitForExactSuccessBadge(STEP9_SUCCESS_BADGE_TIMEOUT_MS, visibleStep); + log(verifiedStatus, 'ok', { step: visibleStep, stepKey: 'platform-verify' }); reportComplete(visibleStep, { localhostUrl, verifiedStatus }); } diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index eb1af75..eb8a119 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -8600,8 +8600,8 @@ function appendLog(entry) { const line = document.createElement('div'); line.className = `log-line log-${entry.level}`; - const stepMatch = entry.message.match(/(?:Step\s+(\d+)|步骤\s*(\d+))/); - const stepNum = stepMatch ? (stepMatch[1] || stepMatch[2]) : null; + const normalizedStep = Math.floor(Number(entry.step) || 0); + const stepNum = normalizedStep > 0 ? String(normalizedStep) : null; let html = `${time} `; html += `${levelLabel} `; diff --git a/tests/background-contribution-mode.test.js b/tests/background-contribution-mode.test.js index 56ddcd6..ad1c3b7 100644 --- a/tests/background-contribution-mode.test.js +++ b/tests/background-contribution-mode.test.js @@ -773,8 +773,8 @@ ${bundle} return { refreshOAuthUrlBeforeStep6 }; `)(); - globalThis.addLog = async (message) => { - calls.push({ type: 'log', message }); + globalThis.addLog = async (message, level, options) => { + calls.push({ type: 'log', message, level, options }); }; globalThis.contributionOAuthManager = { async startContributionFlow(options) { @@ -801,7 +801,12 @@ return { refreshOAuthUrlBeforeStep6 }; assert.equal(oauthUrl, 'https://auth.example.com/oauth?state=oauth-state-001'); assert.deepStrictEqual(calls, [ - { type: 'log', message: '步骤 7:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...' }, + { + type: 'log', + message: 'contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...', + level: 'info', + options: { step: 7, stepKey: 'oauth-login' }, + }, { type: 'contribution', options: { @@ -839,8 +844,8 @@ ${bundle} return { refreshOAuthUrlBeforeStep6 }; `)(); - globalThis.addLog = async (message) => { - calls.push({ type: 'log', message }); + globalThis.addLog = async (message, level, options) => { + calls.push({ type: 'log', message, level, options }); }; globalThis.contributionOAuthManager = { async startContributionFlow() { @@ -868,7 +873,12 @@ return { refreshOAuthUrlBeforeStep6 }; assert.equal(oauthUrl, 'https://panel.example.com/oauth'); assert.deepStrictEqual(calls, [ - { type: 'log', message: '步骤 7:contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:SUB2API),正在刷新 OAuth 登录地址...' }, + { + type: 'log', + message: 'contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:SUB2API),正在刷新 OAuth 登录地址...', + level: 'info', + options: { step: 7, stepKey: 'oauth-login' }, + }, { type: 'panel' }, { type: 'step', diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js index 27e2a0a..4d7212e 100644 --- a/tests/background-message-router-step2-skip.test.js +++ b/tests/background-message-router-step2-skip.test.js @@ -21,8 +21,8 @@ function createRouter(overrides = {}) { }; const router = api.createMessageRouter({ - addLog: async (message, level) => { - events.logs.push({ message, level }); + addLog: async (message, level, options = {}) => { + events.logs.push({ message, level, step: options.step, stepKey: options.stepKey }); }, appendAccountRunRecord: async () => null, batchUpdateLuckmailPurchases: async () => {}, @@ -352,7 +352,7 @@ test('message router marks step 3 failed when post-submit finalize fails', async error: '步骤 3 提交后仍停留在密码页。', }, ]); - assert.equal(events.logs.some(({ message }) => /步骤 3 失败:步骤 3 提交后仍停留在密码页。/.test(message)), true); + assert.equal(events.logs.some(({ message, step }) => /失败:步骤 3 提交后仍停留在密码页。/.test(message) && step === 3), true); assert.deepStrictEqual(response, { ok: true, error: '步骤 3 提交后仍停留在密码页。' }); }); diff --git a/tests/background-platform-verify-codex2api.test.js b/tests/background-platform-verify-codex2api.test.js index bf230a5..da114e4 100644 --- a/tests/background-platform-verify-codex2api.test.js +++ b/tests/background-platform-verify-codex2api.test.js @@ -30,8 +30,8 @@ test('platform verify module supports codex2api protocol callback exchange', asy const completed = []; const logs = []; const executor = api.createStep10Executor({ - addLog: async (message, level = 'info') => { - logs.push({ message, level }); + addLog: async (message, level = 'info', options = {}) => { + logs.push({ message, level, step: options.step, stepKey: options.stepKey }); }, chrome: {}, closeConflictingTabsForSource: async () => {}, @@ -63,8 +63,8 @@ test('platform verify module supports codex2api protocol callback exchange', asy }); assert.deepStrictEqual(logs, [ - { message: '步骤 10:正在向 Codex2API 提交回调并创建账号...', level: 'info' }, - { message: '步骤 10:OAuth 账号 flow@example.com 添加成功', level: 'ok' }, + { message: '正在向 Codex2API 提交回调并创建账号...', level: 'info', step: 10, stepKey: 'platform-verify' }, + { message: 'OAuth 账号 flow@example.com 添加成功', level: 'ok', step: 10, stepKey: 'platform-verify' }, ]); assert.deepStrictEqual(completed, [ { diff --git a/tests/background-platform-verify-cpa-api.test.js b/tests/background-platform-verify-cpa-api.test.js index 0fd9002..f3f9e34 100644 --- a/tests/background-platform-verify-cpa-api.test.js +++ b/tests/background-platform-verify-cpa-api.test.js @@ -8,8 +8,8 @@ function createDeps(overrides = {}) { const uiCalls = []; const deps = { - addLog: async (message, level = 'info') => { - logs.push({ message, level }); + addLog: async (message, level = 'info', options = {}) => { + logs.push({ message, level, step: options.step, stepKey: options.stepKey }); }, chrome: { tabs: { @@ -91,8 +91,8 @@ test('platform verify module submits CPA callback via management API first', asy }, ]); assert.deepStrictEqual(logs, [ - { message: '步骤 10:正在通过 CPA 管理接口提交回调地址...', level: 'info' }, - { message: '步骤 10:CPA API 回调提交成功', level: 'ok' }, + { message: '正在通过 CPA 管理接口提交回调地址...', level: 'info', step: 10, stepKey: 'platform-verify' }, + { message: 'CPA API 回调提交成功', level: 'ok', step: 10, stepKey: 'platform-verify' }, ]); } finally { globalThis.fetch = originalFetch; @@ -160,8 +160,10 @@ test('platform verify module fails fast when CPA API submit fails', async () => assert.equal(uiCalls.length, 0); assert.equal(completed.length, 0); - assert.equal(logs[0].message, '步骤 10:正在通过 CPA 管理接口提交回调地址...'); - assert.match(logs[1].message, /步骤 10:CPA 接口提交失败:failed to persist oauth callback/); + assert.equal(logs[0].message, '正在通过 CPA 管理接口提交回调地址...'); + assert.equal(logs[0].step, 10); + assert.match(logs[1].message, /CPA 接口提交失败:failed to persist oauth callback/); + assert.equal(logs[1].step, 10); assert.equal(logs[1].level, 'error'); } finally { globalThis.fetch = originalFetch; diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js index 35a1f70..b8f725d 100644 --- a/tests/phone-verification-flow.test.js +++ b/tests/phone-verification-flow.test.js @@ -4046,8 +4046,8 @@ test('phone verification helper logs no-supply diagnostics with consecutive stre }; const helpers = api.createPhoneVerificationHelpers({ - addLog: async (message, level = 'info') => { - logs.push({ message, level }); + addLog: async (message, level = 'info', options = {}) => { + logs.push({ message, level, options }); }, ensureStep8SignupPageReady: async () => {}, fetchImpl: async (url) => { @@ -4093,18 +4093,19 @@ test('phone verification helper logs no-supply diagnostics with consecutive stre await runOnce(); const diagnosticsLogs = logs - .filter((entry) => String(entry.message || '').includes('Step 9 diagnostics: 无号连续失败')) - .map((entry) => String(entry.message || '')); + .filter((entry) => String(entry.message || '').includes('diagnostics: 无号连续失败')); assert.equal(diagnosticsLogs.length >= 2, true); - assert.equal(diagnosticsLogs.some((message) => message.includes('无号连续失败 1 次')), true); - assert.equal(diagnosticsLogs.some((message) => message.includes('无号连续失败 2 次')), true); + assert.equal(diagnosticsLogs.every((entry) => entry.options?.step === 9), true); + assert.equal(diagnosticsLogs.every((entry) => entry.options?.stepKey === 'phone-verification'), true); + assert.equal(diagnosticsLogs.some((entry) => entry.message.includes('无号连续失败 1 次')), true); + assert.equal(diagnosticsLogs.some((entry) => entry.message.includes('无号连续失败 2 次')), true); assert.equal( - diagnosticsLogs.some((message) => message.includes('maxPrice=0.06')), + diagnosticsLogs.some((entry) => entry.message.includes('maxPrice=0.06')), true ); assert.equal( - diagnosticsLogs.some((message) => message.includes('国家数 HeroSMS=1, 5sim=0, NexSMS=0')), + diagnosticsLogs.some((entry) => entry.message.includes('国家数 HeroSMS=1, 5sim=0, NexSMS=0')), true ); assert.equal(currentState.phoneNoSupplyFailureStreak, 2); diff --git a/tests/step6-passwordless-otp-login.test.js b/tests/step6-passwordless-otp-login.test.js index f576863..c28d8ad 100644 --- a/tests/step6-passwordless-otp-login.test.js +++ b/tests/step6-passwordless-otp-login.test.js @@ -84,8 +84,8 @@ test('step6LoginFromPasswordPage switches to one-time-code login when password i globalThis.normalizeStep6Snapshot = (value) => value; globalThis.inspectLoginAuthState = () => snapshot; - globalThis.log = (message, level = 'info') => { - logs.push({ message, level }); + globalThis.log = (message, level = 'info', options = {}) => { + logs.push({ message, level, step: options.step, stepKey: options.stepKey }); }; globalThis.step6SwitchToOneTimeCodeLogin = async (payload, value) => { assert.deepStrictEqual(payload, { email: 'user@example.com', password: '' }); @@ -110,7 +110,7 @@ test('step6LoginFromPasswordPage switches to one-time-code login when password i assert.deepStrictEqual(result, { step6Outcome: 'success', via: 'switch_to_one_time_code_login' }); assert.deepStrictEqual(logs, [ - { message: '步骤 7:当前未提供密码,改走一次性验证码登录。', level: 'warn' }, + { message: '当前未提供密码,改走一次性验证码登录。', level: 'warn', step: 7, stepKey: 'oauth-login' }, ]); } finally { cleanupGlobals(); diff --git a/tests/step8-restart-step7-error.test.js b/tests/step8-restart-step7-error.test.js index 6180ab1..8303e01 100644 --- a/tests/step8-restart-step7-error.test.js +++ b/tests/step8-restart-step7-error.test.js @@ -185,7 +185,9 @@ test('step 8 reruns step 7 when auth page enters login timeout retry state', asy assert.equal(calls.logs.some(({ message }) => /重新开始|重新发起/.test(message)), true); assert.deepStrictEqual(calls.rerunOptions, [ { - logMessage: '步骤 8:认证页进入重试/超时报错状态,正在回到步骤 7 重新发起登录流程...', + logMessage: '认证页进入重试/超时报错状态,正在回到步骤 7 重新发起登录流程...', + logStep: 8, + logStepKey: 'fetch-login-code', }, ]); }); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 0a236a1..550cd34 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -118,6 +118,16 @@ - 步骤文件名靠语义 - 新增步骤时不需要重命名后续文件 +### 3.4 日志步骤号链路 + +日志步骤号不再从日志正文里提取,也不再兼容旧的 `步骤 X` / `Step X` 文本解析。 + +- 后台统一通过 `addLog(message, level, { step, stepKey })` 写入结构化日志条目。 +- 内容脚本通过 `log(message, level, { step, stepKey })` 上报结构化日志,`reportComplete` / `reportError` 的步骤号只走消息字段和日志元数据。 +- [sidepanel/sidepanel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.js) 只读取 `entry.step` 渲染步骤标签;日志正文只作为正文展示,不参与步骤号判断。 +- Plus 模式后半段复用普通执行器时,必须先解析当前可见步骤:`oauth-login=10`、`fetch-login-code=11`、`confirm-oauth=12`、`platform-verify=13`,再把该步骤号传入日志、完成信号和内容脚本 payload。 +- 平台验证链路中,CPA / SUB2API / Codex2API 都按当前 `platform-verify` 可见步骤上报;SUB2API 内容脚本请求额外携带 `visibleStep`,避免 Plus 第 13 步落回普通 Step 10。 + ## 4. 状态与存储链路 ### 4.1 `chrome.storage.session` @@ -505,6 +515,8 @@ Plus 模式可见步骤: 8. 第 12 步:复用原 Step 9 OAuth 同意页点击和 localhost callback 捕获执行器,但状态和日志按 Plus 可见第 12 步记录。 9. 第 13 步:复用原 Step 10 CPA / SUB2API / Codex2API 平台回调验证执行器,但状态和日志按 Plus 可见第 13 步记录。 +以上复用步骤的日志标签、完成信号和错误信号都以结构化 `step` 为准,不再允许靠日志正文中的固定步骤号修正显示。 + 隐藏与跳过规则: - 原 Step 6 `清理登录 Cookies`:Plus 模式下隐藏且不执行,因为 Plus Checkout 创建依赖当前 ChatGPT 登录态。 diff --git a/项目开发规范(AI协作).md b/项目开发规范(AI协作).md index 4a09168..209ab83 100644 --- a/项目开发规范(AI协作).md +++ b/项目开发规范(AI协作).md @@ -40,6 +40,14 @@ - 不允许只改 sidepanel 文案而不改共享定义。 - 不允许只改 registry 而不改共享定义。 +### 1.4 日志步骤号原则 + +- 步骤相关日志必须通过结构化元数据传递步骤号:`addLog(message, level, { step, stepKey })` 或内容脚本 `log(message, level, { step, stepKey })`。 +- sidepanel 只能读取日志条目的 `entry.step` 渲染步骤标签,禁止再用正则从日志正文解析 `步骤 X` / `Step X`。 +- Plus 模式复用普通执行器时,必须先按当前运行态解析可见步骤号,再传给日志、完成信号、错误信号和内容脚本 payload;禁止在复用执行器里写死普通模式步骤号。 +- 日志正文可以在业务说明里提到“回到步骤 X”这类操作目标,但不能把正文前缀当作当前日志所属步骤。 +- 不做旧日志文本兼容;如果旧日志没有结构化 `step`,sidepanel 不需要补推断步骤标签。 + ## 2. 模块边界规则 ### 2.1 可以继续增长的文件 @@ -245,6 +253,7 @@ npm test 6. 我新增或修改的文件是否有可见乱码? 7. 我有没有逐个检查本次改动涉及的中文文案、日志、注释、文档没有乱码? 8. 如果改动影响 Gmail / 2925 别名邮箱逻辑,我有没有同步检查 `managed-alias-utils.js`、sidepanel 接线、background 调度、auto-run reset 和回归测试? +9. 如果改动影响步骤日志,我有没有确认日志步骤号来自结构化 `step`,而不是来自日志正文? ## 9. 完成标准 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 95f02ed..c53c846 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -49,16 +49,16 @@ - `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。 - `background/ip-proxy-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。 - `background/ip-proxy-provider-711proxy.js`:711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。 -- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。 +- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;日志条目统一写入结构化 `step / stepKey`,sidepanel 只读取该元数据渲染步骤标签,不再从日志正文解析步骤号;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。 - `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。 -- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息;当侧栏关闭 `oauthFlowTimeoutEnabled` 时,会立即清空已存在的 OAuth 总预算 deadline。 +- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;`LOG / STEP_COMPLETE / STEP_ERROR` 会把消息里的真实 `step` 继续传给结构化日志,跳过登录验证码这类派生日志也按当前步骤 key 写入;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息;当侧栏关闭 `oauthFlowTimeoutEnabled` 时,会立即清空已存在的 OAuth 总预算 deadline。 - `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。 - `background/paypal-account-store.js`:PayPal 账号池持久化模块,负责保存账号列表、切换当前账号,并把当前选中账号同步回兼容字段 `paypalEmail / paypalPassword`。 -- `background/phone-verification-flow.js`:手机号验证共享流程模块,负责在 OAuth 链路命中 `add-phone / phone-verification` 页面后向 HeroSMS 申请或复用号码、轮询短信验证码、提交手机号码与短信验证码,并在号码长期收不到短信时把后续自动流拉回步骤 7 重新拿号。 +- `background/phone-verification-flow.js`:手机号验证共享流程模块,负责在 OAuth 链路命中 `add-phone / phone-verification` 页面后向 HeroSMS 申请或复用号码、轮询短信验证码、提交手机号码与短信验证码,并在号码长期收不到短信时把后续自动流拉回授权登录步骤重新拿号;该流程由 OAuth 确认步骤传入当前可见步骤号,手机号验证日志不会再固定显示普通模式 Step 9。 - `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。 - `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。 -- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。 -- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给步骤 9,而不是误判为普通失败;若登录验证码提交后通信中断但认证页已进入 OAuth 同意页或手机号验证页,会按真实页面状态继续后续流程;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。 +- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;内容脚本恢复等待日志支持 `logStep / logStepKey`,用于保持 Plus 复用步骤的日志标签正确;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。 +- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;日志步骤号使用 `completionStep`,因此 Plus 登录验证码会显示可见第 11 步而不是内部复用的 Step 8;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给 OAuth 确认步骤,而不是误判为普通失败;若登录验证码提交后通信中断但认证页已进入 OAuth 同意页或手机号验证页,会按真实页面状态继续后续流程;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。 ## `background/steps/` @@ -73,7 +73,7 @@ - `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。 - `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。 - `background/steps/paypal-approve.js`:Plus 模式第 8 步实现,负责驱动 PayPal 登录、关闭可见通行密钥提示、点击“同意并继续”,并等待授权流程离开 PayPal 页面。 -- `background/steps/platform-verify.js`:步骤 10 实现,负责 CPA / SUB2API 回调验证,以及 Codex2API 的协议式 callback code/state 交换。 +- `background/steps/platform-verify.js`:平台回调验证实现,普通模式为步骤 10,Plus 模式为可见步骤 13;负责 CPA / SUB2API 回调验证,以及 Codex2API 的协议式 callback code/state 交换,所有平台验证日志和完成信号都按当前可见步骤上报。 - `background/steps/plus-return-confirm.js`:Plus 模式第 9 步实现,负责等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒再完成。 - `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。 - `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与提交后落地页分支判断。 @@ -93,9 +93,9 @@ - `content/plus-checkout.js`:ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。 - `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。 - `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 会在注册弹窗默认处于手机号输入模式时自动切回邮箱输入模式,并兼容本地化邮箱占位与 `aria-label`;登录链路还会显式识别 `phone-verification` 页面,避免把手机验证码页误判成邮箱验证码页或普通未知页。 -- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;当前承接步骤 10。 -- `content/utils.js`:内容脚本公共工具层,负责日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击。 -- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做步骤 10 判定。 +- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;平台验证请求会读取 `visibleStep`,普通模式承接步骤 10,Plus 模式承接步骤 13。 +- `content/utils.js`:内容脚本公共工具层,负责结构化日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击;内容脚本日志通过 `{ step, stepKey }` 上报,正文不再承担步骤号解析职责。 +- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做平台验证判定;当前支持普通步骤 10 与 Plus 可见步骤 13。 ## `data/` @@ -161,7 +161,7 @@ 会在 sidepanel 初始化时拉取并恢复到本地保存的国家/API 设置,接码开关关闭时会直接隐藏相关配置;账号记录快照同步改为默认自动模式,只 要本地 helper 可用就会自动落盘;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑; Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;IP 代理配置保存、服务配置快照、开关应用和运行态回显由这里接入消息路由,具体解析/状态渲染下沉到 `sidepanel/ip-proxy-panel.js`;侧边栏初始化与点击“自动”前会刷新一次贡献站 - 公开内容摘要,并按本地关闭版本决定是否展示轻提示,同时在首次初始化后按现有规则决定是否弹出新手引导提示;当前会保存、回显并热更新 `oauthFlowTimeoutEnabled` 设置。 + 公开内容摘要,并按本地关闭版本决定是否展示轻提示,同时在首次初始化后按现有规则决定是否弹出新手引导提示;当前会保存、回显并热更新 `oauthFlowTimeoutEnabled` 设置;日志渲染只读取结构化 `entry.step` 生成步骤标签,不再正则解析日志正文。 - `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Ultra` / 历史 `Pro` / legacy `v` 版本族排序、缓存读取与版本展示。 ## `tests/`