This commit is contained in:
QLHazyCoder
2026-04-22 21:52:12 +08:00
51 changed files with 4918 additions and 583 deletions
+26 -20
View File
@@ -81,33 +81,34 @@
## 安装
1. 打开 `chrome://extensions/`
2. 开启“开发者模式”
3. 点击“加载已解压的扩展程序”
4. 选择本项目目录
5. 打开扩展侧边栏
## 2026-04-17 更新补充:Gmail / 2925 别名邮箱
本次版本对 `Gmail``2925` 的注册邮箱逻辑做了统一整理:
本次版本对 `Gmail``2925 provide` 的注册邮箱逻辑做了统一整理:
- `Gmail``2925` 现在都走同一套“别名邮箱”逻辑。
- `Gmail``2925 provide` 现在都走同一套“别名邮箱”逻辑。
- 两者都不再使用“只填前缀再特殊拼接”的界面交互。
- 两者都要求先填写“基邮箱”:
- `Gmail`:例如 `name@gmail.com`
- `2925`:例如 `name@2925.com`
- `2925`(仅 provide 模式):例如 `name@2925.com`
- 侧边栏里的“注册邮箱”输入框对这两种模式都已开放,可直接手动填写完整邮箱。
- 侧边栏里的 `获取 / 生成` 按钮对这两种模式也可用,行为与 Duck / Cloudflare 一样,都是“可自动生成,也可手动覆盖”。
-`Mail = 2925` 且模式切到 `接收邮箱` 时,不再走别名基邮箱链路,而是回退到普通“邮箱生成 / 手动填写注册邮箱”路线,2925 只负责后续收信。
具体行为:
- `Gmail` 会基于完整基邮箱生成 `name+tag@gmail.com`
- `2925` 会基于完整基邮箱生成 `name123456@2925.com`
- `2925` 仅在 provide 模式下会基于完整基邮箱生成 `name123456@2925.com`
- 如果当前“注册邮箱”里已经是与当前基邮箱兼容的完整邮箱,流程会优先复用,不会强行重新生成
注意:
- `2925` 旧的“只填前缀”使用方式已经不再推荐,应该改为填写完整基邮箱
- 如果你手动填写了与当前 `Gmail / 2925` 基邮箱不匹配的完整邮箱,侧边栏会在保存或执行 Step 3 时拦截
2. 开启“开发者模式”
3. 点击“加载已解压的扩展程序”
4. 选择本项目目录
5. 打开扩展侧边栏
- 如果你手动填写了与当前 `Gmail / 2925 provide` 基邮箱不匹配的完整邮箱,侧边栏会在保存或执行 Step 3 时拦截
## 快速开始
@@ -143,12 +144,15 @@
1. `Mail` 选择 `2925`
2.`2925 账号池` 中添加 `邮箱 / 密码`
3. 在“2925 基邮箱”右侧打开 `号池` 开关后,基邮箱输入框会切成下拉框,只能从 2925 账号池中选择邮箱;关闭开关则回退到原来的手填基邮箱路线
4. 可先点 `使用此账号` 让当前别名基邮箱切到这条 2925 账号,再点 `登录` 手动验证网页邮箱登录态
5. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会先清理登录 cookie、等待 `3 秒`,再打开登录页,并在页面打开后再等待 `3 秒`,然后使用当前账号自动登录;点击登录后若 `20 秒`内仍未进入收件箱,则会判定当前登录失败
6. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间;如果还有下一个可用账号,就禁用当前账号 24 小时并自动切换登录;如果没有下一个可用账号,或当前未启用号池模式,则会直接复用现有“手动暂停 / 停止”逻辑终止自动流程
7. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入
8. `Mail = 2925` 仍然走 Gmail / 2925 共用的别名邮箱链路;实际注册邮箱会基于当前 2925 账号邮箱生成,例如 `name@2925.com -> name123456@2925.com`
3. 先根据你的用途选择 `2925 模式`
- `提供邮箱`:注册邮箱本身就是 2925 别名,会显示“别名基邮箱”输入
- `接收邮箱`:注册邮箱回退到普通“邮箱生成 / 手动填写”路线,2925 只负责收信
4. `2925 号池` 现在是独立配置行;开启 `号池` 后可从下拉框中选择当前 2925 账号。若当前处于 `提供邮箱` 模式,这个账号也会同步作为别名基邮箱
5. 可先点 `使用此账号` 让当前 2925 账号切到这条记录,再点 `登录` 手动验证网页邮箱登录态
6. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会先清理登录 cookie、等待 `3 秒`,再打开登录页,并在页面打开后再等待 `3 秒`,然后使用当前账号自动登录;填写完账号密码后会额外等待 `1 秒` 再点击登录,点击后若 `40 秒`内仍未进入收件箱,则会判定当前登录失败
7. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间;如果还有下一个可用账号,就禁用当前账号 24 小时并自动切换登录;如果没有下一个可用账号,或当前未启用号池模式,则会直接复用现有“手动暂停 / 停止”逻辑终止自动流程
8. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入
9. 只有 `Mail = 2925` 且模式为 `提供邮箱` 时,才会继续走 Gmail / 2925 共用的别名邮箱链路;例如 `name@2925.com -> name123456@2925.com`
## 侧边栏配置说明
@@ -579,9 +583,11 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
- 已刷新到最新 OAuth 链接
- 认证页已经真正进入登录验证码页面
- 在真正把 Step 7 记为完成前,还会再做一轮收尾确认;如果页面只是短暂进入登录验证码页、随后又掉进登录重试页,则不会直接进入 Step 8,而是先按共享恢复逻辑处理并重跑 Step 7
- 如遇登录超时报错,会先尝试通过共享恢复逻辑最多自动点击 5 次认证页上的 `重试` 恢复当前页面;若仍未恢复,再按既有逻辑重跑整个 Step 7
- 如遇登录页长时间停滞,会由后台刷新 OAuth 后重跑整个 Step 7
- 如果重试页内容中出现 `max_check_attempts`,会立刻完全停止流程,并在侧边栏复用现有确认弹窗提示这是 Cloudflare 风控拦截,确认按钮显示为“我知道了”
- Step 8 不负责替代 Step 7 的收尾确认;它默认消费的是“已经由 Step 7 确认稳定进入”的登录验证码页,只在后台入口做防御性状态兜底
支持:
@@ -598,7 +604,7 @@ Step 8 默认要求当前认证页已经处于登录验证码页。
- 打开邮箱并轮询登录验证码
- 填写并提交登录验证码
- 验证码链路失败后按有限次数回退到 Step 7
- 如果进入登录超时报错/重试页,会直接报错并回到 Step 7,不会在 Step 8 内部点击 `重试`
- 如果进入登录超时报错/重试页,包括 `/email-verification` 上的 `405 / Route Error` 登录重试页,会直接报错并回到 Step 7,不会在 Step 8 内部点击 `重试`
- 如果重试页内容中出现 `max_check_attempts`,会直接完全停止整个流程,并复用现有确认弹窗提醒先等待 15 到 30 分钟或更换浏览器,确认按钮显示为“我知道了”
与 Step 4 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。
@@ -617,7 +623,7 @@ Step 8 默认要求当前认证页已经处于登录验证码页。
- 等待按钮可点击
- 获取按钮坐标
- 通过 Chrome `debugger` 的输入事件点击该按钮
- 点击后会持续检查页面是否真正离开当前状态;如果出现认证页 `重试` 页面,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再重新执行当前轮的“继续”点击
- 点击后会持续检查页面是否真正离开当前状态;如果点击后出现认证页 `重试` 页面,则直接报错,不会在 Step 9 内部点击 `重试`
- 同时监听 `chrome.webNavigation.onBeforeNavigate`
- 一旦捕获本地回调地址,就把结果保存到 `Callback`
@@ -633,9 +639,9 @@ Step 8 默认要求当前认证页已经处于登录验证码页。
- 步骤 10 会拒绝任何不是真实 `/auth/callback`,或缺少 `code` / `state` 的本地回调地址
- 成功后的清理只会针对 `/auth` 这一类真实回调标签页,不会再泛化清理任意 localhost 路径
- 侧边栏可切换“本地 CPA”策略,默认是 `全部回调`
- 选择 `全部回调` 时,即使 CPA 部署在本地,也会执行步骤 10
- 选择 `跳过第10步` 时,仅当本地 CPA 且步骤 9 已拿到回调地址时,才会直接跳过步骤 10
- 侧边栏可切换“回调方式”,默认是 `服务器部署`
- 选择 `服务器部署` 时,即使 CPA 部署在本地,也会执行步骤 10
- 选择 `本地部署` 时,仅当本地 CPA 且步骤 9 已拿到回调地址时,才会直接跳过步骤 10
回到 CPA 面板:
+53 -32
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';
@@ -268,6 +269,7 @@ const PERSISTED_SETTING_DEFAULTS = {
accountRunHistoryHelperBaseUrl: DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL,
gmailBaseEmail: '',
mail2925BaseEmail: '',
currentMail2925AccountId: '',
emailPrefix: '',
inbucketHost: '',
inbucketMailbox: '',
@@ -450,7 +452,7 @@ function normalizeRunCount(value) {
if (!Number.isFinite(numeric)) {
return 1;
}
return Math.min(50, Math.max(1, Math.floor(numeric)));
return Math.max(1, Math.floor(numeric));
}
function normalizeAutoRunTimerKind(value = '') {
@@ -902,6 +904,7 @@ function normalizePersistentSettingValue(key, value) {
return normalizeAccountRunHistoryHelperBaseUrl(value);
case 'gmailBaseEmail':
case 'mail2925BaseEmail':
case 'currentMail2925AccountId':
case 'emailPrefix':
return String(value || '').trim();
case 'inbucketHost':
@@ -2132,11 +2135,21 @@ function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) {
const provider = typeof stateOrProvider === 'string'
? stateOrProvider
: stateOrProvider?.mailProvider;
const resolvedMail2925Mode = mail2925Mode !== undefined
? normalizeMail2925Mode(mail2925Mode)
: getMail2925Mode(stateOrProvider);
const utils = (typeof self !== 'undefined' ? self : globalThis).MultiPageManagedAliasUtils || null;
if (utils?.usesManagedAliasGeneration) {
return utils.usesManagedAliasGeneration(provider, { mail2925Mode: resolvedMail2925Mode });
}
if (utils?.isManagedAliasProvider) {
if (String(provider || '').trim().toLowerCase() === '2925') {
return utils.isManagedAliasProvider(provider) && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE;
}
return utils.isManagedAliasProvider(provider);
}
return provider === GMAIL_PROVIDER || provider === '2925';
return provider === GMAIL_PROVIDER
|| (provider === '2925' && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE);
}
function shouldUseCustomRegistrationEmail(state = {}) {
@@ -2279,11 +2292,21 @@ function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) {
const provider = typeof stateOrProvider === 'string'
? stateOrProvider
: stateOrProvider?.mailProvider;
const resolvedMail2925Mode = mail2925Mode !== undefined
? normalizeMail2925Mode(mail2925Mode)
: getMail2925Mode(stateOrProvider);
const utils = getManagedAliasUtils();
if (utils?.usesManagedAliasGeneration) {
return utils.usesManagedAliasGeneration(provider, { mail2925Mode: resolvedMail2925Mode });
}
if (utils?.isManagedAliasProvider) {
if (String(provider || '').trim().toLowerCase() === '2925') {
return utils.isManagedAliasProvider(provider) && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE;
}
return utils.isManagedAliasProvider(provider);
}
return provider === GMAIL_PROVIDER || provider === '2925';
return provider === GMAIL_PROVIDER
|| (provider === '2925' && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE);
}
function shouldUseCustomRegistrationEmail(state = {}) {
@@ -3980,6 +4003,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',
@@ -4003,6 +4037,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);
@@ -5306,6 +5347,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');
@@ -6564,6 +6609,7 @@ function getMailConfig(state) {
}
if (provider === '2925') {
return {
provider: '2925',
source: 'mail-2925',
url: 'https://2925.com/#/mailList',
label: '2925 邮箱',
@@ -6815,7 +6861,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;
}
@@ -7150,7 +7196,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();
@@ -7162,20 +7207,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) {
@@ -7340,18 +7374,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) {
@@ -7385,8 +7408,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':
+7 -1
View File
@@ -195,6 +195,9 @@
...(state || {}),
mailProvider: provider,
};
if (options.mail2925Mode !== undefined) {
mergedState.mail2925Mode = String(options.mail2925Mode || '').trim();
}
if (options.gmailBaseEmail !== undefined) {
mergedState.gmailBaseEmail = String(options.gmailBaseEmail || '').trim();
}
@@ -227,7 +230,10 @@
async function fetchGeneratedEmail(state, options = {}) {
const currentState = state || await getState();
const provider = String(options.mailProvider || currentState.mailProvider || '').trim().toLowerCase();
if (isGeneratedAliasProvider?.(provider)) {
const mail2925Mode = options.mail2925Mode !== undefined
? options.mail2925Mode
: currentState.mail2925Mode;
if (isGeneratedAliasProvider?.(provider, mail2925Mode)) {
return fetchManagedAliasEmail(currentState, options);
}
const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator);
+189 -199
View File
@@ -6,16 +6,16 @@
addLog,
broadcastDataUpdate,
chrome,
ensureContentScriptReadyOnTab,
findMail2925Account,
getMail2925AccountStatus,
getState,
isAutoRunLockedState,
isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account,
normalizeMail2925Accounts,
pickMail2925AccountForRun,
getState,
isAutoRunLockedState,
ensureContentScriptReadyOnTab,
requestStop,
reuseOrCreateTab,
sendToContentScriptResilient,
@@ -87,7 +87,8 @@
function isMail2925LimitReachedError(error) {
const message = getErrorMessage(error);
return message.startsWith(MAIL2925_LIMIT_ERROR_PREFIX)
|| /子邮箱.{0,12}已达上限|已达上限邮箱|子邮箱上限|邮箱已达上限/i.test(message);
|| message.includes('子邮箱已达上限')
|| message.includes('已达上限邮箱');
}
function isMail2925ThreadTerminatedError(error) {
@@ -135,7 +136,7 @@
try {
const state = await getState();
const tabId = Number(state?.tabRegistry?.[MAIL2925_SOURCE]?.tabId || 0);
if (!Number.isInteger(tabId) || tabId <= 0) {
if (!Number.isInteger(tabId) || tabId <= 0 || typeof chrome.tabs?.get !== 'function') {
return '';
}
const tab = await chrome.tabs.get(tabId);
@@ -145,6 +146,32 @@
}
}
async function getMail2925TabUrlById(tabId) {
try {
if (!Number.isInteger(Number(tabId)) || Number(tabId) <= 0 || typeof chrome.tabs?.get !== 'function') {
return '';
}
const tab = await chrome.tabs.get(Number(tabId));
return String(tab?.url || '').trim();
} catch {
return '';
}
}
function isMail2925LoginUrl(rawUrl = '') {
try {
const parsed = new URL(String(rawUrl || ''));
return (parsed.hostname === '2925.com' || parsed.hostname === 'www.2925.com')
&& /^\/login\/?$/.test(parsed.pathname);
} catch {
return false;
}
}
function normalizeMailboxEmail(value = '') {
return String(value || '').trim().toLowerCase();
}
async function setCurrentMail2925Account(accountId, options = {}) {
const { logMessage = '', updateLastUsedAt = false } = options;
const state = await getState();
@@ -163,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) {
@@ -187,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 });
}
@@ -201,6 +230,7 @@
await syncMail2925Accounts(nextAccounts);
if (state.currentMail2925AccountId === accountId) {
await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
}
@@ -216,6 +246,7 @@
await syncMail2925Accounts(nextAccounts);
if (state.currentMail2925AccountId && !findMail2925Account(nextAccounts, state.currentMail2925AccountId)) {
await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
}
@@ -361,7 +392,7 @@
origins: MAIL2925_COOKIE_ORIGINS,
});
} catch (_) {
// Best-effort cleanup only.
// Best effort cleanup only.
}
}
@@ -373,170 +404,18 @@
accountId = null,
forceRelogin = false,
actionLabel = '确保 2925 邮箱登录态',
allowLoginWhenOnLoginPage = true,
expectedMailboxEmail = '',
} = options;
const account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
});
if (forceRelogin) {
const removedCount = await clearMail2925SessionCookies();
await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info');
}
const normalizedExpectedMailboxEmail = normalizeMailboxEmail(expectedMailboxEmail);
if (forceRelogin && typeof sleepWithStop === 'function') {
await sleepWithStop(3000);
}
throwIfStopped();
const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL;
const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
});
if (forceRelogin && typeof sleepWithStop === 'function') {
await sleepWithStop(3000);
}
let result;
try {
result = await sendToMailContentScriptResilient(
getMail2925MailConfig(),
{
type: 'ENSURE_MAIL2925_SESSION',
step: 0,
source: 'background',
payload: {
email: account.email,
password: account.password,
forceLogin: forceRelogin,
},
},
{
timeoutMs: forceRelogin ? 30000 : 25000,
responseTimeoutMs: forceRelogin ? 30000 : 25000,
maxRecoveryAttempts: 2,
}
);
} catch (err) {
const failedUrl = await getMail2925CurrentTabUrl();
await addLog(`2925ENSURE_MAIL2925_SESSION 通信失败,当前地址=${failedUrl || 'unknown'};原因=${getErrorMessage(err) || 'unknown'}`, 'warn');
const stopped = await stopAutoRunForMail2925LoginFailure(
`2925${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'}),已按手动停止逻辑暂停自动流程。`
);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw err;
}
if (!result?.loggedIn) {
const notLoggedInUrl = await getMail2925CurrentTabUrl();
await addLog(`2925:20 秒登录等待结束但仍未进入收件箱,当前地址=${notLoggedInUrl || 'unknown'}`, 'warn');
const stopped = await stopAutoRunForMail2925LoginFailure(
`2925${actionLabel}失败(20 秒内未进入收件箱),已按手动停止逻辑暂停自动流程。`
);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw new Error(`2925${actionLabel}失败,当前页面仍未进入收件箱。`);
}
if (result?.error) {
const resultErrorUrl = await getMail2925CurrentTabUrl();
await addLog(`2925:登录页返回业务错误,当前地址=${resultErrorUrl || 'unknown'};错误=${result.error}`, 'warn');
const stopped = await stopAutoRunForMail2925LoginFailure(
`2925${actionLabel}失败(${result.error}),已按手动停止逻辑暂停自动流程。`
);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw new Error(result.error);
}
if (result?.limitReached) {
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '2925 子邮箱已达上限邮箱'}`);
}
if (!result?.loggedIn) {
throw new Error(`2925${actionLabel}失败,当前页面仍未进入收件箱。`);
}
await patchMail2925Account(account.id, {
lastLoginAt: Date.now(),
lastError: '',
});
await setState({ currentMail2925AccountId: account.id });
broadcastDataUpdate({ currentMail2925AccountId: account.id });
return {
account: await ensureMail2925AccountForFlow({
allowAllocate: false,
preferredAccountId: account.id,
}),
mail: getMail2925MailConfig(),
result,
};
}
// Override the earlier version with a simpler login-page-only flow.
async function ensureMail2925MailboxSession(options = {}) {
const {
accountId = null,
forceRelogin = false,
actionLabel = '确保 2925 邮箱登录态',
} = options;
const account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
});
if (forceRelogin) {
const removedCount = await clearMail2925SessionCookies();
await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info');
if (typeof sleepWithStop === 'function') {
await addLog('2925:清理 cookie 后等待 3 秒,再打开登录页...', 'info');
await sleepWithStop(3000);
}
}
throwIfStopped();
await addLog(`2925:准备打开登录页 ${MAIL2925_LOGIN_URL}forceRelogin=${forceRelogin ? 'true' : 'false'}`, 'info');
const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL;
const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
});
const openedUrl = await getMail2925CurrentTabUrl();
await addLog(`2925:打开页后当前标签地址:${openedUrl || 'unknown'}`, 'info');
if (forceRelogin && typeof waitForTabUrlMatch === 'function') {
const matchedLoginTab = await waitForTabUrlMatch(
tabId,
(url) => {
try {
const parsed = new URL(String(url || ''));
return (parsed.hostname === '2925.com' || parsed.hostname === 'www.2925.com')
&& /^\/login\/?$/.test(parsed.pathname);
} catch {
return false;
}
},
{ timeoutMs: 15000, retryDelayMs: 300 }
);
await addLog(`2925:等待最终落到登录页结果:${matchedLoginTab?.url || 'timeout'}`, matchedLoginTab ? 'info' : 'warn');
if (matchedLoginTab && typeof ensureContentScriptReadyOnTab === 'function') {
await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, tabId, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
timeoutMs: 20000,
retryDelayMs: 800,
logMessage: '步骤 0:2925 登录页内容脚本未就绪,正在等待页面稳定后继续登录...',
});
}
}
if (forceRelogin && typeof sleepWithStop === 'function') {
await addLog('2925:登录页已打开,等待 3 秒后开始检查输入框并执行登录...', 'info');
await sleepWithStop(3000);
let account = null;
if (forceRelogin || (allowLoginWhenOnLoginPage && normalizedExpectedMailboxEmail)) {
account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
});
}
const sendLoginMessage = typeof sendToContentScriptResilient === 'function'
@@ -551,9 +430,98 @@
}
);
const buildSuccessPayload = () => ({
account,
mail: getMail2925MailConfig(),
result: {
loggedIn: true,
currentView: 'mailbox',
usedExistingSession: true,
},
});
const failMailboxSession = async (message) => {
const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw new Error(message);
};
if (forceRelogin) {
const removedCount = await clearMail2925SessionCookies();
await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info');
if (typeof sleepWithStop === 'function') {
await addLog('2925:清理 cookie 后等待 3 秒,再打开登录页...', 'info');
await sleepWithStop(3000);
}
}
throwIfStopped();
const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL;
await addLog(
forceRelogin
? `2925:准备打开登录页 ${MAIL2925_LOGIN_URL}(强制重登录)`
: `2925:准备打开邮箱页 ${MAIL2925_URL}(登录页自动登录=${allowLoginWhenOnLoginPage ? '开启' : '关闭'}`,
'info'
);
const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
});
let openedUrl = await getMail2925TabUrlById(tabId);
if (!openedUrl) {
openedUrl = await getMail2925CurrentTabUrl();
}
await addLog(`2925:打开页后当前标签地址:${openedUrl || 'unknown'}`, 'info');
if (forceRelogin && typeof waitForTabUrlMatch === 'function') {
const matchedLoginTab = await waitForTabUrlMatch(
tabId,
(url) => isMail2925LoginUrl(url),
{ timeoutMs: 15000, retryDelayMs: 300 }
);
await addLog(`2925:等待最终落到登录页结果:${matchedLoginTab?.url || '超时'}`, matchedLoginTab ? 'info' : 'warn');
if (matchedLoginTab?.url) {
openedUrl = String(matchedLoginTab.url || '').trim();
}
}
if (!forceRelogin && !isMail2925LoginUrl(openedUrl) && !normalizedExpectedMailboxEmail) {
await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info');
return buildSuccessPayload();
}
if (!forceRelogin && isMail2925LoginUrl(openedUrl) && !allowLoginWhenOnLoginPage) {
await failMailboxSession(`2925${actionLabel}失败,当前页面已跳转到登录页,且当前未启用 2925 账号池,不执行自动登录。`);
}
if (!account && (forceRelogin || allowLoginWhenOnLoginPage)) {
account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
});
}
if (typeof ensureContentScriptReadyOnTab === 'function') {
await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, tabId, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
timeoutMs: 20000,
retryDelayMs: 800,
logMessage: '步骤 0:2925 登录页内容脚本未就绪,正在等待页面稳定后继续登录...',
});
}
if (forceRelogin && typeof sleepWithStop === 'function') {
await addLog('2925:登录页已打开,等待 3 秒后开始检查输入框并执行登录...', 'info');
await sleepWithStop(3000);
}
let result;
try {
const beforeSendUrl = await getMail2925CurrentTabUrl();
const beforeSendUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl();
await addLog(`2925:发送 ENSURE_MAIL2925_SESSION 前当前地址:${beforeSendUrl || 'unknown'}`, 'info');
result = await sendLoginMessage(
MAIL2925_SOURCE,
@@ -562,22 +530,22 @@
step: 0,
source: 'background',
payload: {
email: account.email,
password: account.password,
email: account?.email || '',
password: account?.password || '',
forceLogin: forceRelogin,
allowLoginWhenOnLoginPage,
},
},
{
timeoutMs: forceRelogin ? 30000 : 25000,
timeoutMs: 50000,
retryDelayMs: 800,
responseTimeoutMs: forceRelogin ? 30000 : 25000,
logMessage: `步骤 0:2925 登录页通信异常,正在等待当前页面重新就绪后继续确认登录态...`,
responseTimeoutMs: 50000,
logMessage: '步骤 0:2925 登录页通信异常,正在等待页面恢复...',
}
);
} catch (err) {
const stopped = await stopAutoRunForMail2925LoginFailure(
`2925${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'}),已按手动停止逻辑暂停自动流程。`
);
const message = `2925${actionLabel}失败(${getErrorMessage(err) || '40 秒内未进入收件箱'})。`;
const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`);
if (stopped) {
throw new Error('流程已被用户停止。');
}
@@ -585,27 +553,35 @@
}
if (result?.error) {
const stopped = await stopAutoRunForMail2925LoginFailure(
`2925${actionLabel}失败(${result.error}),已按手动停止逻辑暂停自动流程。`
);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw new Error(result.error);
await failMailboxSession(`2925${actionLabel}失败(${result.error})。`);
}
if (result?.limitReached) {
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '2925 子邮箱已达上限邮箱'}`);
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '子邮箱已达上限邮箱'}`);
}
if (!result?.loggedIn) {
const stopped = await stopAutoRunForMail2925LoginFailure(
`2925${actionLabel}失败(20 秒内未进入收件箱),已按手动停止逻辑暂停自动流程。`
);
if (stopped) {
throw new Error('流程已被用户停止。');
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,
});
}
throw new Error(`2925${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}失败,登录后仍未进入收件箱。`);
}
await patchMail2925Account(account.id, {
@@ -614,7 +590,8 @@
});
await setState({ currentMail2925AccountId: account.id });
broadcastDataUpdate({ currentMail2925AccountId: account.id });
const finalUrl = await getMail2925CurrentTabUrl();
const finalUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl();
await addLog(`2925:登录态确认成功,当前地址=${finalUrl || 'unknown'}`, 'ok');
return {
@@ -632,10 +609,21 @@
|| '子邮箱已达上限邮箱';
const state = await getState();
const currentAccount = getCurrentMail2925Account(state);
const poolEnabled = Boolean(state?.mail2925UseAccountPool);
if (!poolEnabled) {
if (typeof requestStop === 'function') {
await requestStop({
logMessage: `步骤 ${step}2925 检测到“${reason}”,当前未启用账号池,已按手动停止逻辑暂停自动流程。`,
});
}
return new Error('流程已被用户停止。');
}
if (!currentAccount) {
if (typeof requestStop === 'function') {
await requestStop({
logMessage: `步骤 ${step}2925 检测到“${reason}”,当前没有可识别账号,已按手动停止逻辑暂停流程`,
logMessage: `步骤 ${step}2925 检测到“${reason}”,当前没有可识别账号可供切换`,
});
}
return new Error('流程已被用户停止。');
@@ -648,7 +636,7 @@
lastError: reason,
});
await addLog(
`步骤 ${step}2925 账号 ${currentAccount.email} 命中“${reason}”,已禁用 24 小时,恢复时间 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })}。`,
`步骤 ${step}2925 账号 ${currentAccount.email} 命中“${reason}”,已禁用 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })}。`,
'warn'
);
@@ -659,11 +647,12 @@
});
if (!nextAccount) {
await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
if (typeof requestStop === 'function') {
await requestStop({
logMessage: `步骤 ${step}2925 账号 ${currentAccount.email} 已因${reason}禁用 24 小时,且当前没有可切换的下一个账号,已按手动停止逻辑暂停流程`,
logMessage: `步骤 ${step}2925 账号 ${currentAccount.email} 命中${reason},但当前没有可切换的下一个账号。`,
});
}
return new Error('流程已被用户停止。');
@@ -673,11 +662,12 @@
await ensureMail2925MailboxSession({
accountId: nextAccount.id,
forceRelogin: true,
allowLoginWhenOnLoginPage: true,
actionLabel: `步骤 ${step}:切换 2925 账号`,
});
await addLog(`步骤 ${step}2925 已自动切换到下一个账号 ${nextAccount.email} 并完成登录,当前尝试将直接结束`, 'warn');
await addLog(`步骤 ${step}2925 已切换到下一个账号 ${nextAccount.email}`, 'warn');
return buildMail2925ThreadTerminatedError(
`步骤 ${step}2925 账号 ${currentAccount.email} 命中“${reason}并已禁用 24 小时,已切换到 ${nextAccount.email},当前尝试结束,等待自动重试进入下一次尝试。`
`步骤 ${step}2925 账号 ${currentAccount.email} 命中“${reason}”,已切换到 ${nextAccount.email},当前尝试结束,等待下一轮重试。`
);
}
-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} 轮点击“继续”后页面仍无反应。`);
}
+25 -26
View File
@@ -9,7 +9,6 @@
chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
confirmCustomVerificationStepBypass,
ensureMail2925MailboxSession,
ensureStep8VerificationPageReady,
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
@@ -58,6 +57,28 @@
return String(value || '').trim().toLowerCase();
}
async function focusOrOpenMailTab(mail) {
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
await chrome.tabs.update(tabId, { active: true });
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
async function runStep8Attempt(state) {
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
@@ -111,33 +132,11 @@
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
) {
await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`);
} else if (mail.provider === '2925') {
if (state?.mail2925UseAccountPool && typeof ensureMail2925MailboxSession === 'function') {
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
actionLabel: '步骤 8:确认 2925 邮箱登录态',
});
}
await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`);
} else {
await addLog(`步骤 8:正在打开${mail.label}...`);
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
} else {
const tabId = await getTabId(mail.source);
await chrome.tabs.update(tabId, { active: true });
}
} else {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
await focusOrOpenMailTab(mail);
if (mail.provider === '2925') {
await addLog(`步骤 8:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
}
}
+59 -22
View File
@@ -24,15 +24,53 @@
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) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
await chrome.tabs.update(tabId, { active: true });
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
async function executeStep4(state) {
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const stepStartedAt = Date.now();
const verificationFilterAfterTimestamp = mail.provider === '2925'
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
const verificationSessionKey = `4:${stepStartedAt}`;
const signupTabId = await getTabId('signup-page');
if (!signupTabId) {
throw new Error('认证页面标签页已关闭,无法继续步骤 4。');
}
@@ -40,6 +78,7 @@
await chrome.tabs.update(signupTabId, { active: true });
throwIfStopped();
await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
const prepareResult = await sendToContentScriptResilient(
'signup-page',
{
@@ -73,43 +112,41 @@
}
throwIfStopped();
if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
if (
mail.provider === HOTMAIL_PROVIDER
|| mail.provider === LUCKMAIL_PROVIDER
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
) {
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
} else if (mail.provider === '2925') {
if (state?.mail2925UseAccountPool && typeof ensureMail2925MailboxSession === 'function') {
await addLog(`步骤 4:正在打开${mail.label}...`);
if (typeof ensureMail2925MailboxSession === 'function') {
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
} else {
await focusOrOpenMailTab(mail);
}
await addLog(`步骤 4正在通过 ${mail.label} 轮询验证码...`);
await addLog(`步骤 4将直接使用当前已登录的 ${mail.label} 轮询验证码`, 'info');
} else {
await addLog(`步骤 4:正在打开${mail.label}...`);
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
} else {
const tabId = await getTabId(mail.source);
await chrome.tabs.update(tabId, { active: true });
}
} else {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
await focusOrOpenMailTab(mail);
}
const shouldRequestFreshCodeFirst = ![
HOTMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
].includes(mail.provider);
await resolveVerificationStep(4, state, mail, {
filterAfterTimestamp: verificationFilterAfterTimestamp,
sessionKey: verificationSessionKey,
disableTimeBudgetCap: mail.provider === '2925',
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
requestFreshCodeFirst: shouldRequestFreshCodeFirst,
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
+2 -1
View File
@@ -79,7 +79,8 @@
source: 'background',
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword },
}, {
timeoutMs: 30000,
timeoutMs: 125000,
responseTimeoutMs: 125000,
retryDelayMs: 700,
logMessage: '步骤 10:CPA 面板通信未就绪,正在等待页面恢复...',
});
+4
View File
@@ -113,12 +113,15 @@
function getVerificationPollPayload(step, state, overrides = {}) {
const is2925Provider = state?.mailProvider === '2925';
const mail2925MatchTargetEmail = is2925Provider
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive';
if (step === 4) {
return {
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(4, state),
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
targetEmail: state.email,
mail2925MatchTargetEmail,
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
...overrides,
@@ -130,6 +133,7 @@
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
targetEmail: String(state?.step8VerificationTargetEmail || '').trim() || state.email,
mail2925MatchTargetEmail,
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
...overrides,
+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 {
+130 -3
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
@@ -629,6 +686,39 @@ function extractVerificationCode(text, strictChatGPTCodeOnly = false) {
return null;
}
function extractEmails(text = '') {
const matches = String(text || '').match(/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi) || [];
return [...new Set(matches.map((item) => item.toLowerCase()))];
}
function emailMatchesTarget(candidate, targetEmail) {
const normalizedCandidate = String(candidate || '').trim().toLowerCase();
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
return Boolean(normalizedCandidate && normalizedTarget && normalizedCandidate === normalizedTarget);
}
function getTargetEmailMatchState(text, targetEmail) {
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
if (!normalizedTarget) {
return { matches: true, hasExplicitEmail: false };
}
const normalizedText = String(text || '').toLowerCase();
if (normalizedText.includes(normalizedTarget)) {
return { matches: true, hasExplicitEmail: true };
}
const extractedEmails = extractEmails(normalizedText);
if (!extractedEmails.length) {
return { matches: true, hasExplicitEmail: false };
}
return {
matches: extractedEmails.some((candidate) => emailMatchesTarget(candidate, normalizedTarget)),
hasExplicitEmail: true,
};
}
function normalizeMinuteTimestamp(timestamp) {
if (!Number.isFinite(timestamp) || timestamp <= 0) return 0;
const date = new Date(timestamp);
@@ -840,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) {
@@ -860,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);
@@ -875,6 +976,7 @@ async function ensureMail2925Session(payload = {}) {
ok: true,
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: loginState.mailboxEmail || '',
};
}
if (loginState.view === 'limit') {
@@ -886,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();
@@ -902,11 +1013,12 @@ async function ensureMail2925Session(payload = {}) {
await sleep(150);
fillInput(passwordInput, password);
await sleep(200);
await sleep(1000);
log(`步骤 0:2925 已定位到登录表单,准备点击“登录”,当前地址 ${location.href}`, 'info');
simulateClick(loginButton);
log(`步骤 0:2925 已点击“登录”,点击后地址 ${location.href}`, 'info');
const finalState = await waitForMail2925View('mailbox', 20000);
const finalState = await waitForMail2925View('mailbox', 40000);
log(`步骤 0:2925 登录等待结束,状态=${finalState.view},地址=${location.href}`, 'info');
if (finalState.view !== 'mailbox') {
throw new Error('2925:提交账号密码后未进入收件箱。');
@@ -917,6 +1029,7 @@ async function ensureMail2925Session(payload = {}) {
loggedIn: true,
currentView: 'mailbox',
usedCredentials: true,
mailboxEmail: finalState.mailboxEmail || getMail2925DisplayedMailboxEmail() || '',
};
}
@@ -930,6 +1043,8 @@ async function handlePollEmail(step, payload) {
filterAfterTimestamp = 0,
excludeCodes = [],
strictChatGPTCodeOnly = false,
targetEmail = '',
mail2925MatchTargetEmail = false,
} = payload || {};
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
@@ -994,9 +1109,21 @@ async function handlePollEmail(step, payload) {
if (!matchesMailFilters(previewText, senderFilters, subjectFilters)) {
continue;
}
const previewTargetState = mail2925MatchTargetEmail
? getTargetEmailMatchState(previewText, targetEmail)
: { matches: true, hasExplicitEmail: false };
if (mail2925MatchTargetEmail && previewTargetState.hasExplicitEmail && !previewTargetState.matches) {
continue;
}
const previewCode = extractVerificationCode(previewText, strictChatGPTCodeOnly);
const openedText = await openMailAndDeleteAfterRead(item, step);
const openedTargetState = mail2925MatchTargetEmail
? getTargetEmailMatchState(openedText, targetEmail)
: { matches: true, hasExplicitEmail: false };
if (mail2925MatchTargetEmail && openedTargetState.hasExplicitEmail && !openedTargetState.matches) {
continue;
}
const bodyCode = extractVerificationCode(openedText, strictChatGPTCodeOnly);
const candidateCode = bodyCode || previewCode;
+509 -58
View File
@@ -114,16 +114,24 @@ function isVisibleElement(el) {
&& rect.height > 0;
}
function getVisibleSplitVerificationInputs() {
return Array.from(document.querySelectorAll('input[maxlength="1"]'))
.filter(isVisibleElement);
}
function getVerificationCodeTarget() {
const splitInputs = getVisibleSplitVerificationInputs();
const codeInput = document.querySelector(VERIFICATION_CODE_INPUT_SELECTOR);
if (codeInput && isVisibleElement(codeInput)) {
const maxLength = Number(codeInput.getAttribute?.('maxlength') || codeInput.maxLength || 0);
if (maxLength === 1 && splitInputs.length >= 6) {
return { type: 'split', elements: splitInputs };
}
return { type: 'single', element: codeInput };
}
const singleInputs = Array.from(document.querySelectorAll('input[maxlength="1"]'))
.filter(isVisibleElement);
if (singleInputs.length >= 6) {
return { type: 'split', elements: singleInputs };
if (splitInputs.length >= 6) {
return { type: 'split', elements: splitInputs };
}
return null;
@@ -377,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(),
@@ -391,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)
@@ -409,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);
@@ -417,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,
@@ -612,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);
}
@@ -627,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;
@@ -1198,7 +1411,7 @@ function getSignupPasswordTimeoutErrorPageState() {
function getLoginTimeoutErrorPageState() {
return getAuthTimeoutErrorPageState({
pathPatterns: [/\/log-in(?:[/?#]|$)/i],
pathPatterns: getLoginAuthRetryPathPatterns(),
});
}
@@ -1263,13 +1476,6 @@ function inspectLoginAuthState() {
consentReady,
};
if (verificationTarget) {
return {
...baseState,
state: 'verification_page',
};
}
if (retryState) {
return {
...baseState,
@@ -1277,6 +1483,13 @@ function inspectLoginAuthState() {
};
}
if (verificationTarget) {
return {
...baseState,
state: 'verification_page',
};
}
if (addPhonePage) {
return {
...baseState,
@@ -1434,6 +1647,86 @@ async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, messag
});
}
async function finalizeStep6VerificationReady(options = {}) {
const {
logLabel = '步骤 7 收尾',
loginVerificationRequestedAt = null,
timeout = 12000,
via = 'verification_page_ready',
} = options;
const start = Date.now();
const maxRounds = 3;
const settleDelayMs = 3000;
let round = 0;
while (Date.now() - start < timeout && round < maxRounds) {
throwIfStopped();
round += 1;
log(`${logLabel}:确认页面是否稳定停留在登录验证码阶段(第 ${round}/${maxRounds} 轮,先等待 3 秒)...`, 'info');
await sleep(settleDelayMs);
const rawSnapshot = inspectLoginAuthState();
const snapshot = normalizeStep6Snapshot(rawSnapshot);
if (snapshot.state === 'verification_page') {
log(`${logLabel}:登录验证码页面已稳定就绪。`, 'ok');
return createStep6SuccessResult(snapshot, {
via,
loginVerificationRequestedAt,
});
}
if (snapshot.state === 'login_timeout_error_page') {
log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn');
return createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page',
snapshot,
'登录验证码页面准备就绪前进入登录超时报错页。'
);
}
if (snapshot.state === 'password_page' || snapshot.state === 'email_page') {
return createStep6RecoverableResult('verification_page_unstable', snapshot, {
message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 7。`,
loginVerificationRequestedAt,
});
}
if (snapshot.state === 'add_phone_page') {
throw new Error(`登录验证码页面准备过程中页面进入手机号页面。URL: ${snapshot.url}`);
}
}
const rawSnapshot = inspectLoginAuthState();
const snapshot = normalizeStep6Snapshot(rawSnapshot);
if (snapshot.state === 'verification_page') {
log(`${logLabel}:登录验证码页面已稳定就绪。`, 'ok');
return createStep6SuccessResult(snapshot, {
via,
loginVerificationRequestedAt,
});
}
if (snapshot.state === 'login_timeout_error_page') {
log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn');
return createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page',
snapshot,
'登录验证码页面准备就绪前进入登录超时报错页。'
);
}
if (snapshot.state === 'password_page' || snapshot.state === 'email_page') {
return createStep6RecoverableResult('verification_page_unstable', snapshot, {
message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 7。`,
loginVerificationRequestedAt,
});
}
return createStep6RecoverableResult('verification_page_finalize_unknown', snapshot, {
message: '登录验证码页面状态在收尾确认阶段未稳定,准备重新执行步骤 7。',
loginVerificationRequestedAt,
});
}
function normalizeStep6Snapshot(snapshot) {
if (snapshot?.state !== 'oauth_consent_page') {
return snapshot;
@@ -1552,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();
@@ -1590,6 +1884,10 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
}
if (snapshot.state === 'password') {
if (!passwordPageDiagnosticsLogged) {
passwordPageDiagnosticsLogged = true;
logSignupPasswordDiagnostics(`${prepareLogLabel}:页面仍停留在密码页`);
}
if (!password) {
throw new Error('当前回到了密码页,但没有可用密码,无法自动重新提交。');
}
@@ -1622,15 +1920,30 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
async function waitForVerificationSubmitOutcome(step, timeout) {
const resolvedTimeout = timeout ?? (step === 8 ? 30000 : 12000);
const start = Date.now();
let recoveryCount = 0;
const maxRecoveryCount = 2;
while (Date.now() - start < resolvedTimeout) {
throwIfStopped();
if (step === 4) {
const signupRetryState = getCurrentAuthRetryPageState('signup');
if (signupRetryState?.userAlreadyExistsBlocked) {
throw createSignupUserAlreadyExistsError();
const retryFlow = step === 4 ? 'signup' : 'login';
const retryState = getCurrentAuthRetryPageState(retryFlow);
if (retryState?.userAlreadyExistsBlocked) {
throw createSignupUserAlreadyExistsError();
}
if (retryState) {
if (recoveryCount >= maxRecoveryCount) {
throw new Error(`步骤 ${step}:验证码提交后连续进入认证重试页 ${maxRecoveryCount} 次,页面仍未恢复。URL: ${location.href}`);
}
recoveryCount += 1;
log(`步骤 ${step}:验证码提交后进入认证重试页,正在自动恢复(${recoveryCount}/${maxRecoveryCount}...`, 'warn');
await recoverCurrentAuthRetryPage({
flow: retryFlow,
logLabel: `步骤 ${step}:验证码提交后检测到认证重试页,正在点击“重试”恢复`,
step,
timeoutMs: 12000,
});
continue;
}
const errorText = getVerificationErrorText();
@@ -1670,6 +1983,96 @@ async function waitForVerificationSubmitOutcome(step, timeout) {
return { success: true, assumed: true };
}
function getVerificationSubmitButtonForTarget(codeInput, options = {}) {
const { allowDisabled = false } = options;
const form = codeInput?.form || codeInput?.closest?.('form') || null;
const isUsableAction = (element) => {
if (!element || !isVisibleElement(element)) return false;
return allowDisabled || isActionEnabled(element);
};
const findSubmitInRoot = (root) => {
if (!root?.querySelectorAll) return null;
const directCandidates = root.querySelectorAll('button[type="submit"], input[type="submit"]');
for (const element of directCandidates) {
if (isUsableAction(element)) {
return element;
}
}
const textCandidates = root.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]');
return Array.from(textCandidates).find((element) => {
if (!isUsableAction(element)) return false;
const text = getActionText(element);
return /verify|confirm|submit|continue|确认|验证|继续/i.test(text);
}) || null;
};
return findSubmitInRoot(form) || findSubmitInRoot(document);
}
async function waitForVerificationSubmitButton(codeInput, timeout = 5000) {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
if (is405MethodNotAllowedPage()) {
throw new Error('当前页面处于 405 错误恢复流程中,暂时无法定位验证码提交按钮。');
}
const button = getVerificationSubmitButtonForTarget(codeInput, { allowDisabled: false });
if (button) {
return button;
}
await sleep(150);
}
return null;
}
async function waitForVerificationCodeTarget(timeout = 10000) {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
if (is405MethodNotAllowedPage()) {
throw new Error('当前页面处于 405 错误恢复流程中,暂时无法定位验证码输入框。');
}
const target = getVerificationCodeTarget();
if (target) {
return target;
}
await sleep(150);
}
throw new Error('未找到验证码输入框。URL: ' + location.href);
}
async function waitForSplitVerificationInputsFilled(inputs, code, timeout = 2500) {
const expected = String(code || '').slice(0, 6);
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
const current = Array.from(inputs || [])
.slice(0, expected.length)
.map((input) => String(input?.value || '').trim())
.join('');
if (current === expected) {
return true;
}
await sleep(100);
}
return false;
}
async function fillVerificationCode(step, payload) {
const { code } = payload;
if (!code) throw new Error('未提供验证码。');
@@ -1684,6 +2087,7 @@ async function fillVerificationCode(step, payload) {
// Retry with 405 error recovery if needed
const maxRetries = 3;
let codeInput = null;
let splitInputs = null;
for (let retry = 0; retry <= maxRetries; retry++) {
throwIfStopped();
@@ -1696,28 +2100,14 @@ async function fillVerificationCode(step, payload) {
}
try {
codeInput = await waitForElement(VERIFICATION_CODE_INPUT_SELECTOR, 10000);
const verificationTarget = await waitForVerificationCodeTarget(10000);
if (verificationTarget.type === 'split') {
splitInputs = verificationTarget.elements;
} else {
codeInput = verificationTarget.element;
}
break; // Found it
} catch {
// Check for multiple single-digit inputs (common pattern)
const singleInputs = document.querySelectorAll('input[maxlength="1"]');
if (singleInputs.length >= 6) {
log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`);
for (let i = 0; i < 6 && i < singleInputs.length; i++) {
fillInput(singleInputs[i], code[i]);
await sleep(100);
}
const outcome = await waitForVerificationSubmitOutcome(step);
if (outcome.invalidCode) {
log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
} else if (outcome.addPhonePage) {
log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn');
} else {
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}`, 'ok');
}
return outcome;
}
// No input found — check if it's a 405 error and can be recovered
if (is405MethodNotAllowedPage() && retry < maxRetries) {
log(`步骤 ${step}:未找到验证码输入框且页面出现 405 错误,正在恢复...`, 'warn');
@@ -1729,6 +2119,51 @@ async function fillVerificationCode(step, payload) {
}
}
if (splitInputs?.length >= 6) {
log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`);
for (let i = 0; i < 6 && i < splitInputs.length; i++) {
const targetInput = splitInputs[i];
try {
targetInput.focus?.();
} catch {}
fillInput(splitInputs[i], code[i]);
try {
targetInput.dispatchEvent(new KeyboardEvent('keyup', { key: code[i], bubbles: true }));
} catch {}
await sleep(100);
}
const filled = await waitForSplitVerificationInputsFilled(splitInputs, code, 2500);
if (!filled) {
const current = Array.from(splitInputs)
.slice(0, 6)
.map((input) => String(input?.value || '').trim() || '_')
.join('');
log(`步骤 ${step}:分格验证码输入框未稳定呈现目标值,当前页面值为 ${current},准备继续观察提交流程。`, 'warn');
} else {
log(`步骤 ${step}:分格验证码输入框已稳定显示 ${code}`, 'info');
}
await sleep(800);
const splitSubmitBtn = await waitForVerificationSubmitButton(splitInputs[0], 2000).catch(() => null);
if (splitSubmitBtn) {
await humanPause(450, 1200);
simulateClick(splitSubmitBtn);
log(`步骤 ${step}:分格验证码已提交`);
} else {
log(`步骤 ${step}:分格验证码页面未找到可点击提交按钮,继续等待页面自动推进。`, 'info');
}
const outcome = await waitForVerificationSubmitOutcome(step);
if (outcome.invalidCode) {
log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
} else if (outcome.addPhonePage) {
log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn');
} else {
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}`, 'ok');
}
return outcome;
}
if (!codeInput) {
throw new Error('未找到验证码输入框。URL: ' + location.href);
}
@@ -1736,17 +2171,16 @@ async function fillVerificationCode(step, payload) {
fillInput(codeInput, code);
log(`步骤 ${step}:验证码已填写`);
// Report complete BEFORE submit (page may navigate away)
// Submit
await sleep(500);
const submitBtn = document.querySelector('button[type="submit"]')
|| await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证/i, 5000).catch(() => null);
await sleep(800);
const submitBtn = await waitForVerificationSubmitButton(codeInput, 5000).catch(() => null);
if (submitBtn) {
await humanPause(450, 1200);
simulateClick(submitBtn);
log(`步骤 ${step}:验证码已提交`);
} else {
log(`步骤 ${step}:未找到可提交的验证码按钮,先等待页面自动推进或反馈结果。`, 'warn');
}
const outcome = await waitForVerificationSubmitOutcome(step);
@@ -1999,7 +2433,15 @@ async function step6SwitchToOneTimeCodeLogin(snapshot) {
simulateClick(switchTrigger);
log('步骤 7:已点击一次性验证码登录');
await sleep(1200);
return waitForStep6SwitchTransition(loginVerificationRequestedAt);
const result = await waitForStep6SwitchTransition(loginVerificationRequestedAt);
if (result?.step6Outcome === 'success') {
return finalizeStep6VerificationReady({
logLabel: '步骤 7 收尾',
loginVerificationRequestedAt: result.loginVerificationRequestedAt || loginVerificationRequestedAt,
via: result.via || 'switch_to_one_time_code_login',
});
}
return result;
}
async function step6LoginFromPasswordPage(payload, snapshot) {
@@ -2030,8 +2472,11 @@ async function step6LoginFromPasswordPage(payload, snapshot) {
const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt);
if (transition.action === 'done') {
log('步骤 7:已进入登录验证码页面。', 'ok');
return transition.result;
return finalizeStep6VerificationReady({
logLabel: '步骤 7 收尾',
loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || passwordSubmittedAt,
via: transition.result.via || 'password_submit',
});
}
if (transition.action === 'recoverable') {
log(`步骤 7${transition.result.message || '提交密码后仍未进入登录验证码页面,准备重新执行步骤 7。'}`, 'warn');
@@ -2077,8 +2522,11 @@ async function step6LoginFromEmailPage(payload, snapshot) {
const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt);
if (transition.action === 'done') {
log('步骤 7:已进入登录验证码页面。', 'ok');
return transition.result;
return finalizeStep6VerificationReady({
logLabel: '步骤 7 收尾',
loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || emailSubmittedAt,
via: transition.result.via || 'email_submit',
});
}
if (transition.action === 'recoverable') {
log(`步骤 7${transition.result.message || '提交邮箱后仍未进入目标页面,准备重新执行步骤 7。'}`, 'warn');
@@ -2102,8 +2550,11 @@ async function step6_login(payload) {
const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000));
if (snapshot.state === 'verification_page') {
log('步骤 7:登录验证码页面已就绪。', 'ok');
return createStep6SuccessResult(snapshot, { via: 'already_on_verification_page' });
return finalizeStep6VerificationReady({
logLabel: '步骤 7 收尾',
loginVerificationRequestedAt: null,
via: 'already_on_verification_page',
});
}
if (snapshot.state === 'login_timeout_error_page') {
+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` 中粘贴脚本时,请先清空旧内容,再完整粘贴新代码并保存。
+29
View File
@@ -3,6 +3,9 @@
})(typeof self !== 'undefined' ? self : globalThis, function createManagedAliasUtilsModule() {
const GMAIL_PROVIDER = 'gmail';
const MAIL_2925_PROVIDER = '2925';
const MAIL_2925_MODE_PROVIDE = 'provide';
const MAIL_2925_MODE_RECEIVE = 'receive';
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
const PROVIDER_CONFIGS = {
[GMAIL_PROVIDER]: {
@@ -64,10 +67,31 @@
return PROVIDER_CONFIGS[String(provider || '').trim().toLowerCase()] || null;
}
function normalizeMail2925Mode(value = '') {
return String(value || '').trim().toLowerCase() === MAIL_2925_MODE_RECEIVE
? MAIL_2925_MODE_RECEIVE
: DEFAULT_MAIL_2925_MODE;
}
function isManagedAliasProvider(provider = '') {
return Boolean(getManagedAliasProviderConfig(provider));
}
function usesManagedAliasGeneration(provider = '', options = {}) {
const normalizedProvider = String(provider || '').trim().toLowerCase();
if (!isManagedAliasProvider(normalizedProvider)) {
return false;
}
if (normalizedProvider !== MAIL_2925_PROVIDER) {
return true;
}
const mail2925Mode = typeof options === 'string'
? options
: options?.mail2925Mode;
return normalizeMail2925Mode(mail2925Mode) === MAIL_2925_MODE_PROVIDE;
}
function parseEmailParts(rawValue = '') {
const value = String(rawValue || '').trim().toLowerCase();
const match = value.match(/^([^@\s]+)@([^@\s]+\.[^@\s]+)$/);
@@ -136,11 +160,16 @@
return {
buildManagedAliasEmail,
DEFAULT_MAIL_2925_MODE,
getManagedAliasProviderConfig,
getManagedAliasProviderUiCopy,
isManagedAliasEmail,
isManagedAliasProvider,
MAIL_2925_MODE_PROVIDE,
MAIL_2925_MODE_RECEIVE,
normalizeMail2925Mode,
parseEmailParts,
parseManagedAliasBaseEmail,
usesManagedAliasGeneration,
};
});
+3 -3
View File
@@ -1,8 +1,8 @@
{
"manifest_version": 3,
"name": "多页面自动化",
"version": "5.0",
"version_name": "Pro5.0",
"name": "codex-oauth-automation-extension",
"version": "5.8",
"version_name": "Pro5.8",
"description": "用于自动执行多步骤 OAuth 注册流程",
"permissions": [
"sidePanel",
+58
View File
@@ -0,0 +1,58 @@
(function attachSidepanelAccountPoolUi(globalScope) {
function createAccountPoolFormController(options = {}) {
const {
formShell = null,
toggleButton = null,
hiddenLabel = '添加账号',
visibleLabel = '取消添加',
onClear = null,
onFocus = null,
} = options;
let visible = false;
function sync() {
if (formShell) {
formShell.hidden = !visible;
}
if (toggleButton) {
toggleButton.textContent = visible ? visibleLabel : hiddenLabel;
toggleButton.setAttribute('aria-expanded', String(visible));
}
}
function setVisible(nextVisible, controlOptions = {}) {
const {
clearForm = false,
focusField = false,
} = controlOptions;
visible = Boolean(nextVisible);
if (clearForm && typeof onClear === 'function') {
onClear();
}
sync();
if (visible && focusField && typeof onFocus === 'function') {
onFocus();
}
}
function isVisible() {
return visible;
}
sync();
return {
isVisible,
setVisible,
sync,
};
}
globalScope.SidepanelAccountPoolUi = {
createAccountPoolFormController,
};
})(window);
+30 -1
View File
@@ -12,6 +12,7 @@
const expandedStorageKey = constants.expandedStorageKey || 'multipage-hotmail-list-expanded';
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
const copyIcon = constants.copyIcon || '';
const createAccountPoolFormController = globalScope.SidepanelAccountPoolUi?.createAccountPoolFormController;
let actionInFlight = false;
let listExpanded = false;
@@ -177,6 +178,25 @@
dom.inputHotmailRefreshToken.value = '';
}
const formController = typeof createAccountPoolFormController === 'function'
? createAccountPoolFormController({
formShell: dom.hotmailFormShell,
toggleButton: dom.btnToggleHotmailForm,
hiddenLabel: '添加账号',
visibleLabel: '取消添加',
onClear: () => {
clearHotmailForm();
},
onFocus: () => {
dom.inputHotmailEmail?.focus?.();
},
})
: {
isVisible: () => false,
setVisible() {},
sync() {},
};
function renderHotmailAccounts() {
if (!dom.hotmailAccountsList) return;
const latestState = state.getLatestState();
@@ -318,7 +338,7 @@
}
helpers.showToast(`已保存 Hotmail 账号 ${email}`, 'success', 1800);
clearHotmailForm();
formController.setVisible(false, { clearForm: true });
} catch (err) {
helpers.showToast(`保存 Hotmail 账号失败:${err.message}`, 'error');
} finally {
@@ -474,6 +494,14 @@
setHotmailListExpanded(!listExpanded);
});
dom.btnToggleHotmailForm?.addEventListener('click', () => {
if (formController.isVisible()) {
formController.setVisible(false, { clearForm: true });
return;
}
formController.setVisible(true, { focusField: true });
});
dom.btnHotmailUsageGuide?.addEventListener('click', async () => {
await helpers.openConfirmModal({
title: '使用教程',
@@ -514,6 +542,7 @@
dom.btnAddHotmailAccount?.addEventListener('click', handleAddHotmailAccount);
dom.btnImportHotmailAccounts?.addEventListener('click', handleImportHotmailAccounts);
dom.hotmailAccountsList?.addEventListener('click', handleAccountListClick);
formController.sync();
}
return {
+33 -9
View File
@@ -12,6 +12,7 @@
const expandedStorageKey = constants.expandedStorageKey || 'multipage-mail2925-list-expanded';
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
const copyIcon = constants.copyIcon || '';
const createAccountPoolFormController = globalScope.SidepanelAccountPoolUi?.createAccountPoolFormController;
let actionInFlight = false;
let listExpanded = false;
@@ -118,13 +119,29 @@
if (dom.inputMail2925Password) dom.inputMail2925Password.value = '';
}
const formController = typeof createAccountPoolFormController === 'function'
? createAccountPoolFormController({
formShell: dom.mail2925FormShell,
toggleButton: dom.btnToggleMail2925Form,
hiddenLabel: '添加账号',
visibleLabel: '取消添加',
onClear: () => {
stopEditingAccount({ clearForm: true });
},
onFocus: () => {
dom.inputMail2925Email?.focus?.();
},
})
: {
isVisible: () => false,
setVisible() {},
sync() {},
};
function syncEditUi() {
if (dom.btnAddMail2925Account) {
dom.btnAddMail2925Account.textContent = editingAccountId ? '保存修改' : '添加账号';
}
if (dom.btnCancelMail2925Edit) {
dom.btnCancelMail2925Edit.style.display = editingAccountId ? '' : 'none';
}
}
function startEditingAccount(account) {
@@ -132,6 +149,7 @@
editingAccountId = account.id;
if (dom.inputMail2925Email) dom.inputMail2925Email.value = String(account.email || '').trim();
if (dom.inputMail2925Password) dom.inputMail2925Password.value = String(account.password || '');
formController.setVisible(true, { focusField: false });
syncEditUi();
}
@@ -234,7 +252,7 @@
}
applyMail2925AccountMutation(response.account);
stopEditingAccount();
formController.setVisible(false, { clearForm: true });
helpers.showToast(
updatingExisting
? `已更新 2925 账号 ${email}`
@@ -332,7 +350,7 @@
mail2925Accounts: [],
currentMail2925AccountId: null,
});
stopEditingAccount();
formController.setVisible(false, { clearForm: true });
refreshManagedAliasBaseEmail();
renderMail2925Accounts();
helpers.showToast(`已删除全部 ${response.deletedCount || 0} 个 2925 账号`, 'success', 2200);
@@ -460,7 +478,7 @@
}
state.syncLatestState(nextState);
if (editingAccountId === accountId) {
stopEditingAccount();
formController.setVisible(false, { clearForm: true });
}
refreshManagedAliasBaseEmail();
renderMail2925Accounts();
@@ -479,6 +497,14 @@
setMail2925ListExpanded(!listExpanded);
});
dom.btnToggleMail2925Form?.addEventListener('click', () => {
if (formController.isVisible()) {
formController.setVisible(false, { clearForm: true });
return;
}
formController.setVisible(true, { clearForm: !editingAccountId, focusField: true });
});
dom.btnDeleteAllMail2925Accounts?.addEventListener('click', async () => {
if (actionInFlight) return;
actionInFlight = true;
@@ -493,12 +519,10 @@
});
dom.btnAddMail2925Account?.addEventListener('click', handleAddMail2925Account);
dom.btnCancelMail2925Edit?.addEventListener('click', () => {
stopEditingAccount();
});
dom.btnImportMail2925Accounts?.addEventListener('click', handleImportMail2925Accounts);
dom.mail2925AccountsList?.addEventListener('click', handleAccountListClick);
syncEditUi();
formController.sync();
}
return {
+15 -1
View File
@@ -1114,6 +1114,20 @@ header {
visibility: hidden;
}
.account-pool-form-shell {
display: flex;
flex-direction: column;
gap: 10px;
}
.account-pool-actions-inline {
width: 100%;
}
.account-pool-import-action {
margin-left: auto;
}
.hotmail-import-row {
align-items: flex-start;
}
@@ -1136,7 +1150,7 @@ header {
}
.hotmail-list-shell.is-collapsed {
max-height: 236px;
max-height: 176px;
}
.hotmail-list-shell.is-expanded {
+73 -60
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
@@ -36,7 +36,7 @@
<div class="run-group">
<button id="btn-contribution-mode" class="btn btn-outline btn-sm btn-contribution-mode" type="button"
aria-pressed="false" title="进入贡献模式并打开上传页">贡献/使用</button>
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="运行次数" />
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" title="运行次数" />
<button id="btn-auto-run" class="btn btn-success" title="自动执行全部步骤">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3" />
@@ -164,10 +164,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;">
@@ -269,20 +269,25 @@
<button id="btn-cf-domain-mode" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
</div>
</div>
<div class="data-row" id="row-email-prefix" style="display:none;">
<span class="data-label" id="label-email-prefix">别名基邮箱</span>
<div class="data-row" id="row-mail2925-pool-settings" style="display:none;">
<span class="data-label">2925 号池</span>
<div class="data-inline mail2925-base-inline">
<input type="text" id="input-email-prefix" class="data-input mail2925-base-input" placeholder="例如 yourname@example.com" />
<select id="select-mail2925-pool-account" class="data-select mail2925-pool-select" style="display:none;">
<option value="">请选择号池邮箱</option>
</select>
<label class="toggle-switch mail2925-pool-toggle" id="label-mail2925-use-account-pool" for="input-mail2925-use-account-pool" style="display:none;" title="开启后只能从号池中选择 2925 基邮箱">
<label class="toggle-switch mail2925-pool-toggle" id="label-mail2925-use-account-pool" for="input-mail2925-use-account-pool" style="display:none;" title="开启后启用 2925 账号池">
<input type="checkbox" id="input-mail2925-use-account-pool" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
<span>号池</span>
</label>
<select id="select-mail2925-pool-account" class="data-select mail2925-pool-select" style="display:none;">
<option value="">请选择号池邮箱</option>
</select>
</div>
</div>
<div class="data-row" id="row-email-prefix" style="display:none;">
<span class="data-label" id="label-email-prefix">别名基邮箱</span>
<div class="data-inline">
<input type="text" id="input-email-prefix" class="data-input mail2925-base-input" placeholder="例如 yourname@example.com" />
</div>
</div>
<div class="data-row" id="row-inbucket-host" style="display:none;">
@@ -390,6 +395,7 @@
<span class="section-label">Hotmail 账号池</span>
</div>
<div class="section-mini-actions">
<button id="btn-toggle-hotmail-form" class="btn btn-outline btn-xs" type="button" aria-expanded="false">添加账号</button>
<button id="btn-hotmail-usage-guide" class="btn btn-ghost btn-xs" type="button">使用教程</button>
<button id="btn-clear-used-hotmail-accounts" class="btn btn-ghost btn-xs" type="button">清空已用</button>
<button id="btn-delete-all-hotmail-accounts" class="btn btn-ghost btn-xs" type="button">全部删除</button>
@@ -412,33 +418,37 @@
<span class="data-label">本地助手</span>
<input type="text" id="input-hotmail-local-base-url" class="data-input mono" placeholder="http://127.0.0.1:17373" />
</div>
<div class="data-row">
<span class="data-label">邮箱</span>
<input type="text" id="input-hotmail-email" class="data-input" placeholder="name@hotmail.com" />
</div>
<div class="data-row">
<span class="data-label">客户端 ID</span>
<input type="text" id="input-hotmail-client-id" class="data-input mono" placeholder="微软应用客户端 ID" />
</div>
<div class="data-row">
<span class="data-label">邮箱密码</span>
<input type="password" id="input-hotmail-password" class="data-input" placeholder="可选,仅用于记录" />
</div>
<div class="data-row">
<span class="data-label">刷新令牌</span>
<input type="password" id="input-hotmail-refresh-token" class="data-input mono"
placeholder="必填,粘贴刷新令牌(refresh token" />
</div>
<div class="data-row hotmail-actions-row">
<span class="data-label"></span>
<button id="btn-add-hotmail-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
</div>
<div class="data-row hotmail-import-row">
<span class="data-label">批量导入</span>
<div class="hotmail-import-box">
<textarea id="input-hotmail-import" class="data-textarea mono"
placeholder="账号----密码----客户端ID----刷新令牌&#10;name@hotmail.com----password----client-id----refresh-token"></textarea>
<button id="btn-import-hotmail-accounts" class="btn btn-outline btn-sm" type="button">导入</button>
<div id="hotmail-form-shell" class="account-pool-form-shell" hidden>
<div class="data-row">
<span class="data-label">邮箱</span>
<input type="text" id="input-hotmail-email" class="data-input" placeholder="name@hotmail.com" />
</div>
<div class="data-row">
<span class="data-label">客户端 ID</span>
<input type="text" id="input-hotmail-client-id" class="data-input mono" placeholder="微软应用客户端 ID" />
</div>
<div class="data-row">
<span class="data-label">邮箱密码</span>
<input type="password" id="input-hotmail-password" class="data-input" placeholder="可选,仅用于记录" />
</div>
<div class="data-row">
<span class="data-label">刷新令牌</span>
<input type="password" id="input-hotmail-refresh-token" class="data-input mono"
placeholder="必填,粘贴刷新令牌(refresh token" />
</div>
<div class="data-row hotmail-actions-row">
<span class="data-label"></span>
<div class="data-inline account-pool-actions-inline">
<button id="btn-add-hotmail-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
<button id="btn-import-hotmail-accounts" class="btn btn-outline btn-sm account-pool-import-action" type="button">批量导入</button>
</div>
</div>
<div class="data-row hotmail-import-row">
<span class="data-label">批量导入</span>
<div class="hotmail-import-box">
<textarea id="input-hotmail-import" class="data-textarea mono"
placeholder="账号----密码----客户端ID----刷新令牌&#10;name@hotmail.com----password----client-id----refresh-token"></textarea>
</div>
</div>
</div>
<div id="hotmail-list-shell" class="hotmail-list-shell is-collapsed">
@@ -451,31 +461,33 @@
<span class="section-label">2925 账号池</span>
</div>
<div class="section-mini-actions">
<button id="btn-toggle-mail2925-form" class="btn btn-outline btn-xs" type="button" aria-expanded="false">添加账号</button>
<button id="btn-delete-all-mail2925-accounts" class="btn btn-ghost btn-xs" type="button">全部删除</button>
<button id="btn-toggle-mail2925-list" class="btn btn-ghost btn-xs" type="button" aria-expanded="false">展开列表</button>
</div>
</div>
<div class="data-row">
<span class="data-label">邮箱</span>
<input type="text" id="input-mail2925-email" class="data-input" placeholder="name@2925.com" />
</div>
<div class="data-row">
<span class="data-label">密码</span>
<input type="password" id="input-mail2925-password" class="data-input" placeholder="2925 登录密码" />
</div>
<div class="data-row hotmail-actions-row">
<span class="data-label"></span>
<div class="data-inline">
<button id="btn-add-mail2925-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
<button id="btn-cancel-mail2925-edit" class="btn btn-outline btn-sm" type="button" style="display:none;">取消编辑</button>
<div id="mail2925-form-shell" class="account-pool-form-shell" hidden>
<div class="data-row">
<span class="data-label">邮箱</span>
<input type="text" id="input-mail2925-email" class="data-input" placeholder="name@2925.com" />
</div>
</div>
<div class="data-row hotmail-import-row">
<span class="data-label">批量导入</span>
<div class="hotmail-import-box">
<textarea id="input-mail2925-import" class="data-textarea mono"
placeholder="邮箱----密码&#10;name@2925.com----password"></textarea>
<button id="btn-import-mail2925-accounts" class="btn btn-outline btn-sm" type="button">导入</button>
<div class="data-row">
<span class="data-label">密码</span>
<input type="password" id="input-mail2925-password" class="data-input" placeholder="2925 登录密码" />
</div>
<div class="data-row hotmail-actions-row">
<span class="data-label"></span>
<div class="data-inline account-pool-actions-inline">
<button id="btn-add-mail2925-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
<button id="btn-import-mail2925-accounts" class="btn btn-outline btn-sm account-pool-import-action" type="button">批量导入</button>
</div>
</div>
<div class="data-row hotmail-import-row">
<span class="data-label">批量导入</span>
<div class="hotmail-import-box">
<textarea id="input-mail2925-import" class="data-textarea mono"
placeholder="邮箱----密码&#10;name@2925.com----password"></textarea>
</div>
</div>
</div>
<div id="mail2925-list-shell" class="hotmail-list-shell is-collapsed">
@@ -721,6 +733,7 @@
<script src="../data/step-definitions.js"></script>
<script src="update-service.js"></script>
<script src="contribution-content-update-service.js"></script>
<script src="account-pool-ui.js"></script>
<script src="hotmail-manager.js"></script>
<script src="mail-2925-manager.js"></script>
<script src="icloud-manager.js"></script>
+75 -27
View File
@@ -98,6 +98,7 @@ const rowCustomPassword = document.getElementById('row-custom-password');
const selectMailProvider = document.getElementById('select-mail-provider');
const btnMailLogin = document.getElementById('btn-mail-login');
const rowMail2925Mode = document.getElementById('row-mail-2925-mode');
const rowMail2925PoolSettings = document.getElementById('row-mail2925-pool-settings');
const mail2925ModeButtons = Array.from(document.querySelectorAll('[data-mail2925-mode]'));
const rowEmailGenerator = document.getElementById('row-email-generator');
const selectEmailGenerator = document.getElementById('select-email-generator');
@@ -149,20 +150,23 @@ const inputHotmailRefreshToken = document.getElementById('input-hotmail-refresh-
const inputHotmailImport = document.getElementById('input-hotmail-import');
const btnAddHotmailAccount = document.getElementById('btn-add-hotmail-account');
const btnImportHotmailAccounts = document.getElementById('btn-import-hotmail-accounts');
const btnToggleHotmailForm = document.getElementById('btn-toggle-hotmail-form');
const btnHotmailUsageGuide = document.getElementById('btn-hotmail-usage-guide');
const btnClearUsedHotmailAccounts = document.getElementById('btn-clear-used-hotmail-accounts');
const btnDeleteAllHotmailAccounts = document.getElementById('btn-delete-all-hotmail-accounts');
const btnToggleHotmailList = document.getElementById('btn-toggle-hotmail-list');
const hotmailFormShell = document.getElementById('hotmail-form-shell');
const hotmailListShell = document.getElementById('hotmail-list-shell');
const hotmailAccountsList = document.getElementById('hotmail-accounts-list');
const inputMail2925Email = document.getElementById('input-mail2925-email');
const inputMail2925Password = document.getElementById('input-mail2925-password');
const inputMail2925Import = document.getElementById('input-mail2925-import');
const btnAddMail2925Account = document.getElementById('btn-add-mail2925-account');
const btnCancelMail2925Edit = document.getElementById('btn-cancel-mail2925-edit');
const btnToggleMail2925Form = document.getElementById('btn-toggle-mail2925-form');
const btnImportMail2925Accounts = document.getElementById('btn-import-mail2925-accounts');
const btnDeleteAllMail2925Accounts = document.getElementById('btn-delete-all-mail2925-accounts');
const btnToggleMail2925List = document.getElementById('btn-toggle-mail2925-list');
const mail2925FormShell = document.getElementById('mail2925-form-shell');
const mail2925ListShell = document.getElementById('mail2925-list-shell');
const mail2925AccountsList = document.getElementById('mail2925-accounts-list');
const inputLuckmailApiKey = document.getElementById('input-luckmail-api-key');
@@ -245,8 +249,7 @@ const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
const AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-skip-failures-prompt-dismissed';
const AUTO_RUN_FALLBACK_RISK_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-run-fallback-risk-prompt-dismissed';
const CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY = 'multipage-contribution-content-prompt-dismissed-version';
const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 15;
const AUTO_RUN_FALLBACK_RISK_RECOMMENDED_THREAD_INTERVAL_MINUTES = 5;
const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 3;
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
const ICLOUD_PROVIDER = 'icloud';
@@ -261,12 +264,24 @@ function getManagedAliasUtils() {
return window.MultiPageManagedAliasUtils || null;
}
function isManagedAliasProvider(provider = selectMailProvider.value) {
function isManagedAliasProvider(provider = selectMailProvider.value, mail2925Mode = getSelectedMail2925Mode()) {
const utils = getManagedAliasUtils();
if (utils?.usesManagedAliasGeneration) {
return utils.usesManagedAliasGeneration(provider, { mail2925Mode });
}
if (utils?.isManagedAliasProvider) {
const normalizedProvider = String(provider || '').trim().toLowerCase();
if (normalizedProvider === '2925') {
return utils.isManagedAliasProvider(provider)
&& normalizeMail2925Mode(mail2925Mode) === MAIL_2925_MODE_PROVIDE;
}
return utils.isManagedAliasProvider(provider);
}
return [GMAIL_PROVIDER, '2925'].includes(String(provider || '').trim().toLowerCase());
const normalizedProvider = String(provider || '').trim().toLowerCase();
if (normalizedProvider === '2925') {
return normalizeMail2925Mode(mail2925Mode) === MAIL_2925_MODE_PROVIDE;
}
return normalizedProvider === GMAIL_PROVIDER;
}
function parseManagedAliasBaseEmail(rawValue, provider = selectMailProvider.value) {
@@ -285,31 +300,34 @@ function isManagedAliasEmail(value, baseEmail = '', provider = selectMailProvide
return false;
}
function getManagedAliasProviderUiCopy(provider = selectMailProvider.value) {
function getManagedAliasProviderUiCopy(provider = selectMailProvider.value, mail2925Mode = getSelectedMail2925Mode()) {
if (!isManagedAliasProvider(provider, mail2925Mode)) {
return null;
}
const utils = getManagedAliasUtils();
if (utils?.getManagedAliasProviderUiCopy) {
return utils.getManagedAliasProviderUiCopy(provider);
}
if (String(provider || '').trim().toLowerCase() === GMAIL_PROVIDER) {
return {
baseLabel: 'Gmail 原邮箱',
baseLabel: '邮箱',
basePlaceholder: '例如 yourname@gmail.com',
buttonLabel: '生成',
successVerb: '生成',
label: 'Gmail +tag 邮箱',
placeholder: '点击生成 Gmail +tag 邮箱,或手动填写完整邮箱',
hint: '先填写 Gmail 原邮箱后点“生成”,也可以直接手动填写完整的 Gmail 邮箱。',
hint: '先填写邮箱后点“生成”,也可以直接手动填写完整的 Gmail 邮箱。',
};
}
if (String(provider || '').trim().toLowerCase() === '2925') {
return {
baseLabel: '2925 基邮箱',
baseLabel: '基邮箱',
basePlaceholder: '例如 yourname@2925.com',
buttonLabel: '生成',
successVerb: '生成',
label: '2925 邮箱',
placeholder: '点击生成 2925 邮箱,或手动填写完整邮箱',
hint: '先填写 2925 基邮箱后点“生成”,也可以直接手动填写完整的 2925 邮箱。',
hint: '先填写基邮箱后点“生成”,也可以直接手动填写完整的 2925 邮箱。',
};
}
return null;
@@ -374,7 +392,10 @@ async function syncSelectedMail2925PoolAccount(options = {}) {
throw new Error(response.error);
}
syncLatestState({ currentMail2925AccountId: response.account?.id || accountId });
syncLatestState({
currentMail2925AccountId: response.account?.id || accountId,
...(response.account?.email ? { mail2925BaseEmail: String(response.account.email).trim() } : {}),
});
setManagedAliasBaseEmailInputForProvider('2925', latestState);
if (!silent) {
showToast(`已切换当前 2925 号池邮箱为 ${response.account?.email || accountId}`, 'success', 1800);
@@ -586,7 +607,7 @@ const LOG_LEVEL_LABELS = {
};
function usesGeneratedAliasMailProvider(provider, mail2925Mode = getSelectedMail2925Mode()) {
return isManagedAliasProvider(provider);
return isManagedAliasProvider(provider, mail2925Mode);
}
function parseGmailBaseEmail(rawValue = '') {
@@ -875,14 +896,10 @@ async function openAutoSkipFailuresConfirmModal() {
};
}
async function openAutoRunFallbackRiskConfirmModal(totalRuns, fallbackThreadIntervalMinutes) {
const intervalLabel = Number.isFinite(fallbackThreadIntervalMinutes)
? `${fallbackThreadIntervalMinutes} 分钟`
: '未设置';
async function openAutoRunFallbackRiskConfirmModal(totalRuns) {
const result = await openConfirmModalWithOption({
title: '自动运行风险提醒',
message: `当前设置为 ${totalRuns} 轮自动化,已开启自动重试,线程间隔为 ${intervalLabel}。轮数过多时,可能会因为 IP 短时间注册过多而集中失败。建议控制在 ${AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS} 轮以下,并将线程间隔设置在 ${AUTO_RUN_FALLBACK_RISK_RECOMMENDED_THREAD_INTERVAL_MINUTES} 分钟以上。是否继续?`,
message: `当前轮数已经不适合单节点情况,请确保已经配置并打开节点轮询功能(若没有配置,请点击贡献/使用按钮,根据网页中使用教程进行配置),避免连续使用一个节点注册,导致出现手机号验证。`,
confirmLabel: '继续',
});
@@ -1157,7 +1174,7 @@ function formatAutoStepDelayInputValue(value) {
}
function getRunCountValue() {
return Math.min(50, Math.max(1, parseInt(inputRunCount.value, 10) || 1));
return Math.max(1, parseInt(inputRunCount.value, 10) || 1);
}
function updateFallbackThreadIntervalInputState() {
@@ -1468,6 +1485,7 @@ function collectSettingsPayload() {
mailProvider: selectMailProvider.value,
mail2925Mode: getSelectedMail2925Mode(),
mail2925UseAccountPool,
currentMail2925AccountId: String(latestState?.currentMail2925AccountId || '').trim(),
emailGenerator: selectEmailGenerator.value,
autoDeleteUsedIcloudAlias: checkboxAutoDeleteIcloud?.checked,
icloudHostPreference: selectIcloudHostPreference?.value || 'auto',
@@ -2465,6 +2483,24 @@ function getCurrentMail2925Email(state = latestState) {
return String(getCurrentMail2925Account(state)?.email || '').trim();
}
function syncMail2925BaseEmailFromCurrentAccount(state = latestState, options = {}) {
const { persist = false } = options;
if (!isMail2925AccountPoolEnabled(state)) {
return false;
}
const currentEmail = getCurrentMail2925Email(state);
if (!currentEmail || currentEmail === String(state?.mail2925BaseEmail || '').trim()) {
return false;
}
syncLatestState({ mail2925BaseEmail: currentEmail });
if (persist) {
saveSettings({ silent: true }).catch(() => {});
}
return true;
}
function getCurrentLuckmailPurchase(state = latestState) {
return state?.currentLuckmailPurchase || null;
}
@@ -2615,13 +2651,18 @@ function updateMailProviderUI() {
const useIcloudProvider = isIcloudMailProvider();
const useEmailGenerator = !useHotmail && !useLuckmail && !useGeneratedAlias && !useCustomEmail;
const useCloudflareTempEmailProvider = selectMailProvider.value === 'cloudflare-temp-email';
const aliasUiCopy = useGeneratedAlias ? getManagedAliasProviderUiCopy(selectMailProvider.value) : null;
const aliasUiCopy = useGeneratedAlias
? getManagedAliasProviderUiCopy(selectMailProvider.value, mail2925Mode)
: null;
const uiCopy = getCurrentRegistrationEmailUiCopy();
updateMailLoginButtonState();
if (rowMail2925Mode) {
rowMail2925Mode.style.display = 'none';
rowMail2925Mode.style.display = use2925 ? '' : 'none';
}
rowEmailPrefix.style.display = useGeneratedAlias ? '' : 'none';
if (rowMail2925PoolSettings) {
rowMail2925PoolSettings.style.display = useMail2925 ? '' : 'none';
}
rowEmailPrefix.style.display = useGeneratedAlias && !useMail2925AccountPool ? '' : 'none';
const hotmailServiceMode = getSelectedHotmailServiceMode();
rowInbucketHost.style.display = useInbucket ? '' : 'none';
rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
@@ -2684,8 +2725,8 @@ function updateMailProviderUI() {
selectMail2925PoolAccount.style.display = useMail2925AccountPool ? '' : 'none';
selectMail2925PoolAccount.disabled = !useMail2925AccountPool || getMail2925Accounts().length === 0;
}
inputEmailPrefix.style.display = useMail2925AccountPool ? 'none' : '';
inputEmailPrefix.readOnly = useMail2925AccountPool;
inputEmailPrefix.style.display = '';
inputEmailPrefix.readOnly = false;
selectEmailGenerator.disabled = useHotmail || useLuckmail || useGeneratedAlias || useCustomEmail;
if (useGmail) {
labelEmailPrefix.textContent = 'Gmail 原邮箱';
@@ -2736,7 +2777,9 @@ function updateMailProviderUI() {
}
if (autoHintText && useMail2925AccountPool) {
autoHintText.textContent = getMail2925Accounts().length
? '当前已启用 2925 号池模式,步骤 3 会基于下拉框选中的号池邮箱生成别名地址'
? (useGeneratedAlias
? '当前已启用 2925 号池模式,步骤 3 会基于下拉框选中的号池邮箱生成别名地址'
: '当前已启用 2925 号池模式,步骤 4 / 8 遇到登录页时会优先使用下拉框选中的账号自动登录')
: '当前已启用 2925 号池模式,请先在下方 2925 账号池中添加账号并选择邮箱';
}
if (autoHintText && showCloudflareTempEmailReceiveMailbox) {
@@ -3064,6 +3107,7 @@ async function fetchGeneratedEmail(options = {}) {
generateNew: true,
generator: selectEmailGenerator.value,
mailProvider: selectMailProvider.value,
mail2925Mode: getSelectedMail2925Mode(),
...buildManagedAliasBaseEmailPayload(),
},
});
@@ -3123,7 +3167,9 @@ const hotmailManager = window.SidepanelHotmailManager?.createHotmailManager({
btnDeleteAllHotmailAccounts,
btnHotmailUsageGuide,
btnImportHotmailAccounts,
btnToggleHotmailForm,
btnToggleHotmailList,
hotmailFormShell,
hotmailAccountsList,
hotmailListShell,
inputEmail,
@@ -3174,14 +3220,15 @@ const mail2925Manager = window.SidepanelMail2925Manager?.createMail2925Manager({
},
dom: {
btnAddMail2925Account,
btnCancelMail2925Edit,
btnDeleteAllMail2925Accounts,
btnImportMail2925Accounts,
btnToggleMail2925Form,
btnToggleMail2925List,
inputMail2925Email,
inputMail2925Import,
inputMail2925Password,
mail2925AccountsList,
mail2925FormShell,
mail2925ListShell,
},
helpers: {
@@ -3190,6 +3237,7 @@ const mail2925Manager = window.SidepanelMail2925Manager?.createMail2925Manager({
getMail2925Accounts,
openConfirmModal,
refreshManagedAliasBaseEmail: () => {
syncMail2925BaseEmailFromCurrentAccount(latestState, { persist: true });
setManagedAliasBaseEmailInputForProvider('2925', latestState);
},
showToast,
@@ -3801,7 +3849,7 @@ async function startAutoRunFromCurrentSettings() {
if (shouldWarnAutoRunFallbackRisk(totalRuns, autoRunSkipFailures)
&& !isAutoRunFallbackRiskPromptDismissed()) {
const result = await openAutoRunFallbackRiskConfirmModal(totalRuns, fallbackThreadIntervalMinutes);
const result = await openAutoRunFallbackRiskConfirmModal(totalRuns);
if (!result.confirmed) {
return false;
}
+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')}
@@ -15,3 +15,64 @@ test('generated email helper module exposes a factory', () => {
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 events = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build alias in receive mode');
},
buildCloudflareTempEmailHeaders: () => ({}),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async () => ({ ok: true, text: async () => '{}' }),
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: () => '',
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
getState: async () => ({
mailProvider: '2925',
mail2925Mode: 'receive',
emailGenerator: 'duck',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate 2925 account in receive mode');
},
joinCloudflareTempEmailUrl: () => '',
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: () => '',
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: (_provider, mail2925Mode) => mail2925Mode === 'provide',
reuseOrCreateTab: async () => {},
sendToContentScript: async (_source, message) => {
events.push(message.type);
return { email: 'duck@example.com', generated: true };
},
setEmailState: async (email) => {
events.push(['email', email]);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
mailProvider: '2925',
mail2925Mode: 'receive',
emailGenerator: 'duck',
}, {
mailProvider: '2925',
mail2925Mode: 'receive',
generator: 'duck',
});
assert.equal(email, 'duck@example.com');
assert.deepStrictEqual(events, [
'FETCH_DUCK_EMAIL',
['email', 'duck@example.com'],
]);
});
@@ -143,3 +143,32 @@ return { getMailConfig };
navigateOnReuse: true,
});
});
test('getMailConfig keeps provider metadata for 2925 mailboxes', () => {
const bundle = extractFunction('getMailConfig');
const api = new Function(`
const ICLOUD_PROVIDER = 'icloud';
const GMAIL_PROVIDER = 'gmail';
const HOTMAIL_PROVIDER = 'hotmail-api';
const LUCKMAIL_PROVIDER = 'luckmail-api';
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
function normalizeIcloudHost(value = '') { return String(value || '').trim().toLowerCase(); }
function normalizeInbucketOrigin(value) { return String(value || '').trim(); }
function getConfiguredIcloudHostPreference() { return ''; }
function getIcloudLoginUrlForHost(host) { return host; }
function getIcloudMailUrlForHost(host) { return host; }
${bundle}
return { getMailConfig };
`)();
assert.deepEqual(api.getMailConfig({
mailProvider: '2925',
}), {
provider: '2925',
source: 'mail-2925',
url: 'https://2925.com/#/mailList',
label: '2925 邮箱',
inject: ['content/utils.js', 'content/mail-2925.js'],
injectSource: 'mail-2925',
});
});
@@ -0,0 +1,428 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const mail2925Utils = require('../mail2925-utils.js');
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
test('ensureMail2925MailboxSession reuses current mailbox page without sending login when page stays on mailList', async () => {
let currentState = {
autoRunning: false,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
let sendCalls = 0;
let readyCalls = 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 () => {
readyCalls += 1;
},
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 () => 9,
sendToMailContentScriptResilient: async () => {
sendCalls += 1;
return { loggedIn: true };
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
const result = await manager.ensureMail2925MailboxSession({
accountId: 'acc-1',
forceRelogin: false,
allowLoginWhenOnLoginPage: true,
actionLabel: '步骤 8:确认 2925 邮箱登录态',
});
assert.equal(sendCalls, 0);
assert.equal(readyCalls, 0);
assert.equal(result.result.usedExistingSession, true);
});
test('ensureMail2925MailboxSession does not require account-pool accounts when pool is off and mailbox page stays on mailList', async () => {
let currentState = {
autoRunning: false,
mail2925UseAccountPool: false,
mail2925Accounts: [],
currentMail2925AccountId: null,
};
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: () => false,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
reuseOrCreateTab: async () => 9,
sendToMailContentScriptResilient: async () => {
sendCalls += 1;
return { loggedIn: true };
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
const result = await manager.ensureMail2925MailboxSession({
accountId: null,
forceRelogin: false,
allowLoginWhenOnLoginPage: false,
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
assert.equal(sendCalls, 0);
assert.equal(result.result.usedExistingSession, true);
assert.equal(result.account, null);
});
test('ensureMail2925MailboxSession stops immediately when login page is detected and account pool is off', async () => {
let currentState = {
autoRunning: true,
autoRunPhase: 'running',
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
const stopCalls = [];
let sendCalls = 0;
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: 'https://2925.com/login/' }),
},
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 };
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
await assert.rejects(
() => manager.ensureMail2925MailboxSession({
accountId: 'acc-1',
forceRelogin: false,
allowLoginWhenOnLoginPage: false,
actionLabel: '步骤 4:确认 2925 邮箱登录态',
}),
/流程已被用户停止。/
);
assert.equal(sendCalls, 0);
assert.equal(stopCalls.length, 1);
});
test('ensureMail2925MailboxSession logs in when login page is detected and account pool is on', async () => {
let currentState = {
autoRunning: false,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
let sendCalls = 0;
let readyCalls = 0;
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: 'https://2925.com/login/' }),
},
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
ensureContentScriptReadyOnTab: async () => {
readyCalls += 1;
},
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 () => 9,
sendToMailContentScriptResilient: async () => {
sendCalls += 1;
return { loggedIn: true, currentView: 'mailbox' };
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
const result = await manager.ensureMail2925MailboxSession({
accountId: 'acc-1',
forceRelogin: false,
allowLoginWhenOnLoginPage: true,
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
assert.equal(sendCalls, 1);
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 不一致/);
});
@@ -8,6 +8,13 @@ test('background mail2925 session uses /login/ as relogin entry url', () => {
assert.match(source, /const MAIL2925_LOGIN_URL = 'https:\/\/2925\.com\/login\/';/);
});
test('background mail2925 session keeps login message timeout above the 40-second mailbox wait', () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
assert.match(source, /timeoutMs:\s*50000,/);
assert.match(source, /responseTimeoutMs:\s*50000,/);
assert.match(source, /40 秒内未进入收件箱/);
});
test('ensureMail2925MailboxSession waits 3 seconds before and after opening login page on force relogin', async () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
const globalScope = {};
@@ -17,6 +17,7 @@ test('handleMail2925LimitReachedError disables current account and switches to t
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
let currentState = {
mail2925UseAccountPool: true,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'current', email: 'current@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
{ id: 'next', email: 'next@2925.com', password: 'p2', enabled: true, lastUsedAt: 20 },
@@ -99,6 +100,7 @@ test('handleMail2925LimitReachedError requests stop when no next mail2925 accoun
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
let currentState = {
mail2925UseAccountPool: true,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'only', email: 'only@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
@@ -223,5 +225,115 @@ test('ensureMail2925MailboxSession requests stop when auto run is active and log
);
assert.equal(events.stopCalls.length, 1);
assert.match(events.stopCalls[0].logMessage, /20 秒内未进入收件箱/);
assert.match(events.stopCalls[0].logMessage, /登录后仍未进入收件箱/);
});
test('handleMail2925LimitReachedError stops immediately when account pool is off even if another account exists', 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 = {
mail2925UseAccountPool: false,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'current', email: 'current@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
{ id: 'next', email: 'next@2925.com', password: 'p2', enabled: true, lastUsedAt: 20 },
]),
currentMail2925AccountId: 'current',
};
const events = {
stopCalls: [],
sessionChecks: 0,
};
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
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,
requestStop: async (options = {}) => {
events.stopCalls.push(options);
},
reuseOrCreateTab: async () => 1,
sendToMailContentScriptResilient: async () => {
events.sessionChecks += 1;
return { loggedIn: true };
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
const error = await manager.handleMail2925LimitReachedError(
4,
new Error('MAIL2925_LIMIT_REACHED::子邮箱已达上限邮箱')
);
assert.equal(error.message, '流程已被用户停止。');
assert.equal(events.sessionChecks, 0);
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' },
]);
});
+71 -7
View File
@@ -7,6 +7,70 @@ const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep4;`)(globalScope);
test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling', async () => {
let capturedOptions = null;
let ensureCalls = 0;
const tabUpdates = [];
const tabReuses = [];
const realDateNow = Date.now;
Date.now = () => 700000;
const executor = api.createStep4Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async (tabId, payload) => {
tabUpdates.push({ tabId, payload });
},
},
},
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {
ensureCalls += 1;
},
getMailConfig: () => ({
provider: '2925',
label: '2925 邮箱',
source: 'mail-2925',
url: 'https://2925.com',
}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
LUCKMAIL_PROVIDER: 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
resolveVerificationStep: async (_step, _state, _mail, options) => {
capturedOptions = options;
},
reuseOrCreateTab: async (source, url) => {
tabReuses.push({ source, url });
},
sendToContentScriptResilient: async () => ({}),
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
});
try {
await executor.executeStep4({
email: 'user@example.com',
password: 'secret',
mail2925UseAccountPool: true,
});
} finally {
Date.now = realDateNow;
}
assert.equal(ensureCalls, 1);
assert.deepStrictEqual(tabReuses, []);
assert.deepStrictEqual(tabUpdates, [
{ tabId: 1, payload: { active: true } },
]);
assert.equal(capturedOptions.filterAfterTimestamp, 100000);
assert.equal(capturedOptions.resendIntervalMs, 0);
});
test('step 4 does not request a fresh code first for Cloudflare temp mail', async () => {
let capturedOptions = null;
const realDateNow = Date.now;
Date.now = () => 700000;
@@ -22,10 +86,10 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {},
getMailConfig: () => ({
provider: '2925',
label: '2925 邮箱',
source: 'mail-2925',
url: 'https://2925.com',
provider: 'cloudflare-temp-email',
label: 'Cloudflare Temp Email',
source: 'cloudflare-temp-email',
url: 'https://temp.peekcart.com',
}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
@@ -46,12 +110,12 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
await executor.executeStep4({
email: 'user@example.com',
password: 'secret',
mail2925UseAccountPool: false,
});
} finally {
Date.now = realDateNow;
}
assert.equal(capturedOptions.filterAfterTimestamp, 100000);
assert.equal(capturedOptions.resendIntervalMs, 0);
assert.equal(capturedOptions.filterAfterTimestamp, 700000);
assert.equal(capturedOptions.requestFreshCodeFirst, false);
assert.equal(capturedOptions.resendIntervalMs, 25000);
});
+19 -2
View File
@@ -91,6 +91,9 @@ test('step 8 submits login verification directly without replaying step 7', asyn
test('step 8 uses a fixed 10-minute lookback window and disables resend interval for 2925 mailbox polling', async () => {
let capturedOptions = null;
let ensureCalls = 0;
const tabUpdates = [];
const tabReuses = [];
const realDateNow = Date.now;
Date.now = () => 900000;
@@ -98,11 +101,16 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
update: async (tabId, payload) => {
tabUpdates.push({ tabId, payload });
},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {
ensureCalls += 1;
},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }),
rerunStep7ForStep8Recovery: async () => {},
getOAuthFlowRemainingMs: async () => 8000,
@@ -123,7 +131,9 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
resolveVerificationStep: async (_step, _state, _mail, options) => {
capturedOptions = options;
},
reuseOrCreateTab: async () => {},
reuseOrCreateTab: async (source, url) => {
tabReuses.push({ source, url });
},
setState: async () => {},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
@@ -137,11 +147,18 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
email: 'user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
mail2925UseAccountPool: true,
});
} finally {
Date.now = realDateNow;
}
assert.equal(ensureCalls, 0);
assert.deepStrictEqual(tabReuses, []);
assert.deepStrictEqual(tabUpdates, [
{ tabId: 1, payload: { active: true } },
{ tabId: 2, payload: { active: true } },
]);
assert.equal(capturedOptions.filterAfterTimestamp, 300000);
assert.equal(capturedOptions.resendIntervalMs, 0);
assert.equal(capturedOptions.targetEmail, '');
+153 -3
View File
@@ -4,8 +4,67 @@ const fs = require('node:fs');
const source = fs.readFileSync('content/mail-2925.js', 'utf8');
test('ensureMail2925Session waits at most 20 seconds for mailbox after clicking login', () => {
assert.match(source, /waitForMail2925View\('mailbox',\s*20000\)/);
test('ensureMail2925Session waits at most 40 seconds for mailbox after clicking login', () => {
assert.match(source, /waitForMail2925View\('mailbox',\s*40000\)/);
});
test('ensureMail2925Session waits 1 second after filling credentials before clicking login', () => {
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) {
@@ -164,7 +223,7 @@ return {
assert.deepEqual(api.getReadAndDeleteCalls(), ['baseline', 'new']);
});
test('handlePollEmail ignores targetEmail and still tests any matching ChatGPT mail', async () => {
test('handlePollEmail keeps ignoring targetEmail when receive-mode matching is disabled', async () => {
const bundle = [
extractFunction('normalizeMinuteTimestamp'),
extractFunction('handlePollEmail'),
@@ -242,12 +301,103 @@ return {
maxAttempts: 4,
intervalMs: 1,
targetEmail: 'expected@example.com',
mail2925MatchTargetEmail: false,
});
assert.equal(result.code, '112233');
assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-1']);
});
test('handlePollEmail skips explicit mismatched target emails when receive-mode matching is enabled', async () => {
const bundle = [
extractFunction('extractEmails'),
extractFunction('emailMatchesTarget'),
extractFunction('getTargetEmailMatchState'),
extractFunction('normalizeMinuteTimestamp'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
let state = 'ready';
const seenCodes = new Set();
const readAndDeleteCalls = [];
const mismatchMail = {
id: 'mail-1',
text: 'ChatGPT verification code 112233 for another.user@example.com',
};
const targetMail = {
id: 'mail-2',
text: 'ChatGPT verification code 445566 for expected@example.com',
};
function findMailItems() {
return state === 'ready' ? [mismatchMail, targetMail] : [];
}
function getMailItemId(item) {
return item.id;
}
function getCurrentMailIds(items = []) {
return new Set(items.map((item) => item.id));
}
function parseMailItemTimestamp() {
return Date.now();
}
function matchesMailFilters(text) {
return /chatgpt|openai|verification/i.test(String(text || ''));
}
function getMailItemText(item) {
return item.text;
}
function extractVerificationCode(text) {
const match = String(text || '').match(/(\\d{6})/);
return match ? match[1] : null;
}
async function sleep() {}
async function sleepRandom() {}
async function returnToInbox() {
return true;
}
async function refreshInbox() {}
async function openMailAndDeleteAfterRead(item) {
readAndDeleteCalls.push(item.id);
return item.text;
}
async function ensureSeenCodesSession() {}
function persistSeenCodes() {}
function log() {}
${bundle}
return {
handlePollEmail,
getReadAndDeleteCalls() {
return readAndDeleteCalls.slice();
},
};
`)();
const result = await api.handlePollEmail(8, {
senderFilters: ['chatgpt'],
subjectFilters: ['verification'],
maxAttempts: 1,
intervalMs: 1,
targetEmail: 'expected@example.com',
mail2925MatchTargetEmail: true,
});
assert.equal(result.code, '445566');
assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-2']);
});
test('handlePollEmail only accepts 2925 mails inside the fixed lookback window', async () => {
const bundle = [
extractFunction('normalizeMinuteTimestamp'),
+9
View File
@@ -26,3 +26,12 @@ test('managed alias utils validate provider email with or without configured bas
assert.equal(api.isManagedAliasEmail('manual@gmail.com', 'gmail', ''), true);
assert.equal(api.isManagedAliasEmail('manual@qq.com', 'gmail', ''), false);
});
test('managed alias utils keep 2925 alias generation behind provide mode only', () => {
assert.equal(api.normalizeMail2925Mode('provide'), 'provide');
assert.equal(api.normalizeMail2925Mode('receive'), 'receive');
assert.equal(api.normalizeMail2925Mode('other'), 'provide');
assert.equal(api.usesManagedAliasGeneration('gmail'), true);
assert.equal(api.usesManagedAliasGeneration('2925', { mail2925Mode: 'provide' }), true);
assert.equal(api.usesManagedAliasGeneration('2925', { mail2925Mode: 'receive' }), false);
});
+93
View File
@@ -0,0 +1,93 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function extractFunction(source, 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;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
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('sidepanel run count input no longer hardcodes max=50', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const inputTag = html.match(/<input type="number" id="input-run-count"[^>]+>/);
assert.ok(inputTag, 'run count input should exist');
assert.doesNotMatch(inputTag[0], /\smax="50"/);
});
test('sidepanel getRunCountValue no longer clamps run count to 50', () => {
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
const bundle = extractFunction(source, 'getRunCountValue');
const api = new Function(`
const inputRunCount = { value: '88' };
${bundle}
return {
getRunCountValue,
setValue(value) {
inputRunCount.value = value;
},
};
`)();
assert.equal(api.getRunCountValue(), 88);
api.setValue('0');
assert.equal(api.getRunCountValue(), 1);
});
test('background normalizeRunCount no longer clamps values to 50', () => {
const source = fs.readFileSync('background.js', 'utf8');
const bundle = extractFunction(source, 'normalizeRunCount');
const api = new Function(`
${bundle}
return { normalizeRunCount };
`)();
assert.equal(api.normalizeRunCount(88), 88);
assert.equal(api.normalizeRunCount('120'), 120);
assert.equal(api.normalizeRunCount(0), 1);
assert.equal(api.normalizeRunCount('bad'), 1);
});
@@ -0,0 +1,96 @@
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;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
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('auto-run fallback risk warning starts at 3 runs', () => {
const bundle = extractFunction('shouldWarnAutoRunFallbackRisk');
const api = new Function(`
const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 3;
${bundle}
return { shouldWarnAutoRunFallbackRisk };
`)();
assert.equal(api.shouldWarnAutoRunFallbackRisk(2, false), false);
assert.equal(api.shouldWarnAutoRunFallbackRisk(3, false), true);
assert.equal(api.shouldWarnAutoRunFallbackRisk(10, true), true);
});
test('auto-run fallback risk modal uses the single-node warning copy', async () => {
const bundle = extractFunction('openAutoRunFallbackRiskConfirmModal');
const api = new Function(`
let capturedOptions = null;
async function openConfirmModalWithOption(options) {
capturedOptions = options;
return { confirmed: true, optionChecked: false };
}
${bundle}
return {
openAutoRunFallbackRiskConfirmModal,
getCapturedOptions() {
return capturedOptions;
},
};
`)();
const result = await api.openAutoRunFallbackRiskConfirmModal(3);
const options = api.getCapturedOptions();
assert.deepStrictEqual(result, { confirmed: true, dismissPrompt: false });
assert.equal(options.title, '自动运行风险提醒');
assert.equal(
options.message,
'当前轮数已经不适合单节点情况,请确保已经配置并打开节点轮询功能(若没有配置,请点击贡献/使用按钮,根据网页中使用教程进行配置),避免连续使用一个节点注册,导致出现手机号验证。'
);
assert.equal(options.confirmLabel, '继续');
});
+252 -1
View File
@@ -2,19 +2,74 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function createAccountPoolUiStub() {
return {
createAccountPoolFormController({
formShell,
toggleButton,
hiddenLabel = '添加账号',
visibleLabel = '取消添加',
onClear,
onFocus,
} = {}) {
let visible = false;
function sync() {
if (formShell) {
formShell.hidden = !visible;
}
if (toggleButton) {
toggleButton.textContent = visible ? visibleLabel : hiddenLabel;
toggleButton.setAttribute?.('aria-expanded', String(visible));
}
}
function setVisible(nextVisible, options = {}) {
visible = Boolean(nextVisible);
if (options.clearForm) {
onClear?.();
}
sync();
if (visible && options.focusField) {
onFocus?.();
}
}
sync();
return {
isVisible: () => visible,
setVisible,
sync,
};
},
};
}
test('sidepanel loads hotmail manager before sidepanel bootstrap', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const helperIndex = html.indexOf('<script src="account-pool-ui.js"></script>');
const hotmailManagerIndex = html.indexOf('<script src="hotmail-manager.js"></script>');
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
assert.notEqual(helperIndex, -1);
assert.notEqual(hotmailManagerIndex, -1);
assert.notEqual(sidepanelIndex, -1);
assert.ok(helperIndex < hotmailManagerIndex);
assert.ok(hotmailManagerIndex < sidepanelIndex);
});
test('sidepanel html contains collapsible hotmail form controls', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="btn-toggle-hotmail-form"/);
assert.match(html, /id="hotmail-form-shell"/);
assert.match(html, /id="btn-import-hotmail-accounts"[^>]*>批量导入</);
});
test('hotmail manager exposes a factory and renders empty state', () => {
const source = fs.readFileSync('sidepanel/hotmail-manager.js', 'utf8');
const windowObject = {};
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
@@ -77,3 +132,199 @@ test('hotmail manager exposes a factory and renders empty state', () => {
manager.renderHotmailAccounts();
assert.match(hotmailAccountsList.innerHTML, /还没有 Hotmail 账号/);
});
test('hotmail manager toggles form container from header button', () => {
const source = fs.readFileSync('sidepanel/hotmail-manager.js', 'utf8');
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
},
setItem() {},
};
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelHotmailManager;`)(
windowObject,
localStorageMock
);
const handlers = {};
const toggleFormButton = {
textContent: '',
disabled: false,
setAttribute(name, value) {
this[name] = value;
},
addEventListener(type, handler) {
if (type === 'click') handlers.toggleForm = handler;
},
};
const formShell = { hidden: true };
const manager = api.createHotmailManager({
state: {
getLatestState: () => ({ currentHotmailAccountId: null }),
syncLatestState() {},
},
dom: {
btnAddHotmailAccount: { textContent: '', disabled: false, addEventListener() {} },
btnClearUsedHotmailAccounts: { textContent: '', disabled: false, addEventListener() {} },
btnDeleteAllHotmailAccounts: { textContent: '', disabled: false, addEventListener() {} },
btnHotmailUsageGuide: { addEventListener() {} },
btnImportHotmailAccounts: { disabled: false, addEventListener() {} },
btnToggleHotmailForm: toggleFormButton,
btnToggleHotmailList: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
hotmailAccountsList: { innerHTML: '', addEventListener() {} },
hotmailFormShell: formShell,
hotmailListShell: { classList: { toggle() {} } },
inputEmail: { value: '' },
inputHotmailClientId: { value: '' },
inputHotmailEmail: { value: '', focus() { this.focused = true; } },
inputHotmailImport: { value: '' },
inputHotmailPassword: { value: '' },
inputHotmailRefreshToken: { value: '' },
selectMailProvider: { value: 'hotmail-api' },
},
helpers: {
getHotmailAccounts: () => [],
getCurrentHotmailEmail: () => '',
escapeHtml: (value) => String(value || ''),
showToast() {},
openConfirmModal: async () => true,
copyTextToClipboard: async () => {},
},
runtime: {
sendMessage: async () => ({}),
},
constants: {
copyIcon: '',
displayTimeZone: 'Asia/Shanghai',
expandedStorageKey: 'multipage-hotmail-list-expanded',
},
hotmailUtils: {},
});
manager.bindHotmailEvents();
assert.equal(formShell.hidden, true);
assert.equal(toggleFormButton.textContent, '添加账号');
handlers.toggleForm();
assert.equal(formShell.hidden, false);
assert.equal(toggleFormButton.textContent, '取消添加');
handlers.toggleForm();
assert.equal(formShell.hidden, true);
assert.equal(toggleFormButton.textContent, '添加账号');
});
test('hotmail manager hides form after save succeeds', async () => {
const source = fs.readFileSync('sidepanel/hotmail-manager.js', 'utf8');
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
},
setItem() {},
};
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelHotmailManager;`)(
windowObject,
localStorageMock
);
const handlers = {};
const formShell = { hidden: true };
const toggleFormButton = {
textContent: '',
disabled: false,
setAttribute() {},
addEventListener(type, handler) {
if (type === 'click') handlers.toggleForm = handler;
},
};
const addButton = {
textContent: '',
disabled: false,
addEventListener(type, handler) {
if (type === 'click') handlers.add = handler;
},
};
const inputHotmailEmail = { value: '', focus() {} };
const inputHotmailClientId = { value: '' };
const inputHotmailPassword = { value: '' };
const inputHotmailRefreshToken = { value: '' };
const toastMessages = [];
const manager = api.createHotmailManager({
state: {
getLatestState: () => ({ currentHotmailAccountId: null }),
syncLatestState() {},
},
dom: {
btnAddHotmailAccount: addButton,
btnClearUsedHotmailAccounts: { textContent: '', disabled: false, addEventListener() {} },
btnDeleteAllHotmailAccounts: { textContent: '', disabled: false, addEventListener() {} },
btnHotmailUsageGuide: { addEventListener() {} },
btnImportHotmailAccounts: { disabled: false, addEventListener() {} },
btnToggleHotmailForm: toggleFormButton,
btnToggleHotmailList: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
hotmailAccountsList: { innerHTML: '', addEventListener() {} },
hotmailFormShell: formShell,
hotmailListShell: { classList: { toggle() {} } },
inputEmail: { value: '' },
inputHotmailClientId,
inputHotmailEmail,
inputHotmailImport: { value: '' },
inputHotmailPassword,
inputHotmailRefreshToken,
selectMailProvider: { value: 'hotmail-api' },
},
helpers: {
getHotmailAccounts: () => [],
getCurrentHotmailEmail: () => '',
escapeHtml: (value) => String(value || ''),
showToast(message) {
toastMessages.push(message);
},
openConfirmModal: async () => true,
copyTextToClipboard: async () => {},
},
runtime: {
sendMessage: async () => ({
account: {
id: 'acc-1',
email: 'demo@hotmail.com',
clientId: 'client-id',
refreshToken: 'refresh-token',
},
}),
},
constants: {
copyIcon: '',
displayTimeZone: 'Asia/Shanghai',
expandedStorageKey: 'multipage-hotmail-list-expanded',
},
hotmailUtils: {},
});
manager.bindHotmailEvents();
handlers.toggleForm();
inputHotmailEmail.value = 'demo@hotmail.com';
inputHotmailClientId.value = 'client-id';
inputHotmailPassword.value = 'secret';
inputHotmailRefreshToken.value = 'refresh-token';
await handlers.add();
assert.equal(formShell.hidden, true);
assert.equal(toggleFormButton.textContent, '添加账号');
assert.equal(inputHotmailEmail.value, '');
assert.equal(inputHotmailClientId.value, '');
assert.equal(inputHotmailPassword.value, '');
assert.equal(inputHotmailRefreshToken.value, '');
assert.match(toastMessages.at(-1) || '', /已保存 Hotmail 账号/);
});
+227
View File
@@ -0,0 +1,227 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => sidepanelSource.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 < sidepanelSource.length; i += 1) {
const ch = sidepanelSource[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 < sidepanelSource.length; end += 1) {
const ch = sidepanelSource[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return sidepanelSource.slice(start, end);
}
test('syncSelectedMail2925PoolAccount writes selected pool email back to mail2925BaseEmail', async () => {
const bundle = [
extractFunction('getMail2925Accounts'),
extractFunction('getCurrentMail2925Account'),
extractFunction('getCurrentMail2925Email'),
extractFunction('isMail2925AccountPoolEnabled'),
extractFunction('syncMail2925PoolAccountOptions'),
extractFunction('getPreferredMail2925PoolAccountId'),
extractFunction('syncSelectedMail2925PoolAccount'),
].join('\n');
const api = new Function(`
let latestState = {
mail2925UseAccountPool: true,
mail2925BaseEmail: 'old@2925.com',
currentMail2925AccountId: '',
mail2925Accounts: [{ id: 'acc-1', email: 'new@2925.com' }],
};
const selectMail2925PoolAccount = { value: 'acc-1', innerHTML: '' };
const chrome = {
runtime: {
async sendMessage() {
return { account: { id: 'acc-1', email: 'new@2925.com' } };
},
},
};
const toastEvents = [];
function syncLatestState(patch) {
latestState = { ...latestState, ...patch };
}
function setManagedAliasBaseEmailInputForProvider() {}
function showToast(message) {
toastEvents.push(message);
}
function escapeHtml(value) {
return String(value || '');
}
${bundle}
return {
syncSelectedMail2925PoolAccount,
getLatestState() {
return latestState;
},
};
`)();
await api.syncSelectedMail2925PoolAccount({ silent: true });
assert.equal(api.getLatestState().currentMail2925AccountId, 'acc-1');
assert.equal(api.getLatestState().mail2925BaseEmail, 'new@2925.com');
});
test('syncMail2925BaseEmailFromCurrentAccount reuses current pool account email for manual base email field', async () => {
const bundle = [
extractFunction('getMail2925Accounts'),
extractFunction('getCurrentMail2925Account'),
extractFunction('getCurrentMail2925Email'),
extractFunction('isMail2925AccountPoolEnabled'),
extractFunction('syncMail2925BaseEmailFromCurrentAccount'),
].join('\n');
const api = new Function(`
let latestState = {
mail2925UseAccountPool: true,
mail2925BaseEmail: 'old@2925.com',
currentMail2925AccountId: 'acc-1',
mail2925Accounts: [{ id: 'acc-1', email: 'new@2925.com' }],
};
let saveCalls = 0;
function syncLatestState(patch) {
latestState = { ...latestState, ...patch };
}
async function saveSettings() {
saveCalls += 1;
}
${bundle}
return {
syncMail2925BaseEmailFromCurrentAccount,
getLatestState() {
return latestState;
},
getSaveCalls() {
return saveCalls;
},
};
`)();
const changed = api.syncMail2925BaseEmailFromCurrentAccount(undefined, { persist: true });
await Promise.resolve();
assert.equal(changed, true);
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 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 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);
});
+52 -4
View File
@@ -2,14 +2,61 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('sidepanel html contains cancel edit button for mail2925 form', () => {
function createAccountPoolUiStub() {
return {
createAccountPoolFormController({
formShell,
toggleButton,
hiddenLabel = '添加账号',
visibleLabel = '取消添加',
onClear,
onFocus,
} = {}) {
let visible = false;
function sync() {
if (formShell) {
formShell.hidden = !visible;
}
if (toggleButton) {
toggleButton.textContent = visible ? visibleLabel : hiddenLabel;
toggleButton.setAttribute?.('aria-expanded', String(visible));
}
}
function setVisible(nextVisible, options = {}) {
visible = Boolean(nextVisible);
if (options.clearForm) {
onClear?.();
}
sync();
if (visible && options.focusField) {
onFocus?.();
}
}
sync();
return {
isVisible: () => visible,
setVisible,
sync,
};
},
};
}
test('sidepanel html contains collapsible mail2925 form controls', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="btn-cancel-mail2925-edit"/);
assert.match(html, /id="btn-toggle-mail2925-form"/);
assert.match(html, /id="mail2925-form-shell"/);
assert.doesNotMatch(html, /id="btn-cancel-mail2925-edit"/);
});
test('mail2925 manager renders edit action for existing accounts', () => {
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
const windowObject = {};
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
@@ -43,14 +90,15 @@ test('mail2925 manager renders edit action for existing accounts', () => {
},
dom: {
btnAddMail2925Account: { textContent: '', disabled: false, addEventListener() {} },
btnCancelMail2925Edit: { style: { display: 'none' }, addEventListener() {} },
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
btnToggleMail2925Form: { textContent: '', setAttribute() {}, addEventListener() {} },
btnToggleMail2925List: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
inputMail2925Email: { value: '' },
inputMail2925Import: { value: '' },
inputMail2925Password: { value: '' },
mail2925AccountsList,
mail2925FormShell: { hidden: true },
mail2925ListShell: { classList: { toggle() {} } },
},
helpers: {
+254 -2
View File
@@ -2,13 +2,59 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function createAccountPoolUiStub() {
return {
createAccountPoolFormController({
formShell,
toggleButton,
hiddenLabel = '添加账号',
visibleLabel = '取消添加',
onClear,
onFocus,
} = {}) {
let visible = false;
function sync() {
if (formShell) {
formShell.hidden = !visible;
}
if (toggleButton) {
toggleButton.textContent = visible ? visibleLabel : hiddenLabel;
toggleButton.setAttribute?.('aria-expanded', String(visible));
}
}
function setVisible(nextVisible, options = {}) {
visible = Boolean(nextVisible);
if (options.clearForm) {
onClear?.();
}
sync();
if (visible && options.focusField) {
onFocus?.();
}
}
sync();
return {
isVisible: () => visible,
setVisible,
sync,
};
},
};
}
test('sidepanel loads mail2925 manager before sidepanel bootstrap', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const helperIndex = html.indexOf('<script src="account-pool-ui.js"></script>');
const managerIndex = html.indexOf('<script src="mail-2925-manager.js"></script>');
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
assert.notEqual(helperIndex, -1);
assert.notEqual(managerIndex, -1);
assert.notEqual(sidepanelIndex, -1);
assert.ok(helperIndex < managerIndex);
assert.ok(managerIndex < sidepanelIndex);
});
@@ -17,12 +63,21 @@ test('sidepanel html contains mail2925 pool toggle and selector controls', () =>
assert.match(html, /id="input-mail2925-use-account-pool"/);
assert.match(html, /id="select-mail2925-pool-account"/);
assert.match(html, /id="btn-cancel-mail2925-edit"/);
assert.match(html, /id="btn-toggle-mail2925-form"/);
assert.match(html, /id="mail2925-form-shell"/);
assert.doesNotMatch(html, /id="btn-cancel-mail2925-edit"/);
});
test('sidepanel css keeps collapsed shared mailbox list near a single card height', () => {
const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8');
assert.match(css, /\.hotmail-list-shell\.is-collapsed\s*\{\s*max-height:\s*176px;\s*\}/);
});
test('mail2925 manager exposes a factory and renders empty state', () => {
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
const windowObject = {};
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
@@ -38,6 +93,12 @@ test('mail2925 manager exposes a factory and renders empty state', () => {
assert.equal(typeof api?.createMail2925Manager, 'function');
const mail2925AccountsList = { innerHTML: '', addEventListener() {} };
const formToggleButton = {
textContent: '',
disabled: false,
setAttribute() {},
addEventListener() {},
};
const toggleButton = {
textContent: '',
disabled: false,
@@ -55,11 +116,13 @@ test('mail2925 manager exposes a factory and renders empty state', () => {
btnAddMail2925Account: { disabled: false, addEventListener() {} },
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
btnToggleMail2925Form: formToggleButton,
btnToggleMail2925List: toggleButton,
inputMail2925Email: { value: '' },
inputMail2925Import: { value: '' },
inputMail2925Password: { value: '' },
mail2925AccountsList,
mail2925FormShell: { hidden: true },
mail2925ListShell: { classList: noopClassList },
},
helpers: {
@@ -88,3 +151,192 @@ test('mail2925 manager exposes a factory and renders empty state', () => {
manager.renderMail2925Accounts();
assert.match(mail2925AccountsList.innerHTML, /还没有 2925 账号/);
});
test('mail2925 manager toggles form container from header button', () => {
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
},
setItem() {},
};
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelMail2925Manager;`)(
windowObject,
localStorageMock
);
const clickHandlers = {};
const formToggleButton = {
textContent: '',
disabled: false,
setAttribute(name, value) {
this[name] = value;
},
addEventListener(type, handler) {
clickHandlers[type] = handler;
},
};
const formShell = { hidden: true };
const manager = api.createMail2925Manager({
state: {
getLatestState: () => ({ currentMail2925AccountId: null, mail2925Accounts: [] }),
syncLatestState() {},
},
dom: {
btnAddMail2925Account: { textContent: '', disabled: false, addEventListener() {} },
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
btnToggleMail2925Form: formToggleButton,
btnToggleMail2925List: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
inputMail2925Email: { value: '', focus() { this.focused = true; } },
inputMail2925Import: { value: '' },
inputMail2925Password: { value: '' },
mail2925AccountsList: { innerHTML: '', addEventListener() {} },
mail2925FormShell: formShell,
mail2925ListShell: { classList: { toggle() {} } },
},
helpers: {
getMail2925Accounts: () => [],
escapeHtml: (value) => String(value || ''),
showToast() {},
openConfirmModal: async () => true,
copyTextToClipboard: async () => {},
refreshManagedAliasBaseEmail() {},
},
runtime: {
sendMessage: async () => ({}),
},
constants: {
copyIcon: '',
displayTimeZone: 'Asia/Shanghai',
expandedStorageKey: 'multipage-mail2925-list-expanded',
},
mail2925Utils: {},
});
manager.bindMail2925Events();
assert.equal(formShell.hidden, true);
assert.equal(formToggleButton.textContent, '添加账号');
clickHandlers.click();
assert.equal(formShell.hidden, false);
assert.equal(formToggleButton.textContent, '取消添加');
clickHandlers.click();
assert.equal(formShell.hidden, true);
assert.equal(formToggleButton.textContent, '添加账号');
});
test('mail2925 manager hides form after save succeeds', async () => {
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
},
setItem() {},
};
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelMail2925Manager;`)(
windowObject,
localStorageMock
);
let latestState = { currentMail2925AccountId: null, mail2925Accounts: [] };
const handlers = {};
const formToggleButton = {
textContent: '',
disabled: false,
setAttribute() {},
addEventListener(type, handler) {
if (type === 'click') handlers.toggle = handler;
},
};
const addButton = {
textContent: '',
disabled: false,
addEventListener(type, handler) {
if (type === 'click') handlers.add = handler;
},
};
const formShell = { hidden: true };
const inputMail2925Email = { value: '', focus() {} };
const inputMail2925Password = { value: '' };
const toastMessages = [];
const manager = api.createMail2925Manager({
state: {
getLatestState: () => latestState,
syncLatestState(patch) {
latestState = { ...latestState, ...patch };
},
},
dom: {
btnAddMail2925Account: addButton,
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
btnToggleMail2925Form: formToggleButton,
btnToggleMail2925List: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
inputMail2925Email,
inputMail2925Import: { value: '' },
inputMail2925Password,
mail2925AccountsList: { innerHTML: '', addEventListener() {} },
mail2925FormShell: formShell,
mail2925ListShell: { classList: { toggle() {} } },
},
helpers: {
getMail2925Accounts: (state) => state.mail2925Accounts || [],
escapeHtml: (value) => String(value || ''),
showToast(message) {
toastMessages.push(message);
},
openConfirmModal: async () => true,
copyTextToClipboard: async () => {},
refreshManagedAliasBaseEmail() {},
},
runtime: {
sendMessage: async () => ({
account: {
id: 'acc-1',
email: 'demo@2925.com',
password: 'secret',
enabled: true,
lastLoginAt: 0,
lastUsedAt: 0,
lastLimitAt: 0,
disabledUntil: 0,
lastError: '',
},
}),
},
constants: {
copyIcon: '',
displayTimeZone: 'Asia/Shanghai',
expandedStorageKey: 'multipage-mail2925-list-expanded',
},
mail2925Utils: {
upsertMail2925AccountInList: (accounts, nextAccount) => accounts.concat(nextAccount),
},
});
manager.bindMail2925Events();
handlers.toggle();
inputMail2925Email.value = 'demo@2925.com';
inputMail2925Password.value = 'secret';
await handlers.add();
assert.equal(formShell.hidden, true);
assert.equal(formToggleButton.textContent, '添加账号');
assert.equal(addButton.textContent, '添加账号');
assert.equal(inputMail2925Email.value, '');
assert.equal(inputMail2925Password.value, '');
assert.match(toastMessages.at(-1) || '', /已保存 2925 账号/);
});
+109
View File
@@ -0,0 +1,109 @@
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;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
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('sidepanel html keeps 2925 mode row and standalone pool settings row', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="row-mail-2925-mode"/);
assert.match(html, /data-mail2925-mode="provide"/);
assert.match(html, /data-mail2925-mode="receive"/);
assert.match(html, /id="row-mail2925-pool-settings"/);
});
test('sidepanel only treats 2925 as generated alias provider in provide mode', () => {
const bundle = [
extractFunction('isManagedAliasProvider'),
extractFunction('usesGeneratedAliasMailProvider'),
].join('\n');
const api = new Function(`
const GMAIL_PROVIDER = 'gmail';
const MAIL_2925_MODE_PROVIDE = 'provide';
const MAIL_2925_MODE_RECEIVE = 'receive';
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
const selectMailProvider = { value: '2925' };
function normalizeMail2925Mode(value = '') {
return String(value || '').trim().toLowerCase() === MAIL_2925_MODE_RECEIVE
? MAIL_2925_MODE_RECEIVE
: DEFAULT_MAIL_2925_MODE;
}
function getSelectedMail2925Mode() {
return MAIL_2925_MODE_PROVIDE;
}
function getManagedAliasUtils() {
return {
usesManagedAliasGeneration(provider, options = {}) {
return String(provider || '').trim().toLowerCase() === 'gmail'
|| (String(provider || '').trim().toLowerCase() === '2925'
&& normalizeMail2925Mode(options.mail2925Mode) === MAIL_2925_MODE_PROVIDE);
},
};
}
${bundle}
return {
isManagedAliasProvider,
usesGeneratedAliasMailProvider,
};
`)();
assert.equal(api.isManagedAliasProvider('2925', 'provide'), true);
assert.equal(api.isManagedAliasProvider('2925', 'receive'), false);
assert.equal(api.usesGeneratedAliasMailProvider('2925', 'provide'), true);
assert.equal(api.usesGeneratedAliasMailProvider('2925', 'receive'), false);
assert.equal(api.usesGeneratedAliasMailProvider('gmail', 'receive'), 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/);
});
+156
View File
@@ -0,0 +1,156 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('content/signup-page.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;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
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('fillVerificationCode submits after split inputs are stably filled', async () => {
const api = new Function(`
const logs = [];
const clicks = [];
const filledValues = [];
let submitClicked = false;
const VERIFICATION_CODE_INPUT_SELECTOR = 'input[data-verification-code]';
const location = { href: 'https://auth.openai.com/email-verification' };
function KeyboardEvent(type, init = {}) {
this.type = type;
Object.assign(this, init);
}
const submitBtn = {
tagName: 'BUTTON',
textContent: 'Continue',
disabled: false,
getAttribute(name) {
if (name === 'type') return 'submit';
if (name === 'aria-disabled') return 'false';
return '';
},
click() {
submitClicked = true;
},
};
const inputs = Array.from({ length: 6 }, () => ({
value: '',
maxLength: 1,
getAttribute(name) {
if (name === 'maxlength') return '1';
if (name === 'aria-disabled') return 'false';
return '';
},
focus() {},
dispatchEvent() {},
closest() { return null; },
}));
const document = {
querySelector(selector) {
if (selector === VERIFICATION_CODE_INPUT_SELECTOR) return inputs[0];
return null;
},
querySelectorAll(selector) {
if (selector === 'input[maxlength="1"]') return inputs;
if (selector === 'button[type="submit"], input[type="submit"]') return [submitBtn];
if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') return [submitBtn];
return [];
},
};
function throwIfStopped() {}
function log(message, level = 'info') { logs.push({ message, level }); }
async function waitForLoginVerificationPageReady() {}
function is405MethodNotAllowedPage() { return false; }
async function handle405ResendError() {}
function fillInput(el, value) {
el.value = value;
filledValues.push(value);
}
async function sleep() {}
function isVisibleElement() { return true; }
function isActionEnabled(el) { return Boolean(el) && !el.disabled; }
function getActionText(el) { return el.textContent || ''; }
async function humanPause() {}
function simulateClick(el) { el.click(); clicks.push(el.textContent); }
async function waitForVerificationSubmitOutcome() { return { success: true }; }
${extractFunction('getVisibleSplitVerificationInputs')}
${extractFunction('getVerificationCodeTarget')}
${extractFunction('getVerificationSubmitButtonForTarget')}
${extractFunction('waitForVerificationSubmitButton')}
${extractFunction('waitForVerificationCodeTarget')}
${extractFunction('waitForSplitVerificationInputsFilled')}
${extractFunction('fillVerificationCode')}
return {
run() {
return fillVerificationCode(4, { code: '123456' });
},
snapshot() {
return {
logs,
clicks,
filledValues,
submitClicked,
currentValue: inputs.map((input) => input.value).join(''),
};
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.deepStrictEqual(result, { success: true });
assert.equal(snapshot.currentValue, '123456');
assert.equal(snapshot.submitClicked, true);
assert.deepStrictEqual(snapshot.clicks, ['Continue']);
});
+151
View File
@@ -0,0 +1,151 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('content/signup-page.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;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
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('waitForVerificationSubmitOutcome recovers signup retry page after submit', async () => {
const api = new Function(`
let retryVisible = true;
let step5Ready = false;
let recoverCalls = 0;
const location = { href: 'https://auth.openai.com/email-verification' };
function throwIfStopped() {}
function log() {}
function getVerificationErrorText() { return ''; }
function isStep5Ready() { return step5Ready; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
function isVerificationPageStillVisible() { return false; }
function createSignupUserAlreadyExistsError() {
return new Error('SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。');
}
function getCurrentAuthRetryPageState(flow) {
if (flow === 'signup' && retryVisible) {
return {
retryEnabled: true,
userAlreadyExistsBlocked: false,
};
}
return null;
}
async function recoverCurrentAuthRetryPage() {
recoverCalls += 1;
retryVisible = false;
step5Ready = true;
}
async function sleep() {}
${extractFunction('waitForVerificationSubmitOutcome')}
return {
run() {
return waitForVerificationSubmitOutcome(4, 1000);
},
snapshot() {
return { recoverCalls };
},
};
`)();
const result = await api.run();
assert.deepStrictEqual(result, { success: true });
assert.equal(api.snapshot().recoverCalls, 1);
});
test('waitForVerificationSubmitOutcome does not assume success after repeated signup retry pages', async () => {
const api = new Function(`
let recoverCalls = 0;
const location = { href: 'https://auth.openai.com/email-verification' };
function throwIfStopped() {}
function log() {}
function getVerificationErrorText() { return ''; }
function isStep5Ready() { return false; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
function isVerificationPageStillVisible() { return false; }
function createSignupUserAlreadyExistsError() {
return new Error('SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。');
}
function getCurrentAuthRetryPageState(flow) {
if (flow === 'signup') {
return {
retryEnabled: true,
userAlreadyExistsBlocked: false,
};
}
return null;
}
async function recoverCurrentAuthRetryPage() {
recoverCalls += 1;
}
async function sleep() {}
${extractFunction('waitForVerificationSubmitOutcome')}
return {
run() {
return waitForVerificationSubmitOutcome(4, 1000);
},
snapshot() {
return { recoverCalls };
},
};
`)();
await assert.rejects(
api.run(),
/连续进入认证重试页 2 次,页面仍未恢复/
);
assert.equal(api.snapshot().recoverCalls, 2);
});
+21
View File
@@ -146,6 +146,27 @@ return {
assert.strictEqual(snapshot.displayedEmail, 'display.user@example.com');
}
{
const api = createApi({
pathname: '/email-verification',
retryState: {
retryEnabled: true,
titleMatched: false,
detailMatched: false,
routeErrorMatched: true,
},
verificationTarget: { id: 'otp' },
verificationVisible: true,
});
const snapshot = api.inspectLoginAuthState();
assert.strictEqual(
snapshot.state,
'login_timeout_error_page',
'第七步在 /email-verification 的登录重试页应优先识别为登录超时报错页'
);
}
{
const api = createApi({
oauthConsentPage: true,
+108
View File
@@ -103,3 +103,111 @@ return {
assert.equal(result.state, 'login_timeout_error_page');
assert.equal(result.message, '当前页面处于登录超时报错页。');
});
test('step 7 finalize converts verification page that falls into retry page into recoverable result', async () => {
const api = new Function(`
const logs = [];
let recoverCalls = 0;
let currentState = 'verification_page';
const location = {
href: 'https://auth.openai.com/email-verification',
};
function inspectLoginAuthState() {
return {
state: currentState,
url: location.href,
};
}
async function recoverCurrentAuthRetryPage() {
recoverCalls += 1;
return { recovered: true };
}
async function sleep() {
currentState = 'login_timeout_error_page';
}
function log(message, level = 'info') {
logs.push({ message, level });
}
function throwIfStopped() {}
function getLoginAuthStateLabel(snapshot) {
switch (snapshot?.state) {
case 'verification_page':
return '登录验证码页';
case 'login_timeout_error_page':
return '登录超时报错页';
default:
return '未知页面';
}
}
${extractFunction('createStep6SuccessResult')}
${extractFunction('createStep6RecoverableResult')}
${extractFunction('normalizeStep6Snapshot')}
${extractFunction('createStep6LoginTimeoutRecoverableResult')}
${extractFunction('finalizeStep6VerificationReady')}
return {
async run() {
return finalizeStep6VerificationReady({
logLabel: '步骤 7 收尾',
loginVerificationRequestedAt: 123,
via: 'password_submit',
});
},
snapshot() {
return { logs, recoverCalls };
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.equal(snapshot.recoverCalls, 1);
assert.equal(result.step6Outcome, 'recoverable');
assert.equal(result.reason, 'login_timeout_error_page');
assert.equal(result.state, 'login_timeout_error_page');
assert.equal(result.message, '登录验证码页面准备就绪前进入登录超时报错页。');
});
test('waitForLoginVerificationPageReady reports login timeout page without step8 restart prefix', async () => {
const api = new Function(`
const location = {
href: 'https://auth.openai.com/email-verification',
};
function inspectLoginAuthState() {
return {
state: 'login_timeout_error_page',
url: location.href,
};
}
function throwIfStopped() {}
async function sleep() {}
function getLoginAuthStateLabel(snapshot) {
return snapshot?.state === 'login_timeout_error_page' ? '登录超时报错页' : '未知页面';
}
${extractFunction('waitForLoginVerificationPageReady')}
return {
run() {
return waitForLoginVerificationPageReady(10);
},
};
`)();
await assert.rejects(
() => api.run(),
/当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:登录超时报错页。URL: https:\/\/auth\.openai\.com\/email-verification/
);
});
+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), /更换浏览器后重新进行注册登录/);
});
+43
View File
@@ -43,6 +43,49 @@ test('verification flow keeps 2925 polling cadence in the default payload', () =
assert.equal(step8Payload.intervalMs, 15000);
});
test('verification flow only enables 2925 target email matching in receive mode', () => {
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async () => ({}),
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
const providePayload = helpers.getVerificationPollPayload(4, {
email: 'user@example.com',
mailProvider: '2925',
mail2925Mode: 'provide',
});
const receivePayload = helpers.getVerificationPollPayload(4, {
email: 'user@example.com',
mailProvider: '2925',
mail2925Mode: 'receive',
});
assert.equal(providePayload.mail2925MatchTargetEmail, false);
assert.equal(receivePayload.mail2925MatchTargetEmail, true);
});
test('verification flow runs beforeSubmit hook before filling the code', async () => {
const events = [];
+86 -17
View File
@@ -28,7 +28,7 @@
### 2.1 Sidepanel
[sidepanel/sidepanel.html](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.html) + [sidepanel/sidepanel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.js) + [sidepanel/update-service.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/update-service.js) + [sidepanel/contribution-content-update-service.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/contribution-content-update-service.js)
[sidepanel/sidepanel.html](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.html) + [sidepanel/sidepanel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.js) + [sidepanel/account-pool-ui.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/account-pool-ui.js) + [sidepanel/update-service.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/update-service.js) + [sidepanel/contribution-content-update-service.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/contribution-content-update-service.js)
职责:
@@ -43,6 +43,7 @@
- 在 sidepanel 初始化和点击“自动”按钮前刷新一次贡献站公开内容摘要;如果刷新失败,不阻塞主自动流程
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Pro` 与 legacy `v` 两个版本族,排序时优先保持版本族语义一致,同时会在读取缓存后重新排序,避免旧缓存把 `v` 版本误显示为比 `Pro` 更新
- 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作
### 2.2 Background Service Worker
@@ -140,7 +141,7 @@
- 标签注册表
- 最近打开的来源地址
- LuckMail 当前运行时选择
- 2925 当前运行时账号选择 `currentMail2925AccountId`
- 2925 当前选中的账号 ID `currentMail2925AccountId`(运行时会同步到持久配置,用于重开浏览器后恢复同一个号池账号)
补充:
@@ -155,6 +156,7 @@
- Hotmail 账号池
- 2925 账号池
- 2925 是否启用号池模式 `mail2925UseAccountPool`
- 2925 当前选中的号池账号 ID `currentMail2925AccountId`
- Cloudflare / Temp Email 设置
- iCloud 相关偏好
- LuckMail API 配置
@@ -330,12 +332,13 @@
补充行为:
- `2925` provider 会关闭 Step 4 / 8 的自动重发间隔 25 秒节流;每次“重新发送验证码”之间,会在邮箱页内部执行一轮固定 15 次的刷新轮询,不再因 OAuth 剩余时间预算而缩短。
- 当 provider 为 `2925` 时,后台会在进入 Step 4 / Step 8 之前先确保当前 2925 账号已登录网页邮箱;如果当前标签页仍未登录,会自动补一次“清理 2925 cookie -> 等待 3 秒 -> 打开登录页 -> 页面打开后再等待 3 秒 -> 使用当前账号密码重新登录”
- 当 provider 为 `2925` 时,Step 4 会优先直接打开当前 2925 邮箱页,并先比对页面顶部显示的邮箱地址是否与当前目标邮箱一致:如果一致,就直接复用当前已登录页面;如果不一致且启用了 2925 账号池,则会先清理 cookie 再登录当前选中的账号;如果不一致且未启用账号池,则直接复用现有停止逻辑结束流程。Step 8 不再额外承接这套登录态处理
- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 当前既不依赖时间窗,也不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中的匹配邮件。
- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
- 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
- `2925` 内容脚本会把每一封实际打开检测的邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交;如果再次遇到相同验证码,对应邮件也会在读取后立即删除,避免后续反复打开。
- `2925` 当前不再对邮件里的收件邮箱做比对,只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。
- `2925` 在 provide 模式下仍保持宽松匹配:只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。
- `2925` 在 receive 模式下会恢复“弱目标邮箱匹配”:只有当邮件里显式出现了其他收件邮箱时才会跳过;如果邮件里没有明确写出邮箱,仍允许继续尝试该验证码。
- 当验证码最终提交成功后,后台会异步向 2925 邮箱页发送 `DELETE_ALL_EMAILS`,执行“全选 + 删除”清理剩余邮件,不阻塞主流程。
- 如果 `2925` 邮箱页在轮询期间出现“子邮箱已达上限邮箱”,后台会记录当前时间,把当前 2925 账号禁用 24 小时,自动切到下一个可用账号并完成登录,然后直接报错结束当前尝试;如果 Auto 开启了自动重试,现有控制器会按原逻辑进入下一次尝试。
@@ -408,7 +411,8 @@
补充:
- 对 `2925` 而言,Step 8 不依赖验证码页显示邮箱做收件匹配,而是直接测试所有命中 ChatGPT / OpenAI 过滤条件的邮件。
- 对 `2925 provide` 而言,Step 8 不依赖验证码页显示邮箱做收件匹配,而是直接测试所有命中 ChatGPT / OpenAI 过滤条件的邮件。
- 对 `2925 receive` 而言,Step 8 会把当前目标注册邮箱一并传给 2925 内容脚本;只有当邮件里显式写出了其他邮箱时才会跳过,从而在不破坏历史兼容性的前提下,尽量降低误收验证码的概率。
### Step 9
@@ -451,25 +455,26 @@
### 7.1 2026-04-17 补充:Gmail / 2925 统一别名邮箱链路
本轮将 Gmail 与 2925 的注册邮箱逻辑统一收敛为“共享别名邮箱链路”:
本轮将 Gmail 与 `2925 + provide` 的注册邮箱逻辑统一收敛为“共享别名邮箱链路”:
- 两者都先填写“基邮箱”
- Gmail`name@gmail.com`
- 2925`name@2925.com`
- 2925(仅 provide 模式)`name@2925.com`
- 两者都允许两种入口:
- 点击侧边栏按钮自动生成完整注册邮箱
- 直接在“注册邮箱”输入框中手动填写完整邮箱
- 当 `mailProvider = 2925``mail2925Mode = receive` 时,不再走别名基邮箱链路,而是回退到普通“邮箱生成 / 手动填写注册邮箱”路线;2925 仅负责后续收信与登录态管理
当前行为约定:
1. sidepanel 展示“别名基邮箱”输入框,并根据当前 provider 显示对应文案
1. sidepanel 会根据当前 provider 与 `mail2925Mode` 决定是否展示“别名基邮箱”输入框
2. 点击 `获取 / 生成` 时:
- Gmail 生成 `name+tag@gmail.com`
- 2925 生成 `name123456@2925.com`
- 2925 仅在 provide 模式下生成 `name123456@2925.com`
3. Step 2 / Step 3 进入注册流程前,会先判断当前 `state.email`
- 如果已经是与当前基邮箱兼容的完整邮箱,则直接复用
- 如果为空或不兼容,则按当前 provider 重新生成
4. 保存或执行 Step 3 时,如果手动填写的完整邮箱与当前 Gmail / 2925 基邮箱不兼容,sidepanel 会直接拦截
- 如果为空或不兼容,则按当前 provider / mode 重新生成
4. 保存或执行 Step 3 时,如果手动填写的完整邮箱与当前 Gmail / 2925 provide 基邮箱不兼容,sidepanel 会直接拦截`2925 receive` 不参与这条兼容性约束
5. auto-run fresh attempt reset 时,会保留:
- `gmailBaseEmail`
- `mail2925BaseEmail`
@@ -509,6 +514,8 @@
- [hotmail-utils.js](c:/Users/projectf/Downloads/codex注册扩展/hotmail-utils.js)
- [microsoft-email.js](c:/Users/projectf/Downloads/codex注册扩展/microsoft-email.js)
- [scripts/hotmail_helper.py](c:/Users/projectf/Downloads/codex注册扩展/scripts/hotmail_helper.py)
- [sidepanel/hotmail-manager.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/hotmail-manager.js)
- [sidepanel/account-pool-ui.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/account-pool-ui.js)
模式:
@@ -519,6 +526,7 @@
- 本地 helper 除了收信与验证码读取,还提供邮箱记录 JSON 快照同步接口。
- 账号运行历史本地同步由独立配置控制,不再绑定 Hotmail 的本地助手模式。
- sidepanel 中 Hotmail 账号池的新增表单默认收起,头部通过共享按钮切换“添加账号 / 取消添加”;表单显隐、按钮文案切换、清空与聚焦都复用 `sidepanel/account-pool-ui.js`,不在 Hotmail manager 内重复实现一套。
### 7.3 LuckMail
@@ -534,6 +542,7 @@
- [mail2925-utils.js](c:/Users/projectf/Downloads/codex注册扩展/mail2925-utils.js)
- [background/mail-2925-session.js](c:/Users/projectf/Downloads/codex注册扩展/background/mail-2925-session.js)
- [sidepanel/mail-2925-manager.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/mail-2925-manager.js)
- [sidepanel/account-pool-ui.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/account-pool-ui.js)
职责:
@@ -543,15 +552,72 @@
统一承接 2925 账号池的持久化、当前账号切换、cookie 清理登出、网页登录态确认、自动登录,以及命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
- `sidepanel/mail-2925-manager.js`
负责 2925 账号池的新增、导入、切换、手动登录、禁用、清冷却与删除。
- `sidepanel/account-pool-ui.js`
负责 Hotmail / 2925 账号池共用的新增表单显隐、头部按钮文案切换、清空表单与首字段聚焦;2925 manager 不再单独维护另一套表单开关状态机。
链路:
1. 用户在 sidepanel 的 2925 账号池中保存 `email / password`
2. 只有当 sidepanel 中`mail2925UseAccountPool` 开关开启时,别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
3. 手动点击 `登录` 或自动流程进入 Step 4 / Step 8 前,只有在号池模式开启时,后台才会确保当前 2925 账号已登录网页邮箱;登录动作提交后,内容脚本会在 20 秒内轮询“是否已进入收件箱”
4. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先记录当前时间;若当前处于号池模式且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录
5. 如果登录提交后 20 秒内仍未进入收件箱,且当前正处于自动运行中,则后台会直接用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态;这类情况常见于图片验证、行为验证或其他阻断登录的中间页
6. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态
2. sidepanel 中会单独展示 `提供邮箱 / 接收邮箱` 模式切换,以及独立的 `2925 号池` 开关 / 当前账号下拉框;这样即使切到 receive 模式,账号池设置也不会被别名基邮箱行一起隐藏
3. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,provide 模式下的别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
4. 手动点击 `登录` 或自动流程进入 Step 4 前,后台会先打开当前 2925 邮箱页,并读取页面顶部当前邮箱地址:如果仍停留在收件箱且顶部邮箱与目标邮箱一致,则直接复用;如果顶部邮箱不一致且启用了号池模式,则先清理 cookie 后登录当前选中的账号;如果顶部邮箱不一致且未启用号池模式,则直接调用现有停止逻辑结束流程;如果页面跳到登录页,则仍然只有号池模式开启时才自动登录
5. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先判断是否启用了号池模式:若已启用且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录;若未启用,则直接用现有停止逻辑结束流程
6. 如果登录页已经识别到账号密码输入框,内容脚本会在填完账号密码后额外等待 1 秒再点击登录;若点击登录后 40 秒内仍未进入收件箱,且当前正处于自动运行中,则后台会直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态;这类情况常见于图片验证、行为验证或其他阻断登录的中间页
7. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态
8. sidepanel 中 2925 账号池的新增表单也走与 Hotmail 相同的共享交互:默认收起,头部按钮切换“添加账号 / 取消添加”,操作行右侧提供“批量导入”,保存成功后自动收起并清空。
9. 当 2925 号池模式开启时,当前选中的号池邮箱会同步回写到同一个 `mail2925BaseEmail` 字段;因此用户切换号池账号后,即使再次关闭号池模式,也会直接沿用刚才选中的邮箱作为手动基邮箱,无需重新输入。
### 7.4.1 2925 双模式维护约定
这是后续维护 `2925` 时最容易被重新打散的一段链路,建议按下面的职责边界理解:
- UI 层:
- `sidepanel/sidepanel.html`
- 负责展示 `提供邮箱 / 接收邮箱`
- 负责展示独立的 `2925 号池` 配置行
- 不负责决定“2925 是否参与别名邮箱生成”
- `sidepanel/sidepanel.js`
- 只负责把当前 `mail2925Mode`、号池开关、当前账号选择同步到 state / runtime message
- 只负责用共享规则决定当前文案、显隐和前端校验
- 不应再次复制一套“provide / receive 对应什么行为”的业务判断
- 共享规则层:
- `managed-alias-utils.js`
- 负责 `mail2925Mode` 归一化
- 负责判断 `2925` 当前是否属于“别名邮箱 provider”
- 负责保证 `2925` 只有在 `provide` 模式下才进入共享别名邮箱链路
- 如果未来要新增第三种模式,应该先改这里,再改 sidepanel / background 接线
- 注册邮箱生成层:
- `background/generated-email-helpers.js`
- 只负责根据当前 provider / mode 选择“共享别名邮箱”还是“普通邮箱生成器”
- 不负责定义 `provide / receive` 的语义本身
- `background/signup-flow-helpers.js`
- 只负责真正提交前的“复用已有邮箱 / 重新生成邮箱”
- 这里看到的 `isGeneratedAliasProvider(state)` 已经包含 `mail2925Mode` 语义,不应该再手写 `state.mail2925Mode === 'provide'`
- 收信与验证码层:
- `background/verification-flow.js`
- 负责决定是否给 2925 内容脚本下发 `mail2925MatchTargetEmail`
- 当前约定:仅 `receive` 模式开启弱目标邮箱匹配
- `content/mail-2925.js`
- 负责真正执行“弱目标邮箱匹配”
- 当前约定:只有邮件里显式出现了其他邮箱才跳过;若邮件里没有写邮箱,仍允许继续尝试
- 这样做的目的,是在不破坏历史兼容性的前提下,降低 receive 模式误收验证码的概率
- 账号池与登录态层:
- `background/mail-2925-session.js`
- 不关心 `provide / receive` 哪个负责生成注册邮箱
- 只关心当前是否允许自动登录、是否需要切号、是否命中上限、是否需要停止自动流程
- 因此后续如果只改别名生成语义,不应把这层重新卷入 provider 判定
后续如果再改 `2925`,建议最少按下面顺序自检:
1. `managed-alias-utils.js` 是否仍是 `provide / receive` 的唯一共享语义来源
2. `sidepanel/sidepanel.js` 是否只做接线,没有复制业务规则
3. `background/generated-email-helpers.js` / `background/signup-flow-helpers.js` 是否仍然只做调度,不重复写模式语义
4. `background/verification-flow.js``content/mail-2925.js` 的“弱目标邮箱匹配”是否保持同步
5. `项目文件结构说明.md` 与当前文件是否已同步更新
### 7.5 iCloud
@@ -639,9 +705,12 @@
- Step 4 在等待注册验证码页时,如果命中认证页 `Try again / 重试` 页,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续回到密码页重提和验证码页确认流程。
- 但如果 Step 4 的认证重试页正文中出现 `user_already_exists`,则会直接视为“当前用户已存在”:不点击 `重试`,不再回到步骤 1 重开当前轮,而是立即结束当前轮;开启自动重试时直接进入下一轮。
- Step 7 在识别到登录超时报错页时,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复当前页面;若仍未恢复,则按原有可恢复失败逻辑重跑 Step 7。
- Step 7 在首次识别到登录验证码页后,不会立刻把步骤判定为成功;还会额外做一轮“收尾确认”,确保页面稳定停留在登录验证码页。如果只是短暂进入验证码页、随后又掉进登录重试页,则会先走共享恢复逻辑,再按既有可恢复失败逻辑重跑 Step 7。
- Step 7 的这轮收尾确认是主要责任边界;Step 8 默认建立在“登录验证码页已经由 Step 7 稳定确认”的前提上,只在后台入口保留防御性回退判断,不替代 Step 7 收尾。
- 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 分钟”。
+3
View File
@@ -93,9 +93,12 @@
当前约定:
- Gmail / 2925 的基邮箱解析、兼容性判断、别名生成、UI 文案优先收敛到 `managed-alias-utils.js`
- `2925` 是否参与“共享别名邮箱链路”必须由共享层统一判断;当前只有 `mail2925Mode = provide` 才允许把 `2925` 视为别名邮箱 provider`receive` 不能在 sidepanel / step / provider 分支里各自偷写一套判断
- `background/generated-email-helpers.js` 只负责调度,不应再次复制 Gmail / 2925 规则
- `background/signup-flow-helpers.js` 只负责“复用已有邮箱还是重新生成”的流程决策
- `sidepanel/sidepanel.js` 只负责 UI 接线、校验触发和状态同步
- Hotmail / 2925 账号池这类跨 provider 的 sidepanel 表单显隐、头部按钮文案切换与共用操作行,应优先收敛到共享 UI helper(当前为 `sidepanel/account-pool-ui.js`),不要在各自 manager 中复制一套近似状态机
- 如果 `2925` 的某个运行态模式会让“别名基邮箱”和“账号池 / 当前账号选择”出现不同显隐规则,必须优先拆行或拆成独立配置块,不能把账号池开关绑死在别名基邮箱那一行里
### 3.3 新增配置项
+12 -10
View File
@@ -27,7 +27,7 @@
- `icloud-utils.js`:iCloud 隐私邮箱相关的纯工具函数,负责 host、别名列表、保留状态、已用状态等归一化。
- `luckmail-utils.js`:LuckMail 相关的纯工具函数,负责邮箱购买记录、标签、邮件 cursor、验证码匹配等归一化。
- `mail2925-utils.js`:2925 账号池相关的纯工具函数,负责账号归一化、冷却期判断、可用账号挑选、批量导入解析与列表更新。
- `managed-alias-utils.js`:共享 Gmail / 2925 的别名邮箱规则,负责解析基邮箱、校验“当前完整注册邮箱”是否仍与基邮箱兼容、生成 Gmail `+tag` 与 2925 随机后缀邮箱,并输出 sidepanel 可复用的 UI 文案。
- `managed-alias-utils.js`:共享 Gmail / 2925 的别名邮箱规则,负责解析基邮箱、校验“当前完整注册邮箱”是否仍与基邮箱兼容、生成 Gmail `+tag` 与 2925 随机后缀邮箱,并输出 sidepanel 可复用的 UI 文案;当前还统一承接 `mail2925Mode` 的归一化与“2925 仅在 provide 模式下参与别名生成”的共享判定
- `manifest.json`:Chrome 扩展清单,声明权限、背景脚本、侧边栏、内容脚本与规则集。
- `microsoft-email.js`Microsoft Graph / Outlook 邮件读取辅助模块,负责刷新令牌换 token、邮箱夹轮询和验证码提取。
- `package.json`:仓库最小 Node 包配置,目前主要提供测试脚本定义。
@@ -44,15 +44,15 @@
- `background/account-run-history.js`:邮箱记录模块,负责以邮箱为主键维护最新记录(同邮箱后续状态覆盖),在步骤 2 设定邮箱后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,支持清理记录,并在启用独立本地同步配置后把完整快照同步到本地 helper。
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 `gmailBaseEmail``mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失别名基邮箱与 2925 切号上下文。
- `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口;当 provider 为 2925 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱。
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口;当 provider 为 2925`mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。
- `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 2925 账号池的新增、导入、切换、登录、禁用与删除消息。
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API 地址、步骤跳转相关判断。
- `background/panel-bridge.js`CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail / 2925 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 时,会先确保账号池中已选中可用账号。
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
## `background/steps/`
@@ -77,7 +77,7 @@
- `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。
- `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。
- `content/mail-163.js`163 / 163 VIP 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。
- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱自动登录态确认、收件轮询、按步骤会话隔离“已试验证码”、在每次重发验证码之间执行一轮最多 15 次的邮箱刷新轮询、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理;若页面出现“子邮箱已达上限邮箱”提示,会立即上报后台进入切号链路。
- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱自动登录态确认、收件轮询、按步骤会话隔离“已试验证码”、在每次重发验证码之间执行一轮最多 15 次的邮箱刷新轮询、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理;若页面出现“子邮箱已达上限邮箱”提示,会立即上报后台进入切号链路;当后台显式开启 `mail2925MatchTargetEmail` 时,会对邮件里显式出现的目标邮箱做弱匹配,避免 `receive` 模式误捞别人的验证码
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击。
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;当前承接步骤 10。
@@ -112,6 +112,7 @@
## `sidepanel/`
- `sidepanel/account-pool-ui.js`:侧边栏号池表单共享 UI helper,负责 Hotmail / 2925 账号池新增表单的显隐、头部“添加账号/取消添加”按钮文案切换、清空表单与首字段聚焦。
- `sidepanel/hotmail-manager.js`:侧边栏 Hotmail 账号池管理器,负责列表、导入、验证、测试收信和批量操作。
- `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。
- `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
@@ -119,9 +120,9 @@
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行的禁用与隐藏。
- `sidepanel/sidepanel.css`:侧边栏样式文件。
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,别名基邮箱行右侧会显示“号池”开关,开启后输入框切换为号池邮箱下拉选择,并显示 2925 账号池管理区
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 2925 号池开关开启时,当前账号变化会同步映射到 2925 基邮箱下拉选择,关闭时则回退到原来的手填基邮箱路线;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示。
- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行与统一的收起态列表高度样式
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,会额外显示 `提供邮箱 / 接收邮箱` 模式切换,并把“2925 号池”从别名基邮箱行拆成独立配置行,避免 receive 模式把账号池开关一起隐藏;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显示并同时服务于 provide / receive 两种模式;Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示。
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。
## `tests/`
@@ -171,7 +172,7 @@
- `tests/mail2925-utils.test.js`:测试 2925 账号池工具层的账号归一化、冷却期判断、可用账号挑选与批量导入解析。
- `tests/managed-alias-utils.test.js`:覆盖 Gmail / 2925 共享别名工具的解析、生成、兼容性判断。
- `tests/microsoft-email.test.js`:测试 Microsoft 邮件拉取与验证码提取逻辑。
- `tests/sidepanel-hotmail-manager.test.js`:测试侧边栏 Hotmail 管理器模块接线与空态渲染。
- `tests/sidepanel-hotmail-manager.test.js`:测试侧边栏 Hotmail 管理器模块接线、共享号池表单显隐交互与空态渲染。
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏邮箱记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
- `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。
@@ -180,7 +181,8 @@
- `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。
- `tests/sidepanel-icloud-provider.test.js`:测试侧边栏 iCloud 登录地址解析逻辑。
- `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。
- `tests/sidepanel-mail2925-manager.test.js`:测试侧边栏 2925 管理器模块接线、脚本加载顺序与空态渲染。
- `tests/sidepanel-mail2925-manager.test.js`:测试侧边栏 2925 管理器模块接线、共享号池表单脚本加载顺序、显隐交互与空态渲染。
- `tests/sidepanel-mail2925-mode.test.js`:测试侧边栏保留 `2925 provide / receive` 模式行与独立的号池配置行,并验证 sidepanel 只在 provide 模式下把 2925 视为别名邮箱 provider。
- `tests/signup-entry-diagnostics.test.js`:测试注册入口诊断快照输出。
- `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
- `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。