diff --git a/background.js b/background.js index a07a4d9..93511a7 100644 --- a/background.js +++ b/background.js @@ -147,6 +147,7 @@ const HOTMAIL_MAILBOXES = ['INBOX', 'Junk']; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; const CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX = 'CF_SECURITY_BLOCKED::'; const CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE = '您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'; +const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::'; const HUMAN_STEP_DELAY_MIN = 700; const HUMAN_STEP_DELAY_MAX = 2200; const STEP6_MAX_ATTEMPTS = 3; @@ -4000,6 +4001,17 @@ function getTerminalSecurityBlockedTitle(error) { return 'Cloudflare 风控拦截'; } +function isBrowserSwitchRequiredError(error) { + return getErrorMessage(error).startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX); +} + +function getBrowserSwitchRequiredMessage(error) { + const message = getErrorMessage(error); + return message.startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX) + ? message.slice(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX.length).trim() + : message; +} + function broadcastSecurityBlockedAlert(title = '流程已完全停止', message = CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE, alertText = '检测到 Cloudflare 风控,请暂停当前操作。') { chrome.runtime.sendMessage({ type: 'SECURITY_BLOCKED_ALERT', @@ -4023,6 +4035,13 @@ async function handleCloudflareSecurityBlocked(error) { return message; } +async function handleBrowserSwitchRequired(error) { + const message = getBrowserSwitchRequiredMessage(error) + || '检测到第 10 步的特殊冲突状态,请更换浏览器后重新进行注册登录。'; + await requestStop({ logMessage: message }); + return message; +} + function isVerificationMailPollingError(error) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.isVerificationMailPollingError) { return loggingStatus.isVerificationMailPollingError(error); @@ -5326,6 +5345,10 @@ async function executeStep(step, options = {}) { await handleCloudflareSecurityBlocked(err); throw new Error(STOP_ERROR_MESSAGE); } + if (isBrowserSwitchRequiredError(err)) { + await handleBrowserSwitchRequired(err); + throw new Error(STOP_ERROR_MESSAGE); + } if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step) && isRetryableContentScriptTransportError(err))) { await setStepStatus(step, 'failed'); await addLog(`步骤 ${step} 失败:${err.message}`, 'error'); diff --git a/content/vps-panel.js b/content/vps-panel.js index eef4337..63af31c 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -438,6 +438,20 @@ function formatStep10StatusSummaryValue(text, emptyText = '无') { return text ? `"${getInlineTextSnippet(text, 80)}"` : emptyText; } +function isStep10BrowserSwitchRequiredConflict(diagnostics = {}) { + return Boolean(diagnostics?.hasExactSuccessVisibleBadge) + && /请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(String(diagnostics?.callbackFailureText || '')); +} + +function getStep10BrowserSwitchRequiredMessage(diagnostics = {}) { + const callbackFailureText = normalizeStep9StatusText(diagnostics?.callbackFailureText || ''); + return [ + '检测到 CPA 页面同时显示“认证成功”和“回调 URL 提交失败: 请更新CLI Proxy API或检查连接”。', + '这类冲突状态通常通过更换浏览器可以解决,请更换浏览器后重新进行注册登录。', + callbackFailureText ? `面板原文:${callbackFailureText}` : '', + ].filter(Boolean).join(' '); +} + function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSnippet = '') { const visibleEntries = entries.filter((entry) => entry.visible); const callbackEntries = visibleEntries.filter((entry) => entry.location === 'callback'); @@ -688,6 +702,13 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS } } + if (isStep10BrowserSwitchRequiredConflict(diagnostics)) { + const browserSwitchMessage = getStep10BrowserSwitchRequiredMessage(diagnostics); + log(`步骤 10:${browserSwitchMessage}`, 'error'); + console.error(LOG_PREFIX, '[Step 9] browser-switch conflict detected', diagnostics); + throw new Error(`BROWSER_SWITCH_REQUIRED::${browserSwitchMessage}`); + } + if (diagnostics.hasExactSuccessVisibleBadge && diagnostics.hasFailureVisibleBadge) { const conflictSignature = JSON.stringify({ exactSuccessSummary: diagnostics.exactSuccessSummary, diff --git a/tests/background-auth-chain-guard.test.js b/tests/background-auth-chain-guard.test.js index 0353613..b781570 100644 --- a/tests/background-auth-chain-guard.test.js +++ b/tests/background-auth-chain-guard.test.js @@ -71,6 +71,7 @@ test('executeStep reuses the active top-level auth chain instead of starting a d const api = new Function(` const LOG_PREFIX = '[test]'; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; +const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::'; const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]); let activeTopLevelAuthChainExecution = null; let stopRequested = false; @@ -106,6 +107,10 @@ function isTerminalSecurityBlockedError() { return false; } async function handleCloudflareSecurityBlocked() {} +function isBrowserSwitchRequiredError() { + return false; +} +async function handleBrowserSwitchRequired() {} function doesStepUseCompletionSignal() { return false; } @@ -159,6 +164,95 @@ return { assert.ok(events.logs.some(({ message }) => /复用当前授权链/.test(message))); }); +test('executeStep stops flow when browser-switch-required error is raised', async () => { + const api = new Function(` +const LOG_PREFIX = '[test]'; +const STOP_ERROR_MESSAGE = '流程已被用户停止。'; +const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::'; +const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]); +let activeTopLevelAuthChainExecution = null; +let stopRequested = false; +const events = { + logs: [], + statusCalls: [], + stopRequests: [], + appendRecords: [], +}; + +async function addLog(message, level = 'info') { + events.logs.push({ message, level }); +} +async function setStepStatus(step, status) { + events.statusCalls.push({ step, status }); +} +async function humanStepDelay() {} +async function getState() { + return { + flowStartTime: null, + stepStatuses: {}, + }; +} +function getErrorMessage(error) { + return error?.message || String(error || ''); +} +async function appendManualAccountRunRecordIfNeeded(status, _state, reason) { + events.appendRecords.push({ status, reason }); +} +function isTerminalSecurityBlockedError() { + return false; +} +async function handleCloudflareSecurityBlocked() {} +async function requestStop(options = {}) { + events.stopRequests.push(options); +} +function doesStepUseCompletionSignal() { + return false; +} +function isRetryableContentScriptTransportError() { + return false; +} +const stepRegistry = { + async executeStep() { + throw new Error('BROWSER_SWITCH_REQUIRED::请更换浏览器进行注册登录。'); + }, +}; + +${extractFunction('isStopError')} +${extractFunction('throwIfStopped')} +${extractFunction('isAuthChainStep')} +${extractFunction('acquireTopLevelAuthChainExecution')} +${extractFunction('isBrowserSwitchRequiredError')} +${extractFunction('getBrowserSwitchRequiredMessage')} +${extractFunction('handleBrowserSwitchRequired')} +${extractFunction('executeStep')} + +return { + executeStep, + snapshot() { + return events; + }, +}; +`)(); + + await assert.rejects( + () => api.executeStep(10), + /流程已被用户停止。/ + ); + + const events = api.snapshot(); + assert.deepStrictEqual(events.stopRequests, [ + { logMessage: '请更换浏览器进行注册登录。' }, + ]); + assert.deepStrictEqual(events.statusCalls, [ + { step: 10, status: 'running' }, + ]); + assert.equal( + events.logs.some(({ message }) => /步骤 10 失败/.test(message)), + false, + 'browser-switch-required error should stop the flow before generic failed logging' + ); +}); + test('oauth timeout budget ignores stale deadlines from an old oauth url', async () => { const api = new Function(` const LOG_PREFIX = '[test]'; diff --git a/tests/step9-status-diagnostics.test.js b/tests/step9-status-diagnostics.test.js index 2f9c9a1..d342666 100644 --- a/tests/step9-status-diagnostics.test.js +++ b/tests/step9-status-diagnostics.test.js @@ -66,6 +66,8 @@ const bundle = [ extractFunction('isStep9SuccessStatus'), extractFunction('isStep9SuccessLikeStatus'), extractFunction('formatStep10StatusSummaryValue'), + extractFunction('isStep10BrowserSwitchRequiredConflict'), + extractFunction('getStep10BrowserSwitchRequiredMessage'), extractFunction('buildStep9StatusDiagnostics'), extractFunction('extractStep10FailureDetail'), extractFunction('explainStep10Failure'), @@ -84,6 +86,8 @@ ${bundle} return { buildStep9StatusDiagnostics, explainStep10Failure, + isStep10BrowserSwitchRequiredConflict, + getStep10BrowserSwitchRequiredMessage, }; `)(); } @@ -200,3 +204,32 @@ test('step 10 explains callback upgrade hint with user-friendly reason', () => { assert.match(explanation.userMessage, /CLI Proxy API 版本过旧|管理接口未启动|连接异常/); assert.match(explanation.userMessage, /回调提交阶段/); }); + +test('step 10 requests browser switch when success badge and callback upgrade failure coexist', () => { + const api = createApi(); + const diagnostics = api.buildStep9StatusDiagnostics( + [ + { + visible: true, + text: '认证成功', + className: 'status-badge success', + location: 'main', + hasErrorVisualSignal: false, + errorVisualSummary: '', + }, + { + visible: true, + text: '回调 URL 提交失败: 请更新CLI Proxy API或检查连接', + className: 'status-badge error', + location: 'callback', + hasErrorVisualSignal: true, + errorVisualSummary: 'color=rgb(220, 38, 38)', + }, + ], + [], + 'page' + ); + + assert.equal(api.isStep10BrowserSwitchRequiredConflict(diagnostics), true); + assert.match(api.getStep10BrowserSwitchRequiredMessage(diagnostics), /更换浏览器后重新进行注册登录/); +});