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/generated-email-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',
|
||||
'hotmail-utils.js',
|
||||
'microsoft-email.js',
|
||||
@@ -6407,6 +6416,123 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
|
||||
SIGNUP_PAGE_INJECT_FILES,
|
||||
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 = {}) {
|
||||
return panelBridge.requestOAuthUrlFromPanel(state, options);
|
||||
@@ -6441,9 +6567,7 @@ async function resolveSignupEmailForFlow(state) {
|
||||
// ============================================================
|
||||
|
||||
async function executeStep1() {
|
||||
await addLog('步骤 1:正在打开 ChatGPT 官网...');
|
||||
await ensureSignupEntryPageReady(1);
|
||||
await completeStepFromBackground(1, {});
|
||||
return step1Executor.executeStep1();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -6451,46 +6575,7 @@ async function executeStep1() {
|
||||
// ============================================================
|
||||
|
||||
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 step2Executor.executeStep2(state);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -6498,41 +6583,7 @@ async function executeStep2(state) {
|
||||
// ============================================================
|
||||
|
||||
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 step3Executor.executeStep3(state);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -7075,77 +7126,7 @@ async function resolveVerificationStep(step, state, mail, options = {}) {
|
||||
}
|
||||
|
||||
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}...`);
|
||||
|
||||
// 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;
|
||||
return step4Executor.executeStep4(state);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -7174,57 +7155,7 @@ async function waitForStep5ChatgptRedirect(tabId, timeoutMs = 15000) {
|
||||
}
|
||||
|
||||
async function executeStep5(state) {
|
||||
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) {
|
||||
// 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;
|
||||
}
|
||||
return step5Executor.executeStep5(state);
|
||||
}
|
||||
|
||||
async function handleChatgptOnboardingSkip(knownTabId) {
|
||||
@@ -7479,186 +7410,15 @@ async function skipLoginVerificationStepsForCpaCallback() {
|
||||
}
|
||||
|
||||
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 step6Executor.executeStep6(state);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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) {
|
||||
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 step7Executor.executeStep7(state);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -7681,6 +7441,34 @@ const STEP8_STRATEGIES = [
|
||||
{ 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() {
|
||||
if (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) {
|
||||
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();
|
||||
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);
|
||||
}
|
||||
})();
|
||||
});
|
||||
return step8Executor.executeStep8(state);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -8049,138 +7739,7 @@ async function executeStep8(state) {
|
||||
// ============================================================
|
||||
|
||||
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 step9Executor.executeStep9(state);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -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 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 start = markers
|
||||
.map(marker => source.indexOf(marker))
|
||||
@@ -50,16 +51,16 @@ function extractFunction(name) {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
const bundle = [
|
||||
extractFunction('throwIfStopped'),
|
||||
extractFunction('cleanupStep8NavigationListeners'),
|
||||
extractFunction('rejectPendingStep8'),
|
||||
extractFunction('throwIfStep8SettledOrStopped'),
|
||||
extractFunction('requestStop'),
|
||||
extractFunction('executeStep8'),
|
||||
const helperBundle = [
|
||||
extractFunction(helperSource, 'throwIfStopped'),
|
||||
extractFunction(helperSource, 'cleanupStep8NavigationListeners'),
|
||||
extractFunction(helperSource, 'rejectPendingStep8'),
|
||||
extractFunction(helperSource, 'throwIfStep8SettledOrStopped'),
|
||||
extractFunction(helperSource, 'requestStop'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const api = new Function('step8ModuleSource', `
|
||||
const self = {};
|
||||
let stopRequested = false;
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
let webNavListener = null;
|
||||
@@ -71,6 +72,13 @@ let autoRunCurrentRun = 2;
|
||||
let autoRunTotalRuns = 3;
|
||||
let autoRunAttemptRun = 4;
|
||||
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 = {
|
||||
beforeNavigate: 0,
|
||||
@@ -89,28 +97,28 @@ let resolveTabId = null;
|
||||
const chrome = {
|
||||
webNavigation: {
|
||||
onBeforeNavigate: {
|
||||
addListener(listener) {
|
||||
addListener() {
|
||||
added.beforeNavigate += 1;
|
||||
},
|
||||
removeListener(listener) {
|
||||
removeListener() {
|
||||
removed.beforeNavigate += 1;
|
||||
},
|
||||
},
|
||||
onCommitted: {
|
||||
addListener(listener) {
|
||||
addListener() {
|
||||
added.committed += 1;
|
||||
},
|
||||
removeListener(listener) {
|
||||
removeListener() {
|
||||
removed.committed += 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
tabs: {
|
||||
onUpdated: {
|
||||
addListener(listener) {
|
||||
addListener() {
|
||||
added.tabUpdated += 1;
|
||||
},
|
||||
removeListener(listener) {
|
||||
removeListener() {
|
||||
removed.tabUpdated += 1;
|
||||
},
|
||||
},
|
||||
@@ -137,6 +145,7 @@ function isAutoRunScheduledState() {
|
||||
}
|
||||
function getStep8CallbackUrlFromNavigation() { return ''; }
|
||||
function getStep8CallbackUrlFromTabUpdate() { return ''; }
|
||||
function getStep8EffectLabel() { return 'no_effect'; }
|
||||
async function completeStepFromBackground() {}
|
||||
async function getTabId() {
|
||||
return await new Promise((resolve) => {
|
||||
@@ -149,18 +158,86 @@ async function reuseOrCreateTab() {
|
||||
async function isTabAlive() {
|
||||
return true;
|
||||
}
|
||||
async function sendToContentScript(source, message) {
|
||||
sentMessages.push({ source, type: message.type });
|
||||
async function ensureStep8SignupPageReady() {}
|
||||
async function prepareStep8DebuggerClick() {
|
||||
sentMessages.push({ source: 'signup-page', type: 'STEP8_FIND_AND_CLICK' });
|
||||
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() {
|
||||
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 {
|
||||
executeStep8,
|
||||
executeStep8: executor.executeStep8,
|
||||
requestStop,
|
||||
resolveTabId(tabId) {
|
||||
if (!resolveTabId) {
|
||||
@@ -183,7 +260,7 @@ return {
|
||||
};
|
||||
},
|
||||
};
|
||||
`)();
|
||||
`)(step8ModuleSource);
|
||||
|
||||
(async () => {
|
||||
const step8Promise = api.executeStep8({ oauthUrl: 'https://example.com/oauth' });
|
||||
@@ -205,7 +282,7 @@ return {
|
||||
{ beforeNavigate: 0, committed: 0, tabUpdated: 0 },
|
||||
'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.webNavListener, null, 'Stop 后 onBeforeNavigate 引用应为空');
|
||||
assert.strictEqual(state.webNavCommittedListener, null, 'Stop 后 onCommitted 引用应为空');
|
||||
|
||||
Reference in New Issue
Block a user