feat: implement multi-step background process for user registration
- Add step 1: Open ChatGPT homepage and prepare for signup. - Add step 2: Handle email submission and ensure the signup entry page is ready. - Add step 3: Fill in the password using either a custom or generated password. - Add step 4: Confirm the registration verification page and handle email verification. - Add step 5: Generate random user details and handle onboarding redirection. - Add step 6: Manage OAuth login process and handle login verification. - Add step 7: Retrieve verification code from email and manage retries. - Add step 8: Handle consent page interactions and manage callback URLs. - Add step 9: Submit callback information to CPA or SUB2API based on user settings. - Implement tests to ensure all steps are correctly imported and functional.
This commit is contained in:
+196
-637
@@ -4,6 +4,15 @@ importScripts(
|
|||||||
'background/panel-bridge.js',
|
'background/panel-bridge.js',
|
||||||
'background/generated-email-helpers.js',
|
'background/generated-email-helpers.js',
|
||||||
'background/signup-flow-helpers.js',
|
'background/signup-flow-helpers.js',
|
||||||
|
'background/steps/step1.js',
|
||||||
|
'background/steps/step2.js',
|
||||||
|
'background/steps/step3.js',
|
||||||
|
'background/steps/step4.js',
|
||||||
|
'background/steps/step5.js',
|
||||||
|
'background/steps/step6.js',
|
||||||
|
'background/steps/step7.js',
|
||||||
|
'background/steps/step8.js',
|
||||||
|
'background/steps/step9.js',
|
||||||
'data/names.js',
|
'data/names.js',
|
||||||
'hotmail-utils.js',
|
'hotmail-utils.js',
|
||||||
'microsoft-email.js',
|
'microsoft-email.js',
|
||||||
@@ -6407,6 +6416,123 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
|
|||||||
SIGNUP_PAGE_INJECT_FILES,
|
SIGNUP_PAGE_INJECT_FILES,
|
||||||
waitForTabUrlMatch,
|
waitForTabUrlMatch,
|
||||||
});
|
});
|
||||||
|
const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({
|
||||||
|
addLog,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureSignupEntryPageReady,
|
||||||
|
});
|
||||||
|
const step2Executor = self.MultiPageBackgroundStep2?.createStep2Executor({
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureContentScriptReadyOnTab,
|
||||||
|
ensureSignupEntryPageReady,
|
||||||
|
ensureSignupPasswordPageReadyInTab,
|
||||||
|
getTabId,
|
||||||
|
isTabAlive,
|
||||||
|
resolveSignupEmailForFlow,
|
||||||
|
sendToContentScriptResilient,
|
||||||
|
SIGNUP_PAGE_INJECT_FILES,
|
||||||
|
});
|
||||||
|
const step3Executor = self.MultiPageBackgroundStep3?.createStep3Executor({
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
ensureContentScriptReadyOnTab,
|
||||||
|
generatePassword,
|
||||||
|
getTabId,
|
||||||
|
isTabAlive,
|
||||||
|
sendToContentScript,
|
||||||
|
setPasswordState,
|
||||||
|
setState,
|
||||||
|
SIGNUP_PAGE_INJECT_FILES,
|
||||||
|
});
|
||||||
|
const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
completeStepFromBackground,
|
||||||
|
confirmCustomVerificationStepBypass,
|
||||||
|
getMailConfig,
|
||||||
|
getTabId,
|
||||||
|
HOTMAIL_PROVIDER,
|
||||||
|
isTabAlive,
|
||||||
|
LUCKMAIL_PROVIDER,
|
||||||
|
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||||
|
resolveVerificationStep,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
sendToContentScriptResilient,
|
||||||
|
shouldUseCustomRegistrationEmail,
|
||||||
|
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||||
|
throwIfStopped,
|
||||||
|
});
|
||||||
|
const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({
|
||||||
|
addLog,
|
||||||
|
generateRandomBirthday,
|
||||||
|
generateRandomName,
|
||||||
|
getTabId,
|
||||||
|
handleChatgptOnboardingSkip,
|
||||||
|
isRetryableContentScriptTransportError,
|
||||||
|
LOG_PREFIX,
|
||||||
|
sendToContentScript,
|
||||||
|
waitForStep5ChatgptRedirect,
|
||||||
|
});
|
||||||
|
const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({
|
||||||
|
addLog,
|
||||||
|
completeStepFromBackground,
|
||||||
|
getLoginAuthStateLabel,
|
||||||
|
getState,
|
||||||
|
isStep6RecoverableResult,
|
||||||
|
isStep6SuccessResult,
|
||||||
|
refreshOAuthUrlBeforeStep6,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
runPreStep6CookieCleanup,
|
||||||
|
sendToContentScriptResilient,
|
||||||
|
shouldSkipLoginVerificationForCpaCallback,
|
||||||
|
skipLoginVerificationStepsForCpaCallback,
|
||||||
|
throwIfStopped,
|
||||||
|
});
|
||||||
|
const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||||
|
confirmCustomVerificationStepBypass,
|
||||||
|
ensureStep7VerificationPageReady,
|
||||||
|
executeStep6: (...args) => executeStep6(...args),
|
||||||
|
getMailConfig,
|
||||||
|
getState,
|
||||||
|
getTabId,
|
||||||
|
HOTMAIL_PROVIDER,
|
||||||
|
isTabAlive,
|
||||||
|
isVerificationMailPollingError,
|
||||||
|
LUCKMAIL_PROVIDER,
|
||||||
|
resolveVerificationStep,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
setState,
|
||||||
|
setStepStatus,
|
||||||
|
shouldSkipLoginVerificationForCpaCallback,
|
||||||
|
shouldUseCustomRegistrationEmail,
|
||||||
|
sleepWithStop,
|
||||||
|
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||||
|
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
|
||||||
|
throwIfStopped,
|
||||||
|
});
|
||||||
|
const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
closeConflictingTabsForSource,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureContentScriptReadyOnTab,
|
||||||
|
getPanelMode,
|
||||||
|
getTabId,
|
||||||
|
isLocalhostOAuthCallbackUrl,
|
||||||
|
isTabAlive,
|
||||||
|
normalizeSub2ApiUrl,
|
||||||
|
rememberSourceLastUrl,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
sendToContentScript,
|
||||||
|
sendToContentScriptResilient,
|
||||||
|
shouldBypassStep9ForLocalCpa,
|
||||||
|
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
|
||||||
async function requestOAuthUrlFromPanel(state, options = {}) {
|
async function requestOAuthUrlFromPanel(state, options = {}) {
|
||||||
return panelBridge.requestOAuthUrlFromPanel(state, options);
|
return panelBridge.requestOAuthUrlFromPanel(state, options);
|
||||||
@@ -6441,9 +6567,7 @@ async function resolveSignupEmailForFlow(state) {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function executeStep1() {
|
async function executeStep1() {
|
||||||
await addLog('步骤 1:正在打开 ChatGPT 官网...');
|
return step1Executor.executeStep1();
|
||||||
await ensureSignupEntryPageReady(1);
|
|
||||||
await completeStepFromBackground(1, {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -6451,46 +6575,7 @@ async function executeStep1() {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function executeStep2(state) {
|
async function executeStep2(state) {
|
||||||
const resolvedEmail = await resolveSignupEmailForFlow(state);
|
return step2Executor.executeStep2(state);
|
||||||
|
|
||||||
let signupTabId = await getTabId('signup-page');
|
|
||||||
if (!signupTabId || !(await isTabAlive('signup-page'))) {
|
|
||||||
await addLog('步骤 2:未发现可用的注册页标签,正在重新打开 ChatGPT 官网...', 'warn');
|
|
||||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
|
||||||
} else {
|
|
||||||
await chrome.tabs.update(signupTabId, { active: true });
|
|
||||||
await ensureContentScriptReadyOnTab('signup-page', signupTabId, {
|
|
||||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
|
||||||
injectSource: 'signup-page',
|
|
||||||
timeoutMs: 45000,
|
|
||||||
retryDelayMs: 900,
|
|
||||||
logMessage: '步骤 2:注册入口页内容脚本未就绪,正在等待页面恢复...',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const step2Result = await sendToContentScriptResilient('signup-page', {
|
|
||||||
type: 'EXECUTE_STEP',
|
|
||||||
step: 2,
|
|
||||||
source: 'background',
|
|
||||||
payload: { email: resolvedEmail },
|
|
||||||
}, {
|
|
||||||
timeoutMs: 20000,
|
|
||||||
retryDelayMs: 700,
|
|
||||||
logMessage: '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (step2Result?.error) {
|
|
||||||
throw new Error(step2Result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!step2Result?.alreadyOnPasswordPage) {
|
|
||||||
await addLog(`步骤 2:邮箱 ${resolvedEmail} 已提交,正在等待进入密码页...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensureSignupPasswordPageReadyInTab(signupTabId, 2, {
|
|
||||||
skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
|
|
||||||
});
|
|
||||||
await completeStepFromBackground(2, {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -6498,41 +6583,7 @@ async function executeStep2(state) {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function executeStep3(state) {
|
async function executeStep3(state) {
|
||||||
const resolvedEmail = state.email;
|
return step3Executor.executeStep3(state);
|
||||||
if (!resolvedEmail) {
|
|
||||||
throw new Error('缺少邮箱地址,请先完成步骤 2。');
|
|
||||||
}
|
|
||||||
|
|
||||||
const signupTabId = await getTabId('signup-page');
|
|
||||||
if (!signupTabId || !(await isTabAlive('signup-page'))) {
|
|
||||||
throw new Error('认证页面标签页已关闭,请先重新完成步骤 2。');
|
|
||||||
}
|
|
||||||
|
|
||||||
const password = state.customPassword || generatePassword();
|
|
||||||
await setPasswordState(password);
|
|
||||||
|
|
||||||
const accounts = state.accounts || [];
|
|
||||||
accounts.push({ email: resolvedEmail, password, createdAt: new Date().toISOString() });
|
|
||||||
await setState({ accounts });
|
|
||||||
|
|
||||||
await chrome.tabs.update(signupTabId, { active: true });
|
|
||||||
await ensureContentScriptReadyOnTab('signup-page', signupTabId, {
|
|
||||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
|
||||||
injectSource: 'signup-page',
|
|
||||||
timeoutMs: 45000,
|
|
||||||
retryDelayMs: 900,
|
|
||||||
logMessage: '步骤 3:密码页内容脚本未就绪,正在等待页面恢复...',
|
|
||||||
});
|
|
||||||
|
|
||||||
await addLog(
|
|
||||||
`步骤 3:正在填写密码,邮箱为 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
|
|
||||||
);
|
|
||||||
await sendToContentScript('signup-page', {
|
|
||||||
type: 'EXECUTE_STEP',
|
|
||||||
step: 3,
|
|
||||||
source: 'background',
|
|
||||||
payload: { email: resolvedEmail, password },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -7075,77 +7126,7 @@ async function resolveVerificationStep(step, state, mail, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function executeStep4(state) {
|
async function executeStep4(state) {
|
||||||
const mail = getMailConfig(state);
|
return step4Executor.executeStep4(state);
|
||||||
if (mail.error) throw new Error(mail.error);
|
|
||||||
const stepStartedAt = Date.now();
|
|
||||||
const signupTabId = await getTabId('signup-page');
|
|
||||||
if (!signupTabId) {
|
|
||||||
throw new Error('认证页面标签页已关闭,无法继续步骤 4。');
|
|
||||||
}
|
|
||||||
|
|
||||||
await chrome.tabs.update(signupTabId, { active: true });
|
|
||||||
throwIfStopped();
|
|
||||||
await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
|
|
||||||
const prepareResult = await sendToContentScriptResilient(
|
|
||||||
'signup-page',
|
|
||||||
{
|
|
||||||
type: 'PREPARE_SIGNUP_VERIFICATION',
|
|
||||||
step: 4,
|
|
||||||
source: 'background',
|
|
||||||
payload: { password: state.password || state.customPassword || '' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timeoutMs: 30000,
|
|
||||||
retryDelayMs: 700,
|
|
||||||
logMessage: '步骤 4:认证页正在切换,等待页面重新就绪后继续检测...',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (prepareResult && prepareResult.error) {
|
|
||||||
throw new Error(prepareResult.error);
|
|
||||||
}
|
|
||||||
if (prepareResult?.alreadyVerified) {
|
|
||||||
await completeStepFromBackground(4, {});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldUseCustomRegistrationEmail(state)) {
|
|
||||||
await confirmCustomVerificationStepBypass(4);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throwIfStopped();
|
|
||||||
if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
|
|
||||||
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
|
|
||||||
} else {
|
|
||||||
await addLog(`步骤 4:正在打开${mail.label}...`);
|
|
||||||
|
|
||||||
// For mail tabs, only create if not alive — don't navigate (preserves login session)
|
|
||||||
const alive = await isTabAlive(mail.source);
|
|
||||||
if (alive) {
|
|
||||||
if (mail.navigateOnReuse) {
|
|
||||||
await reuseOrCreateTab(mail.source, mail.url, {
|
|
||||||
inject: mail.inject,
|
|
||||||
injectSource: mail.injectSource,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const tabId = await getTabId(mail.source);
|
|
||||||
await chrome.tabs.update(tabId, { active: true });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await reuseOrCreateTab(mail.source, mail.url, {
|
|
||||||
inject: mail.inject,
|
|
||||||
injectSource: mail.injectSource,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await resolveVerificationStep(4, state, mail, {
|
|
||||||
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt,
|
|
||||||
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
|
|
||||||
resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -7174,57 +7155,7 @@ async function waitForStep5ChatgptRedirect(tabId, timeoutMs = 15000) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function executeStep5(state) {
|
async function executeStep5(state) {
|
||||||
const { firstName, lastName } = generateRandomName();
|
return step5Executor.executeStep5(state);
|
||||||
const { year, month, day } = generateRandomBirthday();
|
|
||||||
|
|
||||||
await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`);
|
|
||||||
|
|
||||||
let step5Result = null;
|
|
||||||
let step5TransportError = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
step5Result = await sendToContentScript('signup-page', {
|
|
||||||
type: 'EXECUTE_STEP',
|
|
||||||
step: 5,
|
|
||||||
source: 'background',
|
|
||||||
payload: { firstName, lastName, year, month, day },
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
// Cross-origin navigation (auth page → chatgpt.com) destroys the content
|
|
||||||
// script, causing a transport error. Capture it so we can check if the tab
|
|
||||||
// ended up on chatgpt.com onboarding.
|
|
||||||
if (isRetryableContentScriptTransportError(err)) {
|
|
||||||
step5TransportError = err;
|
|
||||||
console.log(LOG_PREFIX, '步骤 5:内容脚本通信中断,正在检查是否跳转到 ChatGPT 引导页...', err?.message);
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 1: content script successfully detected chatgpt redirect before dying
|
|
||||||
if (step5Result?.chatgptOnboarding) {
|
|
||||||
await addLog('步骤 5:检测到 ChatGPT 引导页跳转,正在处理引导页跳过...');
|
|
||||||
await handleChatgptOnboardingSkip();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step5Result?.chatgptHome) {
|
|
||||||
await addLog('步骤 5:检测到已进入 ChatGPT 页面,注册成功。', 'ok');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 2: content script died due to cross-origin navigation — check tab URL
|
|
||||||
if (step5TransportError) {
|
|
||||||
const signupTabId = await getTabId('signup-page');
|
|
||||||
const redirectedTab = await waitForStep5ChatgptRedirect(signupTabId);
|
|
||||||
if (redirectedTab) {
|
|
||||||
await addLog('步骤 5:内容脚本因页面跳转到 ChatGPT 而断开,正在处理引导页跳过...');
|
|
||||||
await handleChatgptOnboardingSkip(redirectedTab.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Not on chatgpt.com — re-throw the original error
|
|
||||||
throw step5TransportError;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleChatgptOnboardingSkip(knownTabId) {
|
async function handleChatgptOnboardingSkip(knownTabId) {
|
||||||
@@ -7479,186 +7410,15 @@ async function skipLoginVerificationStepsForCpaCallback() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function executeStep6(state) {
|
async function executeStep6(state) {
|
||||||
if (shouldSkipLoginVerificationForCpaCallback(state)) {
|
return step6Executor.executeStep6(state);
|
||||||
await skipLoginVerificationStepsForCpaCallback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!state.email) {
|
|
||||||
throw new Error('缺少邮箱地址,请先完成步骤 3。');
|
|
||||||
}
|
|
||||||
|
|
||||||
await runPreStep6CookieCleanup();
|
|
||||||
|
|
||||||
let attempt = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
throwIfStopped();
|
|
||||||
attempt += 1;
|
|
||||||
const currentState = attempt === 1 ? state : await getState();
|
|
||||||
const password = currentState.password || currentState.customPassword || '';
|
|
||||||
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
|
|
||||||
|
|
||||||
if (attempt === 1) {
|
|
||||||
await addLog('步骤 6:正在打开最新 OAuth 链接并登录...');
|
|
||||||
} else {
|
|
||||||
await addLog(`步骤 6:上一轮登录未进入验证码页,正在重新发起第 ${attempt} 轮登录尝试...`, 'warn');
|
|
||||||
}
|
|
||||||
|
|
||||||
await reuseOrCreateTab('signup-page', oauthUrl);
|
|
||||||
|
|
||||||
const result = await sendToContentScriptResilient(
|
|
||||||
'signup-page',
|
|
||||||
{
|
|
||||||
type: 'EXECUTE_STEP',
|
|
||||||
step: 6,
|
|
||||||
source: 'background',
|
|
||||||
payload: {
|
|
||||||
email: currentState.email,
|
|
||||||
password,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timeoutMs: 180000,
|
|
||||||
retryDelayMs: 700,
|
|
||||||
logMessage: '步骤 6:认证页正在切换,等待页面重新就绪后继续登录...',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStep6SuccessResult(result)) {
|
|
||||||
await completeStepFromBackground(6, {
|
|
||||||
loginVerificationRequestedAt: result.loginVerificationRequestedAt || null,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStep6RecoverableResult(result)) {
|
|
||||||
const reasonMessage = result.message
|
|
||||||
|| `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 6。`;
|
|
||||||
await addLog(`步骤 6:${reasonMessage}`, 'warn');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('步骤 6:认证页未返回可识别的登录结果。');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Step 7: Poll login verification mail and submit the login code
|
// Step 7: Poll login verification mail and submit the login code
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function runStep7Attempt(state) {
|
|
||||||
const mail = getMailConfig(state);
|
|
||||||
if (mail.error) throw new Error(mail.error);
|
|
||||||
const stepStartedAt = Date.now();
|
|
||||||
const authTabId = await getTabId('signup-page');
|
|
||||||
|
|
||||||
if (authTabId) {
|
|
||||||
await chrome.tabs.update(authTabId, { active: true });
|
|
||||||
} else {
|
|
||||||
if (!state.oauthUrl) {
|
|
||||||
throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。');
|
|
||||||
}
|
|
||||||
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
throwIfStopped();
|
|
||||||
await ensureStep7VerificationPageReady();
|
|
||||||
await addLog('步骤 7:登录验证码页面已就绪,开始获取验证码。', 'info');
|
|
||||||
|
|
||||||
if (shouldUseCustomRegistrationEmail(state)) {
|
|
||||||
await confirmCustomVerificationStepBypass(7);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throwIfStopped();
|
|
||||||
if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
|
|
||||||
await addLog(`步骤 7:正在通过 ${mail.label} 轮询验证码...`);
|
|
||||||
} else {
|
|
||||||
await addLog(`步骤 7:正在打开${mail.label}...`);
|
|
||||||
|
|
||||||
const alive = await isTabAlive(mail.source);
|
|
||||||
if (alive) {
|
|
||||||
if (mail.navigateOnReuse) {
|
|
||||||
await reuseOrCreateTab(mail.source, mail.url, {
|
|
||||||
inject: mail.inject,
|
|
||||||
injectSource: mail.injectSource,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const tabId = await getTabId(mail.source);
|
|
||||||
await chrome.tabs.update(tabId, { active: true });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await reuseOrCreateTab(mail.source, mail.url, {
|
|
||||||
inject: mail.inject,
|
|
||||||
injectSource: mail.injectSource,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await resolveVerificationStep(7, state, mail, {
|
|
||||||
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : Math.max(0, stepStartedAt - 60000),
|
|
||||||
requestFreshCodeFirst: false,
|
|
||||||
resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rerunStep6ForStep7Recovery() {
|
|
||||||
const currentState = await getState();
|
|
||||||
await addLog('步骤 7:正在回到步骤 6,重新发起登录验证码流程...', 'warn');
|
|
||||||
await executeStep6(currentState);
|
|
||||||
await sleepWithStop(3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeStep7(state) {
|
async function executeStep7(state) {
|
||||||
if (shouldSkipLoginVerificationForCpaCallback(state)) {
|
return step7Executor.executeStep7(state);
|
||||||
await setState({
|
|
||||||
lastLoginCode: null,
|
|
||||||
loginVerificationRequestedAt: null,
|
|
||||||
});
|
|
||||||
await setStepStatus(7, 'skipped');
|
|
||||||
await addLog('步骤 7:当前已选择“第六步回调”,本轮无需获取登录验证码。', 'warn');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentState = state;
|
|
||||||
let mailPollingAttempt = 1;
|
|
||||||
let lastMailPollingError = null;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
await runStep7Attempt(currentState);
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
if (!isVerificationMailPollingError(err)) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastMailPollingError = err;
|
|
||||||
if (mailPollingAttempt >= STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
mailPollingAttempt += 1;
|
|
||||||
await addLog(
|
|
||||||
`步骤 7:检测到邮箱轮询类失败,准备从步骤 6 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`,
|
|
||||||
'warn'
|
|
||||||
);
|
|
||||||
await rerunStep6ForStep7Recovery();
|
|
||||||
currentState = await getState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastMailPollingError) {
|
|
||||||
throw new Error(
|
|
||||||
`步骤 7:登录验证码流程在 ${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS} 轮邮箱轮询恢复后仍未成功。最后一次原因:${lastMailPollingError.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('步骤 7:登录验证码流程未成功完成。');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -7681,6 +7441,34 @@ const STEP8_STRATEGIES = [
|
|||||||
{ mode: 'debugger', label: 'debugger click retry' },
|
{ mode: 'debugger', label: 'debugger click retry' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function setWebNavListener(listener) {
|
||||||
|
webNavListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWebNavListener() {
|
||||||
|
return webNavListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWebNavCommittedListener(listener) {
|
||||||
|
webNavCommittedListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWebNavCommittedListener() {
|
||||||
|
return webNavCommittedListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStep8TabUpdatedListener(listener) {
|
||||||
|
step8TabUpdatedListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStep8TabUpdatedListener() {
|
||||||
|
return step8TabUpdatedListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStep8PendingReject(handler) {
|
||||||
|
step8PendingReject = handler;
|
||||||
|
}
|
||||||
|
|
||||||
function cleanupStep8NavigationListeners() {
|
function cleanupStep8NavigationListeners() {
|
||||||
if (webNavListener) {
|
if (webNavListener) {
|
||||||
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
|
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
|
||||||
@@ -7909,139 +7697,41 @@ function getStep8EffectLabel(effect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
cleanupStep8NavigationListeners,
|
||||||
|
clickWithDebugger,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureStep8SignupPageReady,
|
||||||
|
getStep8CallbackUrlFromNavigation,
|
||||||
|
getStep8CallbackUrlFromTabUpdate,
|
||||||
|
getStep8EffectLabel,
|
||||||
|
getTabId,
|
||||||
|
getWebNavCommittedListener,
|
||||||
|
getWebNavListener,
|
||||||
|
getStep8TabUpdatedListener,
|
||||||
|
isTabAlive,
|
||||||
|
prepareStep8DebuggerClick,
|
||||||
|
reloadStep8ConsentPage,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
setStep8PendingReject,
|
||||||
|
setStep8TabUpdatedListener,
|
||||||
|
setWebNavCommittedListener,
|
||||||
|
setWebNavListener,
|
||||||
|
sleepWithStop,
|
||||||
|
STEP8_CLICK_RETRY_DELAY_MS,
|
||||||
|
STEP8_MAX_ROUNDS,
|
||||||
|
STEP8_READY_WAIT_TIMEOUT_MS,
|
||||||
|
STEP8_STRATEGIES,
|
||||||
|
throwIfStep8SettledOrStopped,
|
||||||
|
triggerStep8ContentStrategy,
|
||||||
|
waitForStep8ClickEffect,
|
||||||
|
waitForStep8Ready,
|
||||||
|
});
|
||||||
|
|
||||||
async function executeStep8(state) {
|
async function executeStep8(state) {
|
||||||
if (!state.oauthUrl) {
|
return step8Executor.executeStep8(state);
|
||||||
throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。');
|
|
||||||
}
|
|
||||||
|
|
||||||
await addLog('步骤 8:正在监听 localhost 回调地址...');
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let resolved = false;
|
|
||||||
let signupTabId = null;
|
|
||||||
|
|
||||||
const cleanupListener = () => {
|
|
||||||
cleanupStep8NavigationListeners();
|
|
||||||
step8PendingReject = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const rejectStep8 = (error) => {
|
|
||||||
if (resolved) return;
|
|
||||||
resolved = true;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
cleanupListener();
|
|
||||||
reject(error);
|
|
||||||
};
|
|
||||||
|
|
||||||
const finalizeStep8Callback = (callbackUrl) => {
|
|
||||||
if (resolved || !callbackUrl) return;
|
|
||||||
|
|
||||||
resolved = true;
|
|
||||||
cleanupListener();
|
|
||||||
clearTimeout(timeout);
|
|
||||||
|
|
||||||
addLog(`步骤 8:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => {
|
|
||||||
return completeStepFromBackground(8, { localhostUrl: callbackUrl });
|
|
||||||
}).then(() => {
|
|
||||||
resolve();
|
|
||||||
}).catch((err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
rejectStep8(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 的点击可能被拦截了。'));
|
|
||||||
}, 120000);
|
|
||||||
|
|
||||||
step8PendingReject = (error) => {
|
|
||||||
rejectStep8(error);
|
|
||||||
};
|
|
||||||
|
|
||||||
webNavListener = (details) => {
|
|
||||||
const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId);
|
|
||||||
finalizeStep8Callback(callbackUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
webNavCommittedListener = (details) => {
|
|
||||||
const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId);
|
|
||||||
finalizeStep8Callback(callbackUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
step8TabUpdatedListener = (tabId, changeInfo, tab) => {
|
|
||||||
const callbackUrl = getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId);
|
|
||||||
finalizeStep8Callback(callbackUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
throwIfStep8SettledOrStopped(resolved);
|
|
||||||
signupTabId = await getTabId('signup-page');
|
|
||||||
throwIfStep8SettledOrStopped(resolved);
|
|
||||||
|
|
||||||
if (signupTabId && await isTabAlive('signup-page')) {
|
|
||||||
await chrome.tabs.update(signupTabId, { active: true });
|
|
||||||
await addLog('步骤 8:已切回认证页,正在准备调试器点击...');
|
|
||||||
} else {
|
|
||||||
signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
|
|
||||||
await addLog('步骤 8:已重新打开认证页,正在准备调试器点击...');
|
|
||||||
}
|
|
||||||
|
|
||||||
throwIfStep8SettledOrStopped(resolved);
|
|
||||||
chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
|
|
||||||
chrome.webNavigation.onCommitted.addListener(webNavCommittedListener);
|
|
||||||
chrome.tabs.onUpdated.addListener(step8TabUpdatedListener);
|
|
||||||
await ensureStep8SignupPageReady(signupTabId, {
|
|
||||||
timeoutMs: 15000,
|
|
||||||
logMessage: '步骤 8:认证页内容脚本尚未就绪,正在等待页面恢复...',
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) {
|
|
||||||
throwIfStep8SettledOrStopped(resolved);
|
|
||||||
const pageState = await waitForStep8Ready(signupTabId);
|
|
||||||
if (!pageState?.consentReady) {
|
|
||||||
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const strategy = STEP8_STRATEGIES[Math.min(round - 1, STEP8_STRATEGIES.length - 1)];
|
|
||||||
|
|
||||||
await addLog(`步骤 8:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`);
|
|
||||||
|
|
||||||
if (strategy.mode === 'debugger') {
|
|
||||||
const clickTarget = await prepareStep8DebuggerClick(signupTabId);
|
|
||||||
throwIfStep8SettledOrStopped(resolved);
|
|
||||||
await clickWithDebugger(signupTabId, clickTarget?.rect);
|
|
||||||
} else {
|
|
||||||
await triggerStep8ContentStrategy(signupTabId, strategy.strategy);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolved) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const effect = await waitForStep8ClickEffect(signupTabId, pageState.url);
|
|
||||||
if (resolved) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (effect.progressed) {
|
|
||||||
await addLog(`步骤 8:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (round >= STEP8_MAX_ROUNDS) {
|
|
||||||
throw new Error(`步骤 8:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await addLog(`步骤 8:${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn');
|
|
||||||
await reloadStep8ConsentPage(signupTabId);
|
|
||||||
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
rejectStep8(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -8049,138 +7739,7 @@ async function executeStep8(state) {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function executeStep9(state) {
|
async function executeStep9(state) {
|
||||||
if (getPanelMode(state) === 'sub2api') {
|
return step9Executor.executeStep9(state);
|
||||||
return executeSub2ApiStep9(state);
|
|
||||||
}
|
|
||||||
return executeCpaStep9(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeCpaStep9(state) {
|
|
||||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
|
||||||
throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。');
|
|
||||||
}
|
|
||||||
if (!state.localhostUrl) {
|
|
||||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 8。');
|
|
||||||
}
|
|
||||||
if (!state.vpsUrl) {
|
|
||||||
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldBypassStep9ForLocalCpa(state)) {
|
|
||||||
await addLog('步骤 9:检测到本地 CPA,且当前策略为“跳过第9步”,本轮不再重复提交回调地址。', 'info');
|
|
||||||
await completeStepFromBackground(9, {
|
|
||||||
localhostUrl: state.localhostUrl,
|
|
||||||
verifiedStatus: 'local-auto',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await addLog('步骤 9:正在打开 CPA 面板...');
|
|
||||||
|
|
||||||
const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
|
|
||||||
let tabId = await getTabId('vps-panel');
|
|
||||||
const alive = tabId && await isTabAlive('vps-panel');
|
|
||||||
|
|
||||||
if (!alive) {
|
|
||||||
tabId = await reuseOrCreateTab('vps-panel', state.vpsUrl, {
|
|
||||||
inject: injectFiles,
|
|
||||||
reloadIfSameUrl: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await closeConflictingTabsForSource('vps-panel', state.vpsUrl, { excludeTabIds: [tabId] });
|
|
||||||
await chrome.tabs.update(tabId, { active: true });
|
|
||||||
await rememberSourceLastUrl('vps-panel', state.vpsUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensureContentScriptReadyOnTab('vps-panel', tabId, {
|
|
||||||
inject: injectFiles,
|
|
||||||
timeoutMs: 45000,
|
|
||||||
retryDelayMs: 900,
|
|
||||||
logMessage: '步骤 9:CPA 面板仍在加载,正在重试连接...',
|
|
||||||
});
|
|
||||||
|
|
||||||
await addLog('步骤 9:正在填写回调地址...');
|
|
||||||
const result = await sendToContentScriptResilient('vps-panel', {
|
|
||||||
type: 'EXECUTE_STEP',
|
|
||||||
step: 9,
|
|
||||||
source: 'background',
|
|
||||||
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword },
|
|
||||||
}, {
|
|
||||||
timeoutMs: 30000,
|
|
||||||
retryDelayMs: 700,
|
|
||||||
logMessage: '步骤 9:CPA 面板通信未就绪,正在等待页面恢复...',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeSub2ApiStep9(state) {
|
|
||||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
|
||||||
throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。');
|
|
||||||
}
|
|
||||||
if (!state.localhostUrl) {
|
|
||||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 8。');
|
|
||||||
}
|
|
||||||
if (!state.sub2apiSessionId) {
|
|
||||||
throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。');
|
|
||||||
}
|
|
||||||
if (!state.sub2apiEmail) {
|
|
||||||
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
|
|
||||||
}
|
|
||||||
if (!state.sub2apiPassword) {
|
|
||||||
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
|
|
||||||
}
|
|
||||||
|
|
||||||
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
|
||||||
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
|
||||||
|
|
||||||
await addLog('步骤 9:正在打开 SUB2API 后台...');
|
|
||||||
|
|
||||||
let tabId = await getTabId('sub2api-panel');
|
|
||||||
const alive = tabId && await isTabAlive('sub2api-panel');
|
|
||||||
|
|
||||||
if (!alive) {
|
|
||||||
tabId = await reuseOrCreateTab('sub2api-panel', sub2apiUrl, {
|
|
||||||
inject: injectFiles,
|
|
||||||
injectSource: 'sub2api-panel',
|
|
||||||
reloadIfSameUrl: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl, { excludeTabIds: [tabId] });
|
|
||||||
await chrome.tabs.update(tabId, { active: true });
|
|
||||||
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensureContentScriptReadyOnTab('sub2api-panel', tabId, {
|
|
||||||
inject: injectFiles,
|
|
||||||
injectSource: 'sub2api-panel',
|
|
||||||
});
|
|
||||||
|
|
||||||
await addLog('步骤 9:正在向 SUB2API 提交回调并创建账号...');
|
|
||||||
const result = await sendToContentScript('sub2api-panel', {
|
|
||||||
type: 'EXECUTE_STEP',
|
|
||||||
step: 9,
|
|
||||||
source: 'background',
|
|
||||||
payload: {
|
|
||||||
localhostUrl: state.localhostUrl,
|
|
||||||
sub2apiUrl,
|
|
||||||
sub2apiEmail: state.sub2apiEmail,
|
|
||||||
sub2apiPassword: state.sub2apiPassword,
|
|
||||||
sub2apiGroupName: state.sub2apiGroupName,
|
|
||||||
sub2apiSessionId: state.sub2apiSessionId,
|
|
||||||
sub2apiOAuthState: state.sub2apiOAuthState,
|
|
||||||
sub2apiGroupId: state.sub2apiGroupId,
|
|
||||||
sub2apiDraftName: state.sub2apiDraftName,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
responseTimeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
(function attachBackgroundStep1(root, factory) {
|
||||||
|
root.MultiPageBackgroundStep1 = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep1Module() {
|
||||||
|
function createStep1Executor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureSignupEntryPageReady,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function executeStep1() {
|
||||||
|
await addLog('步骤 1:正在打开 ChatGPT 官网...');
|
||||||
|
await ensureSignupEntryPageReady(1);
|
||||||
|
await completeStepFromBackground(1, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { executeStep1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { createStep1Executor };
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
(function attachBackgroundStep2(root, factory) {
|
||||||
|
root.MultiPageBackgroundStep2 = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep2Module() {
|
||||||
|
function createStep2Executor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureContentScriptReadyOnTab,
|
||||||
|
ensureSignupEntryPageReady,
|
||||||
|
ensureSignupPasswordPageReadyInTab,
|
||||||
|
getTabId,
|
||||||
|
isTabAlive,
|
||||||
|
resolveSignupEmailForFlow,
|
||||||
|
sendToContentScriptResilient,
|
||||||
|
SIGNUP_PAGE_INJECT_FILES,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function executeStep2(state) {
|
||||||
|
const resolvedEmail = await resolveSignupEmailForFlow(state);
|
||||||
|
|
||||||
|
let signupTabId = await getTabId('signup-page');
|
||||||
|
if (!signupTabId || !(await isTabAlive('signup-page'))) {
|
||||||
|
await addLog('步骤 2:未发现可用的注册页标签,正在重新打开 ChatGPT 官网...', 'warn');
|
||||||
|
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||||
|
} else {
|
||||||
|
await chrome.tabs.update(signupTabId, { active: true });
|
||||||
|
await ensureContentScriptReadyOnTab('signup-page', signupTabId, {
|
||||||
|
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||||
|
injectSource: 'signup-page',
|
||||||
|
timeoutMs: 45000,
|
||||||
|
retryDelayMs: 900,
|
||||||
|
logMessage: '步骤 2:注册入口页内容脚本未就绪,正在等待页面恢复...',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const step2Result = await sendToContentScriptResilient('signup-page', {
|
||||||
|
type: 'EXECUTE_STEP',
|
||||||
|
step: 2,
|
||||||
|
source: 'background',
|
||||||
|
payload: { email: resolvedEmail },
|
||||||
|
}, {
|
||||||
|
timeoutMs: 20000,
|
||||||
|
retryDelayMs: 700,
|
||||||
|
logMessage: '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (step2Result?.error) {
|
||||||
|
throw new Error(step2Result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!step2Result?.alreadyOnPasswordPage) {
|
||||||
|
await addLog(`步骤 2:邮箱 ${resolvedEmail} 已提交,正在等待进入密码页...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureSignupPasswordPageReadyInTab(signupTabId, 2, {
|
||||||
|
skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
|
||||||
|
});
|
||||||
|
await completeStepFromBackground(2, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { executeStep2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { createStep2Executor };
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
(function attachBackgroundStep3(root, factory) {
|
||||||
|
root.MultiPageBackgroundStep3 = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep3Module() {
|
||||||
|
function createStep3Executor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
ensureContentScriptReadyOnTab,
|
||||||
|
generatePassword,
|
||||||
|
getTabId,
|
||||||
|
isTabAlive,
|
||||||
|
sendToContentScript,
|
||||||
|
setPasswordState,
|
||||||
|
setState,
|
||||||
|
SIGNUP_PAGE_INJECT_FILES,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function executeStep3(state) {
|
||||||
|
const resolvedEmail = state.email;
|
||||||
|
if (!resolvedEmail) {
|
||||||
|
throw new Error('缺少邮箱地址,请先完成步骤 2。');
|
||||||
|
}
|
||||||
|
|
||||||
|
const signupTabId = await getTabId('signup-page');
|
||||||
|
if (!signupTabId || !(await isTabAlive('signup-page'))) {
|
||||||
|
throw new Error('认证页面标签页已关闭,请先重新完成步骤 2。');
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = state.customPassword || generatePassword();
|
||||||
|
await setPasswordState(password);
|
||||||
|
|
||||||
|
const accounts = state.accounts || [];
|
||||||
|
accounts.push({ email: resolvedEmail, password, createdAt: new Date().toISOString() });
|
||||||
|
await setState({ accounts });
|
||||||
|
|
||||||
|
await chrome.tabs.update(signupTabId, { active: true });
|
||||||
|
await ensureContentScriptReadyOnTab('signup-page', signupTabId, {
|
||||||
|
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||||
|
injectSource: 'signup-page',
|
||||||
|
timeoutMs: 45000,
|
||||||
|
retryDelayMs: 900,
|
||||||
|
logMessage: '步骤 3:密码页内容脚本未就绪,正在等待页面恢复...',
|
||||||
|
});
|
||||||
|
|
||||||
|
await addLog(
|
||||||
|
`步骤 3:正在填写密码,邮箱为 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
|
||||||
|
);
|
||||||
|
await sendToContentScript('signup-page', {
|
||||||
|
type: 'EXECUTE_STEP',
|
||||||
|
step: 3,
|
||||||
|
source: 'background',
|
||||||
|
payload: { email: resolvedEmail, password },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { executeStep3 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { createStep3Executor };
|
||||||
|
});
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
(function attachBackgroundStep4(root, factory) {
|
||||||
|
root.MultiPageBackgroundStep4 = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep4Module() {
|
||||||
|
function createStep4Executor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
completeStepFromBackground,
|
||||||
|
confirmCustomVerificationStepBypass,
|
||||||
|
getMailConfig,
|
||||||
|
getTabId,
|
||||||
|
HOTMAIL_PROVIDER,
|
||||||
|
isTabAlive,
|
||||||
|
LUCKMAIL_PROVIDER,
|
||||||
|
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||||
|
resolveVerificationStep,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
sendToContentScriptResilient,
|
||||||
|
shouldUseCustomRegistrationEmail,
|
||||||
|
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||||
|
throwIfStopped,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function executeStep4(state) {
|
||||||
|
const mail = getMailConfig(state);
|
||||||
|
if (mail.error) throw new Error(mail.error);
|
||||||
|
const stepStartedAt = Date.now();
|
||||||
|
const signupTabId = await getTabId('signup-page');
|
||||||
|
if (!signupTabId) {
|
||||||
|
throw new Error('认证页面标签页已关闭,无法继续步骤 4。');
|
||||||
|
}
|
||||||
|
|
||||||
|
await chrome.tabs.update(signupTabId, { active: true });
|
||||||
|
throwIfStopped();
|
||||||
|
await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
|
||||||
|
const prepareResult = await sendToContentScriptResilient(
|
||||||
|
'signup-page',
|
||||||
|
{
|
||||||
|
type: 'PREPARE_SIGNUP_VERIFICATION',
|
||||||
|
step: 4,
|
||||||
|
source: 'background',
|
||||||
|
payload: { password: state.password || state.customPassword || '' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeoutMs: 30000,
|
||||||
|
retryDelayMs: 700,
|
||||||
|
logMessage: '步骤 4:认证页正在切换,等待页面重新就绪后继续检测...',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (prepareResult && prepareResult.error) {
|
||||||
|
throw new Error(prepareResult.error);
|
||||||
|
}
|
||||||
|
if (prepareResult?.alreadyVerified) {
|
||||||
|
await completeStepFromBackground(4, {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUseCustomRegistrationEmail(state)) {
|
||||||
|
await confirmCustomVerificationStepBypass(4);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throwIfStopped();
|
||||||
|
if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
|
||||||
|
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
|
||||||
|
} else {
|
||||||
|
await addLog(`步骤 4:正在打开${mail.label}...`);
|
||||||
|
|
||||||
|
const alive = await isTabAlive(mail.source);
|
||||||
|
if (alive) {
|
||||||
|
if (mail.navigateOnReuse) {
|
||||||
|
await reuseOrCreateTab(mail.source, mail.url, {
|
||||||
|
inject: mail.inject,
|
||||||
|
injectSource: mail.injectSource,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const tabId = await getTabId(mail.source);
|
||||||
|
await chrome.tabs.update(tabId, { active: true });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await reuseOrCreateTab(mail.source, mail.url, {
|
||||||
|
inject: mail.inject,
|
||||||
|
injectSource: mail.injectSource,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await resolveVerificationStep(4, state, mail, {
|
||||||
|
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt,
|
||||||
|
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
|
||||||
|
resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { executeStep4 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { createStep4Executor };
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
(function attachBackgroundStep5(root, factory) {
|
||||||
|
root.MultiPageBackgroundStep5 = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep5Module() {
|
||||||
|
function createStep5Executor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
generateRandomBirthday,
|
||||||
|
generateRandomName,
|
||||||
|
getTabId,
|
||||||
|
handleChatgptOnboardingSkip,
|
||||||
|
isRetryableContentScriptTransportError,
|
||||||
|
LOG_PREFIX,
|
||||||
|
sendToContentScript,
|
||||||
|
waitForStep5ChatgptRedirect,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function executeStep5() {
|
||||||
|
const { firstName, lastName } = generateRandomName();
|
||||||
|
const { year, month, day } = generateRandomBirthday();
|
||||||
|
|
||||||
|
await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`);
|
||||||
|
|
||||||
|
let step5Result = null;
|
||||||
|
let step5TransportError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
step5Result = await sendToContentScript('signup-page', {
|
||||||
|
type: 'EXECUTE_STEP',
|
||||||
|
step: 5,
|
||||||
|
source: 'background',
|
||||||
|
payload: { firstName, lastName, year, month, day },
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (isRetryableContentScriptTransportError(err)) {
|
||||||
|
step5TransportError = err;
|
||||||
|
console.log(LOG_PREFIX, '步骤 5:内容脚本通信中断,正在检查是否跳转到 ChatGPT 引导页...', err?.message);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step5Result?.chatgptOnboarding) {
|
||||||
|
await addLog('步骤 5:检测到 ChatGPT 引导页跳转,正在处理引导页跳过...');
|
||||||
|
await handleChatgptOnboardingSkip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step5Result?.chatgptHome) {
|
||||||
|
await addLog('步骤 5:检测到已进入 ChatGPT 页面,注册成功。', 'ok');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step5TransportError) {
|
||||||
|
const signupTabId = await getTabId('signup-page');
|
||||||
|
const redirectedTab = await waitForStep5ChatgptRedirect(signupTabId);
|
||||||
|
if (redirectedTab) {
|
||||||
|
await addLog('步骤 5:内容脚本因页面跳转到 ChatGPT 而断开,正在处理引导页跳过...');
|
||||||
|
await handleChatgptOnboardingSkip(redirectedTab.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw step5TransportError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { executeStep5 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { createStep5Executor };
|
||||||
|
});
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
(function attachBackgroundStep6(root, factory) {
|
||||||
|
root.MultiPageBackgroundStep6 = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep6Module() {
|
||||||
|
function createStep6Executor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
completeStepFromBackground,
|
||||||
|
getLoginAuthStateLabel,
|
||||||
|
getState,
|
||||||
|
isStep6RecoverableResult,
|
||||||
|
isStep6SuccessResult,
|
||||||
|
refreshOAuthUrlBeforeStep6,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
runPreStep6CookieCleanup,
|
||||||
|
sendToContentScriptResilient,
|
||||||
|
shouldSkipLoginVerificationForCpaCallback,
|
||||||
|
skipLoginVerificationStepsForCpaCallback,
|
||||||
|
throwIfStopped,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function executeStep6(state) {
|
||||||
|
if (shouldSkipLoginVerificationForCpaCallback(state)) {
|
||||||
|
await skipLoginVerificationStepsForCpaCallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.email) {
|
||||||
|
throw new Error('缺少邮箱地址,请先完成步骤 3。');
|
||||||
|
}
|
||||||
|
|
||||||
|
await runPreStep6CookieCleanup();
|
||||||
|
|
||||||
|
let attempt = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
throwIfStopped();
|
||||||
|
attempt += 1;
|
||||||
|
const currentState = attempt === 1 ? state : await getState();
|
||||||
|
const password = currentState.password || currentState.customPassword || '';
|
||||||
|
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
|
||||||
|
|
||||||
|
if (attempt === 1) {
|
||||||
|
await addLog('步骤 6:正在打开最新 OAuth 链接并登录...');
|
||||||
|
} else {
|
||||||
|
await addLog(`步骤 6:上一轮登录未进入验证码页,正在重新发起第 ${attempt} 轮登录尝试...`, 'warn');
|
||||||
|
}
|
||||||
|
|
||||||
|
await reuseOrCreateTab('signup-page', oauthUrl);
|
||||||
|
|
||||||
|
const result = await sendToContentScriptResilient(
|
||||||
|
'signup-page',
|
||||||
|
{
|
||||||
|
type: 'EXECUTE_STEP',
|
||||||
|
step: 6,
|
||||||
|
source: 'background',
|
||||||
|
payload: {
|
||||||
|
email: currentState.email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeoutMs: 180000,
|
||||||
|
retryDelayMs: 700,
|
||||||
|
logMessage: '步骤 6:认证页正在切换,等待页面重新就绪后继续登录...',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStep6SuccessResult(result)) {
|
||||||
|
await completeStepFromBackground(6, {
|
||||||
|
loginVerificationRequestedAt: result.loginVerificationRequestedAt || null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStep6RecoverableResult(result)) {
|
||||||
|
const reasonMessage = result.message
|
||||||
|
|| `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 6。`;
|
||||||
|
await addLog(`步骤 6:${reasonMessage}`, 'warn');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('步骤 6:认证页未返回可识别的登录结果。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { executeStep6 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { createStep6Executor };
|
||||||
|
});
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
(function attachBackgroundStep7(root, factory) {
|
||||||
|
root.MultiPageBackgroundStep7 = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep7Module() {
|
||||||
|
function createStep7Executor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||||
|
confirmCustomVerificationStepBypass,
|
||||||
|
ensureStep7VerificationPageReady,
|
||||||
|
executeStep6,
|
||||||
|
getMailConfig,
|
||||||
|
getState,
|
||||||
|
getTabId,
|
||||||
|
HOTMAIL_PROVIDER,
|
||||||
|
isTabAlive,
|
||||||
|
isVerificationMailPollingError,
|
||||||
|
LUCKMAIL_PROVIDER,
|
||||||
|
resolveVerificationStep,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
setState,
|
||||||
|
setStepStatus,
|
||||||
|
shouldSkipLoginVerificationForCpaCallback,
|
||||||
|
shouldUseCustomRegistrationEmail,
|
||||||
|
sleepWithStop,
|
||||||
|
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||||
|
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
|
||||||
|
throwIfStopped,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function runStep7Attempt(state) {
|
||||||
|
const mail = getMailConfig(state);
|
||||||
|
if (mail.error) throw new Error(mail.error);
|
||||||
|
const stepStartedAt = Date.now();
|
||||||
|
const authTabId = await getTabId('signup-page');
|
||||||
|
|
||||||
|
if (authTabId) {
|
||||||
|
await chrome.tabs.update(authTabId, { active: true });
|
||||||
|
} else {
|
||||||
|
if (!state.oauthUrl) {
|
||||||
|
throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。');
|
||||||
|
}
|
||||||
|
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
throwIfStopped();
|
||||||
|
await ensureStep7VerificationPageReady();
|
||||||
|
await addLog('步骤 7:登录验证码页面已就绪,开始获取验证码。', 'info');
|
||||||
|
|
||||||
|
if (shouldUseCustomRegistrationEmail(state)) {
|
||||||
|
await confirmCustomVerificationStepBypass(7);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throwIfStopped();
|
||||||
|
if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
|
||||||
|
await addLog(`步骤 7:正在通过 ${mail.label} 轮询验证码...`);
|
||||||
|
} else {
|
||||||
|
await addLog(`步骤 7:正在打开${mail.label}...`);
|
||||||
|
|
||||||
|
const alive = await isTabAlive(mail.source);
|
||||||
|
if (alive) {
|
||||||
|
if (mail.navigateOnReuse) {
|
||||||
|
await reuseOrCreateTab(mail.source, mail.url, {
|
||||||
|
inject: mail.inject,
|
||||||
|
injectSource: mail.injectSource,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const tabId = await getTabId(mail.source);
|
||||||
|
await chrome.tabs.update(tabId, { active: true });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await reuseOrCreateTab(mail.source, mail.url, {
|
||||||
|
inject: mail.inject,
|
||||||
|
injectSource: mail.injectSource,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await resolveVerificationStep(7, state, mail, {
|
||||||
|
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : Math.max(0, stepStartedAt - 60000),
|
||||||
|
requestFreshCodeFirst: false,
|
||||||
|
resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rerunStep6ForStep7Recovery() {
|
||||||
|
const currentState = await getState();
|
||||||
|
await addLog('步骤 7:正在回到步骤 6,重新发起登录验证码流程...', 'warn');
|
||||||
|
await executeStep6(currentState);
|
||||||
|
await sleepWithStop(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeStep7(state) {
|
||||||
|
if (shouldSkipLoginVerificationForCpaCallback(state)) {
|
||||||
|
await setState({
|
||||||
|
lastLoginCode: null,
|
||||||
|
loginVerificationRequestedAt: null,
|
||||||
|
});
|
||||||
|
await setStepStatus(7, 'skipped');
|
||||||
|
await addLog('步骤 7:当前已选择“第六步回调”,本轮无需获取登录验证码。', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentState = state;
|
||||||
|
let mailPollingAttempt = 1;
|
||||||
|
let lastMailPollingError = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
await runStep7Attempt(currentState);
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
if (!isVerificationMailPollingError(err)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMailPollingError = err;
|
||||||
|
if (mailPollingAttempt >= STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
mailPollingAttempt += 1;
|
||||||
|
await addLog(
|
||||||
|
`步骤 7:检测到邮箱轮询类失败,准备从步骤 6 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
await rerunStep6ForStep7Recovery();
|
||||||
|
currentState = await getState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastMailPollingError) {
|
||||||
|
throw new Error(
|
||||||
|
`步骤 7:登录验证码流程在 ${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS} 轮邮箱轮询恢复后仍未成功。最后一次原因:${lastMailPollingError.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('步骤 7:登录验证码流程未成功完成。');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { executeStep7 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { createStep7Executor };
|
||||||
|
});
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
(function attachBackgroundStep8(root, factory) {
|
||||||
|
root.MultiPageBackgroundStep8 = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() {
|
||||||
|
function createStep8Executor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
cleanupStep8NavigationListeners,
|
||||||
|
clickWithDebugger,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureStep8SignupPageReady,
|
||||||
|
getStep8CallbackUrlFromNavigation,
|
||||||
|
getStep8CallbackUrlFromTabUpdate,
|
||||||
|
getStep8EffectLabel,
|
||||||
|
getTabId,
|
||||||
|
isTabAlive,
|
||||||
|
prepareStep8DebuggerClick,
|
||||||
|
reloadStep8ConsentPage,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
sleepWithStop,
|
||||||
|
STEP8_CLICK_RETRY_DELAY_MS,
|
||||||
|
STEP8_MAX_ROUNDS,
|
||||||
|
STEP8_READY_WAIT_TIMEOUT_MS,
|
||||||
|
STEP8_STRATEGIES,
|
||||||
|
throwIfStep8SettledOrStopped,
|
||||||
|
triggerStep8ContentStrategy,
|
||||||
|
waitForStep8ClickEffect,
|
||||||
|
waitForStep8Ready,
|
||||||
|
setWebNavListener,
|
||||||
|
setWebNavCommittedListener,
|
||||||
|
setStep8PendingReject,
|
||||||
|
setStep8TabUpdatedListener,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function executeStep8(state) {
|
||||||
|
if (!state.oauthUrl) {
|
||||||
|
throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。');
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog('步骤 8:正在监听 localhost 回调地址...');
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let resolved = false;
|
||||||
|
let signupTabId = null;
|
||||||
|
|
||||||
|
const cleanupListener = () => {
|
||||||
|
cleanupStep8NavigationListeners();
|
||||||
|
setStep8PendingReject(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectStep8 = (error) => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
cleanupListener();
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalizeStep8Callback = (callbackUrl) => {
|
||||||
|
if (resolved || !callbackUrl) return;
|
||||||
|
|
||||||
|
resolved = true;
|
||||||
|
cleanupListener();
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
|
addLog(`步骤 8:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => {
|
||||||
|
return completeStepFromBackground(8, { localhostUrl: callbackUrl });
|
||||||
|
}).then(() => {
|
||||||
|
resolve();
|
||||||
|
}).catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
rejectStep8(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 的点击可能被拦截了。'));
|
||||||
|
}, 120000);
|
||||||
|
|
||||||
|
setStep8PendingReject((error) => {
|
||||||
|
rejectStep8(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
setWebNavListener((details) => {
|
||||||
|
const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId);
|
||||||
|
finalizeStep8Callback(callbackUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
setWebNavCommittedListener((details) => {
|
||||||
|
const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId);
|
||||||
|
finalizeStep8Callback(callbackUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
setStep8TabUpdatedListener((tabId, changeInfo, tab) => {
|
||||||
|
const callbackUrl = getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId);
|
||||||
|
finalizeStep8Callback(callbackUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
throwIfStep8SettledOrStopped(resolved);
|
||||||
|
signupTabId = await getTabId('signup-page');
|
||||||
|
throwIfStep8SettledOrStopped(resolved);
|
||||||
|
|
||||||
|
if (signupTabId && await isTabAlive('signup-page')) {
|
||||||
|
await chrome.tabs.update(signupTabId, { active: true });
|
||||||
|
await addLog('步骤 8:已切回认证页,正在准备调试器点击...');
|
||||||
|
} else {
|
||||||
|
signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||||
|
await addLog('步骤 8:已重新打开认证页,正在准备调试器点击...');
|
||||||
|
}
|
||||||
|
|
||||||
|
throwIfStep8SettledOrStopped(resolved);
|
||||||
|
chrome.webNavigation.onBeforeNavigate.addListener(deps.getWebNavListener());
|
||||||
|
chrome.webNavigation.onCommitted.addListener(deps.getWebNavCommittedListener());
|
||||||
|
chrome.tabs.onUpdated.addListener(deps.getStep8TabUpdatedListener());
|
||||||
|
await ensureStep8SignupPageReady(signupTabId, {
|
||||||
|
timeoutMs: 15000,
|
||||||
|
logMessage: '步骤 8:认证页内容脚本尚未就绪,正在等待页面恢复...',
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) {
|
||||||
|
throwIfStep8SettledOrStopped(resolved);
|
||||||
|
const pageState = await waitForStep8Ready(signupTabId, STEP8_READY_WAIT_TIMEOUT_MS);
|
||||||
|
if (!pageState?.consentReady) {
|
||||||
|
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategy = STEP8_STRATEGIES[Math.min(round - 1, STEP8_STRATEGIES.length - 1)];
|
||||||
|
|
||||||
|
await addLog(`步骤 8:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`);
|
||||||
|
|
||||||
|
if (strategy.mode === 'debugger') {
|
||||||
|
const clickTarget = await prepareStep8DebuggerClick(signupTabId);
|
||||||
|
throwIfStep8SettledOrStopped(resolved);
|
||||||
|
await clickWithDebugger(signupTabId, clickTarget?.rect);
|
||||||
|
} else {
|
||||||
|
await triggerStep8ContentStrategy(signupTabId, strategy.strategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const effect = await waitForStep8ClickEffect(signupTabId, pageState.url);
|
||||||
|
if (resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effect.progressed) {
|
||||||
|
await addLog(`步骤 8:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (round >= STEP8_MAX_ROUNDS) {
|
||||||
|
throw new Error(`步骤 8:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog(`步骤 8:${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn');
|
||||||
|
await reloadStep8ConsentPage(signupTabId);
|
||||||
|
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
rejectStep8(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { executeStep8 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { createStep8Executor };
|
||||||
|
});
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
(function attachBackgroundStep9(root, factory) {
|
||||||
|
root.MultiPageBackgroundStep9 = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep9Module() {
|
||||||
|
function createStep9Executor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
closeConflictingTabsForSource,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureContentScriptReadyOnTab,
|
||||||
|
getPanelMode,
|
||||||
|
getTabId,
|
||||||
|
isLocalhostOAuthCallbackUrl,
|
||||||
|
isTabAlive,
|
||||||
|
normalizeSub2ApiUrl,
|
||||||
|
rememberSourceLastUrl,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
sendToContentScript,
|
||||||
|
sendToContentScriptResilient,
|
||||||
|
shouldBypassStep9ForLocalCpa,
|
||||||
|
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function executeStep9(state) {
|
||||||
|
if (getPanelMode(state) === 'sub2api') {
|
||||||
|
return executeSub2ApiStep9(state);
|
||||||
|
}
|
||||||
|
return executeCpaStep9(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeCpaStep9(state) {
|
||||||
|
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||||
|
throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。');
|
||||||
|
}
|
||||||
|
if (!state.localhostUrl) {
|
||||||
|
throw new Error('缺少 localhost 回调地址,请先完成步骤 8。');
|
||||||
|
}
|
||||||
|
if (!state.vpsUrl) {
|
||||||
|
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldBypassStep9ForLocalCpa(state)) {
|
||||||
|
await addLog('步骤 9:检测到本地 CPA,且当前策略为“跳过第9步”,本轮不再重复提交回调地址。', 'info');
|
||||||
|
await completeStepFromBackground(9, {
|
||||||
|
localhostUrl: state.localhostUrl,
|
||||||
|
verifiedStatus: 'local-auto',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog('步骤 9:正在打开 CPA 面板...');
|
||||||
|
|
||||||
|
const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
|
||||||
|
let tabId = await getTabId('vps-panel');
|
||||||
|
const alive = tabId && await isTabAlive('vps-panel');
|
||||||
|
|
||||||
|
if (!alive) {
|
||||||
|
tabId = await reuseOrCreateTab('vps-panel', state.vpsUrl, {
|
||||||
|
inject: injectFiles,
|
||||||
|
reloadIfSameUrl: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await closeConflictingTabsForSource('vps-panel', state.vpsUrl, { excludeTabIds: [tabId] });
|
||||||
|
await chrome.tabs.update(tabId, { active: true });
|
||||||
|
await rememberSourceLastUrl('vps-panel', state.vpsUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureContentScriptReadyOnTab('vps-panel', tabId, {
|
||||||
|
inject: injectFiles,
|
||||||
|
timeoutMs: 45000,
|
||||||
|
retryDelayMs: 900,
|
||||||
|
logMessage: '步骤 9:CPA 面板仍在加载,正在重试连接...',
|
||||||
|
});
|
||||||
|
|
||||||
|
await addLog('步骤 9:正在填写回调地址...');
|
||||||
|
const result = await sendToContentScriptResilient('vps-panel', {
|
||||||
|
type: 'EXECUTE_STEP',
|
||||||
|
step: 9,
|
||||||
|
source: 'background',
|
||||||
|
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword },
|
||||||
|
}, {
|
||||||
|
timeoutMs: 30000,
|
||||||
|
retryDelayMs: 700,
|
||||||
|
logMessage: '步骤 9:CPA 面板通信未就绪,正在等待页面恢复...',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeSub2ApiStep9(state) {
|
||||||
|
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||||
|
throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。');
|
||||||
|
}
|
||||||
|
if (!state.localhostUrl) {
|
||||||
|
throw new Error('缺少 localhost 回调地址,请先完成步骤 8。');
|
||||||
|
}
|
||||||
|
if (!state.sub2apiSessionId) {
|
||||||
|
throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。');
|
||||||
|
}
|
||||||
|
if (!state.sub2apiEmail) {
|
||||||
|
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
|
||||||
|
}
|
||||||
|
if (!state.sub2apiPassword) {
|
||||||
|
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||||
|
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||||
|
|
||||||
|
await addLog('步骤 9:正在打开 SUB2API 后台...');
|
||||||
|
|
||||||
|
let tabId = await getTabId('sub2api-panel');
|
||||||
|
const alive = tabId && await isTabAlive('sub2api-panel');
|
||||||
|
|
||||||
|
if (!alive) {
|
||||||
|
tabId = await reuseOrCreateTab('sub2api-panel', sub2apiUrl, {
|
||||||
|
inject: injectFiles,
|
||||||
|
injectSource: 'sub2api-panel',
|
||||||
|
reloadIfSameUrl: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl, { excludeTabIds: [tabId] });
|
||||||
|
await chrome.tabs.update(tabId, { active: true });
|
||||||
|
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureContentScriptReadyOnTab('sub2api-panel', tabId, {
|
||||||
|
inject: injectFiles,
|
||||||
|
injectSource: 'sub2api-panel',
|
||||||
|
});
|
||||||
|
|
||||||
|
await addLog('步骤 9:正在向 SUB2API 提交回调并创建账号...');
|
||||||
|
const result = await sendToContentScript('sub2api-panel', {
|
||||||
|
type: 'EXECUTE_STEP',
|
||||||
|
step: 9,
|
||||||
|
source: 'background',
|
||||||
|
payload: {
|
||||||
|
localhostUrl: state.localhostUrl,
|
||||||
|
sub2apiUrl,
|
||||||
|
sub2apiEmail: state.sub2apiEmail,
|
||||||
|
sub2apiPassword: state.sub2apiPassword,
|
||||||
|
sub2apiGroupName: state.sub2apiGroupName,
|
||||||
|
sub2apiSessionId: state.sub2apiSessionId,
|
||||||
|
sub2apiOAuthState: state.sub2apiOAuthState,
|
||||||
|
sub2apiGroupId: state.sub2apiGroupId,
|
||||||
|
sub2apiDraftName: state.sub2apiDraftName,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
responseTimeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
executeCpaStep9,
|
||||||
|
executeStep9,
|
||||||
|
executeSub2ApiStep9,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { createStep9Executor };
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
test('background imports step 1~9 modules', () => {
|
||||||
|
const source = fs.readFileSync('background.js', 'utf8');
|
||||||
|
|
||||||
|
[
|
||||||
|
'background/steps/step1.js',
|
||||||
|
'background/steps/step2.js',
|
||||||
|
'background/steps/step3.js',
|
||||||
|
'background/steps/step4.js',
|
||||||
|
'background/steps/step5.js',
|
||||||
|
'background/steps/step6.js',
|
||||||
|
'background/steps/step7.js',
|
||||||
|
'background/steps/step8.js',
|
||||||
|
'background/steps/step9.js',
|
||||||
|
].forEach((path) => {
|
||||||
|
assert.match(source, new RegExp(path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
const source = fs.readFileSync('background.js', 'utf8');
|
const helperSource = fs.readFileSync('background.js', 'utf8');
|
||||||
|
const step8ModuleSource = fs.readFileSync('background/steps/step8.js', 'utf8');
|
||||||
|
|
||||||
function extractFunction(name) {
|
function extractFunction(source, name) {
|
||||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
const start = markers
|
const start = markers
|
||||||
.map(marker => source.indexOf(marker))
|
.map(marker => source.indexOf(marker))
|
||||||
@@ -50,16 +51,16 @@ function extractFunction(name) {
|
|||||||
return source.slice(start, end);
|
return source.slice(start, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bundle = [
|
const helperBundle = [
|
||||||
extractFunction('throwIfStopped'),
|
extractFunction(helperSource, 'throwIfStopped'),
|
||||||
extractFunction('cleanupStep8NavigationListeners'),
|
extractFunction(helperSource, 'cleanupStep8NavigationListeners'),
|
||||||
extractFunction('rejectPendingStep8'),
|
extractFunction(helperSource, 'rejectPendingStep8'),
|
||||||
extractFunction('throwIfStep8SettledOrStopped'),
|
extractFunction(helperSource, 'throwIfStep8SettledOrStopped'),
|
||||||
extractFunction('requestStop'),
|
extractFunction(helperSource, 'requestStop'),
|
||||||
extractFunction('executeStep8'),
|
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
const api = new Function(`
|
const api = new Function('step8ModuleSource', `
|
||||||
|
const self = {};
|
||||||
let stopRequested = false;
|
let stopRequested = false;
|
||||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||||
let webNavListener = null;
|
let webNavListener = null;
|
||||||
@@ -71,6 +72,13 @@ let autoRunCurrentRun = 2;
|
|||||||
let autoRunTotalRuns = 3;
|
let autoRunTotalRuns = 3;
|
||||||
let autoRunAttemptRun = 4;
|
let autoRunAttemptRun = 4;
|
||||||
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
|
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
|
||||||
|
const STEP8_CLICK_RETRY_DELAY_MS = 500;
|
||||||
|
const STEP8_MAX_ROUNDS = 5;
|
||||||
|
const STEP8_READY_WAIT_TIMEOUT_MS = 30000;
|
||||||
|
const STEP8_STRATEGIES = [
|
||||||
|
{ mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
|
||||||
|
{ mode: 'debugger', label: 'debugger click' },
|
||||||
|
];
|
||||||
|
|
||||||
const added = {
|
const added = {
|
||||||
beforeNavigate: 0,
|
beforeNavigate: 0,
|
||||||
@@ -89,28 +97,28 @@ let resolveTabId = null;
|
|||||||
const chrome = {
|
const chrome = {
|
||||||
webNavigation: {
|
webNavigation: {
|
||||||
onBeforeNavigate: {
|
onBeforeNavigate: {
|
||||||
addListener(listener) {
|
addListener() {
|
||||||
added.beforeNavigate += 1;
|
added.beforeNavigate += 1;
|
||||||
},
|
},
|
||||||
removeListener(listener) {
|
removeListener() {
|
||||||
removed.beforeNavigate += 1;
|
removed.beforeNavigate += 1;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onCommitted: {
|
onCommitted: {
|
||||||
addListener(listener) {
|
addListener() {
|
||||||
added.committed += 1;
|
added.committed += 1;
|
||||||
},
|
},
|
||||||
removeListener(listener) {
|
removeListener() {
|
||||||
removed.committed += 1;
|
removed.committed += 1;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tabs: {
|
tabs: {
|
||||||
onUpdated: {
|
onUpdated: {
|
||||||
addListener(listener) {
|
addListener() {
|
||||||
added.tabUpdated += 1;
|
added.tabUpdated += 1;
|
||||||
},
|
},
|
||||||
removeListener(listener) {
|
removeListener() {
|
||||||
removed.tabUpdated += 1;
|
removed.tabUpdated += 1;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -137,6 +145,7 @@ function isAutoRunScheduledState() {
|
|||||||
}
|
}
|
||||||
function getStep8CallbackUrlFromNavigation() { return ''; }
|
function getStep8CallbackUrlFromNavigation() { return ''; }
|
||||||
function getStep8CallbackUrlFromTabUpdate() { return ''; }
|
function getStep8CallbackUrlFromTabUpdate() { return ''; }
|
||||||
|
function getStep8EffectLabel() { return 'no_effect'; }
|
||||||
async function completeStepFromBackground() {}
|
async function completeStepFromBackground() {}
|
||||||
async function getTabId() {
|
async function getTabId() {
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
@@ -149,18 +158,86 @@ async function reuseOrCreateTab() {
|
|||||||
async function isTabAlive() {
|
async function isTabAlive() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async function sendToContentScript(source, message) {
|
async function ensureStep8SignupPageReady() {}
|
||||||
sentMessages.push({ source, type: message.type });
|
async function prepareStep8DebuggerClick() {
|
||||||
|
sentMessages.push({ source: 'signup-page', type: 'STEP8_FIND_AND_CLICK' });
|
||||||
return { rect: { centerX: 10, centerY: 20 } };
|
return { rect: { centerX: 10, centerY: 20 } };
|
||||||
}
|
}
|
||||||
|
async function triggerStep8ContentStrategy() {
|
||||||
|
sentMessages.push({ source: 'signup-page', type: 'STEP8_TRIGGER_CONTINUE' });
|
||||||
|
}
|
||||||
|
async function waitForStep8ClickEffect() {
|
||||||
|
return { progressed: false, reason: 'no_effect' };
|
||||||
|
}
|
||||||
|
async function waitForStep8Ready() {
|
||||||
|
return { consentReady: true, url: 'https://example.com/consent' };
|
||||||
|
}
|
||||||
|
async function reloadStep8ConsentPage() {}
|
||||||
|
async function sleepWithStop() {}
|
||||||
async function clickWithDebugger() {
|
async function clickWithDebugger() {
|
||||||
clickCount += 1;
|
clickCount += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
${bundle}
|
function setWebNavListener(listener) {
|
||||||
|
webNavListener = listener;
|
||||||
|
}
|
||||||
|
function getWebNavListener() {
|
||||||
|
return webNavListener;
|
||||||
|
}
|
||||||
|
function setWebNavCommittedListener(listener) {
|
||||||
|
webNavCommittedListener = listener;
|
||||||
|
}
|
||||||
|
function getWebNavCommittedListener() {
|
||||||
|
return webNavCommittedListener;
|
||||||
|
}
|
||||||
|
function setStep8TabUpdatedListener(listener) {
|
||||||
|
step8TabUpdatedListener = listener;
|
||||||
|
}
|
||||||
|
function getStep8TabUpdatedListener() {
|
||||||
|
return step8TabUpdatedListener;
|
||||||
|
}
|
||||||
|
function setStep8PendingReject(handler) {
|
||||||
|
step8PendingReject = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
${helperBundle}
|
||||||
|
${step8ModuleSource}
|
||||||
|
|
||||||
|
const executor = self.MultiPageBackgroundStep8.createStep8Executor({
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
cleanupStep8NavigationListeners,
|
||||||
|
clickWithDebugger,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureStep8SignupPageReady,
|
||||||
|
getStep8CallbackUrlFromNavigation,
|
||||||
|
getStep8CallbackUrlFromTabUpdate,
|
||||||
|
getStep8EffectLabel,
|
||||||
|
getTabId,
|
||||||
|
getWebNavCommittedListener,
|
||||||
|
getWebNavListener,
|
||||||
|
getStep8TabUpdatedListener,
|
||||||
|
isTabAlive,
|
||||||
|
prepareStep8DebuggerClick,
|
||||||
|
reloadStep8ConsentPage,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
setStep8PendingReject,
|
||||||
|
setStep8TabUpdatedListener,
|
||||||
|
setWebNavCommittedListener,
|
||||||
|
setWebNavListener,
|
||||||
|
sleepWithStop,
|
||||||
|
STEP8_CLICK_RETRY_DELAY_MS,
|
||||||
|
STEP8_MAX_ROUNDS,
|
||||||
|
STEP8_READY_WAIT_TIMEOUT_MS,
|
||||||
|
STEP8_STRATEGIES,
|
||||||
|
throwIfStep8SettledOrStopped,
|
||||||
|
triggerStep8ContentStrategy,
|
||||||
|
waitForStep8ClickEffect,
|
||||||
|
waitForStep8Ready,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
executeStep8,
|
executeStep8: executor.executeStep8,
|
||||||
requestStop,
|
requestStop,
|
||||||
resolveTabId(tabId) {
|
resolveTabId(tabId) {
|
||||||
if (!resolveTabId) {
|
if (!resolveTabId) {
|
||||||
@@ -183,7 +260,7 @@ return {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
`)();
|
`)(step8ModuleSource);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const step8Promise = api.executeStep8({ oauthUrl: 'https://example.com/oauth' });
|
const step8Promise = api.executeStep8({ oauthUrl: 'https://example.com/oauth' });
|
||||||
@@ -205,7 +282,7 @@ return {
|
|||||||
{ beforeNavigate: 0, committed: 0, tabUpdated: 0 },
|
{ beforeNavigate: 0, committed: 0, tabUpdated: 0 },
|
||||||
'Stop 先发生时,不应再注册 Step 8 监听'
|
'Stop 先发生时,不应再注册 Step 8 监听'
|
||||||
);
|
);
|
||||||
assert.strictEqual(state.sentMessages.length, 0, 'Stop 后不应再发送 STEP8_FIND_AND_CLICK 命令');
|
assert.strictEqual(state.sentMessages.length, 0, 'Stop 后不应再发送 Step 8 执行动作');
|
||||||
assert.strictEqual(state.clickCount, 0, 'Stop 后不应再触发 debugger 点击');
|
assert.strictEqual(state.clickCount, 0, 'Stop 后不应再触发 debugger 点击');
|
||||||
assert.strictEqual(state.webNavListener, null, 'Stop 后 onBeforeNavigate 引用应为空');
|
assert.strictEqual(state.webNavListener, null, 'Stop 后 onBeforeNavigate 引用应为空');
|
||||||
assert.strictEqual(state.webNavCommittedListener, null, 'Stop 后 onCommitted 引用应为空');
|
assert.strictEqual(state.webNavCommittedListener, null, 'Stop 后 onCommitted 引用应为空');
|
||||||
|
|||||||
Reference in New Issue
Block a user