Merge origin/dev into codex2api source

- resolve the sidepanel contribution-mode test conflict while keeping the new Codex2API assertions\n- update the mail2925 payload test harness for newly added Codex2API and random-subdomain inputs\n- renumber the README quick-start plan headings and strip trailing whitespace from the new tutorial doc
This commit is contained in:
Q3CC
2026-04-22 22:18:09 +08:00
31 changed files with 2587 additions and 176 deletions
+5 -5
View File
@@ -148,7 +148,7 @@
4. 通过后再执行步骤或 `Auto`
5. 当前项目中,`Mail = Hotmail` 时会直接使用账号池里的邮箱作为注册邮箱,不再走 `Duck / Cloudflare` 自动生成
### 方案 D`2925 账号池`
### 方案 E`2925 账号池`
1. `Mail` 选择 `2925`
2.`2925 账号池` 中添加 `邮箱 / 密码`
@@ -645,7 +645,7 @@ Step 8 默认要求当前认证页已经处于登录验证码页。
- 等待按钮可点击
- 获取按钮坐标
- 通过 Chrome `debugger` 的输入事件点击该按钮
- 点击后会持续检查页面是否真正离开当前状态;如果出现认证页 `重试` 页面,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再重新执行当前轮的“继续”点击
- 点击后会持续检查页面是否真正离开当前状态;如果点击后出现认证页 `重试` 页面,则直接报错,不会在 Step 9 内部点击 `重试`
- 同时监听 `chrome.webNavigation.onBeforeNavigate`
- 一旦捕获本地回调地址,就把结果保存到 `Callback`
@@ -661,9 +661,9 @@ Step 8 默认要求当前认证页已经处于登录验证码页。
- 步骤 10 会拒绝任何不是真实 `/auth/callback`,或缺少 `code` / `state` 的本地回调地址
- 成功后的清理只会针对 `/auth` 这一类真实回调标签页,不会再泛化清理任意 localhost 路径
- 侧边栏可切换“本地 CPA”策略,默认是 `全部回调`
- 选择 `全部回调` 时,即使 CPA 部署在本地,也会执行步骤 10
- 选择 `跳过第10步` 时,仅当本地 CPA 且步骤 9 已拿到回调地址时,才会直接跳过步骤 10
- 侧边栏可切换“回调方式”,默认是 `服务器部署`
- 选择 `服务器部署` 时,即使 CPA 部署在本地,也会执行步骤 10
- 选择 `本地部署` 时,仅当本地 CPA 且步骤 9 已拿到回调地址时,才会直接跳过步骤 10
回到 CPA 面板:
+32 -29
View File
@@ -147,11 +147,12 @@ const HOTMAIL_MAILBOXES = ['INBOX', 'Junk'];
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX = 'CF_SECURITY_BLOCKED::';
const CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE = '您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器';
const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
const HUMAN_STEP_DELAY_MIN = 700;
const HUMAN_STEP_DELAY_MAX = 2200;
const STEP6_MAX_ATTEMPTS = 3;
const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8;
const OAUTH_FLOW_TIMEOUT_MS = 6 * 60 * 1000;
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000;
const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000;
const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts';
@@ -271,6 +272,7 @@ const PERSISTED_SETTING_DEFAULTS = {
accountRunHistoryHelperBaseUrl: DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL,
gmailBaseEmail: '',
mail2925BaseEmail: '',
currentMail2925AccountId: '',
emailPrefix: '',
inbucketHost: '',
inbucketMailbox: '',
@@ -283,6 +285,7 @@ const PERSISTED_SETTING_DEFAULTS = {
cloudflareTempEmailAdminAuth: '',
cloudflareTempEmailCustomAuth: '',
cloudflareTempEmailReceiveMailbox: '',
cloudflareTempEmailUseRandomSubdomain: false,
cloudflareTempEmailDomain: '',
cloudflareTempEmailDomains: [],
hotmailAccounts: [],
@@ -839,6 +842,7 @@ function getCloudflareTempEmailConfig(state = {}) {
adminAuth: String(state.cloudflareTempEmailAdminAuth || ''),
customAuth: String(state.cloudflareTempEmailCustomAuth || ''),
receiveMailbox: normalizeCloudflareTempEmailReceiveMailbox(state.cloudflareTempEmailReceiveMailbox),
useRandomSubdomain: Boolean(state.cloudflareTempEmailUseRandomSubdomain),
domain: normalizeCloudflareTempEmailDomain(state.cloudflareTempEmailDomain),
domains: normalizeCloudflareTempEmailDomains(state.cloudflareTempEmailDomains),
};
@@ -911,6 +915,7 @@ function normalizePersistentSettingValue(key, value) {
return normalizeEmailGenerator(value);
case 'autoDeleteUsedIcloudAlias':
case 'accountRunHistoryTextEnabled':
case 'cloudflareTempEmailUseRandomSubdomain':
return Boolean(value);
case 'icloudHostPreference':
return normalizeIcloudHost(value) || 'auto';
@@ -918,6 +923,7 @@ function normalizePersistentSettingValue(key, value) {
return normalizeAccountRunHistoryHelperBaseUrl(value);
case 'gmailBaseEmail':
case 'mail2925BaseEmail':
case 'currentMail2925AccountId':
case 'emailPrefix':
return String(value || '').trim();
case 'inbucketHost':
@@ -4043,6 +4049,17 @@ function getTerminalSecurityBlockedTitle(error) {
return 'Cloudflare 风控拦截';
}
function isBrowserSwitchRequiredError(error) {
return getErrorMessage(error).startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX);
}
function getBrowserSwitchRequiredMessage(error) {
const message = getErrorMessage(error);
return message.startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX)
? message.slice(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX.length).trim()
: message;
}
function broadcastSecurityBlockedAlert(title = '流程已完全停止', message = CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE, alertText = '检测到 Cloudflare 风控,请暂停当前操作。') {
chrome.runtime.sendMessage({
type: 'SECURITY_BLOCKED_ALERT',
@@ -4066,6 +4083,13 @@ async function handleCloudflareSecurityBlocked(error) {
return message;
}
async function handleBrowserSwitchRequired(error) {
const message = getBrowserSwitchRequiredMessage(error)
|| '检测到第 10 步的特殊冲突状态,请更换浏览器后重新进行注册登录。';
await requestStop({ logMessage: message });
return message;
}
function isVerificationMailPollingError(error) {
if (typeof loggingStatus !== 'undefined' && loggingStatus?.isVerificationMailPollingError) {
return loggingStatus.isVerificationMailPollingError(error);
@@ -5373,6 +5397,10 @@ async function executeStep(step, options = {}) {
await handleCloudflareSecurityBlocked(err);
throw new Error(STOP_ERROR_MESSAGE);
}
if (isBrowserSwitchRequiredError(err)) {
await handleBrowserSwitchRequired(err);
throw new Error(STOP_ERROR_MESSAGE);
}
if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step) && isRetryableContentScriptTransportError(err))) {
await setStepStatus(step, 'failed');
await addLog(`步骤 ${step} 失败:${err.message}`, 'error');
@@ -6885,7 +6913,7 @@ async function startOAuthFlowTimeoutWindow(options = {}) {
oauthFlowDeadlineAt: deadlineAt,
oauthFlowDeadlineSourceUrl: normalizeOAuthFlowSourceUrl(options.oauthUrl),
});
await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 6 分钟倒计时。`, 'info');
await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 ${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟倒计时。`, 'info');
return deadlineAt;
}
@@ -7220,7 +7248,6 @@ async function getStep8PageState(tabId, responseTimeoutMs = 1500) {
async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) {
const start = Date.now();
let recovered = false;
let retryRecovered = false;
while (Date.now() - start < timeoutMs) {
throwIfStopped();
@@ -7232,20 +7259,9 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS)
throw new Error('步骤 9:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
}
if (pageState?.retryPage) {
await recoverAuthRetryPageOnTab(tabId, {
flow: 'auth',
logLabel: '步骤 9:检测到认证页重试页,正在点击“重试”恢复',
step: 8,
timeoutMs: Math.max(1000, Math.min(12000, timeoutMs)),
});
retryRecovered = true;
await sleepWithStop(250);
continue;
throw new Error(`步骤 9:当前认证页已进入重试页,当前流程将直接报错。URL: ${pageState.url || 'unknown'}`);
}
if (pageState?.consentReady) {
if (retryRecovered) {
await addLog('步骤 9:认证页重试页已恢复,准备重新定位“继续”按钮...', 'info');
}
return pageState;
}
if (pageState === null && !recovered) {
@@ -7410,18 +7426,7 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI
throw new Error('步骤 9:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。');
}
if (pageState?.retryPage) {
await recoverAuthRetryPageOnTab(tabId, {
flow: 'auth',
logLabel: '步骤 9:点击“继续”后进入重试页,正在点击“重试”恢复',
step: 8,
timeoutMs: Math.max(1000, Math.min(12000, timeoutMs)),
});
return {
progressed: false,
reason: 'retry_page_recovered',
restartCurrentStep: true,
url: pageState.url || baselineUrl || '',
};
throw new Error(`步骤 9:点击“继续”后页面进入认证页重试页,当前流程将直接报错。URL: ${pageState.url || baselineUrl || 'unknown'}`);
}
if (pageState === null) {
if (!recovered) {
@@ -7455,8 +7460,6 @@ function getStep8EffectLabel(effect) {
switch (effect?.reason) {
case 'url_changed':
return `URL 已变化:${effect.url}`;
case 'retry_page_recovered':
return '页面进入重试页并已恢复,需要重新执行当前步骤';
case 'page_reloading':
return '页面正在跳转或重载';
case 'left_consent_page':
+1
View File
@@ -146,6 +146,7 @@
const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
const payload = {
enablePrefix: true,
enableRandomSubdomain: Boolean(config.useRandomSubdomain),
name: requestedName,
domain: config.domain,
};
+41 -6
View File
@@ -168,6 +168,10 @@
}
}
function normalizeMailboxEmail(value = '') {
return String(value || '').trim().toLowerCase();
}
async function setCurrentMail2925Account(accountId, options = {}) {
const { logMessage = '', updateLastUsedAt = false } = options;
const state = await getState();
@@ -186,6 +190,7 @@
await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
}
await setPersistentSettings({ currentMail2925AccountId: nextAccount.id });
await setState({ currentMail2925AccountId: nextAccount.id });
broadcastDataUpdate({ currentMail2925AccountId: nextAccount.id });
if (logMessage) {
@@ -210,6 +215,7 @@
await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
if (state.currentMail2925AccountId === account.id && nextAccount.enabled === false) {
await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
}
@@ -224,6 +230,7 @@
await syncMail2925Accounts(nextAccounts);
if (state.currentMail2925AccountId === accountId) {
await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
}
@@ -239,6 +246,7 @@
await syncMail2925Accounts(nextAccounts);
if (state.currentMail2925AccountId && !findMail2925Account(nextAccounts, state.currentMail2925AccountId)) {
await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
}
@@ -397,10 +405,13 @@
forceRelogin = false,
actionLabel = '确保 2925 邮箱登录态',
allowLoginWhenOnLoginPage = true,
expectedMailboxEmail = '',
} = options;
const normalizedExpectedMailboxEmail = normalizeMailboxEmail(expectedMailboxEmail);
let account = null;
if (forceRelogin) {
if (forceRelogin || (allowLoginWhenOnLoginPage && normalizedExpectedMailboxEmail)) {
account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
@@ -477,16 +488,16 @@
}
}
if (!forceRelogin && !isMail2925LoginUrl(openedUrl)) {
if (!forceRelogin && !isMail2925LoginUrl(openedUrl) && !normalizedExpectedMailboxEmail) {
await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info');
return buildSuccessPayload();
}
if (!forceRelogin && !allowLoginWhenOnLoginPage) {
if (!forceRelogin && isMail2925LoginUrl(openedUrl) && !allowLoginWhenOnLoginPage) {
await failMailboxSession(`2925${actionLabel}失败,当前页面已跳转到登录页,且当前未启用 2925 账号池,不执行自动登录。`);
}
if (!account) {
if (!account && (forceRelogin || allowLoginWhenOnLoginPage)) {
account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
@@ -519,9 +530,10 @@
step: 0,
source: 'background',
payload: {
email: account.email,
password: account.password,
email: account?.email || '',
password: account?.password || '',
forceLogin: forceRelogin,
allowLoginWhenOnLoginPage,
},
},
{
@@ -546,6 +558,28 @@
if (result?.limitReached) {
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '子邮箱已达上限邮箱'}`);
}
const actualMailboxEmail = normalizeMailboxEmail(result?.mailboxEmail || '');
if (normalizedExpectedMailboxEmail && actualMailboxEmail && actualMailboxEmail !== normalizedExpectedMailboxEmail) {
if (allowLoginWhenOnLoginPage) {
await addLog(
`2925:当前邮箱页显示账号 ${actualMailboxEmail},与目标账号 ${normalizedExpectedMailboxEmail} 不一致,准备登出当前账号并登录目标账号。`,
'warn'
);
return ensureMail2925MailboxSession({
accountId: account?.id || accountId || null,
forceRelogin: true,
allowLoginWhenOnLoginPage: true,
expectedMailboxEmail: normalizedExpectedMailboxEmail,
actionLabel,
});
}
await failMailboxSession(
`2925${actionLabel}失败,当前邮箱页显示账号 ${actualMailboxEmail},与目标账号 ${normalizedExpectedMailboxEmail} 不一致,且当前未启用 2925 账号池。`
);
}
if (normalizedExpectedMailboxEmail && !actualMailboxEmail && result?.currentView === 'mailbox') {
await addLog('2925:未能识别当前邮箱页顶部邮箱地址,已跳过邮箱一致性校验。', 'warn');
}
if (!result?.loggedIn) {
await failMailboxSession(`2925${actionLabel}失败,登录后仍未进入收件箱。`);
}
@@ -613,6 +647,7 @@
});
if (!nextAccount) {
await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
if (typeof requestStop === 'function') {
-6
View File
@@ -200,12 +200,6 @@
break;
}
if (effect.restartCurrentStep) {
await addLog(`步骤 9${getStep8EffectLabel(effect)},准备重新定位“继续”按钮并重试...`, 'warn');
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
continue;
}
if (round >= STEP8_MAX_ROUNDS) {
throw new Error(`步骤 9:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`);
}
+15
View File
@@ -24,6 +24,20 @@
throwIfStopped,
} = deps;
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
if (accountEmail) {
return accountEmail;
}
}
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
}
async function focusOrOpenMailTab(mail) {
const alive = await isTabAlive(mail.source);
if (alive) {
@@ -111,6 +125,7 @@
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
} else {
+2 -1
View File
@@ -159,7 +159,8 @@
source: 'background',
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword },
}, {
timeoutMs: 30000,
timeoutMs: 125000,
responseTimeoutMs: 125000,
retryDelayMs: 700,
logMessage: '步骤 10:CPA 面板通信未就绪,正在等待页面恢复...',
});
+9 -1
View File
@@ -47,7 +47,15 @@
return true;
}
return /(?:认证失败|回调 URL 提交失败):\s*/i.test(text);
if (/请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(text)) {
return true;
}
if (/bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out/i.test(text)) {
return true;
}
return /(?:认证失败|回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[:]?\s*/i.test(text);
}
return {
+81 -2
View File
@@ -515,8 +515,9 @@ function detectMail2925ViewState() {
return { view: 'limit', limitMessage };
}
if (findMailItems().length > 0) {
return { view: 'mailbox', limitMessage: '' };
const mailboxEmail = getMail2925DisplayedMailboxEmail();
if (findMailItems().length > 0 || mailboxEmail) {
return { view: 'mailbox', limitMessage: '', mailboxEmail };
}
if (findMail2925LoginPasswordInput() && findMail2925LoginEmailInput()) {
@@ -531,6 +532,62 @@ function detectMail2925ViewState() {
return { view: 'unknown', limitMessage: '' };
}
function getMail2925DisplayedMailboxEmail() {
const directSelectors = [
'[class*="user"] [class*="mail"]',
'[class*="user"] [class*="email"]',
'[class*="account"] [class*="mail"]',
'[class*="account"] [class*="email"]',
'[class*="header"] [class*="mail"]',
'[class*="header"] [class*="email"]',
];
for (const selector of directSelectors) {
const candidates = document.querySelectorAll(selector);
for (const candidate of candidates) {
if (!isVisibleNode(candidate) || isMailItemNode(candidate)) {
continue;
}
const email = extractEmails(candidate.textContent || candidate.innerText || '')[0] || '';
if (email) {
return email;
}
}
}
const topCandidates = Array.from(document.querySelectorAll('body *'))
.filter((node) => {
if (!isVisibleNode(node) || isMailItemNode(node)) {
return false;
}
const rect = typeof node.getBoundingClientRect === 'function'
? node.getBoundingClientRect()
: null;
if (!rect) return false;
return rect.top >= 0 && rect.top <= Math.max(window.innerHeight * 0.35, 280);
})
.map((node) => {
const email = extractEmails(node.textContent || node.innerText || '')[0] || '';
return { node, email };
})
.filter((entry) => entry.email);
if (!topCandidates.length) {
return '';
}
topCandidates.sort((left, right) => {
const leftRect = left.node.getBoundingClientRect();
const rightRect = right.node.getBoundingClientRect();
if (leftRect.top !== rightRect.top) {
return leftRect.top - rightRect.top;
}
return leftRect.left - rightRect.left;
});
return topCandidates[0]?.email || '';
}
function isCheckboxChecked(node) {
const checkbox = node?.matches?.('input[type="checkbox"], [role="checkbox"]')
? node
@@ -873,6 +930,7 @@ async function ensureMail2925Session(payload = {}) {
const email = String(payload?.email || '').trim();
const password = String(payload?.password || '');
const forceLogin = Boolean(payload?.forceLogin);
const allowLoginWhenOnLoginPage = payload?.allowLoginWhenOnLoginPage !== false;
log(`步骤 0:2925 登录态检查开始,当前地址 ${location.href}forceLogin=${forceLogin ? 'true' : 'false'}`, 'info');
for (let attempt = 0; attempt < 10; attempt += 1) {
@@ -893,9 +951,19 @@ async function ensureMail2925Session(payload = {}) {
ok: true,
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: currentState.mailboxEmail || '',
};
}
if (currentState.view === 'login') {
if (!forceLogin && !allowLoginWhenOnLoginPage) {
return {
ok: false,
loggedIn: false,
currentView: 'login',
requiresLogin: true,
mailboxEmail: '',
};
}
break;
}
await sleep(500);
@@ -908,6 +976,7 @@ async function ensureMail2925Session(payload = {}) {
ok: true,
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: loginState.mailboxEmail || '',
};
}
if (loginState.view === 'limit') {
@@ -919,6 +988,15 @@ async function ensureMail2925Session(payload = {}) {
limitMessage: loginState.limitMessage,
};
}
if (!forceLogin && !allowLoginWhenOnLoginPage && loginState.view === 'login') {
return {
ok: false,
loggedIn: false,
currentView: 'login',
requiresLogin: true,
mailboxEmail: '',
};
}
const emailInput = findMail2925LoginEmailInput();
const passwordInput = findMail2925LoginPasswordInput();
@@ -951,6 +1029,7 @@ async function ensureMail2925Session(payload = {}) {
loggedIn: true,
currentView: 'mailbox',
usedCredentials: true,
mailboxEmail: finalState.mailboxEmail || getMail2925DisplayedMailboxEmail() || '',
};
}
+220 -10
View File
@@ -385,13 +385,82 @@ function getSignupEntryStateSummary(snapshot = inspectSignupEntryState()) {
}
function getSignupEntryDiagnostics() {
const view = typeof window !== 'undefined' ? window : globalThis;
const safeGetComputedStyle = (el) => {
if (!el || typeof view?.getComputedStyle !== 'function') {
return null;
}
try {
return view.getComputedStyle(el);
} catch {
return null;
}
};
const buildRectSummary = (el) => {
const rect = typeof el?.getBoundingClientRect === 'function'
? el.getBoundingClientRect()
: null;
return rect
? {
width: Math.round(rect.width || 0),
height: Math.round(rect.height || 0),
}
: null;
};
const buildVisibilityMeta = (el) => {
const style = safeGetComputedStyle(el);
return {
className: String(el?.className || '').slice(0, 200),
hidden: Boolean(el?.hidden),
ariaHidden: el?.getAttribute?.('aria-hidden') || '',
inert: typeof el?.hasAttribute === 'function' ? el.hasAttribute('inert') : false,
display: style?.display || '',
visibility: style?.visibility || '',
opacity: style?.opacity || '',
pointerEvents: style?.pointerEvents || '',
};
};
const findBlockingAncestor = (el) => {
let current = el?.parentElement || null;
while (current) {
const style = safeGetComputedStyle(current);
const rect = buildRectSummary(current);
const hidden = Boolean(current.hidden);
const ariaHidden = current.getAttribute?.('aria-hidden') || '';
const inert = typeof current.hasAttribute === 'function' ? current.hasAttribute('inert') : false;
const blockedByStyle = Boolean(
style
&& (
style.display === 'none'
|| style.visibility === 'hidden'
|| style.opacity === '0'
|| style.pointerEvents === 'none'
)
);
const blockedByRect = Boolean(rect && (rect.width === 0 || rect.height === 0));
if (hidden || ariaHidden === 'true' || inert || blockedByStyle || blockedByRect) {
return {
tag: (current.tagName || '').toLowerCase(),
id: current.id || '',
className: String(current.className || '').slice(0, 200),
hidden,
ariaHidden,
inert,
display: style?.display || '',
visibility: style?.visibility || '',
opacity: style?.opacity || '',
pointerEvents: style?.pointerEvents || '',
rect,
};
}
current = current.parentElement;
}
return null;
};
const actionCandidates = document.querySelectorAll(
'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
);
const allActions = Array.from(actionCandidates).map((el) => {
const rect = typeof el?.getBoundingClientRect === 'function'
? el.getBoundingClientRect()
: null;
const text = getActionText(el);
return {
tag: (el.tagName || '').toLowerCase(),
@@ -399,12 +468,7 @@ function getSignupEntryDiagnostics() {
text: text.slice(0, 80),
visible: isVisibleElement(el),
enabled: isActionEnabled(el),
rect: rect
? {
width: Math.round(rect.width || 0),
height: Math.round(rect.height || 0),
}
: null,
rect: buildRectSummary(el),
};
});
const visibleActions = Array.from(actionCandidates)
@@ -417,7 +481,20 @@ function getSignupEntryDiagnostics() {
enabled: isActionEnabled(el),
}))
.filter((item) => item.text);
const signupLikeActions = allActions
const signupLikeActions = Array.from(actionCandidates)
.map((el) => {
const text = getActionText(el);
return {
tag: (el.tagName || '').toLowerCase(),
type: el.getAttribute?.('type') || '',
text: text.slice(0, 80),
visible: isVisibleElement(el),
enabled: isActionEnabled(el),
rect: buildRectSummary(el),
...buildVisibilityMeta(el),
blockingAncestor: findBlockingAncestor(el),
};
})
.filter((item) => item.text && SIGNUP_ENTRY_TRIGGER_PATTERN.test(item.text))
.slice(0, 12);
@@ -425,15 +502,133 @@ function getSignupEntryDiagnostics() {
url: location.href,
title: document.title || '',
readyState: document.readyState || '',
viewport: {
innerWidth: Math.round(Number(view?.innerWidth) || 0),
innerHeight: Math.round(Number(view?.innerHeight) || 0),
outerWidth: Math.round(Number(view?.outerWidth) || 0),
outerHeight: Math.round(Number(view?.outerHeight) || 0),
devicePixelRatio: Number(view?.devicePixelRatio) || 0,
},
hasEmailInput: Boolean(getSignupEmailInput()),
hasPasswordInput: Boolean(getSignupPasswordInput()),
bodyContainsSignupText: SIGNUP_ENTRY_TRIGGER_PATTERN.test(getPageTextSnapshot()),
signupLikeActionCounts: {
total: signupLikeActions.length,
visible: signupLikeActions.filter((item) => item.visible).length,
hidden: signupLikeActions.filter((item) => !item.visible).length,
},
signupLikeActions,
visibleActions,
bodyTextPreview: getPageTextSnapshot().slice(0, 240),
};
}
function getSignupPasswordDiagnostics() {
const view = typeof window !== 'undefined' ? window : globalThis;
const safeGetComputedStyle = (el) => {
if (!el || typeof view?.getComputedStyle !== 'function') {
return null;
}
try {
return view.getComputedStyle(el);
} catch {
return null;
}
};
const buildRectSummary = (el) => {
const rect = typeof el?.getBoundingClientRect === 'function'
? el.getBoundingClientRect()
: null;
return rect
? {
width: Math.round(rect.width || 0),
height: Math.round(rect.height || 0),
}
: null;
};
const buildInputSummary = (el) => {
const style = safeGetComputedStyle(el);
return {
tag: (el?.tagName || '').toLowerCase(),
type: el?.getAttribute?.('type') || el?.type || '',
name: el?.getAttribute?.('name') || el?.name || '',
id: el?.id || '',
autocomplete: el?.getAttribute?.('autocomplete') || '',
placeholder: String(el?.getAttribute?.('placeholder') || '').slice(0, 80),
visible: isVisibleElement(el),
enabled: isActionEnabled(el),
valueLength: String(el?.value || '').length,
rect: buildRectSummary(el),
className: String(el?.className || '').slice(0, 200),
display: style?.display || '',
visibility: style?.visibility || '',
opacity: style?.opacity || '',
pointerEvents: style?.pointerEvents || '',
formAction: el?.form?.action || '',
};
};
const buildActionSummary = (el) => {
const style = safeGetComputedStyle(el);
return {
tag: (el?.tagName || '').toLowerCase(),
type: el?.getAttribute?.('type') || el?.type || '',
role: el?.getAttribute?.('role') || '',
text: getActionText(el).slice(0, 120),
visible: isVisibleElement(el),
enabled: isActionEnabled(el),
rect: buildRectSummary(el),
className: String(el?.className || '').slice(0, 200),
display: style?.display || '',
visibility: style?.visibility || '',
opacity: style?.opacity || '',
pointerEvents: style?.pointerEvents || '',
dataDdActionName: el?.getAttribute?.('data-dd-action-name') || '',
formAction: el?.form?.action || '',
};
};
const passwordInputs = Array.from(document.querySelectorAll(
'input[type="password"], input[name*="password" i], input[autocomplete="new-password"], input[autocomplete="current-password"]'
))
.map(buildInputSummary)
.slice(0, 8);
const actionCandidates = Array.from(document.querySelectorAll(
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
))
.map(buildActionSummary)
.filter((item) => item.text)
.slice(0, 16);
const visibleActions = actionCandidates.filter((item) => item.visible).slice(0, 12);
const submitButton = getSignupPasswordSubmitButton({ allowDisabled: true });
const oneTimeCodeTrigger = findOneTimeCodeLoginTrigger();
const retryState = getSignupPasswordTimeoutErrorPageState();
return {
url: location.href,
title: document.title || '',
readyState: document.readyState || '',
displayedEmail: getSignupPasswordDisplayedEmail(),
hasVisiblePasswordInput: Boolean(getSignupPasswordInput()),
passwordInputCount: passwordInputs.length,
visiblePasswordInputCount: passwordInputs.filter((item) => item.visible).length,
passwordInputs,
submitButton: submitButton ? buildActionSummary(submitButton) : null,
oneTimeCodeTrigger: oneTimeCodeTrigger ? buildActionSummary(oneTimeCodeTrigger) : null,
retryPage: Boolean(retryState),
retryEnabled: Boolean(retryState?.retryEnabled),
userAlreadyExistsBlocked: Boolean(retryState?.userAlreadyExistsBlocked),
visibleActions,
bodyTextPreview: getPageTextSnapshot().slice(0, 240),
};
}
function logSignupPasswordDiagnostics(context, level = 'warn') {
try {
log(`${context}:密码页诊断快照:${JSON.stringify(getSignupPasswordDiagnostics())}`, level);
} catch (error) {
console.warn('[MultiPage:signup-page] failed to build signup password diagnostics:', error?.message || error);
}
}
async function waitForSignupEntryState(options = {}) {
const {
timeout = 15000,
@@ -620,6 +815,10 @@ async function step3_fillEmailPassword(payload) {
snapshot = inspectSignupEntryState();
}
if (snapshot.state !== 'password_page' || !snapshot.passwordInput) {
logSignupPasswordDiagnostics('步骤 3:未能识别可填写的密码输入框');
}
if (snapshot.state !== 'password_page' || !snapshot.passwordInput) {
throw new Error('在密码页未找到密码输入框。URL: ' + location.href);
}
@@ -635,6 +834,12 @@ async function step3_fillEmailPassword(payload) {
|| getSignupPasswordSubmitButton({ allowDisabled: true })
|| await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
if (!submitBtn) {
logSignupPasswordDiagnostics('步骤 3:未找到可提交的密码页按钮');
} else if (typeof findOneTimeCodeLoginTrigger === 'function' && findOneTimeCodeLoginTrigger()) {
logSignupPasswordDiagnostics('步骤 3:当前密码页同时存在一次性验证码入口', 'info');
}
// Report complete BEFORE submit, because submit causes page navigation
// which kills the content script connection
const signupVerificationRequestedAt = submitBtn ? Date.now() : null;
@@ -1640,6 +1845,7 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
const start = Date.now();
let recoveryRound = 0;
const maxRecoveryRounds = 3;
let passwordPageDiagnosticsLogged = false;
while (Date.now() - start < timeout && recoveryRound < maxRecoveryRounds) {
throwIfStopped();
@@ -1678,6 +1884,10 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
}
if (snapshot.state === 'password') {
if (!passwordPageDiagnosticsLogged) {
passwordPageDiagnosticsLogged = true;
logSignupPasswordDiagnostics(`${prepareLogLabel}:页面仍停留在密码页`);
}
if (!password) {
throw new Error('当前回到了密码页,但没有可用密码,无法自动重新提交。');
}
+269 -41
View File
@@ -19,7 +19,10 @@
// <div class="OAuthPage-module__callbackSection___8kA31">
// <input class="input" placeholder="http://localhost:1455/auth/callback?code=...&state=...">
// <button class="btn btn-secondary btn-sm"><span>提交回调 URL</span></button>
// <div class="status-badge success">回调 URL 已提交,等待认证中...</div>
// <div class="status-badge error">回调 URL 提交失败: ...</div>
// </div>
// <div class="status-badge">等待认证中... / 认证成功! / 认证失败: ...</div>
// </div>
// </div>
@@ -192,19 +195,19 @@ function isLocalhostOAuthCallbackUrl(rawUrl) {
function getStatusBadgeSelectors() {
return [
'#root > div > div > div > main > div > div > div > div > div:nth-child(1) > div > div.OAuthPage-module__cardContent___1sXLA > div.status-badge',
'#root .OAuthPage-module__cardContent___1sXLA > .status-badge',
'.OAuthPage-module__cardContent___1sXLA > .status-badge',
'#root .OAuthPage-module__cardContent___1sXLA .status-badge',
'[class*="cardContent"] .status-badge',
'.status-badge',
];
}
function getStatusBadgeEntries() {
const searchRoot = findCodexOAuthCard() || document;
const seen = new Set();
const entries = [];
for (const selector of getStatusBadgeSelectors()) {
const candidates = document.querySelectorAll(selector);
const candidates = searchRoot.querySelectorAll(selector);
for (const candidate of candidates) {
if (seen.has(candidate)) continue;
seen.add(candidate);
@@ -238,21 +241,63 @@ function normalizeStep9StatusText(statusText) {
}
function isOAuthCallbackTimeoutFailure(statusText) {
return /认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(statusText || '');
return /(?:认证失败\s*[:]?\s*)?(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded|OAuth flow timed out)/i.test(statusText || '');
}
function getStep10StatusBadgeLocation(element) {
if (element?.closest?.('[class*="callbackSection"]')) {
return 'callback';
}
if (element?.closest?.('[class*="cardContent"]')) {
return 'main';
}
return 'page';
}
function isStep10CallbackSubmittedStatus(statusText) {
const text = normalizeStep9StatusText(statusText);
return /回调\s*url\s*已提交.*等待认证中/i.test(text)
|| /callback\s*url\s*submitted.*waiting/i.test(text);
}
function isStep10CallbackFailureText(statusText) {
const text = normalizeStep9StatusText(statusText);
if (!text) return false;
return /(?:回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[:,]?\s*/i.test(text)
|| /请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(text);
}
function isStep10MainWaitingStatus(statusText) {
const text = normalizeStep9StatusText(statusText);
return /等待认证中/i.test(text);
}
function isStep10MainFailureText(statusText) {
const text = normalizeStep9StatusText(statusText);
if (!text) return false;
if (/^认证失败\s*[:]?\s*/i.test(text)) return true;
return /bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out|request failed with status code \d+|timeout of \d+ms exceeded|network error|failed to fetch/i.test(text);
}
function isStep9FailureText(statusText) {
const text = normalizeStep9StatusText(statusText);
if (!text) return false;
if (isOAuthCallbackTimeoutFailure(text)) return true;
if (isStep10CallbackFailureText(text)) return true;
if (isStep10MainFailureText(text)) return true;
if (typeof isRecoverableStep9AuthFailure === 'function' && isRecoverableStep9AuthFailure(text)) {
return true;
}
return /回调\s*url\s*提交失败|callback\s*url\s*submit\s*failed|oauth flow is not pending/i.test(text);
return /callback\s*url\s*submit\s*failed|oauth flow is not pending/i.test(text);
}
function isStep9SuccessStatus(statusText) {
return STEP9_SUCCESS_STATUSES.has(normalizeStep9StatusText(statusText));
const text = normalizeStep9StatusText(statusText);
if (!text) return false;
return STEP9_SUCCESS_STATUSES.has(text)
|| /^认证成功[!]?$/i.test(text)
|| /^Authentication successful!?$/i.test(text)
|| /^Аутентификация успешна!?$/i.test(text);
}
function isStep9SuccessLikeStatus(statusText) {
@@ -340,6 +385,7 @@ function createStep9Entry(candidate, selector) {
return {
element: candidate,
selector,
location: getStep10StatusBadgeLocation(candidate),
visible: isVisibleElement(candidate),
text: normalizeStep9StatusText(candidate?.textContent || ''),
className,
@@ -364,36 +410,72 @@ function getStep9PageErrorSelectors() {
}
function getStep9PageErrorEntries() {
const cardRoot = findCodexOAuthCard();
const searchRoots = [cardRoot, document].filter(Boolean);
const seen = new Set();
const entries = [];
for (const selector of getStep9PageErrorSelectors()) {
const candidates = document.querySelectorAll(selector);
for (const candidate of candidates) {
if (seen.has(candidate)) continue;
seen.add(candidate);
if (!isVisibleElement(candidate)) continue;
for (const root of searchRoots) {
for (const selector of getStep9PageErrorSelectors()) {
const candidates = root.querySelectorAll(selector);
for (const candidate of candidates) {
if (seen.has(candidate)) continue;
seen.add(candidate);
if (!isVisibleElement(candidate)) continue;
const entry = createStep9Entry(candidate, selector);
if (!isStep9FailureText(entry.text)) continue;
entries.push(entry);
const entry = createStep9Entry(candidate, selector);
if (/\bstatus-badge\b/i.test(entry.className)) continue;
if (!isStep9FailureText(entry.text)) continue;
entries.push(entry);
}
}
}
return entries;
}
function formatStep10StatusSummaryValue(text, emptyText = '无') {
return text ? `"${getInlineTextSnippet(text, 80)}"` : emptyText;
}
function isStep10BrowserSwitchRequiredConflict(diagnostics = {}) {
return Boolean(diagnostics?.hasExactSuccessVisibleBadge)
&& /请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(String(diagnostics?.callbackFailureText || ''));
}
function getStep10BrowserSwitchRequiredMessage(diagnostics = {}) {
const callbackFailureText = normalizeStep9StatusText(diagnostics?.callbackFailureText || '');
return [
'检测到 CPA 页面同时显示“认证成功”和“回调 URL 提交失败: 请更新CLI Proxy API或检查连接”。',
'这类冲突状态通常通过更换浏览器可以解决,请更换浏览器后重新进行注册登录。',
callbackFailureText ? `面板原文:${callbackFailureText}` : '',
].filter(Boolean).join(' ');
}
function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSnippet = '') {
const visibleEntries = entries.filter((entry) => entry.visible);
const successLikeEntries = visibleEntries.filter((entry) => isStep9SuccessLikeStatus(entry.text));
const exactSuccessEntries = visibleEntries.filter((entry) => isStep9SuccessStatus(entry.text) && !entry.hasErrorVisualSignal);
const failureEntries = visibleEntries.filter((entry) => isStep9FailureText(entry.text));
const callbackEntries = visibleEntries.filter((entry) => entry.location === 'callback');
const mainEntries = visibleEntries.filter((entry) => entry.location === 'main');
const successLikeEntries = mainEntries.filter((entry) => isStep9SuccessLikeStatus(entry.text));
const exactSuccessEntries = mainEntries.filter((entry) => isStep9SuccessStatus(entry.text) && !entry.hasErrorVisualSignal);
const callbackSubmittedEntries = callbackEntries.filter((entry) => isStep10CallbackSubmittedStatus(entry.text) && !entry.hasErrorVisualSignal);
const callbackFailureEntries = callbackEntries.filter((entry) => isStep10CallbackFailureText(entry.text));
const mainWaitingEntries = mainEntries.filter((entry) => isStep10MainWaitingStatus(entry.text) && !entry.hasErrorVisualSignal);
const mainFailureEntries = mainEntries.filter((entry) => isStep10MainFailureText(entry.text));
const failureEntries = [...callbackFailureEntries, ...mainFailureEntries];
const errorStyledEntries = visibleEntries.filter((entry) => entry.hasErrorVisualSignal);
const allFailureEntries = [...failureEntries, ...pageErrorEntries];
const decisiveFailureEntry = allFailureEntries[0] || null;
const selectedEntry = decisiveFailureEntry || exactSuccessEntries[0] || visibleEntries[0] || null;
const selectedEntry = decisiveFailureEntry
|| exactSuccessEntries[0]
|| callbackSubmittedEntries[0]
|| mainWaitingEntries[0]
|| visibleEntries[0]
|| null;
const selectedText = selectedEntry?.text || '';
const visibleSummary = summarizeStatusBadgeEntries(visibleEntries);
const callbackSummary = summarizeStatusBadgeEntries(callbackEntries);
const mainSummary = summarizeStatusBadgeEntries(mainEntries);
const successLikeSummary = summarizeStatusBadgeEntries(successLikeEntries);
const exactSuccessSummary = summarizeStatusBadgeEntries(exactSuccessEntries);
const failureSummary = summarizeStatusBadgeEntries(failureEntries);
@@ -406,10 +488,20 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn
selectedText,
exactSuccessText: exactSuccessEntries[0]?.text || '',
failureText: decisiveFailureEntry?.text || '',
failureSource: decisiveFailureEntry?.location || (pageErrorEntries.length ? 'page' : ''),
visibleCount: visibleEntries.length,
visibleSummary,
callbackSummary,
mainSummary,
callbackStatusText: callbackEntries[0]?.text || '',
callbackSubmittedText: callbackSubmittedEntries[0]?.text || '',
callbackFailureText: callbackFailureEntries[0]?.text || '',
mainStatusText: mainEntries[0]?.text || '',
mainWaitingText: mainWaitingEntries[0]?.text || '',
mainFailureText: mainFailureEntries[0]?.text || '',
hasSuccessLikeVisibleBadge: successLikeEntries.length > 0,
hasExactSuccessVisibleBadge: exactSuccessEntries.length > 0,
hasCallbackSubmittedBadge: callbackSubmittedEntries.length > 0,
hasFailureVisibleBadge: allFailureEntries.length > 0,
hasErrorStyledVisibleBadge: errorStyledEntries.length > 0,
successLikeSummary,
@@ -422,6 +514,8 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn
selectedText,
visibleCount: visibleEntries.length,
visibleSummary,
callbackSummary,
mainSummary,
successLikeSummary,
exactSuccessSummary,
failureSummary,
@@ -429,8 +523,8 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn
errorStyledSummary,
}),
summary: selectedText
? `当前聚焦状态="${getInlineTextSnippet(selectedText, 80)}";可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}`
: `当前未选中任何可见状态徽标;可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
? `当前聚焦状态=${formatStep10StatusSummaryValue(selectedText)};回调提示=${formatStep10StatusSummaryValue(callbackEntries[0]?.text || '')};主状态=${formatStep10StatusSummaryValue(mainEntries[0]?.text || '')};页面错误=${formatStep10StatusSummaryValue(pageErrorEntries[0]?.text || '')};可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}`
: `当前未选中任何可见状态;回调提示=${formatStep10StatusSummaryValue(callbackEntries[0]?.text || '')};主状态=${formatStep10StatusSummaryValue(mainEntries[0]?.text || '')};页面错误=${formatStep10StatusSummaryValue(pageErrorEntries[0]?.text || '')};可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
};
}
@@ -452,11 +546,129 @@ function getStatusBadgeText() {
return diagnostics.selectedText;
}
function extractStep10FailureDetail(statusText, sourceKind = '') {
const text = normalizeStep9StatusText(statusText);
if (!text) return '';
if (sourceKind === 'callback' || isStep10CallbackFailureText(text)) {
return text.replace(/^(?:回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[:,]?\s*/i, '').trim();
}
if (sourceKind === 'main' || isStep10MainFailureText(text)) {
return text.replace(/^认证失败\s*[:]?\s*/i, '').trim();
}
return text;
}
function explainStep10Failure(statusText, sourceKind = 'unknown') {
const rawText = normalizeStep9StatusText(statusText);
const detail = extractStep10FailureDetail(rawText, sourceKind) || rawText;
const phaseLabel = sourceKind === 'callback'
? '回调提交阶段'
: sourceKind === 'main'
? '认证结果阶段'
: '页面状态阶段';
const rules = [
{
code: 'callback_submit_api_unavailable',
pattern: /请更新\s*cli\s*proxy\s*api\s*或检查连接/i,
message: 'CPA 面板无法把回调提交给后台,通常是 CLI Proxy API 版本过旧、管理接口未启动,或当前面板与后端连接异常。',
},
{
code: 'oauth_state_expired',
pattern: /unknown or expired state/i,
message: '当前 OAuth 会话在 CPA 中已不存在或已过期,通常是使用了旧回调链接、刷新过新的授权链接后仍提交旧链接,或 CPA 刚重启过。',
},
{
code: 'oauth_not_pending',
pattern: /oauth flow is not pending/i,
message: '当前 OAuth 会话已经不在等待状态,通常是重复提交、提交过慢,或这轮认证此前已经结束。',
},
{
code: 'callback_state_invalid',
pattern: /invalid state|state is required|missing_state/i,
message: '回调链接里的 state 缺失或无效,通常是复制了不完整的 localhost 回调链接,或提交了不属于这一轮的旧链接。',
},
{
code: 'callback_missing_result',
pattern: /code or error is required/i,
message: '回调链接里既没有授权码,也没有错误信息,通常是复制的 localhost 回调链接不完整。',
},
{
code: 'callback_invalid_url',
pattern: /invalid redirect_url/i,
message: '提交给 CPA 的回调链接格式无法解析,通常是粘贴内容不完整、带了多余字符,或并不是 localhost OAuth 回调地址。',
},
{
code: 'callback_provider_mismatch',
pattern: /provider does not match state/i,
message: '这条回调不属于当前这次 Codex OAuth,会话与回调来源对不上,通常是混用了其他轮次或其他提供方的回调。',
},
{
code: 'callback_persist_failed',
pattern: /failed to persist oauth callback/i,
message: 'CPA 已收到回调,但无法把回调结果写入本地缓存文件,通常是认证目录权限、磁盘或运行环境异常。',
},
{
code: 'oauth_bad_request',
pattern: /^bad request$/i,
message: 'CPA 已收到回调,但 OpenAI OAuth 回调本身返回了错误。常见于用户取消授权、请求过期,或这条回调已经失效。',
},
{
code: 'oauth_state_mismatch',
pattern: /state code error/i,
message: 'CPA 校验到回调里的 state 与当前 OAuth 会话不一致,通常是步骤 1 已刷新过新的授权链接,但步骤 10 仍提交旧回调。',
},
{
code: 'oauth_code_exchange_failed',
pattern: /failed to exchange authorization code for tokens/i,
message: 'CPA 已收到授权码,但向 OpenAI 交换令牌失败。常见于 CPA 到 OpenAI 的网络或代理异常,或授权码已过期。',
},
{
code: 'oauth_token_save_failed',
pattern: /failed to save authentication tokens/i,
message: 'CPA 已完成认证,但保存认证文件失败。常见于认证目录权限、磁盘写入,或 post-auth hook 异常。',
},
{
code: 'oauth_callback_timeout',
pattern: /timeout waiting for oauth callback|oauth flow timed out/i,
message: 'CPA 长时间没有把这轮 OAuth 流程走完。常见于提交太晚、面板轮询异常,或后端状态没有及时刷新。',
},
{
code: 'oauth_http_timeout',
pattern: /timeout of \d+ms exceeded/i,
message: 'CPA 面板在请求后台接口时超时,通常是 CLI Proxy API 响应过慢、接口未启动,或网络连接不稳定。',
},
{
code: 'oauth_http_status_error',
pattern: /request failed with status code \d+/i,
message: 'CPA 面板请求后台接口时收到了异常 HTTP 状态码,通常是接口异常、反向代理配置错误,或当前会话已失效。',
},
{
code: 'oauth_network_error',
pattern: /network error|failed to fetch/i,
message: 'CPA 面板与后台通信失败,通常是网络不通、管理接口未启动,或浏览器当前连接已断开。',
},
];
const matchedRule = rules.find((rule) => rule.pattern.test(detail) || rule.pattern.test(rawText));
const message = matchedRule
? matchedRule.message
: `CPA 在${phaseLabel}返回了未归类的失败,请结合面板原文进一步排查。`;
return {
code: matchedRule?.code || 'oauth_unknown_failure',
phaseLabel,
rawText,
detail,
userMessage: `CPA 在${phaseLabel}返回失败:${message} 面板原文:${rawText}`,
};
}
async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS) {
const start = Date.now();
let lastDiagnosticsSignature = '';
let lastHeartbeatLoggedAt = 0;
let lastSuccessLikeMismatchSignature = '';
let lastCallbackSubmittedSignature = '';
let lastSuccessFailureConflictSignature = '';
while (Date.now() - start < timeout) {
@@ -475,26 +687,28 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
console.log(LOG_PREFIX, '[Step 9] still waiting for success badge', diagnostics);
}
if (diagnostics.hasSuccessLikeVisibleBadge && !diagnostics.hasExactSuccessVisibleBadge) {
const mismatchSignature = JSON.stringify({
selectedText: diagnostics.selectedText,
successLikeSummary: diagnostics.successLikeSummary,
visibleSummary: diagnostics.visibleSummary,
errorStyledSummary: diagnostics.errorStyledSummary,
if (diagnostics.hasCallbackSubmittedBadge && !diagnostics.hasExactSuccessVisibleBadge) {
const callbackSubmittedSignature = JSON.stringify({
callbackStatusText: diagnostics.callbackStatusText,
mainStatusText: diagnostics.mainStatusText,
});
if (mismatchSignature !== lastSuccessLikeMismatchSignature) {
lastSuccessLikeMismatchSignature = mismatchSignature;
const errorStyledSuffix = diagnostics.hasErrorStyledVisibleBadge
? `;错误样式徽标:${diagnostics.errorStyledSummary}`
: '';
if (callbackSubmittedSignature !== lastCallbackSubmittedSignature) {
lastCallbackSubmittedSignature = callbackSubmittedSignature;
log(
`步骤 10检测到“认证成功”相关徽标,但未命中精确成功条件。当前聚焦="${getInlineTextSnippet(diagnostics.selectedText || '(空)', 80)}";成功相关徽标:${diagnostics.successLikeSummary}${errorStyledSuffix}`,
'warn'
`步骤 10CPA 已接受 localhost 回调,正在等待后台完成认证。回调提示=${formatStep10StatusSummaryValue(diagnostics.callbackStatusText)};主状态=${formatStep10StatusSummaryValue(diagnostics.mainStatusText)}`,
'info'
);
console.warn(LOG_PREFIX, '[Step 9] success-like badge detected without exact match', diagnostics);
console.info(LOG_PREFIX, '[Step 9] callback accepted and waiting for auth completion', diagnostics);
}
}
if (isStep10BrowserSwitchRequiredConflict(diagnostics)) {
const browserSwitchMessage = getStep10BrowserSwitchRequiredMessage(diagnostics);
log(`步骤 10${browserSwitchMessage}`, 'error');
console.error(LOG_PREFIX, '[Step 9] browser-switch conflict detected', diagnostics);
throw new Error(`BROWSER_SWITCH_REQUIRED::${browserSwitchMessage}`);
}
if (diagnostics.hasExactSuccessVisibleBadge && diagnostics.hasFailureVisibleBadge) {
const conflictSignature = JSON.stringify({
exactSuccessSummary: diagnostics.exactSuccessSummary,
@@ -515,10 +729,11 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
}
if (diagnostics.failureText) {
const failureExplanation = explainStep10Failure(diagnostics.failureText, diagnostics.failureSource || 'unknown');
if (isOAuthCallbackTimeoutFailure(diagnostics.failureText)) {
throw new Error(`STEP9_OAUTH_TIMEOUT::${diagnostics.failureText}`);
throw new Error(`STEP9_OAUTH_TIMEOUT::${failureExplanation.userMessage}`);
}
throw new Error(`STEP9_OAUTH_RETRY::${diagnostics.failureText}`);
throw new Error(`STEP9_OAUTH_RETRY::${failureExplanation.userMessage}`);
}
if (diagnostics.exactSuccessText) {
return diagnostics.exactSuccessText;
@@ -530,10 +745,18 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
const finalText = finalDiagnostics.failureText || finalDiagnostics.selectedText;
const diagnosticsSuffix = ` 当前诊断:${finalDiagnostics.summary}`;
if (isOAuthCallbackTimeoutFailure(finalText)) {
throw new Error(`STEP9_OAUTH_TIMEOUT::${finalText}${diagnosticsSuffix}`);
const failureExplanation = explainStep10Failure(finalText, finalDiagnostics.failureSource || 'main');
throw new Error(`STEP9_OAUTH_TIMEOUT::${failureExplanation.userMessage}${diagnosticsSuffix}`);
}
if (isStep9FailureText(finalText)) {
throw new Error(`STEP9_OAUTH_RETRY::${finalText}${diagnosticsSuffix}`);
const failureExplanation = explainStep10Failure(finalText, finalDiagnostics.failureSource || 'unknown');
throw new Error(`STEP9_OAUTH_RETRY::${failureExplanation.userMessage}${diagnosticsSuffix}`);
}
if (finalDiagnostics.hasCallbackSubmittedBadge || finalDiagnostics.mainWaitingText) {
throw new Error(
'STEP9_OAUTH_TIMEOUT::CPA 已接受回调,但 120 秒内仍未进入认证成功状态。通常是 CPA 后台处理过慢、面板轮询异常,或 CPA 到 OpenAI 的网络/代理存在问题。'
+ diagnosticsSuffix
);
}
throw new Error(finalText
? `CPA 面板状态未进入成功状态,当前为“${finalText}”。${diagnosticsSuffix}`
@@ -583,6 +806,11 @@ function findCodexOAuthHeader() {
}) || null;
}
function findCodexOAuthCard() {
const header = findCodexOAuthHeader();
return header?.closest('.card, [class*="card"]') || header || null;
}
function findOAuthCardLoginButton(header) {
const card = header?.closest('.card, [class*="card"]') || header?.parentElement || document;
const candidates = card.querySelectorAll('button.btn.btn-primary, button.btn-primary, button.btn');
+190
View File
@@ -0,0 +1,190 @@
本教程用于说明相关项目地址、扩展更新方式,以及 `Clash Verge``🔁 非港轮询` 配置方法。
## 适用场景
- 需要拉取并部署 `cpa``sub2api` 项目
- 已经安装本扩展,想更新到最新版本
- 想用更方便的方式长期同步扩展更新
- 需要在 `Clash Verge` 中启用 `🔁 非港轮询`
## 准备内容
- 可以访问 `GitHub`
- 已安装好的扩展文件夹
- 可以打开浏览器的 `扩展程序管理` 页面
- 如需部署 `cpa`,部署环境必须可以访问 `OpenAI`
- 已安装 `Clash Verge`,并已导入可用订阅
## 操作步骤
### 第一部分:相关项目地址与部署说明
1. 查看项目地址
`cpa` 项目地址:<https://github.com/router-for-me/CLIProxyAPI>
`sub2api` 项目地址:<https://github.com/Wei-Shaw/sub2api>
2. 拉取项目到本地
先将你需要的项目拉取到本地目录。
可以使用 `git clone`,也可以直接下载项目压缩包后解压。
3. 让 AI 直接协助部署
部署教程这里不单独展开。
将项目拉取下来后,直接让 AI 帮你部署即可。
4. 确认 `cpa` 的部署环境
如果你部署的是 `cpa`,部署所在环境必须可以访问 `OpenAI`
否则可能会出现第十步显示认证成功,但实际上没有生成认证文件的情况。
### 第二部分:更新扩展
1. 使用 `GitHub Desktop` 更新
先安装 `GitHub Desktop`
把当前扩展仓库交给 `GitHub Desktop` 管理后,之后就可以随时更新。
`GitHub Desktop` 的具体使用方法本教程不展开,需要时可直接询问豆包。
2. 使用 `git pull` 更新(最方便,最推荐)
先在本地安装 `git`
打开终端后执行 `cd 扩展文件夹路径` 进入当前扩展目录。
接着执行 `git pull` 拉取最新更新。
这是最方便、最推荐的更新方式。
3. 手动下载最新版本覆盖更新
点击左上角的版本号或 `更新` 按钮。
下载最新版本到本地后,直接覆盖现有扩展文件夹即可。
4. 更新完成后重新加载扩展
不论使用哪种更新方式,更新完成后都必须打开浏览器的 `扩展程序管理` 页面。
找到该扩展后,手动点击一次 `重新加载`
这一步一定要做,否则浏览器可能仍在使用旧版本。
### 第三部分:配置 `Clash Verge` 的 `🔁 非港轮询`
#### 第一步:添加扩展脚本
1. 打开 `Clash Verge`,进入左侧的 `订阅``Profiles`)界面。
2. 在右上角或对应位置找到并双击打开 `全局扩展脚本``Merge/Script`)。
3. 将里面的内容全部清空,替换为下方脚本代码。
4. 点击保存,使用右上角保存按钮或按 `Ctrl+S`
💻 脚本代码(如果遇到格式错误,可让豆包帮你修复后再粘贴)
```javascript
function uniqPrepend(arr, items) {
if (!Array.isArray(arr)) arr = [];
for (var i = items.length - 1; i >= 0; i--) {
var item = items[i];
var exists = false;
for (var j = 0; j < arr.length; j++) {
if (arr[j] === item) {
exists = true;
break;
}
}
if (!exists) arr.unshift(item);
}
return arr;
}
function upsertGroup(groups, group) {
for (var i = 0; i < groups.length; i++) {
if (groups[i] && groups[i].name === group.name) {
groups[i] = group;
return groups;
}
}
groups.unshift(group);
return groups;
}
function main(config, profileName) {
if (!config) return config;
if (!Array.isArray(config["proxy-groups"])) {
config["proxy-groups"] = [];
}
var groups = config["proxy-groups"];
var LB_NAME = "🔁 非港轮询";
var excludeRegex =
"(?i)(" +
"香港|hong[ -]?kong|\\bhk\\b|\\bhkg\\b|🇭🇰" +
"|剩余流量|套餐到期|下次重置剩余|重置剩余|到期时间|流量重置" +
"|traffic|expire|expiration|subscription|subscribe|reset|plan" +
")";
groups = upsertGroup(groups, {
name: LB_NAME,
type: "load-balance",
strategy: "round-robin",
"include-all-proxies": true,
"exclude-filter": excludeRegex,
url: "https://www.gstatic.com/generate_204",
interval: 300,
lazy: true,
"expected-status": 204
});
var injected = false;
var entryNameRegex = /节点选择|代理|Proxy|PROXY|默认|GLOBAL|全局|选择/i;
for (var i = 0; i < groups.length; i++) {
var g = groups[i];
if (!g || g.type !== "select") continue;
if (entryNameRegex.test(g.name || "")) {
if (!Array.isArray(g.proxies)) g.proxies = [];
g.proxies = uniqPrepend(g.proxies, [LB_NAME]);
injected = true;
}
}
if (!injected) {
for (var k = 0; k < groups.length; k++) {
var g2 = groups[k];
if (g2 && g2.type === "select") {
if (!Array.isArray(g2.proxies)) g2.proxies = [];
g2.proxies = uniqPrepend(g2.proxies, [LB_NAME]);
break;
}
}
}
config["proxy-groups"] = groups;
return config;
}
```
#### 第二步:应用设置
1. 切换到左侧的 `代理``Proxies`)界面,也就是首页。
2. 在顶部的分类中,通常会看到 `节点选择``Proxy``当前节点` 之类的分组。
3. 在对应下拉框中选择 `🔁 非港轮询`
4. 确认右上角的 `代理模式``Mode`)已经设置为 `规则模式``Rule`)。
5. 确认左侧 `设置` 中的 `系统代理``System Proxy`)已经开启。
## 常见问题
### 为什么第十步显示认证成功,但没有认证文件?
这通常是因为 `cpa` 部署所在环境无法访问 `OpenAI`。请先确认部署环境可以正常访问 `OpenAI`,然后再重新执行相关认证步骤。
### 为什么更新完扩展后还是旧版本?
如果你只是替换了本地文件,但没有去浏览器的 `扩展程序管理` 页面点击 `重新加载`,浏览器不会立即启用新版本。请更新完成后手动重新加载一次该扩展。
### `git pull` 提示不是 Git 仓库怎么办?
这通常说明你进入的不是正确目录,或者当前文件夹不是通过 `git` 管理的版本。此时可以改用 `GitHub Desktop`,或者直接使用手动下载覆盖的方式更新。
### 为什么没有看到 `🔁 非港轮询`?
请先确认脚本已经完整粘贴并保存,然后回到 `代理` 页面重新查看。若仍未出现,请继续确认当前订阅可用、`代理模式``规则模式`,并且 `系统代理` 已开启。
## 注意事项
- 不论使用哪种方式更新扩展,更新完成后都必须在浏览器的 `扩展程序管理` 页面重新加载一次该扩展。
- 如需部署 `cpa`,请先确认部署环境可以访问 `OpenAI`,否则可能出现第十步认证成功但没有认证文件的问题。
- 如果你长期更新扩展,优先使用 `git pull`,这是最方便、最推荐的方式。
- 使用手动覆盖更新时,请直接覆盖当前扩展文件夹,避免同时保留多份不同版本的目录。
- 在 `Clash Verge` 中粘贴脚本时,请先清空旧内容,再完整粘贴新代码并保存。
+1 -1
View File
@@ -1,6 +1,6 @@
{
"manifest_version": 3,
"name": "多页面自动化",
"name": "codex-oauth-automation-extension",
"version": "5.8",
"version_name": "Pro5.8",
"description": "用于自动执行多步骤 OAuth 注册流程",
+13
View File
@@ -2404,6 +2404,19 @@ header {
line-height: 1.55;
color: var(--text-secondary);
margin-bottom: 14px;
white-space: pre-line;
}
.modal-message a,
.modal-alert a {
color: var(--blue);
text-decoration: underline;
text-underline-offset: 2px;
}
.modal-message a:hover,
.modal-alert a:hover {
color: var(--cyan);
}
.modal-alert {
+55 -30
View File
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多页面自动化面板</title>
<title>codex-oauth-automation-extension</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
@@ -165,10 +165,10 @@
</div>
</div>
<div class="data-row" id="row-local-cpa-step9-mode">
<span class="data-label">本地 CPA</span>
<div id="local-cpa-step9-mode-group" class="choice-group" role="group" aria-label="本地 CPA 第 10 步策略">
<button type="button" class="choice-btn" data-local-cpa-step9-mode="submit">全部回调</button>
<button type="button" class="choice-btn" data-local-cpa-step9-mode="bypass">跳过第10步</button>
<span class="data-label">回调方式</span>
<div id="local-cpa-step9-mode-group" class="choice-group" role="group" aria-label="回调方式">
<button type="button" class="choice-btn" data-local-cpa-step9-mode="submit">服务器部署</button>
<button type="button" class="choice-btn" data-local-cpa-step9-mode="bypass">本地部署</button>
</div>
</div>
<div class="data-row" id="row-sub2api-url" style="display:none;">
@@ -246,31 +246,6 @@
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
</select>
</div>
<div class="data-row" id="row-temp-email-base-url" style="display:none;">
<span class="data-label">Temp API</span>
<input type="text" id="input-temp-email-base-url" class="data-input" placeholder="https://your-worker-domain" />
</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" />
</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。" />
</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" />
</div>
<div class="data-row" id="row-temp-email-domain" style="display:none;">
<span class="data-label">Temp 域名</span>
<div class="data-inline">
<select id="select-temp-email-domain" class="data-select"></select>
<input type="text" id="input-temp-email-domain" class="data-input" placeholder="例如 mail.example.com"
style="display:none;" />
<button id="btn-temp-email-domain-mode" class="btn btn-outline btn-sm" type="button">添加</button>
</div>
</div>
<div class="data-row" id="row-cf-domain" style="display:none;">
<span class="data-label">CF 域名</span>
<div class="data-inline">
@@ -400,6 +375,56 @@
</div>
</div>
</div>
<div id="cloudflare-temp-email-section" class="data-card hotmail-card" style="display:none;">
<div class="section-mini-header">
<div class="section-mini-copy">
<span class="section-label">Cloudflare Temp Email</span>
<span class="data-value">用于生成邮箱或接收转发邮件</span>
</div>
<div class="section-mini-actions">
<button id="btn-cloudflare-temp-email-usage-guide" class="btn btn-ghost btn-xs" type="button">使用教程</button>
<button id="btn-cloudflare-temp-email-github" class="btn btn-ghost btn-xs" type="button">GitHub</button>
</div>
</div>
<div class="data-row" id="row-temp-email-base-url" style="display:none;">
<span class="data-label">Temp API</span>
<input type="text" id="input-temp-email-base-url" class="data-input" placeholder="https://your-worker-domain" />
</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" />
</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。" />
</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" />
</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 配置">
<input type="checkbox" id="input-temp-email-use-random-subdomain" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
<span>启用</span>
</label>
<span class="setting-caption">依赖后端 RANDOM_SUBDOMAIN_DOMAINS</span>
</div>
</div>
<div class="data-row" id="row-temp-email-domain" style="display:none;">
<span class="data-label">Temp 域名</span>
<div class="data-inline">
<select id="select-temp-email-domain" class="data-select"></select>
<input type="text" id="input-temp-email-domain" class="data-input" placeholder="例如 mail.example.com"
style="display:none;" />
<button id="btn-temp-email-domain-mode" class="btn btn-outline btn-sm" type="button">添加</button>
</div>
</div>
</div>
<div id="hotmail-section" class="data-card hotmail-card" style="display:none;">
<div class="section-mini-header">
<div class="section-mini-copy">
+119 -8
View File
@@ -114,10 +114,15 @@ const rowTempEmailCustomAuth = document.getElementById('row-temp-email-custom-au
const inputTempEmailCustomAuth = document.getElementById('input-temp-email-custom-auth');
const rowTempEmailReceiveMailbox = document.getElementById('row-temp-email-receive-mailbox');
const inputTempEmailReceiveMailbox = document.getElementById('input-temp-email-receive-mailbox');
const rowTempEmailRandomSubdomainToggle = document.getElementById('row-temp-email-random-subdomain-toggle');
const inputTempEmailUseRandomSubdomain = document.getElementById('input-temp-email-use-random-subdomain');
const rowTempEmailDomain = document.getElementById('row-temp-email-domain');
const selectTempEmailDomain = document.getElementById('select-temp-email-domain');
const inputTempEmailDomain = document.getElementById('input-temp-email-domain');
const btnTempEmailDomainMode = document.getElementById('btn-temp-email-domain-mode');
const cloudflareTempEmailSection = document.getElementById('cloudflare-temp-email-section');
const btnCloudflareTempEmailUsageGuide = document.getElementById('btn-cloudflare-temp-email-usage-guide');
const btnCloudflareTempEmailGithub = document.getElementById('btn-cloudflare-temp-email-github');
const hotmailSection = document.getElementById('hotmail-section');
const mail2925Section = document.getElementById('mail2925-section');
const luckmailSection = document.getElementById('luckmail-section');
@@ -610,6 +615,10 @@ const LOG_LEVEL_LABELS = {
error: '错误',
};
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, mail2925Mode = getSelectedMail2925Mode()) {
return isManagedAliasProvider(provider, mail2925Mode);
}
@@ -674,6 +683,19 @@ function resetActionModalAlert() {
autoStartAlert.className = 'modal-alert';
}
function setActionModalMessageContent({ text = '', html = '' } = {}) {
if (!autoStartMessage) {
return;
}
if (html) {
autoStartMessage.innerHTML = html;
return;
}
autoStartMessage.textContent = text;
}
function resetActionModalButtons() {
const buttons = [btnAutoStartCancel, btnAutoStartRestart, btnAutoStartContinue];
buttons.forEach((button) => {
@@ -749,7 +771,7 @@ function resolveModalChoice(choice) {
}
}
function openActionModal({ title, message, actions, option, alert, buildResult }) {
function openActionModal({ title, message, messageHtml, actions, option, alert, buildResult }) {
if (!autoStartModal) {
return Promise.resolve(null);
}
@@ -760,7 +782,7 @@ function openActionModal({ title, message, actions, option, alert, buildResult }
resetActionModalButtons();
autoStartTitle.textContent = title;
autoStartMessage.textContent = message;
setActionModalMessageContent({ text: message, html: messageHtml });
currentModalActions = actions || [];
modalResultBuilder = typeof buildResult === 'function' ? buildResult : null;
const buttonSlots = currentModalActions.length <= 2
@@ -1460,6 +1482,18 @@ function setCloudflareTempEmailDomainEditMode(editing, options = {}) {
}
}
function applyCloudflareTempEmailSettingsState(state = {}) {
inputTempEmailBaseUrl.value = state?.cloudflareTempEmailBaseUrl || '';
inputTempEmailAdminAuth.value = state?.cloudflareTempEmailAdminAuth || '';
inputTempEmailCustomAuth.value = state?.cloudflareTempEmailCustomAuth || '';
inputTempEmailReceiveMailbox.value = state?.cloudflareTempEmailReceiveMailbox || '';
if (inputTempEmailUseRandomSubdomain) {
inputTempEmailUseRandomSubdomain.checked = Boolean(state?.cloudflareTempEmailUseRandomSubdomain);
}
renderCloudflareTempEmailDomainOptions(state?.cloudflareTempEmailDomain || '');
setCloudflareTempEmailDomainEditMode(false, { clearInput: true });
}
function collectSettingsPayload() {
const { domains, activeDomain } = getCloudflareDomainsFromState();
const selectedCloudflareDomain = normalizeCloudflareDomainValue(
@@ -1491,6 +1525,7 @@ function collectSettingsPayload() {
mailProvider: selectMailProvider.value,
mail2925Mode: getSelectedMail2925Mode(),
mail2925UseAccountPool,
currentMail2925AccountId: String(latestState?.currentMail2925AccountId || '').trim(),
emailGenerator: selectEmailGenerator.value,
autoDeleteUsedIcloudAlias: checkboxAutoDeleteIcloud?.checked,
icloudHostPreference: selectIcloudHostPreference?.value || 'auto',
@@ -1514,6 +1549,7 @@ function collectSettingsPayload() {
cloudflareTempEmailAdminAuth: inputTempEmailAdminAuth.value,
cloudflareTempEmailCustomAuth: inputTempEmailCustomAuth.value,
cloudflareTempEmailReceiveMailbox: normalizeCloudflareTempEmailReceiveMailboxValue(inputTempEmailReceiveMailbox.value),
cloudflareTempEmailUseRandomSubdomain: Boolean(inputTempEmailUseRandomSubdomain?.checked),
cloudflareTempEmailDomain: selectedCloudflareTempEmailDomain,
cloudflareTempEmailDomains: tempEmailDomains,
autoRunSkipFailures: inputAutoSkipFailures.checked,
@@ -1920,14 +1956,9 @@ function applySettingsState(state) {
inputLuckmailBaseUrl.value = normalizeLuckmailBaseUrl(state?.luckmailBaseUrl);
selectLuckmailEmailType.value = normalizeLuckmailEmailType(state?.luckmailEmailType);
inputLuckmailDomain.value = state?.luckmailDomain || '';
inputTempEmailBaseUrl.value = state?.cloudflareTempEmailBaseUrl || '';
inputTempEmailAdminAuth.value = state?.cloudflareTempEmailAdminAuth || '';
inputTempEmailCustomAuth.value = state?.cloudflareTempEmailCustomAuth || '';
inputTempEmailReceiveMailbox.value = state?.cloudflareTempEmailReceiveMailbox || '';
applyCloudflareTempEmailSettingsState(state);
renderCloudflareDomainOptions(state?.cloudflareDomain || '');
setCloudflareDomainEditMode(false, { clearInput: true });
renderCloudflareTempEmailDomainOptions(state?.cloudflareTempEmailDomain || '');
setCloudflareTempEmailDomainEditMode(false, { clearInput: true });
inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
inputAutoSkipFailuresThreadIntervalMinutes.value = String(normalizeAutoRunThreadIntervalMinutes(state?.autoRunFallbackThreadIntervalMinutes));
inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
@@ -2046,6 +2077,51 @@ 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 域名;随机子域按需开启。';
}
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' },
],
});
}
function openCloudflareTempEmailRepositoryPage() {
openExternalUrl(CLOUDFLARE_TEMP_EMAIL_REPOSITORY_URL);
}
function createUpdateNoteList(notes = []) {
if (!Array.isArray(notes) || notes.length === 0) {
const empty = document.createElement('p');
@@ -2680,10 +2756,14 @@ function updateMailProviderUI() {
const showCloudflareDomain = useEmailGenerator && useCloudflare;
const showCloudflareTempEmailSettings = useCloudflareTempEmailProvider || (useEmailGenerator && useCloudflareTempEmailGenerator);
const showCloudflareTempEmailReceiveMailbox = useCloudflareTempEmailProvider && !useCloudflareTempEmailGenerator;
const showCloudflareTempEmailRandomSubdomainToggle = useEmailGenerator && useCloudflareTempEmailGenerator;
const showCloudflareTempEmailDomain = useEmailGenerator && useCloudflareTempEmailGenerator;
if (rowEmailGenerator) {
rowEmailGenerator.style.display = useEmailGenerator ? '' : 'none';
}
if (cloudflareTempEmailSection) {
cloudflareTempEmailSection.style.display = showCloudflareTempEmailSettings ? '' : 'none';
}
if (icloudSection) {
const showIcloudSection = (useEmailGenerator && useIcloud) || useIcloudProvider;
icloudSection.style.display = showIcloudSection ? '' : 'none';
@@ -2705,6 +2785,9 @@ function updateMailProviderUI() {
rowTempEmailAdminAuth.style.display = showCloudflareTempEmailSettings ? '' : 'none';
rowTempEmailCustomAuth.style.display = showCloudflareTempEmailSettings ? '' : 'none';
rowTempEmailReceiveMailbox.style.display = showCloudflareTempEmailReceiveMailbox ? '' : 'none';
if (rowTempEmailRandomSubdomainToggle) {
rowTempEmailRandomSubdomainToggle.style.display = showCloudflareTempEmailRandomSubdomainToggle ? '' : 'none';
}
rowTempEmailDomain.style.display = showCloudflareTempEmailDomain ? '' : 'none';
const { domains: tempEmailDomains } = getCloudflareTempEmailDomainsFromState();
if (showCloudflareTempEmailDomain) {
@@ -2792,6 +2875,9 @@ function updateMailProviderUI() {
if (autoHintText && showCloudflareTempEmailReceiveMailbox) {
autoHintText.textContent = '若注册邮箱会转发到 Cloudflare Temp Email,请在“邮件接收”中填写实际接收转发邮件的邮箱。';
}
if (autoHintText && showCloudflareTempEmailRandomSubdomainToggle && inputTempEmailUseRandomSubdomain?.checked) {
autoHintText.textContent = '已启用随机子域名:扩展会按当前选中的 Temp 域名提交,并额外携带 enableRandomSubdomain;是否生效取决于后端 RANDOM_SUBDOMAIN_DOMAINS 配置。';
}
if (useHotmail) {
inputEmail.value = getCurrentHotmailEmail();
} else if (useLuckmail) {
@@ -3790,6 +3876,14 @@ btnRepoHome?.addEventListener('click', () => {
openRepositoryHomePage();
});
btnCloudflareTempEmailUsageGuide?.addEventListener('click', () => {
showCloudflareTempEmailUsageGuide();
});
btnCloudflareTempEmailGithub?.addEventListener('click', () => {
openCloudflareTempEmailRepositoryPage();
});
extensionUpdateStatus?.addEventListener('click', () => {
openReleaseListPage();
});
@@ -4417,6 +4511,13 @@ inputTempEmailReceiveMailbox.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
});
inputTempEmailUseRandomSubdomain?.addEventListener('change', () => {
updateMailProviderUI();
clearRegistrationEmail({ silent: true }).catch(() => { });
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
});
inputAutoSkipFailuresThreadIntervalMinutes.addEventListener('input', () => {
markSettingsDirty(true);
scheduleSettingsAutoSave();
@@ -4639,9 +4740,19 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.payload.cloudflareTempEmailReceiveMailbox !== undefined) {
inputTempEmailReceiveMailbox.value = message.payload.cloudflareTempEmailReceiveMailbox || '';
}
if (message.payload.cloudflareTempEmailUseRandomSubdomain !== undefined && inputTempEmailUseRandomSubdomain) {
inputTempEmailUseRandomSubdomain.checked = Boolean(message.payload.cloudflareTempEmailUseRandomSubdomain);
}
if (message.payload.cloudflareTempEmailDomain !== undefined || message.payload.cloudflareTempEmailDomains !== undefined) {
renderCloudflareTempEmailDomainOptions(message.payload.cloudflareTempEmailDomain || latestState?.cloudflareTempEmailDomain || '');
}
if (
message.payload.cloudflareTempEmailUseRandomSubdomain !== undefined
|| message.payload.cloudflareTempEmailDomain !== undefined
|| message.payload.cloudflareTempEmailDomains !== undefined
) {
updateMailProviderUI();
}
if (message.payload.currentHotmailAccountId !== undefined || message.payload.hotmailAccounts !== undefined) {
renderHotmailAccounts();
if (selectMailProvider.value === 'hotmail-api') {
+10
View File
@@ -81,6 +81,16 @@ test('isRecoverableStep9AuthFailure matches timeout and CPA auth failure statuse
true
);
assert.equal(
isRecoverableStep9AuthFailure('提交回调失败:请更新CLI Proxy API或检查连接'),
true
);
assert.equal(
isRecoverableStep9AuthFailure('认证失败:Bad Request'),
true
);
assert.equal(
isRecoverableStep9AuthFailure('认证成功!'),
false
+95 -1
View File
@@ -71,6 +71,7 @@ test('executeStep reuses the active top-level auth chain instead of starting a d
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
let activeTopLevelAuthChainExecution = null;
let stopRequested = false;
@@ -106,6 +107,10 @@ function isTerminalSecurityBlockedError() {
return false;
}
async function handleCloudflareSecurityBlocked() {}
function isBrowserSwitchRequiredError() {
return false;
}
async function handleBrowserSwitchRequired() {}
function doesStepUseCompletionSignal() {
return false;
}
@@ -159,10 +164,99 @@ return {
assert.ok(events.logs.some(({ message }) => /复用当前授权链/.test(message)));
});
test('executeStep stops flow when browser-switch-required error is raised', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
let activeTopLevelAuthChainExecution = null;
let stopRequested = false;
const events = {
logs: [],
statusCalls: [],
stopRequests: [],
appendRecords: [],
};
async function addLog(message, level = 'info') {
events.logs.push({ message, level });
}
async function setStepStatus(step, status) {
events.statusCalls.push({ step, status });
}
async function humanStepDelay() {}
async function getState() {
return {
flowStartTime: null,
stepStatuses: {},
};
}
function getErrorMessage(error) {
return error?.message || String(error || '');
}
async function appendManualAccountRunRecordIfNeeded(status, _state, reason) {
events.appendRecords.push({ status, reason });
}
function isTerminalSecurityBlockedError() {
return false;
}
async function handleCloudflareSecurityBlocked() {}
async function requestStop(options = {}) {
events.stopRequests.push(options);
}
function doesStepUseCompletionSignal() {
return false;
}
function isRetryableContentScriptTransportError() {
return false;
}
const stepRegistry = {
async executeStep() {
throw new Error('BROWSER_SWITCH_REQUIRED::请更换浏览器进行注册登录。');
},
};
${extractFunction('isStopError')}
${extractFunction('throwIfStopped')}
${extractFunction('isAuthChainStep')}
${extractFunction('acquireTopLevelAuthChainExecution')}
${extractFunction('isBrowserSwitchRequiredError')}
${extractFunction('getBrowserSwitchRequiredMessage')}
${extractFunction('handleBrowserSwitchRequired')}
${extractFunction('executeStep')}
return {
executeStep,
snapshot() {
return events;
},
};
`)();
await assert.rejects(
() => api.executeStep(10),
/流程已被用户停止。/
);
const events = api.snapshot();
assert.deepStrictEqual(events.stopRequests, [
{ logMessage: '请更换浏览器进行注册登录。' },
]);
assert.deepStrictEqual(events.statusCalls, [
{ step: 10, status: 'running' },
]);
assert.equal(
events.logs.some(({ message }) => /步骤 10 失败/.test(message)),
false,
'browser-switch-required error should stop the flow before generic failed logging'
);
});
test('oauth timeout budget ignores stale deadlines from an old oauth url', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 6 * 60 * 1000;
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')}
@@ -0,0 +1,145 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('cloudflare temp email settings normalize and expose the random-subdomain toggle', () => {
const bundle = [
extractFunction('normalizeCloudflareTempEmailReceiveMailbox'),
extractFunction('getCloudflareTempEmailConfig'),
extractFunction('normalizePersistentSettingValue'),
extractFunction('buildPersistentSettingsPayload'),
].join('\n');
const api = new Function(`
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PERSISTED_SETTING_DEFAULTS = {
panelMode: 'cpa',
autoStepDelaySeconds: null,
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
mailProvider: '163',
mail2925Mode: 'provide',
emailGenerator: 'duck',
autoDeleteUsedIcloudAlias: false,
accountRunHistoryTextEnabled: false,
cloudflareTempEmailUseRandomSubdomain: false,
cloudflareTempEmailDomain: '',
cloudflareTempEmailDomains: [],
};
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : 'cpa'; }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value, fallback = null) { return value == null || value === '' ? fallback : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; }
function normalizeMailProvider(value) { return String(value || '').trim().toLowerCase() || '163'; }
function normalizeMail2925Mode(value) { return String(value || '').trim().toLowerCase() === 'receive' ? 'receive' : 'provide'; }
function normalizeEmailGenerator(value) { return String(value || '').trim().toLowerCase() || 'duck'; }
function normalizeIcloudHost(value) { const normalized = String(value || '').trim().toLowerCase(); return normalized === 'icloud.com' || normalized === 'icloud.com.cn' ? normalized : ''; }
function normalizeAccountRunHistoryHelperBaseUrl(value) { return String(value || '').trim(); }
function normalizeHotmailServiceMode(value) { return String(value || '').trim().toLowerCase() === 'remote' ? 'remote' : 'local'; }
function normalizeHotmailRemoteBaseUrl(value) { return String(value || '').trim(); }
function normalizeHotmailLocalBaseUrl(value) { return String(value || '').trim(); }
function normalizeCloudflareDomain(value) { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareDomains(value) { return Array.isArray(value) ? value.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean) : []; }
function normalizeCloudflareTempEmailBaseUrl(value) { return String(value || '').trim(); }
function normalizeCloudflareTempEmailAddress(value = '') { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareTempEmailDomain(value) { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareTempEmailDomains(value) {
const seen = new Set();
const domains = [];
for (const item of Array.isArray(value) ? value : []) {
const normalized = normalizeCloudflareTempEmailDomain(item);
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
domains.push(normalized);
}
return domains;
}
function normalizeHotmailAccounts(value) { return Array.isArray(value) ? value : []; }
function normalizeMail2925Accounts(value) { return Array.isArray(value) ? value : []; }
function resolveLegacyAutoStepDelaySeconds() { return undefined; }
${bundle}
return {
buildPersistentSettingsPayload,
getCloudflareTempEmailConfig,
normalizePersistentSettingValue,
};
`)();
assert.equal(api.normalizePersistentSettingValue('cloudflareTempEmailUseRandomSubdomain', 1), true);
const payload = api.buildPersistentSettingsPayload({
cloudflareTempEmailUseRandomSubdomain: true,
cloudflareTempEmailDomain: 'mail.example.com',
cloudflareTempEmailDomains: ['mail.example.com', 'alt.example.com'],
});
assert.equal(payload.cloudflareTempEmailUseRandomSubdomain, true);
assert.equal(payload.cloudflareTempEmailDomain, 'mail.example.com');
assert.deepEqual(payload.cloudflareTempEmailDomains, ['mail.example.com', 'alt.example.com']);
const config = api.getCloudflareTempEmailConfig({
cloudflareTempEmailBaseUrl: 'https://temp.example.com',
cloudflareTempEmailAdminAuth: 'admin-secret',
cloudflareTempEmailCustomAuth: 'custom-secret',
cloudflareTempEmailReceiveMailbox: 'Forward@Example.com',
cloudflareTempEmailUseRandomSubdomain: true,
cloudflareTempEmailDomain: 'mail.example.com',
cloudflareTempEmailDomains: ['mail.example.com'],
});
assert.deepEqual(config, {
baseUrl: 'https://temp.example.com',
adminAuth: 'admin-secret',
customAuth: 'custom-secret',
receiveMailbox: 'forward@example.com',
useRandomSubdomain: true,
domain: 'mail.example.com',
domains: ['mail.example.com'],
});
});
+162 -7
View File
@@ -2,24 +2,25 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function loadGeneratedEmailHelpersApi() {
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
const globalScope = {};
return new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
}
test('background imports generated email helper module', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /importScripts\([\s\S]*'background\/generated-email-helpers\.js'/);
});
test('generated email helper module exposes a factory', () => {
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
const api = loadGeneratedEmailHelpersApi();
assert.equal(typeof api?.createGeneratedEmailHelpers, 'function');
});
test('generated email helper falls back to normal generator when 2925 is in receive mode', async () => {
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
const api = loadGeneratedEmailHelpersApi();
const events = [];
const helpers = api.createGeneratedEmailHelpers({
@@ -76,3 +77,157 @@ test('generated email helper falls back to normal generator when 2925 is in rece
['email', 'duck@example.com'],
]);
});
test('generated email helper uses the regular temp email domain when random subdomain mode is disabled', async () => {
const api = loadGeneratedEmailHelpersApi();
const requests = [];
const savedEmails = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build managed alias');
},
buildCloudflareTempEmailHeaders: () => ({ 'x-admin-auth': 'admin-secret' }),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async (url, options = {}) => {
requests.push({
url,
method: options.method,
body: options.body ? JSON.parse(options.body) : null,
});
return {
ok: true,
text: async () => JSON.stringify({ address: 'user@mail.example.com' }),
};
},
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: (payload) => payload.address,
getCloudflareTempEmailConfig: () => ({
baseUrl: 'https://temp.example.com',
adminAuth: 'admin-secret',
customAuth: '',
useRandomSubdomain: false,
domain: 'mail.example.com',
}),
getState: async () => ({
mailProvider: '163',
emailGenerator: 'cloudflare-temp-email',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate mail2925 account');
},
joinCloudflareTempEmailUrl: (baseUrl, path) => `${baseUrl}${path}`,
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: (value) => String(value || '').trim().toLowerCase(),
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not use duck generator');
},
setEmailState: async (email) => {
savedEmails.push(email);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
emailGenerator: 'cloudflare-temp-email',
}, {
generator: 'cloudflare-temp-email',
});
assert.equal(email, 'user@mail.example.com');
assert.deepEqual(savedEmails, ['user@mail.example.com']);
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://temp.example.com/admin/new_address');
assert.equal(requests[0].method, 'POST');
assert.deepEqual(requests[0].body, {
enablePrefix: true,
enableRandomSubdomain: false,
name: requests[0].body.name,
domain: 'mail.example.com',
});
assert.match(requests[0].body.name, /^[a-z0-9]+$/);
});
test('generated email helper requests random subdomain creation while preserving the returned address', async () => {
const api = loadGeneratedEmailHelpersApi();
const requests = [];
const savedEmails = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build managed alias');
},
buildCloudflareTempEmailHeaders: () => ({ 'x-admin-auth': 'admin-secret' }),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async (url, options = {}) => {
requests.push({
url,
method: options.method,
body: options.body ? JSON.parse(options.body) : null,
});
return {
ok: true,
text: async () => JSON.stringify({ address: 'user@a1b2c3d4.example.com' }),
};
},
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: (payload) => payload.address,
getCloudflareTempEmailConfig: () => ({
baseUrl: 'https://temp.example.com',
adminAuth: 'admin-secret',
customAuth: '',
useRandomSubdomain: true,
domain: 'mail.example.com',
}),
getState: async () => ({
mailProvider: '163',
emailGenerator: 'cloudflare-temp-email',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate mail2925 account');
},
joinCloudflareTempEmailUrl: (baseUrl, path) => `${baseUrl}${path}`,
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: (value) => String(value || '').trim().toLowerCase(),
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not use duck generator');
},
setEmailState: async (email) => {
savedEmails.push(email);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
emailGenerator: 'cloudflare-temp-email',
}, {
generator: 'cloudflare-temp-email',
localPart: 'user',
});
assert.equal(email, 'user@a1b2c3d4.example.com');
assert.deepEqual(savedEmails, ['user@a1b2c3d4.example.com']);
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://temp.example.com/admin/new_address');
assert.equal(requests[0].method, 'POST');
assert.deepEqual(requests[0].body, {
enablePrefix: true,
enableRandomSubdomain: true,
name: 'user',
domain: 'mail.example.com',
});
});
@@ -266,3 +266,163 @@ test('ensureMail2925MailboxSession logs in when login page is detected and accou
assert.equal(readyCalls, 1);
assert.equal(result.result.loggedIn, true);
});
test('ensureMail2925MailboxSession relogs with selected account when mailbox page email mismatches and pool is on', async () => {
let currentState = {
autoRunning: false,
mail2925UseAccountPool: true,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'target@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
const openedUrls = [];
const sendPayloads = [];
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: openedUrls.at(-1) || 'https://2925.com/#/mailList' }),
},
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
ensureContentScriptReadyOnTab: async () => {},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isAutoRunLockedState: () => false,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
reuseOrCreateTab: async (_source, url) => {
openedUrls.push(url);
return 9;
},
sendToMailContentScriptResilient: async (_mail, message) => {
sendPayloads.push(message.payload);
if (sendPayloads.length === 1) {
return {
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: 'wrong@2925.com',
};
}
return {
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: 'target@2925.com',
};
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
waitForTabUrlMatch: async () => ({ url: 'https://2925.com/login/' }),
});
const result = await manager.ensureMail2925MailboxSession({
accountId: 'acc-1',
forceRelogin: false,
allowLoginWhenOnLoginPage: true,
expectedMailboxEmail: 'target@2925.com',
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
assert.equal(result.result.loggedIn, true);
assert.deepStrictEqual(openedUrls, [
'https://2925.com/#/mailList',
'https://2925.com/login/',
]);
assert.equal(sendPayloads.length, 2);
assert.equal(sendPayloads[0].allowLoginWhenOnLoginPage, true);
assert.equal(sendPayloads[1].forceLogin, true);
});
test('ensureMail2925MailboxSession stops when mailbox page email mismatches and pool is off', async () => {
let currentState = {
autoRunning: true,
autoRunPhase: 'running',
mail2925UseAccountPool: false,
mail2925Accounts: [],
currentMail2925AccountId: null,
};
const stopCalls = [];
let sendCalls = 0;
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: 'https://2925.com/#/mailList' }),
},
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
ensureContentScriptReadyOnTab: async () => {},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
requestStop: async (options = {}) => {
stopCalls.push(options);
},
reuseOrCreateTab: async () => 9,
sendToMailContentScriptResilient: async () => {
sendCalls += 1;
return {
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: 'wrong@2925.com',
};
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
await assert.rejects(
() => manager.ensureMail2925MailboxSession({
accountId: null,
forceRelogin: false,
allowLoginWhenOnLoginPage: false,
expectedMailboxEmail: 'target@2925.com',
actionLabel: '步骤 4:确认 2925 邮箱登录态',
}),
/流程已被用户停止。/
);
assert.equal(sendCalls, 1);
assert.equal(stopCalls.length, 1);
assert.match(stopCalls[0].logMessage, /与目标账号 target@2925\.com 不一致/);
});
@@ -294,3 +294,46 @@ test('handleMail2925LimitReachedError stops immediately when account pool is off
assert.equal(events.stopCalls.length, 1);
assert.equal(currentState.currentMail2925AccountId, 'current');
});
test('setCurrentMail2925Account persists currentMail2925AccountId for browser restart restore', async () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
let currentState = {
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: null,
};
const persistedUpdates = [];
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
setPersistentSettings: async (payload) => {
persistedUpdates.push(payload);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
await manager.setCurrentMail2925Account('acc-1');
assert.equal(currentState.currentMail2925AccountId, 'acc-1');
assert.deepStrictEqual(persistedUpdates, [
{ currentMail2925AccountId: 'acc-1' },
]);
});
+55
View File
@@ -12,6 +12,61 @@ test('ensureMail2925Session waits 1 second after filling credentials before clic
assert.match(source, /fillInput\(passwordInput,\s*password\);[\s\S]*?await sleep\(200\);[\s\S]*?await sleep\(1000\);[\s\S]*?simulateClick\(loginButton\);/);
});
test('detectMail2925ViewState treats top mailbox email as mailbox view', () => {
const bundle = [
extractFunction('normalizeNodeText'),
extractFunction('isVisibleNode'),
extractFunction('isMailItemNode'),
extractFunction('resolveActionTarget'),
extractFunction('findMailItems'),
extractFunction('extractEmails'),
extractFunction('getMail2925DisplayedMailboxEmail'),
extractFunction('detectMail2925ViewState'),
].join('\n');
const api = new Function(`
const MAIL_ITEM_SELECTORS = ['.mail-item'];
const MAIL_ITEM_SELECTOR_GROUP = '.mail-item';
const MAIL2925_REMEMBER_LOGIN_PATTERNS = [];
const MAIL2925_AGREEMENT_PATTERNS = [];
const document = {
querySelectorAll(selector) {
if (selector === '.mail-item') return [];
if (selector === 'body *') return [headerEmail];
if (selector.includes('[class*="user"]')) return [headerEmail];
return [];
},
body: {
innerText: 'QLHazycoder qlhazycoder@2925.com',
textContent: 'QLHazycoder qlhazycoder@2925.com',
},
};
const window = {
innerHeight: 900,
getComputedStyle() {
return { display: 'block', visibility: 'visible' };
},
};
const headerEmail = {
hidden: false,
textContent: 'qlhazycoder@2925.com',
innerText: 'qlhazycoder@2925.com',
getBoundingClientRect() { return { top: 40, left: 400, width: 120, height: 20 }; },
closest() { return null; },
};
function detectMail2925LimitMessage() { return ''; }
function findMail2925LoginPasswordInput() { return null; }
function findMail2925LoginEmailInput() { return null; }
function getPageTextSample() { return 'qlhazycoder@2925.com'; }
${bundle}
return { detectMail2925ViewState };
`)();
const state = api.detectMail2925ViewState();
assert.equal(state.view, 'mailbox');
assert.equal(state.mailboxEmail, 'qlhazycoder@2925.com');
});
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
@@ -0,0 +1,297 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
function createRow(initialDisplay = 'none') {
return {
style: { display: initialDisplay },
};
}
test('sidepanel html places cloudflare temp email controls in a standalone section', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="cloudflare-temp-email-section"/);
assert.match(html, /id="btn-cloudflare-temp-email-usage-guide"/);
assert.match(html, /id="btn-cloudflare-temp-email-github"/);
assert.match(html, /id="row-temp-email-random-subdomain-toggle"/);
assert.match(html, /id="input-temp-email-use-random-subdomain"/);
assert.doesNotMatch(html, /id="row-temp-email-random-subdomain-domain"/);
});
test('sidepanel modal message preserves line breaks and supports inline links', () => {
const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8');
assert.match(css, /\.modal-message\s*\{[\s\S]*white-space:\s*pre-line;/);
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');
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(); }
${bundle}
return {
buildCloudflareTempEmailUsageGuideModalConfig,
};
`)();
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);
});
test('buildCloudflareTempEmailUsageGuideModalConfig returns a distinct alert for provider mode', () => {
const bundle = extractFunction('buildCloudflareTempEmailUsageGuideModalConfig');
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(); }
${bundle}
return {
buildCloudflareTempEmailUsageGuideModalConfig,
};
`)();
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);
});
test('openCloudflareTempEmailRepositoryPage opens the upstream repository', () => {
const bundle = extractFunction('openCloudflareTempEmailRepositoryPage');
const api = new Function(`
const calls = [];
const CLOUDFLARE_TEMP_EMAIL_REPOSITORY_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email';
function openExternalUrl(url) { calls.push(url); }
${bundle}
return {
calls,
openCloudflareTempEmailRepositoryPage,
};
`)();
api.openCloudflareTempEmailRepositoryPage();
assert.deepEqual(api.calls, ['https://github.com/dreamhunter2333/cloudflare_temp_email']);
});
test('applyCloudflareTempEmailSettingsState restores the random subdomain toggle and temp domain list', () => {
const bundle = extractFunction('applyCloudflareTempEmailSettingsState');
const api = new Function(`
const inputTempEmailBaseUrl = { value: '' };
const inputTempEmailAdminAuth = { value: '' };
const inputTempEmailCustomAuth = { value: '' };
const inputTempEmailReceiveMailbox = { value: '' };
const inputTempEmailUseRandomSubdomain = { checked: false };
const calls = {
domainOptions: [],
domainEditMode: [],
};
function renderCloudflareTempEmailDomainOptions(value) { calls.domainOptions.push(value); }
function setCloudflareTempEmailDomainEditMode(editing, options) { calls.domainEditMode.push({ editing, options }); }
${bundle}
return {
applyCloudflareTempEmailSettingsState,
calls,
inputTempEmailBaseUrl,
inputTempEmailAdminAuth,
inputTempEmailCustomAuth,
inputTempEmailReceiveMailbox,
inputTempEmailUseRandomSubdomain,
};
`)();
api.applyCloudflareTempEmailSettingsState({
cloudflareTempEmailBaseUrl: 'https://temp.example.com',
cloudflareTempEmailAdminAuth: 'admin-secret',
cloudflareTempEmailCustomAuth: 'custom-secret',
cloudflareTempEmailReceiveMailbox: 'relay@example.com',
cloudflareTempEmailUseRandomSubdomain: true,
cloudflareTempEmailDomain: 'mail.example.com',
});
assert.equal(api.inputTempEmailBaseUrl.value, 'https://temp.example.com');
assert.equal(api.inputTempEmailAdminAuth.value, 'admin-secret');
assert.equal(api.inputTempEmailCustomAuth.value, 'custom-secret');
assert.equal(api.inputTempEmailReceiveMailbox.value, 'relay@example.com');
assert.equal(api.inputTempEmailUseRandomSubdomain.checked, true);
assert.deepEqual(api.calls.domainOptions, ['mail.example.com']);
assert.deepEqual(api.calls.domainEditMode, [{ editing: false, options: { clearInput: true } }]);
});
test('updateMailProviderUI keeps the temp domain selector visible and updates the hint when random subdomain is enabled', () => {
const bundle = extractFunction('updateMailProviderUI');
const api = new Function(`
let latestState = {
cloudflareTempEmailDomains: ['mail.example.com'],
};
let cloudflareTempEmailDomainEditMode = false;
const ICLOUD_PROVIDER = 'icloud';
const GMAIL_PROVIDER = 'gmail';
const LUCKMAIL_PROVIDER = 'luckmail-api';
const rowMail2925Mode = ${JSON.stringify(createRow('none'))};
const rowMail2925PoolSettings = ${JSON.stringify(createRow('none'))};
const rowEmailPrefix = ${JSON.stringify(createRow('none'))};
const rowInbucketHost = ${JSON.stringify(createRow('none'))};
const rowInbucketMailbox = ${JSON.stringify(createRow('none'))};
const rowEmailGenerator = ${JSON.stringify(createRow(''))};
const rowCfDomain = ${JSON.stringify(createRow('none'))};
const rowTempEmailBaseUrl = ${JSON.stringify(createRow('none'))};
const rowTempEmailAdminAuth = ${JSON.stringify(createRow('none'))};
const rowTempEmailCustomAuth = ${JSON.stringify(createRow('none'))};
const rowTempEmailReceiveMailbox = ${JSON.stringify(createRow('none'))};
const rowTempEmailRandomSubdomainToggle = ${JSON.stringify(createRow('none'))};
const rowTempEmailDomain = ${JSON.stringify(createRow('none'))};
const cloudflareTempEmailSection = ${JSON.stringify(createRow('none'))};
const hotmailSection = ${JSON.stringify(createRow('none'))};
const mail2925Section = ${JSON.stringify(createRow('none'))};
const luckmailSection = ${JSON.stringify(createRow('none'))};
const icloudSection = ${JSON.stringify(createRow('none'))};
const labelEmailPrefix = { textContent: '' };
const inputEmailPrefix = { placeholder: '', style: { display: '' }, readOnly: false };
const labelMail2925UseAccountPool = ${JSON.stringify(createRow('none'))};
const selectMail2925PoolAccount = { style: { display: 'none' }, disabled: false };
const btnFetchEmail = { hidden: false, disabled: false, textContent: '' };
const btnMailLogin = { disabled: false, textContent: '', title: '' };
const inputEmail = { readOnly: false, placeholder: '', value: '' };
const autoHintText = { textContent: '' };
const rowHotmailServiceMode = ${JSON.stringify(createRow('none'))};
const rowHotmailRemoteBaseUrl = ${JSON.stringify(createRow('none'))};
const rowHotmailLocalBaseUrl = ${JSON.stringify(createRow('none'))};
const inputMail2925UseAccountPool = { checked: false };
const selectMailProvider = { value: '163' };
const selectEmailGenerator = { value: 'cloudflare-temp-email', disabled: false };
const inputTempEmailUseRandomSubdomain = { checked: false };
const calls = {
tempDomainEditMode: [],
};
function isLuckmailProvider() { return false; }
function isCustomMailProvider() { return false; }
function isIcloudMailProvider() { return false; }
function usesGeneratedAliasMailProvider() { return false; }
function getSelectedMail2925Mode() { return 'provide'; }
function getManagedAliasProviderUiCopy() { return null; }
function getCurrentRegistrationEmailUiCopy() {
return {
buttonLabel: '生成 Temp',
placeholder: '点击生成 Cloudflare Temp Email,或手动粘贴邮箱',
label: 'Cloudflare Temp Email',
};
}
function updateMailLoginButtonState() {}
function getSelectedHotmailServiceMode() { return 'local'; }
function getCloudflareDomainsFromState() { return { domains: [], activeDomain: '' }; }
function setCloudflareDomainEditMode() {}
function getCloudflareTempEmailDomainsFromState() { return { domains: ['mail.example.com'], activeDomain: 'mail.example.com' }; }
function setCloudflareTempEmailDomainEditMode(editing) { calls.tempDomainEditMode.push(editing); }
function queueIcloudAliasRefresh() {}
function hideIcloudLoginHelp() {}
function syncMail2925PoolAccountOptions() {}
function getMail2925Accounts() { return []; }
function renderHotmailAccounts() {}
function renderMail2925Accounts() {}
function renderLuckmailPurchases() {}
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
function isAutoRunLockedPhase() { return false; }
${bundle}
return {
updateMailProviderUI,
cloudflareTempEmailSection,
rowTempEmailRandomSubdomainToggle,
rowTempEmailDomain,
inputTempEmailUseRandomSubdomain,
autoHintText,
calls,
};
`)();
api.updateMailProviderUI();
assert.equal(api.cloudflareTempEmailSection.style.display, '');
assert.equal(api.rowTempEmailRandomSubdomainToggle.style.display, '');
assert.equal(api.rowTempEmailDomain.style.display, '');
api.inputTempEmailUseRandomSubdomain.checked = true;
api.updateMailProviderUI();
assert.equal(api.cloudflareTempEmailSection.style.display, '');
assert.equal(api.rowTempEmailDomain.style.display, '');
assert.match(api.autoHintText.textContent, /RANDOM_SUBDOMAIN_DOMAINS/);
});
@@ -156,6 +156,7 @@ const inputTempEmailBaseUrl = { value: 'https://temp.example.com' };
const inputTempEmailAdminAuth = { value: 'admin-secret' };
const inputTempEmailCustomAuth = { value: 'custom-secret' };
const inputTempEmailReceiveMailbox = { value: 'relay@example.com' };
const inputTempEmailUseRandomSubdomain = { checked: true };
const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
const inputAutoDelayEnabled = { checked: true };
@@ -192,6 +193,7 @@ return {
assert.equal('customPassword' in contributionPayload, false);
assert.equal('accountRunHistoryTextEnabled' in contributionPayload, false);
assert.equal('accountRunHistoryHelperBaseUrl' in contributionPayload, false);
assert.equal(contributionPayload.cloudflareTempEmailUseRandomSubdomain, true);
api.setLatestState({ contributionMode: false });
const normalPayload = api.collectSettingsPayload();
@@ -200,6 +202,7 @@ return {
assert.equal(normalPayload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
assert.equal(normalPayload.codex2apiUrl, 'http://localhost:8080/admin/accounts');
assert.equal(normalPayload.codex2apiAdminKey, 'codex-admin-secret');
assert.equal(normalPayload.cloudflareTempEmailUseRandomSubdomain, true);
});
test('contribution mode manager enters mode, starts main auto flow, polls contribution status, and exits cleanly', async () => {
@@ -142,3 +142,89 @@ return {
assert.equal(api.getLatestState().mail2925BaseEmail, 'new@2925.com');
assert.equal(api.getSaveCalls(), 1);
});
test('collectSettingsPayload persists currentMail2925AccountId for 2925 account pool restore', () => {
const bundle = [
extractFunction('collectSettingsPayload'),
].join('\n');
const api = new Function(`
let latestState = {
contributionMode: false,
mail2925UseAccountPool: true,
currentMail2925AccountId: 'acc-2',
};
let cloudflareDomainEditMode = false;
let cloudflareTempEmailDomainEditMode = false;
const selectCfDomain = { value: 'example.com' };
const selectTempEmailDomain = { value: 'mail.example.com' };
const selectPanelMode = { value: 'cpa' };
const inputVpsUrl = { value: '' };
const inputVpsPassword = { value: '' };
const inputSub2ApiUrl = { value: '' };
const inputSub2ApiEmail = { value: '' };
const inputSub2ApiPassword = { value: '' };
const inputSub2ApiGroup = { value: '' };
const inputSub2ApiDefaultProxy = { value: '' };
const inputCodex2ApiUrl = { value: '' };
const inputCodex2ApiAdminKey = { value: '' };
const inputPassword = { value: '' };
const selectMailProvider = { value: '2925' };
const selectEmailGenerator = { value: 'duck' };
const checkboxAutoDeleteIcloud = { checked: false };
const selectIcloudHostPreference = { value: 'auto' };
const inputAccountRunHistoryTextEnabled = { checked: false };
const inputAccountRunHistoryHelperBaseUrl = { value: '' };
const inputMail2925UseAccountPool = { checked: true };
const inputInbucketHost = { value: '' };
const inputInbucketMailbox = { value: '' };
const inputHotmailRemoteBaseUrl = { value: '' };
const inputHotmailLocalBaseUrl = { value: '' };
const inputLuckmailApiKey = { value: '' };
const inputLuckmailBaseUrl = { value: '' };
const selectLuckmailEmailType = { value: 'ms_graph' };
const inputLuckmailDomain = { value: '' };
const inputTempEmailBaseUrl = { value: '' };
const inputTempEmailAdminAuth = { value: '' };
const inputTempEmailCustomAuth = { value: '' };
const inputTempEmailReceiveMailbox = { value: '' };
const inputTempEmailUseRandomSubdomain = { checked: false };
const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputVerificationResendCount = { value: '4' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
function getCloudflareDomainsFromState() {
return { domains: [], activeDomain: '' };
}
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
function getCloudflareTempEmailDomainsFromState() {
return { domains: [], activeDomain: '' };
}
function normalizeCloudflareTempEmailDomainValue(value) { return String(value || '').trim(); }
function getSelectedLocalCpaStep9Mode() { return 'submit'; }
function getSelectedMail2925Mode() { return 'provide'; }
function getSelectedHotmailServiceMode() { return 'local'; }
function buildManagedAliasBaseEmailPayload() {
return { gmailBaseEmail: '', mail2925BaseEmail: 'demo@2925.com', emailPrefix: '' };
}
function normalizeLuckmailBaseUrl(value) { return String(value || '').trim(); }
function normalizeLuckmailEmailType(value) { return String(value || '').trim() || 'ms_graph'; }
function normalizeCloudflareTempEmailBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
${bundle}
return { collectSettingsPayload };
`)();
const payload = api.collectSettingsPayload();
assert.equal(payload.currentMail2925AccountId, 'acc-2');
assert.equal(payload.mail2925UseAccountPool, true);
});
+314
View File
@@ -144,3 +144,317 @@ return {
]);
assert.match(result.bodyTextPreview, /Welcome to ChatGPT/);
});
test('signup entry diagnostics captures hidden signup button style and blocking ancestor details', () => {
const api = new Function(`
const SIGNUP_ENTRY_TRIGGER_PATTERN = /免费注册|立即注册|注册|sign\\s*up|register|create\\s*account|create\\s+account/i;
const location = { href: 'https://chatgpt.com/' };
const hiddenSection = {
tagName: 'DIV',
id: 'mobile-cta',
className: 'max-xs:hidden',
hidden: false,
parentElement: null,
hasAttribute() {
return false;
},
getAttribute(name) {
if (name === 'aria-hidden') return '';
return '';
},
getBoundingClientRect() {
return { width: 0, height: 0 };
},
_style: {
display: 'none',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const hiddenSignupButton = {
tagName: 'BUTTON',
textContent: 'Sign up for free',
disabled: false,
className: 'signup-button',
hidden: false,
parentElement: hiddenSection,
hasAttribute() {
return false;
},
getBoundingClientRect() {
return { width: 0, height: 0 };
},
getAttribute(name) {
if (name === 'type') return '';
if (name === 'aria-hidden') return '';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const document = {
title: 'ChatGPT',
readyState: 'complete',
querySelector() {
return null;
},
querySelectorAll(selector) {
if (selector === 'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
return [hiddenSignupButton];
}
return [];
},
};
const window = {
innerWidth: 390,
innerHeight: 844,
outerWidth: 390,
outerHeight: 844,
devicePixelRatio: 3,
getComputedStyle(el) {
return el?._style || {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
};
},
};
function isVisibleElement(el) {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getSignupEmailInput() {
return null;
}
function getSignupPasswordInput() {
return null;
}
function getPageTextSnapshot() {
return 'ChatGPT 登录';
}
${extractFunction('getSignupEntryDiagnostics')}
return {
run() {
return getSignupEntryDiagnostics();
},
};
`)();
const result = api.run();
assert.deepStrictEqual(result.viewport, {
innerWidth: 390,
innerHeight: 844,
outerWidth: 390,
outerHeight: 844,
devicePixelRatio: 3,
});
assert.deepStrictEqual(result.signupLikeActionCounts, {
total: 1,
visible: 0,
hidden: 1,
});
assert.equal(result.signupLikeActions[0]?.text, 'Sign up for free');
assert.equal(result.signupLikeActions[0]?.className, 'signup-button');
assert.equal(result.signupLikeActions[0]?.display, 'block');
assert.equal(result.signupLikeActions[0]?.blockingAncestor?.className, 'max-xs:hidden');
assert.equal(result.signupLikeActions[0]?.blockingAncestor?.display, 'none');
});
test('signup password diagnostics summarizes password inputs, submit button, and alternate code entry', () => {
const api = new Function(`
const location = { href: 'https://auth.openai.com/create-account/password' };
const form = { action: 'https://auth.openai.com/u/signup/password' };
const passwordInput = {
tagName: 'INPUT',
type: 'password',
name: 'new-password',
id: 'password-field',
value: 'SecretLength14',
className: 'password-input',
disabled: false,
form,
getBoundingClientRect() {
return { width: 320, height: 44 };
},
getAttribute(name) {
if (name === 'type') return 'password';
if (name === 'name') return 'new-password';
if (name === 'autocomplete') return 'new-password';
if (name === 'placeholder') return 'Password';
if (name === 'aria-disabled') return 'false';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const submitButton = {
tagName: 'BUTTON',
textContent: 'Continue',
className: 'submit-btn',
disabled: false,
form,
getBoundingClientRect() {
return { width: 160, height: 40 };
},
getAttribute(name) {
if (name === 'type') return 'submit';
if (name === 'aria-disabled') return 'false';
if (name === 'data-dd-action-name') return 'Continue';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const oneTimeCodeButton = {
tagName: 'BUTTON',
textContent: 'Use a one-time code instead',
className: 'switch-btn',
disabled: false,
getBoundingClientRect() {
return { width: 220, height: 36 };
},
getAttribute(name) {
if (name === 'type') return 'button';
if (name === 'aria-disabled') return 'false';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const document = {
title: 'Create your account',
readyState: 'complete',
querySelectorAll(selector) {
if (selector === 'input[type="password"], input[name*="password" i], input[autocomplete="new-password"], input[autocomplete="current-password"]') {
return [passwordInput];
}
if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
return [submitButton, oneTimeCodeButton];
}
return [];
},
};
const window = {
getComputedStyle(el) {
return el?._style || {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
};
},
};
function isVisibleElement(el) {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function getPageTextSnapshot() {
return 'Create your account Use a one-time code instead';
}
function getSignupPasswordInput() {
return passwordInput;
}
function getSignupPasswordSubmitButton() {
return submitButton;
}
function getSignupPasswordDisplayedEmail() {
return 'user@example.com';
}
function findOneTimeCodeLoginTrigger() {
return oneTimeCodeButton;
}
function getSignupPasswordTimeoutErrorPageState() {
return {
retryEnabled: true,
userAlreadyExistsBlocked: false,
};
}
${extractFunction('getSignupPasswordDiagnostics')}
return {
run() {
return getSignupPasswordDiagnostics();
},
};
`)();
const result = api.run();
assert.equal(result.url, 'https://auth.openai.com/create-account/password');
assert.equal(result.displayedEmail, 'user@example.com');
assert.equal(result.hasVisiblePasswordInput, true);
assert.equal(result.passwordInputCount, 1);
assert.equal(result.visiblePasswordInputCount, 1);
assert.equal(result.passwordInputs[0]?.name, 'new-password');
assert.equal(result.passwordInputs[0]?.valueLength, 14);
assert.equal(result.submitButton?.text, 'Continue');
assert.equal(result.oneTimeCodeTrigger?.text, 'Use a one-time code instead');
assert.equal(result.retryPage, true);
assert.equal(result.retryEnabled, true);
assert.match(result.bodyTextPreview, /one-time code/);
});
+44 -19
View File
@@ -51,10 +51,8 @@ function extractFunction(name) {
return source.slice(start, end);
}
test('step 8 click effect returns restart_current_step when retry page is recovered', async () => {
test('step 8 click effect throws when retry page appears after clicking continue', async () => {
const api = new Function(`
let recoverCalls = 0;
const chrome = {
tabs: {
async get() {
@@ -69,10 +67,6 @@ const chrome = {
function throwIfStopped() {}
async function sleepWithStop() {}
async function ensureStep8SignupPageReady() {}
async function recoverAuthRetryPageOnTab() {
recoverCalls += 1;
return { recovered: true };
}
async function getStep8PageState() {
return {
url: 'https://auth.openai.com/authorize',
@@ -89,20 +83,51 @@ return {
async run() {
return waitForStep8ClickEffect(88, 'https://auth.openai.com/authorize', 1000);
},
snapshot() {
return { recoverCalls };
};
`)();
await assert.rejects(
() => api.run(),
/点击“继续”后页面进入认证页重试页/
);
});
test('step 8 ready check throws when consent page is already a retry page before clicking', async () => {
const api = new Function(`
const chrome = {
tabs: {
async get() {
return {
id: 88,
url: 'https://auth.openai.com/authorize',
};
},
},
};
function throwIfStopped() {}
async function sleepWithStop() {}
async function ensureStep8SignupPageReady() {}
async function getStep8PageState() {
return {
url: 'https://auth.openai.com/authorize',
retryPage: true,
addPhonePage: false,
consentReady: false,
};
}
${extractFunction('waitForStep8Ready')}
return {
async run() {
return waitForStep8Ready(88, 1000);
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.deepStrictEqual(result, {
progressed: false,
reason: 'retry_page_recovered',
restartCurrentStep: true,
url: 'https://auth.openai.com/authorize',
});
assert.equal(snapshot.recoverCalls, 1);
await assert.rejects(
() => api.run(),
/当前认证页已进入重试页/
);
});
+114 -5
View File
@@ -58,23 +58,36 @@ const bundle = [
extractFunction('summarizeStatusBadgeEntries'),
extractFunction('normalizeStep9StatusText'),
extractFunction('isOAuthCallbackTimeoutFailure'),
extractFunction('isStep10CallbackSubmittedStatus'),
extractFunction('isStep10CallbackFailureText'),
extractFunction('isStep10MainWaitingStatus'),
extractFunction('isStep10MainFailureText'),
extractFunction('isStep9FailureText'),
extractFunction('isStep9SuccessStatus'),
extractFunction('isStep9SuccessLikeStatus'),
extractFunction('formatStep10StatusSummaryValue'),
extractFunction('isStep10BrowserSwitchRequiredConflict'),
extractFunction('getStep10BrowserSwitchRequiredMessage'),
extractFunction('buildStep9StatusDiagnostics'),
extractFunction('extractStep10FailureDetail'),
extractFunction('explainStep10Failure'),
].join('\n');
function createApi() {
return new Function(`
function isRecoverableStep9AuthFailure(text) {
return /(?:认证失败|回调 URL 提交失败):\\s*/i.test(String(text || '').trim())
|| /oauth flow is not pending/i.test(String(text || '').trim());
const normalized = String(text || '').trim();
return /(?:认证失败|回调\\s*url\\s*提交失败|回调url提交失败|提交回调失败)\\s*[:]?/i.test(normalized)
|| /oauth flow is not pending|请更新\\s*cli\\s*proxy\\s*api\\s*或检查连接|bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out/i.test(normalized);
}
${bundle}
return {
buildStep9StatusDiagnostics,
explainStep10Failure,
isStep10BrowserSwitchRequiredConflict,
getStep10BrowserSwitchRequiredMessage,
};
`)();
}
@@ -86,6 +99,7 @@ test('step 9 does not treat red success badges as exact success', () => {
visible: true,
text: '认证成功!',
className: 'status-badge text-danger',
location: 'main',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
@@ -104,23 +118,118 @@ test('step 9 keeps failure state dominant when success badge and error banner co
visible: true,
text: '认证成功!',
className: 'status-badge',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[
{
visible: true,
text: '回调 URL 提交失败: oauth flow is not pending',
className: 'alert alert-danger',
className: 'status-badge error',
location: 'callback',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
],
[],
'page'
);
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
assert.equal(diagnostics.hasFailureVisibleBadge, true);
assert.equal(diagnostics.failureText, '回调 URL 提交失败: oauth flow is not pending');
assert.equal(diagnostics.failureSource, 'callback');
});
test('step 10 treats plain 认证成功 as success when no failure is visible', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '认证成功',
className: 'status-badge',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[],
'page'
);
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
assert.equal(diagnostics.exactSuccessText, '认证成功');
});
test('step 10 recognizes callback accepted badge as in-progress signal', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '回调 URL 已提交,等待认证中...',
className: 'status-badge success',
location: 'callback',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
{
visible: true,
text: '等待认证中...',
className: 'status-badge',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[],
'page'
);
assert.equal(diagnostics.hasCallbackSubmittedBadge, true);
assert.equal(diagnostics.callbackSubmittedText, '回调 URL 已提交,等待认证中...');
assert.equal(diagnostics.mainWaitingText, '等待认证中...');
assert.equal(diagnostics.hasExactSuccessVisibleBadge, false);
});
test('step 10 explains callback upgrade hint with user-friendly reason', () => {
const api = createApi();
const explanation = api.explainStep10Failure(
'回调 URL 提交失败: 请更新CLI Proxy API或检查连接',
'callback'
);
assert.equal(explanation.code, 'callback_submit_api_unavailable');
assert.match(explanation.userMessage, /CLI Proxy API 版本过旧|管理接口未启动|连接异常/);
assert.match(explanation.userMessage, /回调提交阶段/);
});
test('step 10 requests browser switch when success badge and callback upgrade failure coexist', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '认证成功',
className: 'status-badge success',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
{
visible: true,
text: '回调 URL 提交失败: 请更新CLI Proxy API或检查连接',
className: 'status-badge error',
location: 'callback',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
],
[],
'page'
);
assert.equal(api.isStep10BrowserSwitchRequiredConflict(diagnostics), true);
assert.match(api.getStep10BrowserSwitchRequiredMessage(diagnostics), /更换浏览器后重新进行注册登录/);
});
+5 -4
View File
@@ -141,7 +141,7 @@
- 标签注册表
- 最近打开的来源地址
- LuckMail 当前运行时选择
- 2925 当前运行时账号选择 `currentMail2925AccountId`
- 2925 当前选中的账号 ID `currentMail2925AccountId`(运行时会同步到持久配置,用于重开浏览器后恢复同一个号池账号)
补充:
@@ -157,6 +157,7 @@
- Hotmail 账号池
- 2925 账号池
- 2925 是否启用号池模式 `mail2925UseAccountPool`
- 2925 当前选中的号池账号 ID `currentMail2925AccountId`
- Cloudflare / Temp Email 设置
- iCloud 相关偏好
- LuckMail API 配置
@@ -332,7 +333,7 @@
补充行为:
- `2925` provider 会关闭 Step 4 / 8 的自动重发间隔 25 秒节流;每次“重新发送验证码”之间,会在邮箱页内部执行一轮固定 15 次的刷新轮询,不再因 OAuth 剩余时间预算而缩短。
- 当 provider 为 `2925` 时,Step 4 会优先直接打开当前 2925 邮箱页:如果页面仍停留在收件箱,就直接复用当前已登录页面;如果页面已经跳到登录页,则只有在启用了 2925 账号池时才会自动登录,未启用账号池时会直接复用现有停止逻辑结束流程。Step 8 不再额外承接这套登录态处理。
- 当 provider 为 `2925` 时,Step 4 会优先直接打开当前 2925 邮箱页,并先比对页面顶部显示的邮箱地址是否与当前目标邮箱一致:如果一致,就直接复用当前已登录页面;如果不一致且启用了 2925 账号池,则会先清理 cookie 再登录当前选中的账号;如果不一致且未启用账号池,则直接复用现有停止逻辑结束流程。Step 8 不再额外承接这套登录态处理。
- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 当前既不依赖时间窗,也不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中的匹配邮件。
- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
- 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
@@ -570,7 +571,7 @@ Codex2API 补充:
1. 用户在 sidepanel 的 2925 账号池中保存 `email / password`
2. sidepanel 中会单独展示 `提供邮箱 / 接收邮箱` 模式切换,以及独立的 `2925 号池` 开关 / 当前账号下拉框;这样即使切到 receive 模式,账号池设置也不会被别名基邮箱行一起隐藏
3. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,provide 模式下的别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
4. 手动点击 `登录` 或自动流程进入 Step 4 前,后台会先打开当前 2925 邮箱页:如果仍停留在收件箱则直接复用;如果跳到登录页,则仅在号池模式开启时才自动登录,关闭号池时直接调用现有停止逻辑结束流程
4. 手动点击 `登录` 或自动流程进入 Step 4 前,后台会先打开当前 2925 邮箱页,并读取页面顶部当前邮箱地址:如果仍停留在收件箱且顶部邮箱与目标邮箱一致,则直接复用;如果顶部邮箱不一致且启用了号池模式,则先清理 cookie 后登录当前选中的账号;如果顶部邮箱不一致且未启用号池模式,则直接调用现有停止逻辑结束流程;如果页面跳到登录页,则仍然只有号池模式开启时才自动登录
5. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先判断是否启用了号池模式:若已启用且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录;若未启用,则直接调用现有停止逻辑结束流程
6. 如果登录页已经识别到账号密码输入框,内容脚本会在填完账号密码后额外等待 1 秒再点击登录;若点击登录后 40 秒内仍未进入收件箱,且当前正处于自动运行中,则后台会直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态;这类情况常见于图片验证、行为验证或其他阻断登录的中间页
7. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态
@@ -724,7 +725,7 @@ Codex2API 补充:
- Step 8 如果发现认证页已经进入登录超时报错/重试页,会直接报错并回到 Step 7 重新开始,而不是在 Step 8 内部点击 `重试`
- Step 8 的登录重试页判定也覆盖 `/email-verification` 上的 `405 / Route Error`,避免这类页面被误当成普通未知页。
- 任意认证页重试页如果正文中出现 `max_check_attempts`,会被视为 Cloudflare 风控触发:后台立刻完全停止流程,侧边栏会复用现有确认弹窗提示等待 15~30 分钟后再试,避免继续刷新或反复重试加重风控,确认按钮显示为“我知道了”。
- Step 9 在点击 OAuth 同意页 `继续` 后,会额外检查是否进入认证页重试页;若命中则先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再重新执行当前轮的 `继续` 点击
- Step 9 在点击 OAuth 同意页 `继续` 后,会持续等待页面跳转;若点击后命中认证页重试页,则直接报错,不会在 Step 9 内部点击 `重试`
## 2026-04-21 2925 邮件时间窗补充
- `2925` 在 Step 4 / Step 8 现在会携带固定的步骤开始时间窗口,实际筛选下限为“步骤开始时间向前回看 10 分钟”。
+1
View File
@@ -92,6 +92,7 @@
## `docs/`
- `docs/仓库协作者AI分析PR与合并标准流程.md`:仓库协作者进行 AI 分析 PR 与合并时的流程说明。
- `docs/使用教程.md`:面向使用者的补充教程文档,当前说明扩展更新流程与 Clash Verge 的“非港轮询”配置步骤。
- `docs/images/交流群.jpg`:README 中展示的交流群图片资源。
- `docs/images/十轮自动.png`:README 中展示的自动运行效果图。
- `docs/images/微信.png`:README 中展示的微信收款码图片。