feat: 更新 OAuth 流程,调整步骤 6 和步骤 7 的逻辑,添加相关测试
This commit is contained in:
+143
-14
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
? await getOAuthFlowStepTimeoutMs(180000, {
|
||||
step: 7,
|
||||
actionLabel: 'OAuth 登录并进入验证码页',
|
||||
oauthUrl,
|
||||
})
|
||||
: 180000;
|
||||
|
||||
|
||||
@@ -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,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):
|
||||
|
||||
@@ -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,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)));
|
||||
});
|
||||
|
||||
@@ -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
@@ -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`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
|
||||
|
||||
Reference in New Issue
Block a user