feat: 更新 OAuth 流程,调整步骤 6 和步骤 7 的逻辑,添加相关测试

This commit is contained in:
QLHazyCoder
2026-04-19 18:59:32 +08:00
parent 17e1c672f1
commit 7cb1bda672
12 changed files with 372 additions and 68 deletions
+143 -14
View File
@@ -321,6 +321,7 @@ const DEFAULT_STATE = {
signupVerificationRequestedAt: null,
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
currentHotmailAccountId: null,
preferredIcloudHost: '',
};
@@ -3975,6 +3976,7 @@ function getDownstreamStateResets(step) {
signupVerificationRequestedAt: null,
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
lastSignupCode: null,
lastLoginCode: null,
localhostUrl: null,
@@ -3987,6 +3989,7 @@ function getDownstreamStateResets(step) {
signupVerificationRequestedAt: null,
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
lastSignupCode: null,
lastLoginCode: null,
localhostUrl: null,
@@ -3998,6 +4001,7 @@ function getDownstreamStateResets(step) {
signupVerificationRequestedAt: null,
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
lastSignupCode: null,
lastLoginCode: null,
localhostUrl: null,
@@ -4008,6 +4012,7 @@ function getDownstreamStateResets(step) {
lastLoginCode: null,
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
localhostUrl: null,
};
}
@@ -4612,7 +4617,11 @@ async function skipStep(step) {
return { ok: true, step, status: 'skipped' };
}
function throwIfStopped() {
function throwIfStopped(error = null) {
const errorMessage = typeof error === 'string' ? error : error?.message;
if (errorMessage === STOP_ERROR_MESSAGE) {
throw error instanceof Error ? error : new Error(STOP_ERROR_MESSAGE);
}
if (stopRequested) {
throw new Error(STOP_ERROR_MESSAGE);
}
@@ -4796,6 +4805,7 @@ async function handleStepData(step, payload) {
await setState({
localhostUrl: payload.localhostUrl,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
});
broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
}
@@ -4993,6 +5003,59 @@ async function waitForRunningStepsToFinish(payload = {}) {
return currentState;
}
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
let activeTopLevelAuthChainExecution = null;
function isAuthChainStep(step) {
return AUTH_CHAIN_STEP_IDS.has(Number(step));
}
async function acquireTopLevelAuthChainExecution(step) {
const normalizedStep = Number(step);
if (!isAuthChainStep(normalizedStep)) {
return {
joined: false,
release() {},
};
}
if (activeTopLevelAuthChainExecution) {
const activeExecution = activeTopLevelAuthChainExecution;
await addLog(
`步骤 ${normalizedStep}:检测到步骤 ${activeExecution.step} 正在运行,本次请求将复用当前授权链,不再重复启动。`,
'warn'
);
const result = await activeExecution.promise;
if (result?.error) {
throw result.error;
}
return {
joined: true,
release() {},
};
}
let settleExecution = () => {};
const promise = new Promise((resolve) => {
settleExecution = (error = null) => resolve({ error });
});
const execution = {
step: normalizedStep,
promise,
};
activeTopLevelAuthChainExecution = execution;
return {
joined: false,
release(error = null) {
if (activeTopLevelAuthChainExecution === execution) {
activeTopLevelAuthChainExecution = null;
}
settleExecution(error);
},
};
}
async function markRunningStepsStopped() {
const state = await getState();
const runningSteps = getRunningSteps(state.stepStatuses);
@@ -5089,21 +5152,29 @@ async function requestStop(options = {}) {
async function executeStep(step, options = {}) {
const { deferRetryableTransportError = false } = options;
console.log(LOG_PREFIX, `Executing step ${step}`);
throwIfStopped();
await setStepStatus(step, 'running');
await addLog(`步骤 ${step} 开始执行`);
await humanStepDelay();
const state = await getState();
// Set flow start time on first step
if (step === 1 && !state.flowStartTime) {
await setState({ flowStartTime: Date.now() });
const authChainClaim = await acquireTopLevelAuthChainExecution(step);
if (authChainClaim.joined) {
return;
}
let executionError = null;
throwIfStopped();
try {
await setStepStatus(step, 'running');
await addLog(`步骤 ${step} 开始执行`);
await humanStepDelay();
const state = await getState();
// Set flow start time on first step
if (step === 1 && !state.flowStartTime) {
await setState({ flowStartTime: Date.now() });
}
await stepRegistry.executeStep(step, state);
} catch (err) {
executionError = err;
const state = await getState();
if (isStopError(err)) {
await setStepStatus(step, 'stopped');
await addLog(`步骤 ${step} 已被用户停止`, 'warn');
@@ -5125,6 +5196,8 @@ async function executeStep(step, options = {}) {
);
}
throw err;
} finally {
authChainClaim.release(executionError);
}
}
@@ -5963,7 +6036,6 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
ensureStep8VerificationPageReady,
executeStep7: (...args) => executeStep7(...args),
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
getPanelMode,
@@ -5975,9 +6047,9 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
isVerificationMailPollingError,
LUCKMAIL_PROVIDER,
resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep,
rerunStep7ForStep8Recovery: (...args) => rerunStep7ForStep8Recovery(...args),
reuseOrCreateTab,
setState,
setStepStatus,
shouldUseCustomRegistrationEmail,
sleepWithStop,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
@@ -6455,10 +6527,18 @@ function normalizeOAuthFlowDeadlineAt(value) {
return Math.floor(numeric);
}
function normalizeOAuthFlowSourceUrl(value) {
const normalized = String(value || '').trim();
return normalized || null;
}
async function startOAuthFlowTimeoutWindow(options = {}) {
const step = Number(options.step) || 7;
const deadlineAt = Date.now() + OAUTH_FLOW_TIMEOUT_MS;
await setState({ oauthFlowDeadlineAt: deadlineAt });
await setState({
oauthFlowDeadlineAt: deadlineAt,
oauthFlowDeadlineSourceUrl: normalizeOAuthFlowSourceUrl(options.oauthUrl),
});
await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 6 分钟倒计时。`, 'info');
return deadlineAt;
}
@@ -6468,10 +6548,22 @@ async function getOAuthFlowRemainingMs(options = {}) {
const actionLabel = String(options.actionLabel || '后续授权流程').trim() || '后续授权流程';
const state = options.state || await getState();
const deadlineAt = normalizeOAuthFlowDeadlineAt(state?.oauthFlowDeadlineAt);
const deadlineSourceUrl = normalizeOAuthFlowSourceUrl(state?.oauthFlowDeadlineSourceUrl);
const currentOauthUrl = normalizeOAuthFlowSourceUrl(options.oauthUrl !== undefined ? options.oauthUrl : state?.oauthUrl);
if (!deadlineAt) {
return null;
}
if (deadlineSourceUrl && currentOauthUrl && deadlineSourceUrl !== currentOauthUrl) {
console.warn(LOG_PREFIX, '[oauth-flow] ignoring stale deadline due to oauth url mismatch', {
step,
actionLabel,
deadlineSourceUrl,
currentOauthUrl,
});
return null;
}
const remainingMs = deadlineAt - Date.now();
if (remainingMs <= 0) {
throw buildOAuthFlowTimeoutError(step, actionLabel);
@@ -6617,6 +6709,43 @@ async function ensureStep8VerificationPageReady(options = {}) {
throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:${stateLabel}.${urlPart}`.trim());
}
async function rerunStep7ForStep8Recovery(options = {}) {
const {
logMessage = '步骤 8:正在回到步骤 7,重新发起登录验证码流程...',
postStepDelayMs = 3000,
} = options;
throwIfStopped();
const initialState = await getState();
await addLog(logMessage, 'warn');
await setStepStatus(7, 'running');
await addLog('步骤 7 开始执行');
try {
await step7Executor.executeStep7(initialState);
} catch (err) {
const latestState = await getState();
if (isStopError(err)) {
await setStepStatus(7, 'stopped');
await addLog('步骤 7 已被用户停止', 'warn');
await appendManualAccountRunRecordIfNeeded('step7_stopped', latestState, getErrorMessage(err));
throw err;
}
if (isTerminalSecurityBlockedError(err)) {
await handleCloudflareSecurityBlocked(err);
throw new Error(STOP_ERROR_MESSAGE);
}
await setStepStatus(7, 'failed');
await addLog(`步骤 7 失败:${getErrorMessage(err)}`, 'error');
await appendManualAccountRunRecordIfNeeded('step7_failed', latestState, getErrorMessage(err));
throw err;
}
if (postStepDelayMs > 0) {
await sleepWithStop(postStepDelayMs);
}
}
async function executeStep6() {
return step6Executor.executeStep6();
}
+2 -2
View File
@@ -60,7 +60,7 @@
source: 'background',
payload: {
vpsPassword: state.vpsPassword,
logStep: 6,
logStep: 7,
},
}, {
timeoutMs: 30000,
@@ -121,7 +121,7 @@
sub2apiPassword: state.sub2apiPassword,
sub2apiGroupName: groupName,
sub2apiDefaultProxyName: state.sub2apiDefaultProxyName,
logStep: 6,
logStep: 7,
},
}, {
responseTimeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
+7 -20
View File
@@ -8,7 +8,6 @@
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
confirmCustomVerificationStepBypass,
ensureStep8VerificationPageReady,
executeStep7,
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
getMailConfig,
@@ -19,17 +18,16 @@
isVerificationMailPollingError,
LUCKMAIL_PROVIDER,
resolveVerificationStep,
rerunStep7ForStep8Recovery,
reuseOrCreateTab,
setState,
setStepStatus,
shouldUseCustomRegistrationEmail,
sleepWithStop,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
throwIfStopped,
} = deps;
async function getStep8ReadyTimeoutMs(actionLabel) {
async function getStep8ReadyTimeoutMs(actionLabel, expectedOauthUrl = '') {
if (typeof getOAuthFlowStepTimeoutMs !== 'function') {
return 15000;
}
@@ -37,10 +35,11 @@
return getOAuthFlowStepTimeoutMs(15000, {
step: 8,
actionLabel,
oauthUrl: expectedOauthUrl,
});
}
function getStep8RemainingTimeResolver() {
function getStep8RemainingTimeResolver(expectedOauthUrl = '') {
if (typeof getOAuthFlowRemainingMs !== 'function') {
return undefined;
}
@@ -48,6 +47,7 @@
return async (details = {}) => getOAuthFlowRemainingMs({
step: 8,
actionLabel: details.actionLabel || '登录验证码流程',
oauthUrl: expectedOauthUrl,
});
}
@@ -73,7 +73,7 @@
throwIfStopped();
const pageState = await ensureStep8VerificationPageReady({
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪'),
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || ''),
});
const shouldCompareVerificationEmail = mail.provider !== '2925';
const displayedVerificationEmail = shouldCompareVerificationEmail
@@ -131,7 +131,7 @@
step8VerificationTargetEmail: displayedVerificationEmail || '',
}, mail, {
filterAfterTimestamp: stepStartedAt,
getRemainingTimeMs: getStep8RemainingTimeResolver(),
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || ''),
requestFreshCodeFirst: false,
targetEmail: fixedTargetEmail,
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
@@ -140,19 +140,6 @@
});
}
async function rerunStep7ForStep8Recovery(options = {}) {
const {
logMessage = '步骤 8:正在回到步骤 7,重新发起登录验证码流程...',
postStepDelayMs = 3000,
} = options;
const currentState = await getState();
await addLog(logMessage, 'warn');
await executeStep7(currentState);
if (postStepDelayMs > 0) {
await sleepWithStop(postStepDelayMs);
}
}
function isStep8RestartStep7Error(error) {
const message = String(error?.message || error || '');
return /STEP8_RESTART_STEP7::/i.test(message);
+1
View File
@@ -45,6 +45,7 @@
? await getOAuthFlowStepTimeoutMs(180000, {
step: 7,
actionLabel: 'OAuth 登录并进入验证码页',
oauthUrl,
})
: 180000;
+1 -1
View File
@@ -1671,7 +1671,7 @@ async function fillVerificationCode(step, payload) {
}
// ============================================================
// Step 6: Login with registered account (on OAuth auth page)
// Step 7: Login with registered account (on OAuth auth page)
// ============================================================
async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000) {
+1 -1
View File
@@ -1,4 +1,4 @@
// content/vps-panel.js — Content script for CPA panel (steps 1, 9)
// content/vps-panel.js — Content script for CPA panel (steps 7, 10 / OAuth URL request)
// Injected on: CPA panel (user-configured URL)
//
// Actual DOM structure (after login click):
+186
View File
@@ -0,0 +1,186 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('throwIfStopped rethrows an explicit stop error even when stopRequested has been cleared', () => {
const api = new Function(`
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
let stopRequested = false;
${extractFunction('isStopError')}
${extractFunction('throwIfStopped')}
return {
run(error) {
throwIfStopped(error);
},
};
`)();
assert.throws(
() => api.run(new Error('流程已被用户停止。')),
/流程已被用户停止。/
);
});
test('executeStep reuses the active top-level auth chain instead of starting a duplicate step', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
let activeTopLevelAuthChainExecution = null;
let stopRequested = false;
let releaseStep8 = null;
const events = {
logs: [],
statusCalls: [],
registryCalls: [],
};
const state = {
stepStatuses: {},
};
async function addLog(message, level = 'info') {
events.logs.push({ message, level });
}
async function setStepStatus(step, status) {
state.stepStatuses[step] = status;
events.statusCalls.push({ step, status });
}
async function humanStepDelay() {}
async function getState() {
return {
flowStartTime: null,
stepStatuses: { ...state.stepStatuses },
};
}
function getErrorMessage(error) {
return error?.message || String(error || '');
}
async function appendManualAccountRunRecordIfNeeded() {}
function isTerminalSecurityBlockedError() {
return false;
}
async function handleCloudflareSecurityBlocked() {}
function doesStepUseCompletionSignal() {
return false;
}
function isRetryableContentScriptTransportError() {
return false;
}
const stepRegistry = {
async executeStep(step) {
events.registryCalls.push(step);
if (step === 8) {
await new Promise((resolve) => {
releaseStep8 = resolve;
});
}
},
};
${extractFunction('isStopError')}
${extractFunction('throwIfStopped')}
${extractFunction('isAuthChainStep')}
${extractFunction('acquireTopLevelAuthChainExecution')}
${extractFunction('executeStep')}
return {
executeStep,
releaseStep8() {
if (releaseStep8) {
releaseStep8();
}
},
snapshot() {
return events;
},
};
`)();
const firstRun = api.executeStep(8);
await new Promise((resolve) => setImmediate(resolve));
const duplicateRun = api.executeStep(7);
await new Promise((resolve) => setImmediate(resolve));
api.releaseStep8();
await firstRun;
await duplicateRun;
const events = api.snapshot();
assert.deepStrictEqual(events.registryCalls, [8]);
assert.deepStrictEqual(events.statusCalls, [
{ step: 8, status: 'running' },
]);
assert.ok(events.logs.some(({ message }) => /复用当前授权链/.test(message)));
});
test('oauth timeout budget ignores stale deadlines from an old oauth url', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 6 * 60 * 1000;
${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')}
${extractFunction('getOAuthFlowStepTimeoutMs')}
return {
getOAuthFlowStepTimeoutMs,
};
`)();
const timeoutMs = await api.getOAuthFlowStepTimeoutMs(15000, {
step: 8,
actionLabel: '登录验证码流程',
state: {
oauthUrl: 'https://oauth.example/current',
oauthFlowDeadlineAt: Date.now() + 1200,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/old',
},
});
assert.equal(timeoutMs, 15000);
});
@@ -15,3 +15,9 @@ test('panel bridge module exposes a factory', () => {
assert.equal(typeof api?.createPanelBridge, 'function');
});
test('panel bridge requests oauth url with step 7 log label payload', () => {
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
assert.match(source, /logStep:\s*7/);
assert.doesNotMatch(source, /logStep:\s*6/);
});
@@ -172,6 +172,7 @@ test('step 7 starts a new oauth timeout window for each refreshed oauth url', as
options: {
step: 7,
actionLabel: 'OAuth 登录并进入验证码页',
oauthUrl: 'https://oauth.example/latest',
},
},
]);
+10 -18
View File
@@ -10,8 +10,7 @@ test('step 8 submits login verification directly without replaying step 7', asyn
const calls = {
ensureReady: 0,
ensureReadyOptions: [],
executeStep7: 0,
sleep: [],
rerunStep7: 0,
resolveOptions: null,
setStates: [],
};
@@ -32,8 +31,8 @@ test('step 8 submits login verification directly without replaying step 7', asyn
calls.ensureReadyOptions.push(options || null);
return { state: 'verification_page', displayedEmail: 'display.user@example.com' };
},
executeStep7: async () => {
calls.executeStep7 += 1;
rerunStep7ForStep8Recovery: async () => {
calls.rerunStep7 += 1;
},
getOAuthFlowRemainingMs: async () => 5000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 5000),
@@ -59,9 +58,6 @@ test('step 8 submits login verification directly without replaying step 7', asyn
},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
sleepWithStop: async (ms) => {
calls.sleep.push(ms);
},
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
@@ -79,8 +75,7 @@ test('step 8 submits login verification directly without replaying step 7', asyn
assert.equal(calls.resolveOptions.beforeSubmit, undefined);
assert.equal(calls.ensureReady, 1);
assert.equal(calls.executeStep7, 0);
assert.deepStrictEqual(calls.sleep, []);
assert.equal(calls.rerunStep7, 0);
assert.equal(calls.resolveOptions.filterAfterTimestamp, 123456);
assert.equal(typeof calls.resolveOptions.getRemainingTimeMs, 'function');
assert.equal(await calls.resolveOptions.getRemainingTimeMs({ actionLabel: '登录验证码流程' }), 5000);
@@ -107,7 +102,7 @@ test('step 8 disables resend interval for 2925 mailbox polling', async () => {
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }),
executeStep7: async () => {},
rerunStep7ForStep8Recovery: async () => {},
getOAuthFlowRemainingMs: async () => 8000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000),
getMailConfig: () => ({
@@ -130,7 +125,6 @@ test('step 8 disables resend interval for 2925 mailbox polling', async () => {
setState: async () => {},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
sleepWithStop: async () => {},
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
@@ -161,7 +155,7 @@ test('step 8 falls back to the run email when the verification page does not exp
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: '' }),
executeStep7: async () => {},
rerunStep7ForStep8Recovery: async () => {},
getOAuthFlowRemainingMs: async () => 8000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000),
getMailConfig: () => ({
@@ -184,7 +178,6 @@ test('step 8 falls back to the run email when the verification page does not exp
setState: async () => {},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
sleepWithStop: async () => {},
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
@@ -201,7 +194,7 @@ test('step 8 falls back to the run email when the verification page does not exp
test('step 8 does not rerun step 7 when verification submit lands on add-phone', async () => {
const calls = {
executeStep7: 0,
rerunStep7: 0,
logs: [],
};
@@ -217,8 +210,8 @@ test('step 8 does not rerun step 7 when verification submit lands on add-phone',
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }),
executeStep7: async () => {
calls.executeStep7 += 1;
rerunStep7ForStep8Recovery: async () => {
calls.rerunStep7 += 1;
},
getOAuthFlowRemainingMs: async () => 8000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000),
@@ -242,7 +235,6 @@ test('step 8 does not rerun step 7 when verification submit lands on add-phone',
setState: async () => {},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
sleepWithStop: async () => {},
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
@@ -257,6 +249,6 @@ test('step 8 does not rerun step 7 when verification submit lands on add-phone',
/add-phone/
);
assert.equal(calls.executeStep7, 0);
assert.equal(calls.rerunStep7, 0);
assert.ok(!calls.logs.some(({ message }) => /准备从步骤 7 重新开始/.test(message)));
});
+11 -9
View File
@@ -117,10 +117,10 @@ return {
test('step 8 reruns step 7 when auth page enters login timeout retry state', async () => {
const calls = {
executeStep7: 0,
rerunStep7: 0,
ensureReady: 0,
logs: [],
sleeps: [],
rerunOptions: [],
resolveCalls: 0,
};
@@ -142,8 +142,9 @@ test('step 8 reruns step 7 when auth page enters login timeout retry state', asy
}
return { state: 'verification_page' };
},
executeStep7: async () => {
calls.executeStep7 += 1;
rerunStep7ForStep8Recovery: async (options) => {
calls.rerunStep7 += 1;
calls.rerunOptions.push(options || null);
},
getOAuthFlowRemainingMs: async () => 8000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000),
@@ -167,9 +168,6 @@ test('step 8 reruns step 7 when auth page enters login timeout retry state', asy
setState: async () => {},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
sleepWithStop: async (ms) => {
calls.sleeps.push(ms);
},
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
@@ -181,9 +179,13 @@ test('step 8 reruns step 7 when auth page enters login timeout retry state', asy
oauthUrl: 'https://oauth.example/latest',
});
assert.equal(calls.executeStep7, 1);
assert.equal(calls.rerunStep7, 1);
assert.equal(calls.ensureReady, 2);
assert.equal(calls.resolveCalls, 1);
assert.equal(calls.logs.some(({ message }) => /重新开始|重新发起/.test(message)), true);
assert.deepStrictEqual(calls.sleeps, [3000]);
assert.deepStrictEqual(calls.rerunOptions, [
{
logMessage: '步骤 8:认证页进入重试/超时报错状态,正在回到步骤 7 重新发起登录流程...',
},
]);
});
+3 -3
View File
@@ -46,7 +46,7 @@
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息。
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API 地址、步骤跳转相关判断。
- `background/panel-bridge.js`CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。
- `background/panel-bridge.js`CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信;当前 `REQUEST_OAUTH_URL` 链路的面板日志会统一按步骤 7 记录,避免继续误标为旧的步骤 6
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail / 2925 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成。
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 7 次,步骤 8 提交登录验证码后若页面进入 `add-phone / 手机号页`,会直接抛出 fatal 错误而不是把当前步骤视为成功,并在 2925 提交成功后异步触发“删除全部邮件”的清理消息。
@@ -55,11 +55,11 @@
- `background/steps/clear-login-cookies.js`:步骤 6 实现,负责登录前 Cookie 清理。
- `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成。
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责登录验证码阶段的邮箱轮询、验证码回填与回退控制;验证码获取后直接提交,不再在提交前回放步骤 7 刷新 OAuth;对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱。
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责登录验证码阶段的邮箱轮询、验证码回填与回退控制;验证码获取后直接提交,不再在提交前回放步骤 7 刷新 OAuth;对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱;当步骤 8 需要回退到步骤 7 重开授权链时,会走后台统一恢复入口,避免直接并发拉起新的 Step 7
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口。
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交。
- `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。
- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。
- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试;当前每次拿到新的 OAuth 链接都会刷新对应的 6 分钟授权预算,并把预算与具体 OAuth URL 绑定,避免旧链路 deadline 污染新链路
- `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。
- `background/steps/platform-verify.js`:步骤 10 实现,负责 CPA / SUB2API 回调验证。
- `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。