Merge remote-tracking branch 'origin/dev' into pr-112
This commit is contained in:
+80
-17
@@ -272,6 +272,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
customEmailPool: [],
|
||||
autoDeleteUsedIcloudAlias: false,
|
||||
icloudHostPreference: 'auto',
|
||||
icloudFetchMode: 'reuse_existing',
|
||||
accountRunHistoryTextEnabled: false,
|
||||
accountRunHistoryHelperBaseUrl: DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL,
|
||||
gmailBaseEmail: '',
|
||||
@@ -674,6 +675,11 @@ function normalizeEmailGenerator(value = '') {
|
||||
return 'duck';
|
||||
}
|
||||
|
||||
function normalizeIcloudFetchMode(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'always_new' ? 'always_new' : 'reuse_existing';
|
||||
}
|
||||
|
||||
function normalizeCustomEmailPool(value = []) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
@@ -979,6 +985,8 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return Boolean(value);
|
||||
case 'icloudHostPreference':
|
||||
return normalizeIcloudHost(value) || 'auto';
|
||||
case 'icloudFetchMode':
|
||||
return normalizeIcloudFetchMode(value);
|
||||
case 'accountRunHistoryHelperBaseUrl':
|
||||
return normalizeAccountRunHistoryHelperBaseUrl(value);
|
||||
case 'gmailBaseEmail':
|
||||
@@ -3106,6 +3114,16 @@ async function pollLuckmailVerificationCode(step, state, pollPayload = {}) {
|
||||
excludeCodes: pollPayload.excludeCodes || [],
|
||||
};
|
||||
|
||||
const initialCursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor);
|
||||
if (!initialCursor.messageId && !initialCursor.receivedAt) {
|
||||
const mailList = await client.user.getTokenMails(purchase.token);
|
||||
const baselineCursor = buildLuckmailBaselineCursor(mailList?.mails || []);
|
||||
await setLuckmailMailCursorState(baselineCursor);
|
||||
if (baselineCursor?.messageId || baselineCursor?.receivedAt) {
|
||||
await addLog(`步骤 ${step}:LuckMail 已保存当前邮箱旧邮件快照,后续仅使用新收到的验证码。`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
throwIfStopped();
|
||||
@@ -3449,10 +3467,16 @@ async function validateIcloudSession(setupUrl) {
|
||||
return data;
|
||||
}
|
||||
|
||||
async function resolveIcloudPremiumMailService() {
|
||||
async function resolveIcloudPremiumMailService(options = {}) {
|
||||
const errors = [];
|
||||
const state = await getState();
|
||||
const setupUrls = await getPreferredIcloudSetupUrls(state);
|
||||
const explicitHost = normalizeIcloudHost(options?.hostPreference || options?.preferredHost || '');
|
||||
const setupUrls = explicitHost
|
||||
? (() => {
|
||||
const forcedSetupUrl = getIcloudSetupUrlForHost(explicitHost);
|
||||
return forcedSetupUrl ? [forcedSetupUrl] : [];
|
||||
})()
|
||||
: await getPreferredIcloudSetupUrls(state);
|
||||
|
||||
for (const setupUrl of setupUrls) {
|
||||
try {
|
||||
@@ -3481,17 +3505,19 @@ function getIcloudAliasLabel() {
|
||||
return `MultiPage ${dateStr}`;
|
||||
}
|
||||
|
||||
async function checkIcloudSession() {
|
||||
return withIcloudLoginHelp('检查 iCloud 会话', async () => {
|
||||
const { setupUrl } = await resolveIcloudPremiumMailService();
|
||||
async function checkIcloudSession(options = {}) {
|
||||
const actionLabel = String(options?.actionLabel || '检查 iCloud 会话').trim() || '检查 iCloud 会话';
|
||||
const { actionLabel: _ignoredActionLabel, ...resolveOptions } = options || {};
|
||||
return withIcloudLoginHelp(actionLabel, async () => {
|
||||
const { setupUrl } = await resolveIcloudPremiumMailService(resolveOptions);
|
||||
await addLog(`iCloud:会话校验通过(${new URL(setupUrl).host})`, 'ok');
|
||||
return { ok: true, setupUrl };
|
||||
});
|
||||
}
|
||||
|
||||
async function listIcloudAliases() {
|
||||
async function listIcloudAliases(options = {}) {
|
||||
return withIcloudLoginHelp('加载 iCloud 隐私邮箱列表', async () => {
|
||||
const { serviceUrl } = await resolveIcloudPremiumMailService();
|
||||
const { serviceUrl } = await resolveIcloudPremiumMailService(options);
|
||||
const response = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`);
|
||||
const state = await getState();
|
||||
return normalizeIcloudAliasList(response, {
|
||||
@@ -3581,9 +3607,10 @@ async function deleteUsedIcloudAliases() {
|
||||
return { deleted, skipped };
|
||||
}
|
||||
|
||||
async function fetchIcloudHideMyEmail() {
|
||||
async function fetchIcloudHideMyEmail(options = {}) {
|
||||
return withIcloudLoginHelp('获取 iCloud 隐私邮箱', async () => {
|
||||
throwIfStopped();
|
||||
const generateNew = Boolean(options?.generateNew);
|
||||
await addLog('iCloud:正在校验当前浏览器登录状态...', 'info');
|
||||
|
||||
const { serviceUrl, setupUrl } = await resolveIcloudPremiumMailService();
|
||||
@@ -3596,12 +3623,16 @@ async function fetchIcloudHideMyEmail() {
|
||||
preservedEmails: getPreservedAliasMap(state),
|
||||
});
|
||||
|
||||
const reusableAlias = pickReusableIcloudAlias(existingAliases);
|
||||
if (reusableAlias) {
|
||||
await setEmailState(reusableAlias.email);
|
||||
await addLog(`iCloud:复用未使用别名 ${reusableAlias.email}`, 'ok');
|
||||
broadcastIcloudAliasesChanged({ reason: 'selected', email: reusableAlias.email });
|
||||
return reusableAlias.email;
|
||||
if (!generateNew) {
|
||||
const reusableAlias = pickReusableIcloudAlias(existingAliases);
|
||||
if (reusableAlias) {
|
||||
await setEmailState(reusableAlias.email);
|
||||
await addLog(`iCloud:复用未使用别名 ${reusableAlias.email}`, 'ok');
|
||||
broadcastIcloudAliasesChanged({ reason: 'selected', email: reusableAlias.email });
|
||||
return reusableAlias.email;
|
||||
}
|
||||
} else {
|
||||
await addLog('iCloud:已启用“始终创建新别名”,本次将跳过复用。', 'info');
|
||||
}
|
||||
|
||||
await addLog('iCloud:没有可复用别名,开始生成新的 Hide My Email 地址...', 'warn');
|
||||
@@ -5973,7 +6004,10 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
|
||||
if (attempt > 1) {
|
||||
await addLog(`${generatorLabel}:正在进行第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次自动获取重试...`, 'warn');
|
||||
}
|
||||
const generatedEmail = await fetchGeneratedEmail(currentState, { generateNew: true, generator });
|
||||
const generatedEmail = await fetchGeneratedEmail(currentState, {
|
||||
generateNew: generator !== 'icloud' || normalizeIcloudFetchMode(currentState.icloudFetchMode) === 'always_new',
|
||||
generator,
|
||||
});
|
||||
await addLog(
|
||||
`=== 目标 ${targetRun}/${totalRuns} 轮:${generatorLabel}已就绪:${generatedEmail}(第 ${attemptRuns} 次尝试,第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次获取)===`,
|
||||
'ok'
|
||||
@@ -6119,7 +6153,10 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
|
||||
if (attempt > 1) {
|
||||
await addLog(`${generatorLabel}:正在进行第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次自动获取重试...`, 'warn');
|
||||
}
|
||||
const generatedEmail = await fetchGeneratedEmail(currentState, { generateNew: true, generator });
|
||||
const generatedEmail = await fetchGeneratedEmail(currentState, {
|
||||
generateNew: generator !== 'icloud' || normalizeIcloudFetchMode(currentState.icloudFetchMode) === 'always_new',
|
||||
generator,
|
||||
});
|
||||
await addLog(
|
||||
`=== 目标 ${targetRun}/${totalRuns} 轮:${generatorLabel}已就绪:${generatedEmail}(第 ${attemptRuns} 次尝试,第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次获取)===`,
|
||||
'ok'
|
||||
@@ -6204,9 +6241,15 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
|
||||
let restartFromStep1WithCurrentEmail = false;
|
||||
let step = Math.max(currentStartStep, 4);
|
||||
while (step <= LAST_STEP_ID) {
|
||||
const latestState = await getState();
|
||||
const currentStatus = latestState.stepStatuses?.[step] || 'pending';
|
||||
if (isStepDoneStatus(currentStatus)) {
|
||||
await addLog(`自动运行:步骤 ${step} 当前状态为 ${currentStatus},将直接继续后续流程。`, 'info');
|
||||
step += 1;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await executeStepAndWait(step, AUTO_STEP_DELAYS[step]);
|
||||
const latestState = await getState();
|
||||
step += 1;
|
||||
} catch (err) {
|
||||
if (isStopError(err)) {
|
||||
@@ -6445,6 +6488,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create
|
||||
getTabId,
|
||||
HOTMAIL_PROVIDER,
|
||||
isMail2925LimitReachedError,
|
||||
isRetryableContentScriptTransportError,
|
||||
isStopError,
|
||||
LUCKMAIL_PROVIDER,
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS,
|
||||
@@ -6453,6 +6497,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create
|
||||
pollHotmailVerificationCode,
|
||||
pollLuckmailVerificationCode,
|
||||
sendToContentScript,
|
||||
sendToContentScriptResilient,
|
||||
sendToMailContentScriptResilient,
|
||||
setState,
|
||||
setStepStatus,
|
||||
@@ -6470,6 +6515,7 @@ const step2Executor = self.MultiPageBackgroundStep2?.createStep2Executor({
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
ensureContentScriptReadyOnTab,
|
||||
ensureSignupAuthEntryPageReady,
|
||||
ensureSignupEntryPageReady,
|
||||
ensureSignupPostEmailPageReadyInTab,
|
||||
getTabId,
|
||||
@@ -6490,12 +6536,24 @@ const step3Executor = self.MultiPageBackgroundStep3?.createStep3Executor({
|
||||
setState,
|
||||
SIGNUP_PAGE_INJECT_FILES,
|
||||
});
|
||||
|
||||
async function ensureIcloudMailSessionForVerification(options = {}) {
|
||||
const flowState = options?.state || await getState().catch(() => ({}));
|
||||
const hostPreference = getConfiguredIcloudHostPreference(flowState)
|
||||
|| normalizeIcloudHost(flowState?.preferredIcloudHost);
|
||||
return checkIcloudSession({
|
||||
...(hostPreference ? { hostPreference } : {}),
|
||||
actionLabel: options?.actionLabel || '检查 iCloud 会话',
|
||||
});
|
||||
}
|
||||
|
||||
const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({
|
||||
addLog,
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
|
||||
getMailConfig,
|
||||
getTabId,
|
||||
HOTMAIL_PROVIDER,
|
||||
@@ -6542,6 +6600,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
|
||||
ensureStep8VerificationPageReady,
|
||||
getOAuthFlowRemainingMs,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
@@ -6724,6 +6783,10 @@ async function ensureSignupEntryPageReady(step = 1) {
|
||||
return signupFlowHelpers.ensureSignupEntryPageReady(step);
|
||||
}
|
||||
|
||||
async function ensureSignupAuthEntryPageReady(step = 1) {
|
||||
return signupFlowHelpers.ensureSignupEntryPageReady(step);
|
||||
}
|
||||
|
||||
async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) {
|
||||
return signupFlowHelpers.ensureSignupPasswordPageReadyInTab(tabId, step, options);
|
||||
}
|
||||
|
||||
@@ -283,7 +283,10 @@
|
||||
return fetchManagedAliasEmail(mergedState, options);
|
||||
}
|
||||
if (generator === 'icloud') {
|
||||
return fetchIcloudHideMyEmail();
|
||||
const stateFetchMode = String(mergedState.icloudFetchMode || '').trim().toLowerCase();
|
||||
return fetchIcloudHideMyEmail({
|
||||
generateNew: Boolean(options.generateNew) || stateFetchMode === 'always_new',
|
||||
});
|
||||
}
|
||||
if (generator === 'cloudflare') {
|
||||
return fetchCloudflareEmail(mergedState, options);
|
||||
|
||||
@@ -151,6 +151,18 @@
|
||||
if (payload.email) {
|
||||
await setEmailState(payload.email);
|
||||
}
|
||||
if (payload.skipRegistrationFlow) {
|
||||
const latestState = await getState();
|
||||
for (const skipStep of [3, 4, 5]) {
|
||||
const status = latestState.stepStatuses?.[skipStep];
|
||||
if (status === 'running' || status === 'completed' || status === 'manual_completed') {
|
||||
continue;
|
||||
}
|
||||
await setStepStatus(skipStep, 'skipped');
|
||||
}
|
||||
await addLog('步骤 2:检测到当前已登录会话,已自动跳过步骤 3/4/5,流程将直接进入步骤 6。', 'warn');
|
||||
break;
|
||||
}
|
||||
if (payload.skippedPasswordStep) {
|
||||
const latestState = await getState();
|
||||
const step3Status = latestState.stepStatuses?.[3];
|
||||
@@ -179,6 +191,14 @@
|
||||
lastEmailTimestamp: payload.emailTimestamp || null,
|
||||
signupVerificationRequestedAt: null,
|
||||
});
|
||||
if (payload.skipProfileStep) {
|
||||
const latestState = await getState();
|
||||
const step5Status = latestState.stepStatuses?.[5];
|
||||
if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') {
|
||||
await setStepStatus(5, 'skipped');
|
||||
await addLog('步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。', 'warn');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
await setState({
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
confirmCustomVerificationStepBypass,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureIcloudMailSession,
|
||||
ensureStep8VerificationPageReady,
|
||||
getOAuthFlowRemainingMs,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
@@ -140,6 +141,15 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
||||
await addLog('步骤 8:正在确认 iCloud 邮箱登录态...', 'info');
|
||||
await ensureIcloudMailSession({
|
||||
state,
|
||||
step: 8,
|
||||
actionLabel: '步骤 8:确认 iCloud 邮箱登录态',
|
||||
});
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
if (
|
||||
mail.provider === HOTMAIL_PROVIDER
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
completeStepFromBackground,
|
||||
confirmCustomVerificationStepBypass,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureIcloudMailSession,
|
||||
getMailConfig,
|
||||
getTabId,
|
||||
HOTMAIL_PROVIDER,
|
||||
@@ -111,6 +112,15 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
||||
await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info');
|
||||
await ensureIcloudMailSession({
|
||||
state,
|
||||
step: 4,
|
||||
actionLabel: '步骤 4:确认 iCloud 邮箱登录态',
|
||||
});
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
if (
|
||||
mail.provider === HOTMAIL_PROVIDER
|
||||
|
||||
@@ -1,14 +1,142 @@
|
||||
(function attachBackgroundStep1(root, factory) {
|
||||
root.MultiPageBackgroundStep1 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep1Module() {
|
||||
const STEP1_COOKIE_CLEAR_DOMAINS = [
|
||||
'chatgpt.com',
|
||||
'chat.openai.com',
|
||||
'openai.com',
|
||||
'auth.openai.com',
|
||||
'auth0.openai.com',
|
||||
'accounts.openai.com',
|
||||
];
|
||||
const STEP1_COOKIE_CLEAR_ORIGINS = [
|
||||
'https://chatgpt.com',
|
||||
'https://chat.openai.com',
|
||||
'https://auth.openai.com',
|
||||
'https://auth0.openai.com',
|
||||
'https://accounts.openai.com',
|
||||
'https://openai.com',
|
||||
];
|
||||
|
||||
function normalizeCookieDomainForStep1(domain) {
|
||||
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
|
||||
}
|
||||
|
||||
function shouldClearStep1Cookie(cookie) {
|
||||
const domain = normalizeCookieDomainForStep1(cookie?.domain);
|
||||
if (!domain) return false;
|
||||
return STEP1_COOKIE_CLEAR_DOMAINS.some((target) => (
|
||||
domain === target || domain.endsWith(`.${target}`)
|
||||
));
|
||||
}
|
||||
|
||||
function buildStep1CookieRemovalUrl(cookie) {
|
||||
const host = normalizeCookieDomainForStep1(cookie?.domain);
|
||||
const rawPath = String(cookie?.path || '/');
|
||||
const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
|
||||
return `https://${host}${path}`;
|
||||
}
|
||||
|
||||
function getStep1ErrorMessage(error) {
|
||||
return error?.message || String(error || '未知错误');
|
||||
}
|
||||
|
||||
async function collectStep1Cookies(chromeApi) {
|
||||
if (!chromeApi.cookies?.getAll) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stores = chromeApi.cookies.getAllCookieStores
|
||||
? await chromeApi.cookies.getAllCookieStores()
|
||||
: [{ id: undefined }];
|
||||
const cookies = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const store of stores) {
|
||||
const storeId = store?.id;
|
||||
const batch = await chromeApi.cookies.getAll(storeId ? { storeId } : {});
|
||||
for (const cookie of batch || []) {
|
||||
if (!shouldClearStep1Cookie(cookie)) continue;
|
||||
const key = [
|
||||
cookie.storeId || storeId || '',
|
||||
cookie.domain || '',
|
||||
cookie.path || '',
|
||||
cookie.name || '',
|
||||
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
||||
].join('|');
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
cookies.push(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
async function removeStep1Cookie(chromeApi, cookie) {
|
||||
const details = {
|
||||
url: buildStep1CookieRemovalUrl(cookie),
|
||||
name: cookie.name,
|
||||
};
|
||||
if (cookie.storeId) {
|
||||
details.storeId = cookie.storeId;
|
||||
}
|
||||
if (cookie.partitionKey) {
|
||||
details.partitionKey = cookie.partitionKey;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await chromeApi.cookies.remove(details);
|
||||
return Boolean(result);
|
||||
} catch (error) {
|
||||
console.warn('[MultiPage:step1] remove cookie failed', {
|
||||
domain: cookie?.domain,
|
||||
name: cookie?.name,
|
||||
message: getStep1ErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createStep1Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
chrome: chromeApi = globalThis.chrome,
|
||||
completeStepFromBackground,
|
||||
openSignupEntryTab,
|
||||
} = deps;
|
||||
|
||||
async function clearOpenAiCookiesBeforeStep1() {
|
||||
if (!chromeApi?.cookies?.getAll || !chromeApi.cookies?.remove) {
|
||||
await addLog('步骤 1:当前浏览器不支持 cookies API,跳过打开官网前 cookie 清理。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
await addLog('步骤 1:打开 ChatGPT 官网前清理 ChatGPT / OpenAI cookies...', 'info');
|
||||
const cookies = await collectStep1Cookies(chromeApi);
|
||||
let removedCount = 0;
|
||||
for (const cookie of cookies) {
|
||||
if (await removeStep1Cookie(chromeApi, cookie)) {
|
||||
removedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (chromeApi.browsingData?.removeCookies) {
|
||||
try {
|
||||
await chromeApi.browsingData.removeCookies({
|
||||
since: 0,
|
||||
origins: STEP1_COOKIE_CLEAR_ORIGINS,
|
||||
});
|
||||
} catch (error) {
|
||||
await addLog(`步骤 1:browsingData 补扫 cookies 失败:${getStep1ErrorMessage(error)}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
await addLog(`步骤 1:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies。`, 'ok');
|
||||
}
|
||||
|
||||
async function executeStep1() {
|
||||
await clearOpenAiCookiesBeforeStep1();
|
||||
await addLog('步骤 1:正在打开 ChatGPT 官网...');
|
||||
await openSignupEntryTab(1);
|
||||
await completeStepFromBackground(1, {});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
ensureContentScriptReadyOnTab,
|
||||
ensureSignupAuthEntryPageReady,
|
||||
ensureSignupEntryPageReady,
|
||||
ensureSignupPostEmailPageReadyInTab,
|
||||
getTabId,
|
||||
@@ -16,6 +17,107 @@
|
||||
SIGNUP_PAGE_INJECT_FILES,
|
||||
} = deps;
|
||||
|
||||
function getErrorMessage(error) {
|
||||
return String(typeof error === 'string' ? error : error?.message || '');
|
||||
}
|
||||
|
||||
function isSignupEntryUnavailableErrorMessage(errorLike) {
|
||||
const message = getErrorMessage(errorLike);
|
||||
return /未找到可用的邮箱输入入口|当前页面没有可用的注册入口,也不在邮箱\/密码页/.test(message);
|
||||
}
|
||||
|
||||
function isRetryableStep2TransportErrorMessage(errorLike) {
|
||||
const message = getErrorMessage(errorLike);
|
||||
return /Content script on signup-page did not respond in \d+s|Receiving end does not exist|message channel closed|A listener indicated an asynchronous response|port closed before a response was received|did not respond in \d+s/i.test(message);
|
||||
}
|
||||
|
||||
function isLikelyLoggedInChatgptHomeUrl(rawUrl) {
|
||||
const url = String(rawUrl || '').trim();
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const host = String(parsed.hostname || '').toLowerCase();
|
||||
if (!['chatgpt.com', 'www.chatgpt.com'].includes(host)) {
|
||||
return false;
|
||||
}
|
||||
const path = String(parsed.pathname || '');
|
||||
if (/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function shouldForceAuthEntryRetry(tabId) {
|
||||
if (!Number.isInteger(tabId) || typeof chrome?.tabs?.get !== 'function') {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
const currentUrl = String(tab?.url || '');
|
||||
return isLikelyLoggedInChatgptHomeUrl(currentUrl);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getTabUrl(tabId) {
|
||||
if (!Number.isInteger(tabId) || typeof chrome?.tabs?.get !== 'function') {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
return String(tab?.url || '');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function completeStep2AsLoggedInSession(tabId, resolvedEmail, reasonMessage = '') {
|
||||
const currentUrl = await getTabUrl(tabId);
|
||||
if (!isLikelyLoggedInChatgptHomeUrl(currentUrl)) {
|
||||
return false;
|
||||
}
|
||||
const reasonText = getErrorMessage(reasonMessage);
|
||||
const reasonSuffix = reasonText ? `(触发原因:${reasonText})` : '';
|
||||
await addLog(`步骤 2:检测到当前会话已登录 ChatGPT,已跳过注册链路(步骤 3/4/5),将直接进入步骤 6。${reasonSuffix}`, 'warn');
|
||||
await completeStepFromBackground(2, {
|
||||
email: resolvedEmail,
|
||||
nextSignupState: 'already_logged_in_home',
|
||||
nextSignupUrl: currentUrl || 'https://chatgpt.com/',
|
||||
skippedPasswordStep: true,
|
||||
skipRegistrationFlow: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async function submitSignupEmail(resolvedEmail, options = {}) {
|
||||
const {
|
||||
timeoutMs = 35000,
|
||||
retryDelayMs = 700,
|
||||
logMessage = '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...',
|
||||
} = options;
|
||||
|
||||
try {
|
||||
return await sendToContentScriptResilient('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 2,
|
||||
source: 'background',
|
||||
payload: { email: resolvedEmail },
|
||||
}, {
|
||||
timeoutMs,
|
||||
retryDelayMs,
|
||||
logMessage,
|
||||
});
|
||||
} catch (error) {
|
||||
return { error: getErrorMessage(error) };
|
||||
}
|
||||
}
|
||||
|
||||
async function executeStep2(state) {
|
||||
const resolvedEmail = await resolveSignupEmailForFlow(state);
|
||||
|
||||
@@ -34,19 +136,73 @@
|
||||
});
|
||||
}
|
||||
|
||||
const step2Result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 2,
|
||||
source: 'background',
|
||||
payload: { email: resolvedEmail },
|
||||
}, {
|
||||
timeoutMs: 20000,
|
||||
if (await shouldForceAuthEntryRetry(signupTabId)) {
|
||||
await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交邮箱。', 'warn');
|
||||
try {
|
||||
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
|
||||
} catch (entryError) {
|
||||
const entryErrorMessage = getErrorMessage(entryError);
|
||||
if (await completeStep2AsLoggedInSession(signupTabId, resolvedEmail, entryErrorMessage)) {
|
||||
return;
|
||||
}
|
||||
await addLog('步骤 2:切换认证入口失败,正在重新打开官网入口并重试提交邮箱...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
}
|
||||
}
|
||||
|
||||
let step2Result = await submitSignupEmail(resolvedEmail, {
|
||||
timeoutMs: 35000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...',
|
||||
});
|
||||
|
||||
if (step2Result?.error) {
|
||||
throw new Error(step2Result.error);
|
||||
const errorMessage = getErrorMessage(step2Result.error);
|
||||
if (isSignupEntryUnavailableErrorMessage(errorMessage)) {
|
||||
await addLog('步骤 2:未找到邮箱输入入口,正在切换认证入口页后重试一次...', 'warn');
|
||||
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
|
||||
step2Result = await submitSignupEmail(resolvedEmail, {
|
||||
timeoutMs: 35000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:认证入口页已打开,正在重新提交邮箱...',
|
||||
});
|
||||
|
||||
if (step2Result?.error) {
|
||||
const retryErrorMessage = getErrorMessage(step2Result.error);
|
||||
if (isSignupEntryUnavailableErrorMessage(retryErrorMessage)) {
|
||||
if (await completeStep2AsLoggedInSession(signupTabId, resolvedEmail, retryErrorMessage)) {
|
||||
return;
|
||||
}
|
||||
await addLog('步骤 2:认证入口仍不可用,正在重新进入官网注册入口再重试一次...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
step2Result = await submitSignupEmail(resolvedEmail, {
|
||||
timeoutMs: 35000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:重试官网注册入口后正在重新提交邮箱...',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (isRetryableStep2TransportErrorMessage(errorMessage)) {
|
||||
await addLog('步骤 2:注册入口页通信超时,正在切换认证入口页并重试提交邮箱...', 'warn');
|
||||
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
|
||||
step2Result = await submitSignupEmail(resolvedEmail, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:认证入口页已打开,正在重新提交邮箱...',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (step2Result?.error) {
|
||||
const finalErrorMessage = getErrorMessage(step2Result.error);
|
||||
if (
|
||||
(isSignupEntryUnavailableErrorMessage(finalErrorMessage)
|
||||
|| isRetryableStep2TransportErrorMessage(finalErrorMessage))
|
||||
&& await completeStep2AsLoggedInSession(signupTabId, resolvedEmail, finalErrorMessage)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
throw new Error(finalErrorMessage);
|
||||
}
|
||||
|
||||
if (!step2Result?.alreadyOnPasswordPage) {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
pollHotmailVerificationCode,
|
||||
pollLuckmailVerificationCode,
|
||||
sendToContentScript,
|
||||
sendToContentScriptResilient,
|
||||
sendToMailContentScriptResilient,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
@@ -30,6 +31,12 @@
|
||||
VERIFICATION_POLL_MAX_ROUNDS,
|
||||
} = deps;
|
||||
|
||||
const isRetryableVerificationTransportError = typeof deps.isRetryableContentScriptTransportError === 'function'
|
||||
? deps.isRetryableContentScriptTransportError
|
||||
: ((error) => /back\/forward cache|message channel is closed|Receiving end does not exist|port closed before a response was received|A listener indicated an asynchronous response|did not respond in \d+s/i.test(
|
||||
String(typeof error === 'string' ? error : error?.message || '')
|
||||
));
|
||||
|
||||
function getVerificationCodeStateKey(step) {
|
||||
return step === 4 ? 'lastSignupCode' : 'lastLoginCode';
|
||||
}
|
||||
@@ -38,6 +45,89 @@
|
||||
return step === 4 ? '注册' : '登录';
|
||||
}
|
||||
|
||||
function isLikelyLoggedInChatgptHomeUrl(rawUrl) {
|
||||
const url = String(rawUrl || '').trim();
|
||||
if (!url) return false;
|
||||
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const host = String(parsed.hostname || '').toLowerCase();
|
||||
if (!['chatgpt.com', 'www.chatgpt.com'].includes(host)) {
|
||||
return false;
|
||||
}
|
||||
const path = String(parsed.pathname || '');
|
||||
if (/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isSignupProfilePageUrl(rawUrl) {
|
||||
const url = String(rawUrl || '').trim();
|
||||
if (!url) return false;
|
||||
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const host = String(parsed.hostname || '').toLowerCase();
|
||||
if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) {
|
||||
return false;
|
||||
}
|
||||
return /\/create-account\/profile(?:[/?#]|$)/i.test(String(parsed.pathname || ''));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function detectStep4PostSubmitFallback(tabId, options = {}) {
|
||||
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 8000);
|
||||
const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || 250);
|
||||
const startedAt = Date.now();
|
||||
let lastUrl = '';
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
throwIfStopped();
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
const currentUrl = String(tab?.url || '').trim();
|
||||
if (currentUrl) {
|
||||
lastUrl = currentUrl;
|
||||
}
|
||||
|
||||
if (isLikelyLoggedInChatgptHomeUrl(currentUrl)) {
|
||||
return {
|
||||
success: true,
|
||||
reason: 'chatgpt_home',
|
||||
skipProfileStep: true,
|
||||
url: currentUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (isSignupProfilePageUrl(currentUrl)) {
|
||||
return {
|
||||
success: true,
|
||||
reason: 'signup_profile',
|
||||
skipProfileStep: false,
|
||||
url: currentUrl,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Keep polling until timeout; tab may be mid-navigation.
|
||||
}
|
||||
|
||||
await sleepWithStop(pollIntervalMs);
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
reason: 'unknown',
|
||||
skipProfileStep: false,
|
||||
url: lastUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function getVerificationResendStateKey() {
|
||||
return 'verificationResendCount';
|
||||
}
|
||||
@@ -448,6 +538,17 @@
|
||||
`步骤 ${step}:距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,继续刷新邮箱(第 ${round}/${maxRounds} 轮)...`,
|
||||
'info'
|
||||
);
|
||||
const configuredIntervalMs = Math.max(
|
||||
1,
|
||||
Number(payloadOverrides.intervalMs)
|
||||
|| Number(pollOverrides.intervalMs)
|
||||
|| 3000
|
||||
);
|
||||
const cooldownSleepMs = Math.min(
|
||||
remainingBeforeResendMs,
|
||||
Math.max(1000, Math.min(configuredIntervalMs, 3000))
|
||||
);
|
||||
await sleepWithStop(cooldownSleepMs);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -597,19 +698,54 @@
|
||||
}
|
||||
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
const result = await sendToContentScript('signup-page', {
|
||||
const baseResponseTimeoutMs = await getResponseTimeoutMsForStep(
|
||||
step,
|
||||
options,
|
||||
step === 7 ? 45000 : 30000,
|
||||
`填写${getVerificationCodeLabel(step)}验证码`
|
||||
);
|
||||
const message = {
|
||||
type: 'FILL_CODE',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: { code },
|
||||
}, {
|
||||
responseTimeoutMs: await getResponseTimeoutMsForStep(
|
||||
step,
|
||||
options,
|
||||
step === 7 ? 45000 : 30000,
|
||||
`填写${getVerificationCodeLabel(step)}验证码`
|
||||
),
|
||||
});
|
||||
};
|
||||
let result;
|
||||
if (typeof sendToContentScriptResilient === 'function') {
|
||||
try {
|
||||
result = await sendToContentScriptResilient('signup-page', message, {
|
||||
timeoutMs: Math.max(baseResponseTimeoutMs + 15000, 30000),
|
||||
retryDelayMs: 700,
|
||||
responseTimeoutMs: baseResponseTimeoutMs,
|
||||
logMessage: `步骤 ${step}:认证页正在切换,等待页面重新就绪后继续确认验证码提交结果...`,
|
||||
});
|
||||
} catch (err) {
|
||||
if (step === 4 && isRetryableVerificationTransportError(err)) {
|
||||
const fallback = await detectStep4PostSubmitFallback(signupTabId, {
|
||||
timeoutMs: 9000,
|
||||
pollIntervalMs: 300,
|
||||
});
|
||||
if (fallback.success) {
|
||||
const fallbackLabel = fallback.reason === 'chatgpt_home'
|
||||
? 'ChatGPT 已登录首页'
|
||||
: '注册资料页';
|
||||
await addLog(`步骤 4:验证码提交后页面已切换到${fallbackLabel},按提交成功继续。`, 'warn');
|
||||
return {
|
||||
success: true,
|
||||
assumed: true,
|
||||
transportRecovered: true,
|
||||
skipProfileStep: Boolean(fallback.skipProfileStep),
|
||||
url: fallback.url,
|
||||
};
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
result = await sendToContentScript('signup-page', message, {
|
||||
responseTimeoutMs: baseResponseTimeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
if (result && result.error) {
|
||||
throw new Error(result.error);
|
||||
@@ -735,6 +871,7 @@
|
||||
`步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts})...`,
|
||||
'warn'
|
||||
);
|
||||
await sleepWithStop(Math.min(remainingBeforeResendMs, 2000));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -763,6 +900,7 @@
|
||||
await completeStepFromBackground(step, {
|
||||
emailTimestamp: result.emailTimestamp,
|
||||
code: result.code,
|
||||
...(step === 4 && submitResult?.skipProfileStep ? { skipProfileStep: true } : {}),
|
||||
});
|
||||
triggerPostSuccessMailboxCleanup(step, mail);
|
||||
return;
|
||||
|
||||
@@ -2122,6 +2122,20 @@ async function fillVerificationCode(step, payload) {
|
||||
const { code } = payload;
|
||||
if (!code) throw new Error('未提供验证码。');
|
||||
|
||||
if (step === 4 && isStep5Ready()) {
|
||||
log(`步骤 ${step}:检测到页面已进入下一阶段,本次验证码提交按成功处理。`, 'ok');
|
||||
return { success: true, assumed: true, alreadyAdvanced: true };
|
||||
}
|
||||
if (step === 8) {
|
||||
if (isStep8Ready()) {
|
||||
log(`步骤 ${step}:检测到页面已进入 OAuth 同意页,本次验证码提交按成功处理。`, 'ok');
|
||||
return { success: true, assumed: true, alreadyAdvanced: true };
|
||||
}
|
||||
if (isAddPhonePageReady()) {
|
||||
return { success: true, addPhonePage: true, url: location.href };
|
||||
}
|
||||
}
|
||||
|
||||
log(`步骤 ${step}:正在填写验证码:${code}`);
|
||||
|
||||
if (step === 8) {
|
||||
|
||||
+45
-6
@@ -1,11 +1,10 @@
|
||||
# Codex 注册扩展相关项目、更新、QQ 邮箱切换与 Clash Verge 配置教程
|
||||
|
||||
本教程用于说明相关项目地址、扩展更新方式、`QQ 邮箱`切换邮箱的使用方法,以及 `Clash Verge` 的 `🔁 非港轮询` 配置方法。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 需要拉取并部署 `cpa` 或 `sub2api` 项目
|
||||
- 已经安装本扩展,想更新到最新版本
|
||||
- 需要把 `Cloudflare Temp Email` 用作 `邮箱生成` 或 `邮箱服务`
|
||||
- 需要临时切换 `QQ 邮箱` 地址继续使用
|
||||
- 需要在 `Clash Verge` 中启用 `🔁 非港轮询`
|
||||
|
||||
@@ -14,6 +13,7 @@
|
||||
- 可以访问 `GitHub`
|
||||
- 已安装好的扩展文件夹
|
||||
- 可以打开浏览器的 `扩展程序管理` 页面
|
||||
- 已准备好 `Cloudflare Temp Email` 后端地址;如需随机子域,域名解析也已配置完成
|
||||
- 一个可正常登录的 `QQ 邮箱`
|
||||
- 如需部署 `cpa`,部署环境必须可以访问 `OpenAI`
|
||||
- 已安装 `Clash Verge`,并已导入可用订阅
|
||||
@@ -60,7 +60,42 @@
|
||||
找到该扩展后,手动点击一次 `重新加载`。
|
||||
这一步一定要做,否则浏览器可能仍在使用旧版本。
|
||||
|
||||
### 第三部分:`QQ 邮箱`切换邮箱使用教程
|
||||
### 第三部分:`Cloudflare Temp Email` 使用说明
|
||||
|
||||
1. 先确认当前用途
|
||||
`Cloudflare Temp Email` 可以作为 `邮箱生成`,也可以作为 `邮箱服务`。
|
||||
如果两边都选择了它,就需要把两套配置都填完整。
|
||||
|
||||
2. 填写 `Temp API`
|
||||
这里填写 `Cloudflare Temp Email` 后端地址,例如 `https://your-worker-domain`。
|
||||
不论你是拿它来生成邮箱,还是接收转发邮件,这一项都需要先配好。
|
||||
|
||||
3. 作为 `邮箱生成` 使用时填写 `Admin Auth`
|
||||
`Admin Auth` 对应后端的 `admin auth`。
|
||||
只有在 `邮箱生成 = Cloudflare Temp Email` 时,这一项才是必填。
|
||||
|
||||
4. 按需填写 `Custom Auth`
|
||||
`Custom Auth` 只在站点额外开启访问密码时才需要填写。
|
||||
如果没有开启额外访问密码,留空即可。
|
||||
这一项不会替代 `Admin Auth`。
|
||||
|
||||
5. 配置 `Temp 域名`
|
||||
这里填写允许创建邮箱的基础域名。
|
||||
即使开启了 `随机子域`,这里仍然填写基础域名,而不是随机出来的子域名。
|
||||
|
||||
6. 按需开启 `随机子域`
|
||||
`随机子域` 只在 `邮箱生成 = Cloudflare Temp Email` 时使用。
|
||||
后端需要提前配置 `RANDOM_SUBDOMAIN_DOMAINS`,Cloudflare DNS 也需要设置 `MX *`。
|
||||
相关说明可参考 [Issue #942](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942)。
|
||||
|
||||
7. 作为 `邮箱服务` 使用时填写 `邮件接收`
|
||||
只有在 `邮箱服务 = Cloudflare Temp Email` 时,这一项才需要填写。
|
||||
这里填写真正用于接收转发邮件的目标邮箱。
|
||||
|
||||
8. 查看搭建参考
|
||||
如果你还没有部署后端,可参考 [LINUX DO 教程](https://linux.do/t/topic/316819)。
|
||||
|
||||
### 第四部分:`QQ 邮箱`切换邮箱使用教程
|
||||
|
||||
1. 登录 `QQ 邮箱`
|
||||
先登录你当前正在使用的 `QQ 邮箱` 账号。
|
||||
@@ -79,13 +114,15 @@
|
||||
这两个邮箱地址使用完成后,可以直接删除。
|
||||
删除后再次创建新的英文邮箱和 `Foxmail` 邮箱,即可重复注册使用。
|
||||
|
||||
### 第四部分:配置 `Clash Verge` 的 `🔁 非港轮询`
|
||||
### 第五部分:配置 `Clash Verge` 的 `🔁 非港轮询`
|
||||
|
||||
#### 第一步:添加扩展脚本
|
||||
|
||||
1. 打开 `Clash Verge`,进入左侧的 `订阅`(`Profiles`)界面。
|
||||
2. 在右上角或对应位置找到并双击打开 `全局扩展脚本`(`Merge/Script`)。
|
||||
2. 在右上角或对应位置找到并双击打开 `全局扩展脚本`。
|
||||

|
||||
3. 将里面的内容全部清空,替换为下方脚本代码。
|
||||

|
||||
4. 点击保存,使用右上角保存按钮或按 `Ctrl+S`。
|
||||
|
||||
💻 脚本代码(如果遇到格式错误,可让豆包帮你修复后再粘贴)
|
||||
@@ -184,7 +221,7 @@ function main(config, profileName) {
|
||||
3. 在对应下拉框中选择 `🔁 非港轮询`。
|
||||
4. 确认右上角的 `代理模式`(`Mode`)已经设置为 `规则模式`(`Rule`)。
|
||||
5. 确认左侧 `设置` 中的 `系统代理`(`System Proxy`)已经开启。
|
||||
|
||||

|
||||
## 常见问题
|
||||
|
||||
### 为什么第十步显示认证成功,但没有认证文件?
|
||||
@@ -206,6 +243,8 @@ function main(config, profileName) {
|
||||
## 注意事项
|
||||
|
||||
- 不论使用哪种方式更新扩展,更新完成后都必须在浏览器的 `扩展程序管理` 页面重新加载一次该扩展。
|
||||
- 如果同时把 `Cloudflare Temp Email` 用作 `邮箱生成` 和 `邮箱服务`,请同时检查 `Admin Auth`、`Custom Auth`、`Temp 域名` 和 `邮件接收` 是否都已配置。
|
||||
- 开启 `随机子域` 前,请先确认后端已经配置 `RANDOM_SUBDOMAIN_DOMAINS`,并且 Cloudflare DNS 已完成 `MX *` 设置。
|
||||
- 如需部署 `cpa`,请先确认部署环境可以访问 `OpenAI`,否则可能出现第十步认证成功但没有认证文件的问题。
|
||||
- `QQ 邮箱`切换邮箱时,建议在使用完成后及时删除,再重新创建,避免混淆当前正在使用的邮箱地址。
|
||||
- 使用 `git pull` 更新扩展是最方便、最推荐的方式。
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "codex-oauth-automation-extension",
|
||||
"version": "7.5",
|
||||
"version_name": "Pro7.5",
|
||||
"version": "7.6",
|
||||
"version_name": "Pro7.6",
|
||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||
"permissions": [
|
||||
"sidePanel",
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
constants = {},
|
||||
} = context;
|
||||
|
||||
const contributionUploadUrl = constants.contributionUploadUrl || 'https://apikey.qzz.io';
|
||||
const contributionPortalUrl = constants.contributionPortalUrl || 'https://apikey.qzz.io';
|
||||
const contributionUploadUrl = constants.contributionUploadUrl || 'https://apikey.qzz.io/upload';
|
||||
const pollIntervalMs = Math.max(1500, Math.floor(Number(constants.pollIntervalMs) || 2500));
|
||||
|
||||
const hiddenRows = [
|
||||
@@ -175,10 +176,22 @@
|
||||
return normalizeString(currentState.contributionStatusMessage) || DEFAULT_COPY;
|
||||
}
|
||||
|
||||
function getContributionPortalPageUrl() {
|
||||
return normalizeString(contributionPortalUrl);
|
||||
}
|
||||
|
||||
function getContributionUploadPageUrl() {
|
||||
return normalizeString(contributionUploadUrl);
|
||||
}
|
||||
|
||||
function openContributionPortalPage() {
|
||||
const targetUrl = getContributionPortalPageUrl();
|
||||
if (!targetUrl) {
|
||||
return;
|
||||
}
|
||||
helpers.openExternalUrl?.(targetUrl);
|
||||
}
|
||||
|
||||
function openContributionUploadPage() {
|
||||
const targetUrl = getContributionUploadPageUrl();
|
||||
if (!targetUrl) {
|
||||
@@ -375,9 +388,9 @@
|
||||
}
|
||||
actionInFlight = true;
|
||||
try {
|
||||
openContributionUploadPage();
|
||||
openContributionPortalPage();
|
||||
} catch (error) {
|
||||
helpers.showToast?.(`打开上传页面失败:${error.message}`, 'error');
|
||||
helpers.showToast?.(`打开官网页面失败:${error.message}`, 'error');
|
||||
}
|
||||
render();
|
||||
try {
|
||||
|
||||
@@ -523,6 +523,18 @@ header {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.update-card-reminder {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid color-mix(in srgb, var(--orange) 28%, var(--border));
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in srgb, var(--orange-soft) 70%, var(--bg-base));
|
||||
color: var(--orange);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.update-release-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
+63
-37
@@ -16,8 +16,7 @@
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-left">
|
||||
<button id="btn-repo-home" class="header-icon-link" type="button" aria-label="打开 GitHub 仓库"
|
||||
title="打开 GitHub 仓库">
|
||||
<button id="btn-repo-home" class="header-icon-link" type="button" aria-label="打开 GitHub 仓库" title="打开 GitHub 仓库">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
|
||||
@@ -35,7 +34,7 @@
|
||||
<div class="header-btns">
|
||||
<div class="run-group">
|
||||
<button id="btn-contribution-mode" class="btn btn-outline btn-sm btn-contribution-mode" type="button"
|
||||
aria-pressed="false" title="进入贡献模式并打开上传页">贡献/使用</button>
|
||||
aria-pressed="false" title="进入贡献模式并打开官网页">贡献/使用教程</button>
|
||||
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" title="运行次数" />
|
||||
<button id="btn-auto-run" class="btn btn-success" title="自动执行全部步骤">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||
@@ -84,7 +83,7 @@
|
||||
<div id="contribution-update-layer" class="contribution-update-layer" hidden>
|
||||
<div id="contribution-update-hint" class="contribution-update-hint" aria-live="polite" hidden>
|
||||
<p id="contribution-update-hint-text" class="contribution-update-hint-text">
|
||||
公告 / 使用教程有更新了,可点上方“贡献/使用”查看。
|
||||
公告 / 使用教程有更新了,可点上方“贡献/使用教程”查看。
|
||||
</p>
|
||||
<button id="btn-dismiss-contribution-update-hint" class="contribution-update-hint-close" type="button"
|
||||
aria-label="关闭更新提示" title="关闭更新提示">×</button>
|
||||
@@ -101,6 +100,7 @@
|
||||
</div>
|
||||
<button id="btn-open-release" class="btn btn-primary btn-sm" type="button">前往更新</button>
|
||||
</div>
|
||||
<p class="update-card-reminder">一定请先导出配置,再执行更新</p>
|
||||
<div id="update-release-list" class="update-release-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -120,14 +120,16 @@
|
||||
<span class="section-label">贡献模式</span>
|
||||
<span class="contribution-mode-badge">CPA</span>
|
||||
</div>
|
||||
<p id="contribution-mode-text" class="contribution-mode-text">当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,无需手动复制。</p>
|
||||
<p id="contribution-mode-text" class="contribution-mode-text">
|
||||
当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,无需手动复制。</p>
|
||||
<div class="data-row contribution-mode-field">
|
||||
<span class="data-label">贡献昵称</span>
|
||||
<input type="text" id="input-contribution-nickname" class="data-input" placeholder="可留空,将显示为匿名贡献者" />
|
||||
</div>
|
||||
<div class="data-row contribution-mode-field">
|
||||
<span class="data-label">QQ</span>
|
||||
<input type="text" id="input-contribution-qq" class="data-input" inputmode="numeric" placeholder="可留空,只能填写数字" />
|
||||
<input type="text" id="input-contribution-qq" class="data-input" inputmode="numeric"
|
||||
placeholder="可留空,只能填写数字" />
|
||||
</div>
|
||||
<div class="contribution-mode-status-grid">
|
||||
<div class="contribution-mode-status-card">
|
||||
@@ -190,8 +192,7 @@
|
||||
</div>
|
||||
<div class="data-row" id="row-sub2api-default-proxy" style="display:none;">
|
||||
<span class="data-label">默认代理</span>
|
||||
<input type="text" id="input-sub2api-default-proxy" class="data-input"
|
||||
placeholder="留空则不使用代理;或填写代理名称 / ID" />
|
||||
<input type="text" id="input-sub2api-default-proxy" class="data-input" placeholder="留空则不使用代理;或填写代理名称 / ID" />
|
||||
</div>
|
||||
<div class="data-row" id="row-codex2api-url" style="display:none;">
|
||||
<span class="data-label">Codex2API</span>
|
||||
@@ -271,7 +272,8 @@
|
||||
<div class="data-row" id="row-mail2925-pool-settings" style="display:none;">
|
||||
<span class="data-label">2925 号池</span>
|
||||
<div class="data-inline mail2925-base-inline">
|
||||
<label class="toggle-switch mail2925-pool-toggle" id="label-mail2925-use-account-pool" for="input-mail2925-use-account-pool" style="display:none;" title="开启后启用 2925 账号池">
|
||||
<label class="toggle-switch mail2925-pool-toggle" id="label-mail2925-use-account-pool"
|
||||
for="input-mail2925-use-account-pool" style="display:none;" title="开启后启用 2925 账号池">
|
||||
<input type="checkbox" id="input-mail2925-use-account-pool" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
@@ -286,7 +288,8 @@
|
||||
<div class="data-row" id="row-email-prefix" style="display:none;">
|
||||
<span class="data-label" id="label-email-prefix">别名基邮箱</span>
|
||||
<div class="data-inline">
|
||||
<input type="text" id="input-email-prefix" class="data-input mail2925-base-input" placeholder="例如 yourname@example.com" />
|
||||
<input type="text" id="input-email-prefix" class="data-input mail2925-base-input"
|
||||
placeholder="例如 yourname@example.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-inbucket-host" style="display:none;">
|
||||
@@ -344,8 +347,8 @@
|
||||
<div class="setting-group setting-group-secondary">
|
||||
<span class="setting-caption">线程间隔</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-auto-skip-failures-thread-interval-minutes" class="data-input auto-delay-input" value="0"
|
||||
min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" />
|
||||
<input type="number" id="input-auto-skip-failures-thread-interval-minutes"
|
||||
class="data-input auto-delay-input" value="0" min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" />
|
||||
<span class="data-unit">分钟</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -374,19 +377,20 @@
|
||||
</div>
|
||||
<div class="data-row" id="row-account-run-history-helper-base-url" style="display:none;">
|
||||
<span class="data-label">同步服务</span>
|
||||
<input type="text" id="input-account-run-history-helper-base-url" class="data-input mono" placeholder="http://127.0.0.1:17373" />
|
||||
<input type="text" id="input-account-run-history-helper-base-url" class="data-input mono"
|
||||
placeholder="http://127.0.0.1:17373" />
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">OAuth</span>
|
||||
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">回调</span>
|
||||
<div class="data-inline data-value-actions">
|
||||
<span id="display-localhost-url" class="data-value data-value-fill mono truncate">等待中...</span>
|
||||
<button id="btn-save-settings" class="btn btn-outline btn-sm data-inline-btn" type="button">保存</button>
|
||||
<span class="data-label">OAuth</span>
|
||||
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">回调</span>
|
||||
<div class="data-inline data-value-actions">
|
||||
<span id="display-localhost-url" class="data-value data-value-fill mono truncate">等待中...</span>
|
||||
<button id="btn-save-settings" class="btn btn-outline btn-sm data-inline-btn" type="button">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cloudflare-temp-email-section" class="data-card hotmail-card" style="display:none;">
|
||||
<div class="section-mini-header">
|
||||
@@ -405,20 +409,24 @@
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
|
||||
<span class="data-label">Admin Auth</span>
|
||||
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon" placeholder="Cloudflare Temp Email admin password" />
|
||||
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon"
|
||||
placeholder="Cloudflare Temp Email admin password" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
|
||||
<span class="data-label">Custom Auth</span>
|
||||
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon" placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
|
||||
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon"
|
||||
placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-receive-mailbox" style="display:none;">
|
||||
<span class="data-label">邮件接收</span>
|
||||
<input type="text" id="input-temp-email-receive-mailbox" class="data-input" placeholder="用于接收转发邮件的邮箱,例如 1@email.example.com" />
|
||||
<input type="text" id="input-temp-email-receive-mailbox" class="data-input"
|
||||
placeholder="用于接收转发邮件的邮箱,例如 1@email.example.com" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-random-subdomain-toggle" style="display:none;">
|
||||
<span class="data-label">随机子域</span>
|
||||
<div class="data-inline">
|
||||
<label class="toggle-switch" for="input-temp-email-use-random-subdomain" title="依赖后端 RANDOM_SUBDOMAIN_DOMAINS 配置">
|
||||
<label class="toggle-switch" for="input-temp-email-use-random-subdomain"
|
||||
title="依赖后端 RANDOM_SUBDOMAIN_DOMAINS 配置">
|
||||
<input type="checkbox" id="input-temp-email-use-random-subdomain" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
@@ -444,7 +452,8 @@
|
||||
<span class="section-label">Hotmail 账号池</span>
|
||||
</div>
|
||||
<div class="section-mini-actions">
|
||||
<button id="btn-toggle-hotmail-form" class="btn btn-outline btn-xs" type="button" aria-expanded="false">添加账号</button>
|
||||
<button id="btn-toggle-hotmail-form" class="btn btn-outline btn-xs" type="button"
|
||||
aria-expanded="false">添加账号</button>
|
||||
<button id="btn-hotmail-usage-guide" class="btn btn-ghost btn-xs" type="button">使用教程</button>
|
||||
<button id="btn-clear-used-hotmail-accounts" class="btn btn-ghost btn-xs" type="button">清空已用</button>
|
||||
<button id="btn-delete-all-hotmail-accounts" class="btn btn-ghost btn-xs" type="button">全部删除</button>
|
||||
@@ -461,11 +470,13 @@
|
||||
</div>
|
||||
<div class="data-row" id="row-hotmail-remote-base-url">
|
||||
<span class="data-label">API对接</span>
|
||||
<input type="text" id="input-hotmail-remote-base-url" class="data-input mono" placeholder="微软邮箱 API 对接模式无需填写地址" />
|
||||
<input type="text" id="input-hotmail-remote-base-url" class="data-input mono"
|
||||
placeholder="微软邮箱 API 对接模式无需填写地址" />
|
||||
</div>
|
||||
<div class="data-row" id="row-hotmail-local-base-url" style="display:none;">
|
||||
<span class="data-label">本地助手</span>
|
||||
<input type="text" id="input-hotmail-local-base-url" class="data-input mono" placeholder="http://127.0.0.1:17373" />
|
||||
<input type="text" id="input-hotmail-local-base-url" class="data-input mono"
|
||||
placeholder="http://127.0.0.1:17373" />
|
||||
</div>
|
||||
<div id="hotmail-form-shell" class="account-pool-form-shell" hidden>
|
||||
<div class="data-row">
|
||||
@@ -489,7 +500,8 @@
|
||||
<span class="data-label"></span>
|
||||
<div class="data-inline account-pool-actions-inline">
|
||||
<button id="btn-add-hotmail-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
|
||||
<button id="btn-import-hotmail-accounts" class="btn btn-outline btn-sm account-pool-import-action" type="button">批量导入</button>
|
||||
<button id="btn-import-hotmail-accounts" class="btn btn-outline btn-sm account-pool-import-action"
|
||||
type="button">批量导入</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row hotmail-import-row">
|
||||
@@ -510,9 +522,11 @@
|
||||
<span class="section-label">2925 账号池</span>
|
||||
</div>
|
||||
<div class="section-mini-actions">
|
||||
<button id="btn-toggle-mail2925-form" class="btn btn-outline btn-xs" type="button" aria-expanded="false">添加账号</button>
|
||||
<button id="btn-toggle-mail2925-form" class="btn btn-outline btn-xs" type="button"
|
||||
aria-expanded="false">添加账号</button>
|
||||
<button id="btn-delete-all-mail2925-accounts" class="btn btn-ghost btn-xs" type="button">全部删除</button>
|
||||
<button id="btn-toggle-mail2925-list" class="btn btn-ghost btn-xs" type="button" aria-expanded="false">展开列表</button>
|
||||
<button id="btn-toggle-mail2925-list" class="btn btn-ghost btn-xs" type="button"
|
||||
aria-expanded="false">展开列表</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mail2925-form-shell" class="account-pool-form-shell" hidden>
|
||||
@@ -528,7 +542,8 @@
|
||||
<span class="data-label"></span>
|
||||
<div class="data-inline account-pool-actions-inline">
|
||||
<button id="btn-add-mail2925-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
|
||||
<button id="btn-import-mail2925-accounts" class="btn btn-outline btn-sm account-pool-import-action" type="button">批量导入</button>
|
||||
<button id="btn-import-mail2925-accounts" class="btn btn-outline btn-sm account-pool-import-action"
|
||||
type="button">批量导入</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row hotmail-import-row">
|
||||
@@ -555,7 +570,8 @@
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">Base URL</span>
|
||||
<input type="text" id="input-luckmail-base-url" class="data-input mono" placeholder="https://mails.luckyous.com" />
|
||||
<input type="text" id="input-luckmail-base-url" class="data-input mono"
|
||||
placeholder="https://mails.luckyous.com" />
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">邮箱类型</span>
|
||||
@@ -587,7 +603,8 @@
|
||||
</div>
|
||||
<div id="luckmail-summary" class="luckmail-summary">加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。</div>
|
||||
<div class="luckmail-toolbar">
|
||||
<input id="input-luckmail-search" class="data-input luckmail-search" type="text" placeholder="搜索邮箱 / 标签 / 项目" />
|
||||
<input id="input-luckmail-search" class="data-input luckmail-search" type="text"
|
||||
placeholder="搜索邮箱 / 标签 / 项目" />
|
||||
<select id="select-luckmail-filter" class="data-select luckmail-filter">
|
||||
<option value="all">全部</option>
|
||||
<option value="reusable">可复用</option>
|
||||
@@ -632,6 +649,13 @@
|
||||
<option value="icloud.com.cn">iCloud.com.cn</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">获取策略</span>
|
||||
<select id="select-icloud-fetch-mode" class="data-select">
|
||||
<option value="reuse_existing">优先复用已有未用别名</option>
|
||||
<option value="always_new">始终创建新别名</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">自动删除</span>
|
||||
<label class="option-toggle" for="checkbox-auto-delete-icloud">
|
||||
@@ -644,7 +668,8 @@
|
||||
<div id="icloud-login-help" class="icloud-login-help" style="display:none;">
|
||||
<div class="icloud-login-help-main">
|
||||
<div id="icloud-login-help-title" class="icloud-login-help-title">需要登录 iCloud</div>
|
||||
<div id="icloud-login-help-text" class="icloud-login-help-text">我已经为你打开 iCloud 登录页。请在那个页面完成登录,然后回到这里点击“我已登录”。</div>
|
||||
<div id="icloud-login-help-text" class="icloud-login-help-text">我已经为你打开 iCloud
|
||||
登录页。请在那个页面完成登录,然后回到这里点击“我已登录”。</div>
|
||||
</div>
|
||||
<button id="btn-icloud-login-done" class="btn btn-primary btn-xs" type="button">我已登录</button>
|
||||
</div>
|
||||
@@ -737,7 +762,8 @@
|
||||
<div id="account-records-stats" class="account-records-stats" role="group" aria-label="邮箱记录筛选"></div>
|
||||
<div class="account-records-toolbar-actions">
|
||||
<button id="btn-toggle-account-records-selection" class="btn btn-ghost btn-xs" type="button">多选</button>
|
||||
<button id="btn-delete-selected-account-records" class="btn btn-ghost btn-xs" type="button" hidden disabled>删除选中</button>
|
||||
<button id="btn-delete-selected-account-records" class="btn btn-ghost btn-xs" type="button" hidden
|
||||
disabled>删除选中</button>
|
||||
<button id="btn-clear-account-records" class="btn btn-ghost btn-xs" type="button">清理记录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+46
-45
@@ -140,6 +140,7 @@ const btnIcloudLoginDone = document.getElementById('btn-icloud-login-done');
|
||||
const btnIcloudRefresh = document.getElementById('btn-icloud-refresh');
|
||||
const btnIcloudDeleteUsed = document.getElementById('btn-icloud-delete-used');
|
||||
const selectIcloudHostPreference = document.getElementById('select-icloud-host-preference');
|
||||
const selectIcloudFetchMode = document.getElementById('select-icloud-fetch-mode');
|
||||
const checkboxAutoDeleteIcloud = document.getElementById('checkbox-auto-delete-icloud');
|
||||
const inputIcloudSearch = document.getElementById('input-icloud-search');
|
||||
const selectIcloudFilter = document.getElementById('select-icloud-filter');
|
||||
@@ -527,6 +528,7 @@ let currentAutoRun = {
|
||||
let settingsDirty = false;
|
||||
let settingsSaveInFlight = false;
|
||||
let settingsAutoSaveTimer = null;
|
||||
let settingsSaveRevision = 0;
|
||||
let cloudflareDomainEditMode = false;
|
||||
let cloudflareTempEmailDomainEditMode = false;
|
||||
let modalChoiceResolver = null;
|
||||
@@ -562,6 +564,10 @@ const normalizeIcloudHost = window.IcloudUtils?.normalizeIcloudHost
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'icloud.com' || normalized === 'icloud.com.cn' ? normalized : '';
|
||||
});
|
||||
const normalizeIcloudFetchMode = (value) => {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'always_new' ? 'always_new' : 'reuse_existing';
|
||||
};
|
||||
const getIcloudLoginUrlForHost = window.IcloudUtils?.getIcloudLoginUrlForHost
|
||||
|| ((host) => host === 'icloud.com.cn' ? 'https://www.icloud.com.cn/' : (host === 'icloud.com' ? 'https://www.icloud.com/' : ''));
|
||||
|
||||
@@ -628,8 +634,6 @@ const LOG_LEVEL_LABELS = {
|
||||
};
|
||||
|
||||
const CLOUDFLARE_TEMP_EMAIL_REPOSITORY_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email';
|
||||
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
|
||||
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
|
||||
|
||||
function usesGeneratedAliasMailProvider(
|
||||
provider,
|
||||
@@ -1649,6 +1653,9 @@ function collectSettingsPayload() {
|
||||
!cloudflareTempEmailDomainEditMode ? selectTempEmailDomain.value : tempEmailActiveDomain
|
||||
) || tempEmailActiveDomain;
|
||||
const contributionModeEnabled = Boolean(latestState?.contributionMode);
|
||||
const icloudFetchModeRawValue = typeof selectIcloudFetchMode !== 'undefined'
|
||||
? String(selectIcloudFetchMode?.value || '')
|
||||
: '';
|
||||
const mail2925UseAccountPool = typeof inputMail2925UseAccountPool !== 'undefined'
|
||||
? Boolean(inputMail2925UseAccountPool?.checked)
|
||||
: Boolean(latestState?.mail2925UseAccountPool);
|
||||
@@ -1680,6 +1687,9 @@ function collectSettingsPayload() {
|
||||
: [],
|
||||
autoDeleteUsedIcloudAlias: checkboxAutoDeleteIcloud?.checked,
|
||||
icloudHostPreference: selectIcloudHostPreference?.value || 'auto',
|
||||
icloudFetchMode: (icloudFetchModeRawValue.trim().toLowerCase() === 'always_new'
|
||||
? 'always_new'
|
||||
: 'reuse_existing'),
|
||||
...(contributionModeEnabled ? {} : {
|
||||
accountRunHistoryTextEnabled: Boolean(inputAccountRunHistoryTextEnabled?.checked),
|
||||
accountRunHistoryHelperBaseUrl: normalizeAccountRunHistoryHelperBaseUrlValue(inputAccountRunHistoryHelperBaseUrl?.value),
|
||||
@@ -1858,6 +1868,9 @@ async function clearRegistrationEmail(options = {}) {
|
||||
|
||||
function markSettingsDirty(isDirty = true) {
|
||||
settingsDirty = isDirty;
|
||||
if (isDirty) {
|
||||
settingsSaveRevision += 1;
|
||||
}
|
||||
updateSaveButtonState();
|
||||
}
|
||||
|
||||
@@ -1883,6 +1896,7 @@ async function saveSettings(options = {}) {
|
||||
}
|
||||
|
||||
const payload = collectSettingsPayload();
|
||||
const saveRevision = settingsSaveRevision;
|
||||
settingsSaveInFlight = true;
|
||||
updateSaveButtonState();
|
||||
|
||||
@@ -1897,11 +1911,13 @@ async function saveSettings(options = {}) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
if (response?.state) {
|
||||
if (response?.state && saveRevision === settingsSaveRevision) {
|
||||
applySettingsState(response.state);
|
||||
} else {
|
||||
syncLatestState(payload);
|
||||
markSettingsDirty(false);
|
||||
if (saveRevision === settingsSaveRevision) {
|
||||
markSettingsDirty(false);
|
||||
}
|
||||
updatePanelModeUI();
|
||||
updateMailProviderUI();
|
||||
updateButtonStates();
|
||||
@@ -2086,6 +2102,9 @@ function applySettingsState(state) {
|
||||
? 'icloud.com'
|
||||
: (String(state?.icloudHostPreference || '').trim().toLowerCase() === 'icloud.com.cn' ? 'icloud.com.cn' : 'auto');
|
||||
}
|
||||
if (selectIcloudFetchMode) {
|
||||
selectIcloudFetchMode.value = normalizeIcloudFetchMode(state?.icloudFetchMode);
|
||||
}
|
||||
if (checkboxAutoDeleteIcloud) {
|
||||
checkboxAutoDeleteIcloud.checked = Boolean(state?.autoDeleteUsedIcloudAlias);
|
||||
}
|
||||
@@ -2241,45 +2260,12 @@ function openReleaseListPage() {
|
||||
openExternalUrl(getReleaseListUrl());
|
||||
}
|
||||
|
||||
function buildCloudflareTempEmailUsageGuideModalConfig() {
|
||||
const useCloudflareTempEmailProvider = String(selectMailProvider?.value || '').trim().toLowerCase() === 'cloudflare-temp-email';
|
||||
const useCloudflareTempEmailGenerator = getSelectedEmailGenerator() === 'cloudflare-temp-email';
|
||||
let alertText = '当前还没有把 Cloudflare Temp Email 选为邮箱服务或邮箱生成器。下面说明同时覆盖“生成邮箱”和“接收转发邮件”两种用法。';
|
||||
if (useCloudflareTempEmailProvider && useCloudflareTempEmailGenerator) {
|
||||
alertText = '当前同时把 Cloudflare Temp Email 用作“邮箱服务”和“邮箱生成器”。请同时配置生成邮箱和接收转发两套必填项。';
|
||||
} else if (useCloudflareTempEmailProvider) {
|
||||
alertText = '当前把 Cloudflare Temp Email 用作“邮箱服务”。重点填写 Temp API、Custom Auth 和 邮件接收。';
|
||||
} else if (useCloudflareTempEmailGenerator) {
|
||||
alertText = '当前把 Cloudflare Temp Email 用作“邮箱生成器”。重点填写 Temp API、Admin Auth 和 Temp 域名;随机子域按需开启。';
|
||||
function openCloudflareTempEmailUsageGuidePage() {
|
||||
const targetUrl = getContributionPortalUrl();
|
||||
if (!targetUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Cloudflare Temp Email 使用教程',
|
||||
alert: { text: alertText },
|
||||
messageHtml: `Temp API:填写 Cloudflare Temp Email 后端地址。<br><br>
|
||||
Admin Auth:填写后端 admin auth。<br>
|
||||
仅在“邮箱生成 = Cloudflare Temp Email”时必填。<br><br>
|
||||
Custom Auth:仅当站点额外开启访问密码时填写。没有开启就留空。<br><br>
|
||||
Temp 域名:填写允许创建的基础域名。启用随机子域时,这里仍然填写基础域名。<br><br>
|
||||
随机子域:仅在“邮箱生成 = Cloudflare Temp Email”时使用。<br>
|
||||
后端需要已配置 RANDOM_SUBDOMAIN_DOMAINS。<br>
|
||||
CF DNS 需要设置 MX *,参考 <a href="${CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL}" target="_blank" rel="noopener noreferrer">Issue #942</a>。<br><br>
|
||||
邮件接收:仅在“邮箱服务 = Cloudflare Temp Email”时填写。<br>
|
||||
这里填写真正接收转发邮件的目标邮箱。<br><br>
|
||||
搭建教程:<a href="${CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL}" target="_blank" rel="noopener noreferrer">LINUX DO 教程</a>。`,
|
||||
};
|
||||
}
|
||||
|
||||
function showCloudflareTempEmailUsageGuide() {
|
||||
const modalConfig = buildCloudflareTempEmailUsageGuideModalConfig();
|
||||
return openActionModal({
|
||||
title: modalConfig.title,
|
||||
messageHtml: modalConfig.messageHtml,
|
||||
alert: modalConfig.alert,
|
||||
actions: [
|
||||
{ id: 'ok', label: '知道了', variant: 'btn-primary' },
|
||||
],
|
||||
});
|
||||
openExternalUrl(targetUrl);
|
||||
}
|
||||
|
||||
function openCloudflareTempEmailRepositoryPage() {
|
||||
@@ -3401,6 +3387,12 @@ function updateButtonStates() {
|
||||
if (btnIcloudRefresh) btnIcloudRefresh.disabled = disableIcloudControls;
|
||||
if (btnIcloudDeleteUsed) btnIcloudDeleteUsed.disabled = disableIcloudControls || !hasDeletableUsedIcloudAliases();
|
||||
if (selectIcloudHostPreference) selectIcloudHostPreference.disabled = disableIcloudControls;
|
||||
if (selectIcloudFetchMode) {
|
||||
const allowIcloudFetchMode = getSelectedEmailGenerator() === ICLOUD_PROVIDER
|
||||
&& !isCustomMailProvider()
|
||||
&& !isManagedAliasProvider();
|
||||
selectIcloudFetchMode.disabled = disableIcloudControls || !allowIcloudFetchMode;
|
||||
}
|
||||
if (checkboxAutoDeleteIcloud) checkboxAutoDeleteIcloud.disabled = disableIcloudControls;
|
||||
if (btnContributionMode) btnContributionMode.disabled = isContributionButtonLocked();
|
||||
updateStopButtonState(anyRunning || autoScheduled || isAutoRunPausedPhase() || autoLocked);
|
||||
@@ -3893,8 +3885,9 @@ const contributionModeManager = window.SidepanelContributionMode?.createContribu
|
||||
sendMessage: (message) => chrome.runtime.sendMessage(message),
|
||||
},
|
||||
constants: {
|
||||
contributionOauthUrl: `${contributionContentService?.portalUrl || 'https://apikey.qzz.io'}/oauth/`,
|
||||
contributionUploadUrl: contributionContentService?.portalUrl || 'https://apikey.qzz.io',
|
||||
contributionOauthUrl: `${String(contributionContentService?.portalUrl || 'https://apikey.qzz.io').replace(/\/+$/, '')}/oauth/`,
|
||||
contributionPortalUrl: String(contributionContentService?.portalUrl || 'https://apikey.qzz.io').replace(/\/+$/, ''),
|
||||
contributionUploadUrl: `${String(contributionContentService?.portalUrl || 'https://apikey.qzz.io').replace(/\/+$/, '')}/upload`,
|
||||
},
|
||||
});
|
||||
const baseRenderContributionMode = contributionModeManager?.render
|
||||
@@ -4220,7 +4213,7 @@ btnRepoHome?.addEventListener('click', () => {
|
||||
});
|
||||
|
||||
btnCloudflareTempEmailUsageGuide?.addEventListener('click', () => {
|
||||
showCloudflareTempEmailUsageGuide();
|
||||
openCloudflareTempEmailUsageGuidePage();
|
||||
});
|
||||
|
||||
btnCloudflareTempEmailGithub?.addEventListener('click', () => {
|
||||
@@ -4619,6 +4612,11 @@ selectIcloudHostPreference?.addEventListener('change', () => {
|
||||
}
|
||||
});
|
||||
|
||||
selectIcloudFetchMode?.addEventListener('change', () => {
|
||||
markSettingsDirty(true);
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
checkboxAutoDeleteIcloud?.addEventListener('change', () => {
|
||||
markSettingsDirty(true);
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
@@ -5188,6 +5186,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
? 'icloud.com'
|
||||
: (hostPreference === 'icloud.com.cn' ? 'icloud.com.cn' : 'auto');
|
||||
}
|
||||
if (message.payload.icloudFetchMode !== undefined && selectIcloudFetchMode) {
|
||||
selectIcloudFetchMode.value = normalizeIcloudFetchMode(message.payload.icloudFetchMode);
|
||||
}
|
||||
if (message.payload.autoRunSkipFailures !== undefined) {
|
||||
inputAutoSkipFailures.checked = Boolean(message.payload.autoRunSkipFailures);
|
||||
updateFallbackThreadIntervalInputState();
|
||||
|
||||
@@ -330,3 +330,124 @@ return {
|
||||
assert.deepStrictEqual(result.events.steps, [1, 2, 3, 4]);
|
||||
assert.equal(result.events.logs.some(({ message }) => /沿用当前邮箱回到步骤 1 重新开始/.test(message)), false);
|
||||
});
|
||||
|
||||
test('auto-run skips steps 4/5 when step 2 has already marked registration chain as skipped', async () => {
|
||||
const api = new Function(`
|
||||
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
|
||||
const LAST_STEP_ID = 10;
|
||||
const FINAL_OAUTH_CHAIN_START_STEP = 7;
|
||||
const chrome = {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
runtime: {
|
||||
sendMessage: async () => {},
|
||||
},
|
||||
};
|
||||
|
||||
let currentState = {
|
||||
email: 'already@login.example',
|
||||
password: 'Secret123!',
|
||||
mailProvider: 'icloud',
|
||||
stepStatuses: {
|
||||
1: 'pending',
|
||||
2: 'pending',
|
||||
3: 'pending',
|
||||
4: 'pending',
|
||||
5: 'pending',
|
||||
6: 'pending',
|
||||
7: 'pending',
|
||||
8: 'pending',
|
||||
9: 'pending',
|
||||
10: 'pending',
|
||||
},
|
||||
};
|
||||
const events = {
|
||||
steps: [],
|
||||
logs: [],
|
||||
};
|
||||
|
||||
async function addLog(message, level = 'info') {
|
||||
events.logs.push({ message, level });
|
||||
}
|
||||
|
||||
async function ensureAutoEmailReady() {
|
||||
return currentState.email;
|
||||
}
|
||||
|
||||
async function broadcastAutoRunStatus() {}
|
||||
|
||||
async function getState() {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
async function setState(updates) {
|
||||
currentState = {
|
||||
...currentState,
|
||||
...updates,
|
||||
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
|
||||
};
|
||||
}
|
||||
|
||||
function isStopError(error) {
|
||||
return (error?.message || String(error || '')) === '流程已被用户停止。';
|
||||
}
|
||||
|
||||
function isStepDoneStatus(status) {
|
||||
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
|
||||
}
|
||||
|
||||
async function executeStepAndWait(step) {
|
||||
events.steps.push(step);
|
||||
if (step === 2) {
|
||||
currentState = {
|
||||
...currentState,
|
||||
stepStatuses: {
|
||||
...currentState.stepStatuses,
|
||||
3: 'skipped',
|
||||
4: 'skipped',
|
||||
5: 'skipped',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getTabId() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
async function invalidateDownstreamAfterStepRestart() {}
|
||||
|
||||
function getLoginAuthStateLabel(state) {
|
||||
return state || 'unknown';
|
||||
}
|
||||
|
||||
function getErrorMessage(error) {
|
||||
return error?.message || String(error || '');
|
||||
}
|
||||
|
||||
async function getLoginAuthStateFromContent() {
|
||||
return { state: 'password_page', url: 'https://auth.openai.com/log-in' };
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
async run() {
|
||||
await runAutoSequenceFromStep(1, {
|
||||
targetRun: 1,
|
||||
totalRuns: 1,
|
||||
attemptRuns: 1,
|
||||
continued: false,
|
||||
});
|
||||
return { events, currentState };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const { events } = await api.run();
|
||||
|
||||
assert.deepStrictEqual(events.steps, [1, 2, 6, 7, 8, 9, 10]);
|
||||
assert.equal(events.logs.some(({ message }) => /步骤 4 当前状态为 skipped/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /步骤 5 当前状态为 skipped/.test(message)), true);
|
||||
});
|
||||
|
||||
@@ -361,3 +361,55 @@ test('generated email helper requests random subdomain creation while preserving
|
||||
domain: 'mail.example.com',
|
||||
});
|
||||
});
|
||||
|
||||
test('generated email helper honors iCloud always-new fetch mode', async () => {
|
||||
const api = loadGeneratedEmailHelpersApi();
|
||||
const icloudOptions = [];
|
||||
|
||||
const helpers = api.createGeneratedEmailHelpers({
|
||||
addLog: async () => {},
|
||||
buildGeneratedAliasEmail: () => {
|
||||
throw new Error('should not build managed alias');
|
||||
},
|
||||
buildCloudflareTempEmailHeaders: () => ({}),
|
||||
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
|
||||
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
|
||||
fetch: async () => ({ ok: true, text: async () => '{}' }),
|
||||
fetchIcloudHideMyEmail: async (options) => {
|
||||
icloudOptions.push(options);
|
||||
return 'fresh@icloud.example.com';
|
||||
},
|
||||
getCloudflareTempEmailAddressFromResponse: () => '',
|
||||
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
|
||||
getState: async () => ({
|
||||
emailGenerator: 'icloud',
|
||||
icloudFetchMode: 'always_new',
|
||||
mailProvider: 'gmail',
|
||||
}),
|
||||
ensureMail2925AccountForFlow: async () => {
|
||||
throw new Error('should not allocate mail2925 account');
|
||||
},
|
||||
joinCloudflareTempEmailUrl: () => '',
|
||||
normalizeCloudflareDomain: () => '',
|
||||
normalizeCloudflareTempEmailAddress: () => '',
|
||||
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
|
||||
isGeneratedAliasProvider: () => false,
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScript: async () => {
|
||||
throw new Error('should not use duck generator');
|
||||
},
|
||||
setEmailState: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const email = await helpers.fetchGeneratedEmail({
|
||||
emailGenerator: 'icloud',
|
||||
icloudFetchMode: 'always_new',
|
||||
mailProvider: 'gmail',
|
||||
}, {
|
||||
generator: 'icloud',
|
||||
});
|
||||
|
||||
assert.equal(email, 'fresh@icloud.example.com');
|
||||
assert.deepEqual(icloudOptions, [{ generateNew: true }]);
|
||||
});
|
||||
|
||||
@@ -291,6 +291,152 @@ return {
|
||||
assert.equal(snapshot.buildCalls.length, 1);
|
||||
});
|
||||
|
||||
test('pollLuckmailVerificationCode snapshots existing mails before accepting new LuckMail code', async () => {
|
||||
const bundle = extractFunction('pollLuckmailVerificationCode');
|
||||
|
||||
const factory = new Function(`
|
||||
let currentState = {
|
||||
currentLuckmailPurchase: {
|
||||
id: 7,
|
||||
email_address: 'luck@example.com',
|
||||
token: 'tok-luck',
|
||||
},
|
||||
currentLuckmailMailCursor: null,
|
||||
};
|
||||
const logs = [];
|
||||
const cursorWrites = [];
|
||||
let tokenCodeCalls = 0;
|
||||
|
||||
function getCurrentLuckmailPurchase(state) {
|
||||
return state.currentLuckmailPurchase;
|
||||
}
|
||||
function createLuckmailClient() {
|
||||
return {
|
||||
user: {
|
||||
async getTokenMails() {
|
||||
if (tokenCodeCalls === 0) {
|
||||
return {
|
||||
mails: [
|
||||
{ message_id: 'old-mail', received_at: '2026-04-14 13:31:15', verification_code: '111111' },
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
mails: [
|
||||
{ message_id: 'new-mail', received_at: '2026-04-14 13:32:05', verification_code: '222222' },
|
||||
{ message_id: 'old-mail', received_at: '2026-04-14 13:31:15', verification_code: '111111' },
|
||||
],
|
||||
};
|
||||
},
|
||||
async getTokenCode() {
|
||||
tokenCodeCalls += 1;
|
||||
return tokenCodeCalls === 1
|
||||
? {
|
||||
has_new_mail: true,
|
||||
verification_code: '111111',
|
||||
mail: { message_id: 'old-mail', received_at: '2026-04-14 13:31:15', verification_code: '111111' },
|
||||
}
|
||||
: {
|
||||
has_new_mail: true,
|
||||
verification_code: '222222',
|
||||
mail: { message_id: 'new-mail', received_at: '2026-04-14 13:32:05', verification_code: '222222' },
|
||||
};
|
||||
},
|
||||
async getTokenMailDetail(_token, messageId) {
|
||||
return { message_id: messageId, received_at: '2026-04-14 13:32:05', verification_code: '222222' };
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
async function getState() {
|
||||
return currentState;
|
||||
}
|
||||
async function setLuckmailMailCursorState(cursor) {
|
||||
currentState = { ...currentState, currentLuckmailMailCursor: cursor };
|
||||
cursorWrites.push(cursor);
|
||||
}
|
||||
function normalizeLuckmailMailCursor(cursor) {
|
||||
return {
|
||||
messageId: cursor?.messageId || cursor?.message_id || '',
|
||||
receivedAt: cursor?.receivedAt || cursor?.received_at || '',
|
||||
};
|
||||
}
|
||||
function normalizeLuckmailTimestamp(value) {
|
||||
return Date.parse(String(value || '').replace(' ', 'T') + 'Z') || 0;
|
||||
}
|
||||
function buildLuckmailMailCursor(mail) {
|
||||
return { messageId: mail.message_id || '', receivedAt: mail.received_at || '' };
|
||||
}
|
||||
function buildLuckmailBaselineCursor(mails) {
|
||||
const latest = mails[0] || null;
|
||||
return latest ? buildLuckmailMailCursor(latest) : null;
|
||||
}
|
||||
function isLuckmailMailNewerThanCursor(mail, cursor) {
|
||||
if (!cursor?.messageId && !cursor?.receivedAt) return true;
|
||||
if (mail.message_id === cursor.messageId) return false;
|
||||
return normalizeLuckmailTimestamp(mail.received_at) > normalizeLuckmailTimestamp(cursor.receivedAt);
|
||||
}
|
||||
function pickLuckmailVerificationMail(mails, filters) {
|
||||
const excludeCodes = new Set(filters.excludeCodes || []);
|
||||
for (const mail of mails || []) {
|
||||
if (!mail.verification_code || excludeCodes.has(mail.verification_code)) continue;
|
||||
return { mail, code: mail.verification_code };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function normalizeLuckmailTokenCode(result) {
|
||||
return result;
|
||||
}
|
||||
async function resolveLuckmailVerificationMail(client, token, filters, tokenCodeResult) {
|
||||
if (tokenCodeResult?.mail) {
|
||||
const inline = pickLuckmailVerificationMail([tokenCodeResult.mail], filters);
|
||||
if (inline) return inline;
|
||||
}
|
||||
const mailList = await client.user.getTokenMails(token);
|
||||
return pickLuckmailVerificationMail(mailList.mails, filters);
|
||||
}
|
||||
async function addLog(message, level) {
|
||||
logs.push({ message, level });
|
||||
}
|
||||
function throwIfStopped() {}
|
||||
function isStopError() {
|
||||
return false;
|
||||
}
|
||||
async function sleepWithStop() {}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
pollLuckmailVerificationCode,
|
||||
snapshot() {
|
||||
return { currentState, cursorWrites, logs, tokenCodeCalls };
|
||||
},
|
||||
};
|
||||
`);
|
||||
|
||||
const api = factory();
|
||||
const result = await api.pollLuckmailVerificationCode(4, await api.snapshot().currentState, {
|
||||
maxAttempts: 2,
|
||||
intervalMs: 1000,
|
||||
senderFilters: ['openai'],
|
||||
subjectFilters: ['code'],
|
||||
excludeCodes: [],
|
||||
});
|
||||
|
||||
assert.equal(result.code, '222222');
|
||||
const snapshot = api.snapshot();
|
||||
assert.deepStrictEqual(snapshot.cursorWrites[0], {
|
||||
messageId: 'old-mail',
|
||||
receivedAt: '2026-04-14 13:31:15',
|
||||
});
|
||||
assert.deepStrictEqual(snapshot.cursorWrites.at(-1), {
|
||||
messageId: 'new-mail',
|
||||
receivedAt: '2026-04-14 13:32:05',
|
||||
});
|
||||
assert.equal(snapshot.logs.some((entry) => /已保存当前邮箱旧邮件快照/.test(entry.message)), true);
|
||||
assert.equal(snapshot.tokenCodeCalls, 2);
|
||||
});
|
||||
|
||||
test('listLuckmailPurchasesByProject only keeps openai purchases', async () => {
|
||||
const bundle = extractFunction('listLuckmailPurchasesByProject');
|
||||
|
||||
|
||||
@@ -151,6 +151,39 @@ test('message router does not overwrite a completed step 3 when step 2 is replay
|
||||
assert.deepStrictEqual(events.stepStatuses, []);
|
||||
});
|
||||
|
||||
test('message router skips steps 3/4/5 when step 2 detects already logged-in session', async () => {
|
||||
const { router, events } = createRouter({
|
||||
state: { stepStatuses: { 3: 'pending', 4: 'completed', 5: 'pending' } },
|
||||
});
|
||||
|
||||
await router.handleStepData(2, {
|
||||
email: 'user@example.com',
|
||||
skipRegistrationFlow: true,
|
||||
skippedPasswordStep: true,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(events.emailStates, ['user@example.com']);
|
||||
assert.deepStrictEqual(events.stepStatuses, [
|
||||
{ step: 3, status: 'skipped' },
|
||||
{ step: 5, status: 'skipped' },
|
||||
]);
|
||||
assert.equal(events.logs[0]?.message, '步骤 2:检测到当前已登录会话,已自动跳过步骤 3/4/5,流程将直接进入步骤 6。');
|
||||
});
|
||||
|
||||
test('message router skips step 5 when step 4 reports already logged-in transition', async () => {
|
||||
const { router, events } = createRouter({
|
||||
state: { stepStatuses: { 5: 'pending' } },
|
||||
});
|
||||
|
||||
await router.handleStepData(4, {
|
||||
emailTimestamp: 123,
|
||||
skipProfileStep: true,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(events.stepStatuses, [{ step: 5, status: 'skipped' }]);
|
||||
assert.equal(events.logs[0]?.message, '步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。');
|
||||
});
|
||||
|
||||
test('message router finalizes step 3 before marking it completed', async () => {
|
||||
const { router, events } = createRouter();
|
||||
|
||||
|
||||
@@ -84,6 +84,55 @@ test('step 2 keeps password flow when landing on password page', async () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('step 2 falls back to already-logged-in branch when auth entry recovery fails on chatgpt home', async () => {
|
||||
const completedPayloads = [];
|
||||
const logs = [];
|
||||
|
||||
const executor = step2Api.createStep2Executor({
|
||||
addLog: async (message, level = 'info') => {
|
||||
logs.push({ message, level });
|
||||
},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
get: async () => ({ url: 'https://chatgpt.com/' }),
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completedPayloads.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
ensureSignupAuthEntryPageReady: async () => {
|
||||
throw new Error('当前页面没有可用的注册入口,也不在邮箱/密码页。URL: https://chatgpt.com/');
|
||||
},
|
||||
ensureSignupEntryPageReady: async () => ({ tabId: 13 }),
|
||||
ensureSignupPostEmailPageReadyInTab: async () => ({
|
||||
state: 'password_page',
|
||||
url: 'https://auth.openai.com/create-account/password',
|
||||
}),
|
||||
getTabId: async () => 13,
|
||||
isTabAlive: async () => true,
|
||||
resolveSignupEmailForFlow: async () => 'user@example.com',
|
||||
sendToContentScriptResilient: async () => ({ submitted: true }),
|
||||
SIGNUP_PAGE_INJECT_FILES: [],
|
||||
});
|
||||
|
||||
await executor.executeStep2({ email: 'user@example.com' });
|
||||
|
||||
assert.equal(completedPayloads.length, 1);
|
||||
assert.deepStrictEqual(completedPayloads[0], {
|
||||
step: 2,
|
||||
payload: {
|
||||
email: 'user@example.com',
|
||||
nextSignupState: 'already_logged_in_home',
|
||||
nextSignupUrl: 'https://chatgpt.com/',
|
||||
skippedPasswordStep: true,
|
||||
skipRegistrationFlow: true,
|
||||
},
|
||||
});
|
||||
assert.ok(logs.some((item) => /已跳过注册链路/.test(item.message)));
|
||||
});
|
||||
|
||||
test('signup flow helper recognizes email verification page as post-email landing page', async () => {
|
||||
let ensureCalls = 0;
|
||||
let passwordReadyChecks = 0;
|
||||
|
||||
@@ -119,3 +119,49 @@ test('step 4 does not request a fresh code first for Cloudflare temp mail', asyn
|
||||
assert.equal(capturedOptions.requestFreshCodeFirst, false);
|
||||
assert.equal(capturedOptions.resendIntervalMs, 25000);
|
||||
});
|
||||
|
||||
test('step 4 checks iCloud session before polling iCloud mailbox', async () => {
|
||||
let icloudChecks = 0;
|
||||
let resolved = false;
|
||||
|
||||
const executor = api.createStep4Executor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureIcloudMailSession: async () => {
|
||||
icloudChecks += 1;
|
||||
},
|
||||
ensureMail2925MailboxSession: async () => {},
|
||||
getMailConfig: () => ({
|
||||
source: 'icloud-mail',
|
||||
url: 'https://www.icloud.com/mail/',
|
||||
label: 'iCloud 邮箱',
|
||||
}),
|
||||
getTabId: async () => 1,
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isTabAlive: async () => true,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
resolveVerificationStep: async () => {
|
||||
resolved = true;
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
shouldUseCustomRegistrationEmail: () => false,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep4({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
});
|
||||
|
||||
assert.equal(icloudChecks, 1);
|
||||
assert.equal(resolved, true);
|
||||
});
|
||||
|
||||
@@ -296,3 +296,57 @@ test('step 8 does not rerun step 7 when verification submit lands on add-phone',
|
||||
assert.equal(calls.rerunStep7, 0);
|
||||
assert.ok(!calls.logs.some(({ message }) => /准备从步骤 7 重新开始/.test(message)));
|
||||
});
|
||||
|
||||
test('step 8 checks iCloud session before polling iCloud mailbox', async () => {
|
||||
let icloudChecks = 0;
|
||||
let resolved = false;
|
||||
|
||||
const executor = api.createStep8Executor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureIcloudMailSession: async () => {
|
||||
icloudChecks += 1;
|
||||
},
|
||||
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: '' }),
|
||||
rerunStep7ForStep8Recovery: async () => {},
|
||||
getOAuthFlowRemainingMs: async () => 8000,
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000),
|
||||
getMailConfig: () => ({
|
||||
source: 'icloud-mail',
|
||||
url: 'https://www.icloud.com/mail/',
|
||||
label: 'iCloud 邮箱',
|
||||
navigateOnReuse: true,
|
||||
}),
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isTabAlive: async () => true,
|
||||
isVerificationMailPollingError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
resolveVerificationStep: async () => {
|
||||
resolved = true;
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
shouldUseCustomRegistrationEmail: () => false,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep8({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
});
|
||||
|
||||
assert.equal(icloudChecks, 1);
|
||||
assert.equal(resolved, true);
|
||||
});
|
||||
|
||||
@@ -70,68 +70,40 @@ test('sidepanel modal message preserves line breaks and supports inline links',
|
||||
assert.match(css, /\.modal-message a,\s*[\s\S]*\.modal-alert a/);
|
||||
});
|
||||
|
||||
test('buildCloudflareTempEmailUsageGuideModalConfig returns a modal payload with inline links for generator mode', () => {
|
||||
const bundle = extractFunction('buildCloudflareTempEmailUsageGuideModalConfig');
|
||||
test('openCloudflareTempEmailUsageGuidePage opens the contribution portal home page', () => {
|
||||
const bundle = extractFunction('openCloudflareTempEmailUsageGuidePage');
|
||||
|
||||
const api = new Function(`
|
||||
const selectMailProvider = { value: '163' };
|
||||
const selectEmailGenerator = { value: 'cloudflare-temp-email' };
|
||||
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
|
||||
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
|
||||
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
|
||||
const openedUrls = [];
|
||||
function getContributionPortalUrl() { return 'https://apikey.qzz.io'; }
|
||||
function openExternalUrl(url) { openedUrls.push(url); }
|
||||
${bundle}
|
||||
return {
|
||||
buildCloudflareTempEmailUsageGuideModalConfig,
|
||||
openedUrls,
|
||||
openCloudflareTempEmailUsageGuidePage,
|
||||
};
|
||||
`)();
|
||||
|
||||
const modalConfig = api.buildCloudflareTempEmailUsageGuideModalConfig();
|
||||
assert.equal(typeof modalConfig.title, 'string');
|
||||
assert.equal(typeof modalConfig.messageHtml, 'string');
|
||||
assert.equal(typeof modalConfig.alert?.text, 'string');
|
||||
assert.equal(modalConfig.title.length > 0, true);
|
||||
assert.equal(modalConfig.messageHtml.length > 0, true);
|
||||
assert.equal(modalConfig.alert.text.length > 0, true);
|
||||
assert.equal(modalConfig.messageHtml.includes('<a '), true);
|
||||
assert.equal(modalConfig.messageHtml.includes('Issue #942'), true);
|
||||
assert.equal(modalConfig.messageHtml.includes('LINUX DO 教程'), true);
|
||||
api.openCloudflareTempEmailUsageGuidePage();
|
||||
assert.deepEqual(api.openedUrls, ['https://apikey.qzz.io']);
|
||||
});
|
||||
|
||||
test('buildCloudflareTempEmailUsageGuideModalConfig returns a distinct alert for provider mode', () => {
|
||||
const bundle = extractFunction('buildCloudflareTempEmailUsageGuideModalConfig');
|
||||
test('openCloudflareTempEmailUsageGuidePage skips opening when the contribution portal URL is empty', () => {
|
||||
const bundle = extractFunction('openCloudflareTempEmailUsageGuidePage');
|
||||
|
||||
const api = new Function(`
|
||||
const selectMailProvider = { value: 'cloudflare-temp-email' };
|
||||
const selectEmailGenerator = { value: 'duck' };
|
||||
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
|
||||
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
|
||||
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
|
||||
const openedUrls = [];
|
||||
function getContributionPortalUrl() { return ''; }
|
||||
function openExternalUrl(url) { openedUrls.push(url); }
|
||||
${bundle}
|
||||
return {
|
||||
buildCloudflareTempEmailUsageGuideModalConfig,
|
||||
openedUrls,
|
||||
openCloudflareTempEmailUsageGuidePage,
|
||||
};
|
||||
`)();
|
||||
|
||||
const providerConfig = api.buildCloudflareTempEmailUsageGuideModalConfig();
|
||||
|
||||
const generatorApi = new Function(`
|
||||
const selectMailProvider = { value: '163' };
|
||||
const selectEmailGenerator = { value: 'cloudflare-temp-email' };
|
||||
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
|
||||
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
|
||||
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
|
||||
${bundle}
|
||||
return {
|
||||
buildCloudflareTempEmailUsageGuideModalConfig,
|
||||
};
|
||||
`)();
|
||||
|
||||
const generatorConfig = generatorApi.buildCloudflareTempEmailUsageGuideModalConfig();
|
||||
assert.equal(typeof providerConfig.alert?.text, 'string');
|
||||
assert.equal(typeof providerConfig.messageHtml, 'string');
|
||||
assert.equal(providerConfig.alert.text.length > 0, true);
|
||||
assert.equal(providerConfig.messageHtml.length > 0, true);
|
||||
assert.notEqual(providerConfig.alert.text, generatorConfig.alert.text);
|
||||
api.openCloudflareTempEmailUsageGuidePage();
|
||||
assert.deepEqual(api.openedUrls, []);
|
||||
});
|
||||
|
||||
test('openCloudflareTempEmailRepositoryPage opens the upstream repository', () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ test('sidepanel html keeps a single contribution mode button in header', () => {
|
||||
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
|
||||
|
||||
assert.equal(matches.length, 1);
|
||||
assert.match(html, /id="btn-contribution-mode"[^>]*title="进入贡献模式并打开上传页"/);
|
||||
assert.match(html, /id="btn-contribution-mode"[^>]*title="进入贡献模式并打开官网页"/);
|
||||
assert.match(html, />贡献\/使用<\/button>/);
|
||||
assert.match(html, /<\/header>\s*<div id="contribution-update-layer"/);
|
||||
assert.match(html, /id="contribution-update-layer"/);
|
||||
|
||||
@@ -402,7 +402,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
|
||||
},
|
||||
},
|
||||
constants: {
|
||||
contributionUploadUrl: 'https://apikey.qzz.io',
|
||||
contributionPortalUrl: 'https://apikey.qzz.io',
|
||||
contributionUploadUrl: 'https://apikey.qzz.io/upload',
|
||||
pollIntervalMs: 2500,
|
||||
},
|
||||
});
|
||||
@@ -452,7 +453,7 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
|
||||
assert.equal(dom.contributionModeSummary.textContent, '\u5df2\u63d0\u4ea4\u56de\u8c03\uff0c\u7b49\u5f85 CPA \u786e\u8ba4');
|
||||
|
||||
dom.btnOpenContributionUpload.listeners.click();
|
||||
assert.deepStrictEqual(openedUrls, ['https://apikey.qzz.io', 'https://apikey.qzz.io']);
|
||||
assert.deepStrictEqual(openedUrls, ['https://apikey.qzz.io', 'https://apikey.qzz.io/upload']);
|
||||
|
||||
await dom.btnExitContributionMode.listeners.click();
|
||||
manager.render();
|
||||
|
||||
@@ -12,6 +12,23 @@ test('sidepanel loads icloud manager before sidepanel bootstrap', () => {
|
||||
assert.ok(icloudManagerIndex < sidepanelIndex);
|
||||
});
|
||||
|
||||
test('sidepanel source binds the icloud fetch mode control before using it', () => {
|
||||
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||
|
||||
assert.match(source, /const selectIcloudFetchMode = document\.getElementById\('select-icloud-fetch-mode'\);/);
|
||||
assert.match(source, /selectIcloudFetchMode\?\.addEventListener\('change'/);
|
||||
});
|
||||
|
||||
test('update card highlights exporting config before upgrade', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8');
|
||||
|
||||
assert.match(html, /<p class="update-card-reminder">一定请先导出配置,再执行更新<\/p>/);
|
||||
assert.match(css, /\.update-card-reminder\s*\{/);
|
||||
assert.match(css, /font-weight:\s*700;/);
|
||||
assert.match(css, /color:\s*var\(--orange\);/);
|
||||
});
|
||||
|
||||
test('icloud manager exposes a factory and renders empty state', () => {
|
||||
const source = fs.readFileSync('sidepanel/icloud-manager.js', 'utf8');
|
||||
const windowObject = {};
|
||||
|
||||
@@ -115,6 +115,9 @@ function fillInput(el, value) {
|
||||
filledValues.push(value);
|
||||
}
|
||||
async function sleep() {}
|
||||
function isStep5Ready() { return false; }
|
||||
function isStep8Ready() { return false; }
|
||||
function isAddPhonePageReady() { return false; }
|
||||
function isVisibleElement() { return true; }
|
||||
function isActionEnabled(el) { return Boolean(el) && !el.disabled; }
|
||||
function getActionText(el) { return el.textContent || ''; }
|
||||
|
||||
@@ -758,3 +758,165 @@ test('verification flow uses configured login resend count for step 8', async ()
|
||||
assert.deepStrictEqual(resendSteps, [8, 8]);
|
||||
assert.equal(pollCalls, 3);
|
||||
});
|
||||
|
||||
test('verification flow waits during resend cooldown instead of tight-looping', async () => {
|
||||
const sleepCalls = [];
|
||||
let pollCalls = 0;
|
||||
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
getState: async () => ({}),
|
||||
getTabId: async () => 1,
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isStopError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
|
||||
pollCloudflareTempEmailVerificationCode: async () => ({}),
|
||||
pollHotmailVerificationCode: async () => ({}),
|
||||
pollLuckmailVerificationCode: async () => ({}),
|
||||
sendToContentScript: async () => ({}),
|
||||
sendToMailContentScriptResilient: async (_mail, message) => {
|
||||
if (message.type !== 'POLL_EMAIL') {
|
||||
return {};
|
||||
}
|
||||
pollCalls += 1;
|
||||
return pollCalls === 1
|
||||
? {}
|
||||
: { code: '654321', emailTimestamp: 123 };
|
||||
},
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
sleepWithStop: async (ms) => {
|
||||
sleepCalls.push(ms);
|
||||
},
|
||||
throwIfStopped: () => {},
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
});
|
||||
|
||||
const result = await helpers.pollFreshVerificationCodeWithResendInterval(
|
||||
4,
|
||||
{
|
||||
email: 'user@example.com',
|
||||
lastSignupCode: null,
|
||||
},
|
||||
{ provider: 'qq', label: 'QQ 邮箱' },
|
||||
{
|
||||
maxResendRequests: 0,
|
||||
resendIntervalMs: 25000,
|
||||
lastResendAt: Date.now(),
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(result.code, '654321');
|
||||
assert.equal(pollCalls, 2);
|
||||
assert.ok(sleepCalls.length >= 1);
|
||||
assert.ok(sleepCalls[0] >= 1000);
|
||||
});
|
||||
|
||||
test('verification flow uses resilient signup-page transport when submitting verification code', async () => {
|
||||
const resilientCalls = [];
|
||||
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
getState: async () => ({}),
|
||||
getTabId: async () => 1,
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isStopError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
|
||||
pollCloudflareTempEmailVerificationCode: async () => ({}),
|
||||
pollHotmailVerificationCode: async () => ({}),
|
||||
pollLuckmailVerificationCode: async () => ({}),
|
||||
sendToContentScript: async () => {
|
||||
throw new Error('should not use non-resilient channel');
|
||||
},
|
||||
sendToContentScriptResilient: async (_source, message, options) => {
|
||||
resilientCalls.push({ message, options });
|
||||
return { success: true };
|
||||
},
|
||||
sendToMailContentScriptResilient: async () => ({}),
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
});
|
||||
|
||||
const result = await helpers.submitVerificationCode(4, '654321');
|
||||
|
||||
assert.deepStrictEqual(result, { success: true });
|
||||
assert.equal(resilientCalls.length, 1);
|
||||
assert.equal(resilientCalls[0].message.type, 'FILL_CODE');
|
||||
assert.equal(resilientCalls[0].message.payload.code, '654321');
|
||||
assert.ok(resilientCalls[0].options.timeoutMs >= 30000);
|
||||
});
|
||||
|
||||
test('verification flow treats retryable submit transport failure as success when step 4 already redirected to logged-in home', async () => {
|
||||
const logs = [];
|
||||
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async (message, level = 'info') => {
|
||||
logs.push({ message, level });
|
||||
},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
get: async () => ({ url: 'https://chatgpt.com/' }),
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
getState: async () => ({}),
|
||||
getTabId: async () => 1,
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isRetryableContentScriptTransportError: (error) => /message channel is closed/i.test(String(error?.message || error || '')),
|
||||
isStopError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
|
||||
pollCloudflareTempEmailVerificationCode: async () => ({}),
|
||||
pollHotmailVerificationCode: async () => ({}),
|
||||
pollLuckmailVerificationCode: async () => ({}),
|
||||
sendToContentScript: async () => {
|
||||
throw new Error('should not use non-resilient channel');
|
||||
},
|
||||
sendToContentScriptResilient: async () => {
|
||||
throw new Error('The page keeping the extension port is moved into back/forward cache, so the message channel is closed.');
|
||||
},
|
||||
sendToMailContentScriptResilient: async () => ({}),
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
});
|
||||
|
||||
const result = await helpers.submitVerificationCode(4, '654321');
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.skipProfileStep, true);
|
||||
assert.equal(result.assumed, true);
|
||||
assert.equal(result.transportRecovered, true);
|
||||
assert.equal(logs.some(({ message }) => /验证码提交后页面已切换到ChatGPT 已登录首页/.test(message)), true);
|
||||
});
|
||||
|
||||
+2
-2
@@ -45,7 +45,7 @@
|
||||
- `background/account-run-history.js`:邮箱记录模块,负责以邮箱为主键维护最新记录(同邮箱后续状态覆盖),在步骤 2 设定邮箱后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,支持清理记录,并在启用独立本地同步配置后把完整快照同步到本地 helper。
|
||||
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 `gmailBaseEmail`、`mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失别名基邮箱与 2925 切号上下文。
|
||||
- `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。
|
||||
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。
|
||||
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。
|
||||
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。
|
||||
- `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
|
||||
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 2925 账号池的新增、导入、切换、登录、禁用与删除消息。
|
||||
@@ -142,7 +142,7 @@
|
||||
- `tests/background-account-history-settings.test.js`:测试账号运行历史的独立配置项归一化,不再复用 Hotmail 模式作为启停条件。
|
||||
- `tests/background-contribution-mode.test.js`:测试贡献模式的后台运行态与公开 OAuth 接入,覆盖 `contributionMode` 只存在于运行态、`SET_CONTRIBUTION_MODE / POLL_CONTRIBUTION_STATUS` 消息接入、reset 保留贡献运行态,以及 callback 自动提交在“无需手动提交”场景下的兼容处理。
|
||||
- `tests/background-custom-email-pool.test.js`:测试后台对自定义邮箱池生成方式,以及自定义邮箱服务号池的归一化、标签文案和按轮次取邮箱逻辑。
|
||||
- `tests/background-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂。
|
||||
- `tests/background-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂,并覆盖 2925 receive 模式回退普通邮箱生成、自定义邮箱池按轮次取值、2925 provide 模式账号池预热,以及 iCloud `always_new` 传参。
|
||||
- `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。
|
||||
- `tests/background-icloud-mail-provider.test.js`:测试 iCloud 邮箱 provider 配置解析。
|
||||
- `tests/background-logging-status-module.test.js`:测试日志 / 状态模块已接入且导出工厂。
|
||||
|
||||
Reference in New Issue
Block a user