Merge remote-tracking branch 'origin/master'
# Conflicts: # content/signup-page.js # tests/step6-login-state.test.js
This commit is contained in:
@@ -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 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。
|
||||
|
||||
+24
-3
@@ -461,7 +461,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 = '') {
|
||||
@@ -2149,11 +2149,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 = {}) {
|
||||
@@ -2296,11 +2306,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 = {}) {
|
||||
@@ -6596,6 +6616,7 @@ function getMailConfig(state) {
|
||||
}
|
||||
if (provider === '2925') {
|
||||
return {
|
||||
provider: '2925',
|
||||
source: 'mail-2925',
|
||||
url: 'https://2925.com/#/mailList',
|
||||
label: '2925 邮箱',
|
||||
|
||||
@@ -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);
|
||||
|
||||
+152
-197
@@ -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,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async function setCurrentMail2925Account(accountId, options = {}) {
|
||||
const { logMessage = '', updateLastUsedAt = false } = options;
|
||||
const state = await getState();
|
||||
@@ -361,7 +384,7 @@
|
||||
origins: MAIL2925_COOKIE_ORIGINS,
|
||||
});
|
||||
} catch (_) {
|
||||
// Best-effort cleanup only.
|
||||
// Best effort cleanup only.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,170 +396,15 @@
|
||||
accountId = null,
|
||||
forceRelogin = false,
|
||||
actionLabel = '确保 2925 邮箱登录态',
|
||||
allowLoginWhenOnLoginPage = true,
|
||||
} = options;
|
||||
const account = await ensureMail2925AccountForFlow({
|
||||
allowAllocate: true,
|
||||
preferredAccountId: accountId,
|
||||
});
|
||||
|
||||
let account = null;
|
||||
if (forceRelogin) {
|
||||
const removedCount = await clearMail2925SessionCookies();
|
||||
await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info');
|
||||
}
|
||||
|
||||
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(`2925:ENSURE_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);
|
||||
account = await ensureMail2925AccountForFlow({
|
||||
allowAllocate: true,
|
||||
preferredAccountId: accountId,
|
||||
});
|
||||
}
|
||||
|
||||
const sendLoginMessage = typeof sendToContentScriptResilient === 'function'
|
||||
@@ -551,9 +419,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)) {
|
||||
await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info');
|
||||
return buildSuccessPayload();
|
||||
}
|
||||
|
||||
if (!forceRelogin && !allowLoginWhenOnLoginPage) {
|
||||
await failMailboxSession(`2925:${actionLabel}失败,当前页面已跳转到登录页,且当前未启用 2925 账号池,不执行自动登录。`);
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
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,
|
||||
@@ -568,16 +525,15 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
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 +541,13 @@
|
||||
}
|
||||
|
||||
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('流程已被用户停止。');
|
||||
}
|
||||
throw new Error(`2925:${actionLabel}失败,当前页面仍未进入收件箱。`);
|
||||
await failMailboxSession(`2925:${actionLabel}失败,登录后仍未进入收件箱。`);
|
||||
}
|
||||
|
||||
await patchMail2925Account(account.id, {
|
||||
@@ -614,7 +556,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 +575,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 +602,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'
|
||||
);
|
||||
|
||||
@@ -663,7 +617,7 @@
|
||||
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 +627,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},当前尝试结束,等待下一轮重试。`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,15 +24,39 @@
|
||||
throwIfStopped,
|
||||
} = deps;
|
||||
|
||||
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 +64,7 @@
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
throwIfStopped();
|
||||
await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
|
||||
|
||||
const prepareResult = await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
{
|
||||
@@ -73,43 +98,40 @@
|
||||
}
|
||||
|
||||
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),
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+49
-1
@@ -629,6 +629,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);
|
||||
@@ -902,11 +935,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:提交账号密码后未进入收件箱。');
|
||||
@@ -930,6 +964,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 +1030,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;
|
||||
|
||||
|
||||
+289
-48
@@ -126,16 +126,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;
|
||||
@@ -1272,7 +1280,7 @@ function getSignupPasswordTimeoutErrorPageState() {
|
||||
|
||||
function getLoginTimeoutErrorPageState() {
|
||||
return getAuthTimeoutErrorPageState({
|
||||
pathPatterns: [/\/log-in(?:[/?#]|$)/i],
|
||||
pathPatterns: getLoginAuthRetryPathPatterns(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1339,6 +1347,13 @@ function inspectLoginAuthState() {
|
||||
consentReady,
|
||||
};
|
||||
|
||||
if (retryState) {
|
||||
return {
|
||||
...baseState,
|
||||
state: 'login_timeout_error_page',
|
||||
};
|
||||
}
|
||||
|
||||
if (phoneVerificationPage) {
|
||||
return {
|
||||
...baseState,
|
||||
@@ -1354,13 +1369,6 @@ function inspectLoginAuthState() {
|
||||
};
|
||||
}
|
||||
|
||||
if (retryState) {
|
||||
return {
|
||||
...baseState,
|
||||
state: 'login_timeout_error_page',
|
||||
};
|
||||
}
|
||||
|
||||
if (addPhonePage) {
|
||||
return {
|
||||
...baseState,
|
||||
@@ -1519,6 +1527,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;
|
||||
@@ -1707,15 +1795,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();
|
||||
@@ -1755,6 +1858,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('未提供验证码。');
|
||||
@@ -1769,6 +1962,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();
|
||||
@@ -1781,28 +1975,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');
|
||||
@@ -1814,6 +1994,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);
|
||||
}
|
||||
@@ -1821,17 +2046,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);
|
||||
@@ -2084,7 +2308,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) {
|
||||
@@ -2115,8 +2347,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');
|
||||
@@ -2162,8 +2397,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');
|
||||
@@ -2187,8 +2425,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') {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "多页面自动化",
|
||||
"version": "5.0",
|
||||
"version_name": "Pro5.0",
|
||||
"version": "5.8",
|
||||
"version_name": "Pro5.8",
|
||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||
"permissions": [
|
||||
"sidePanel",
|
||||
|
||||
@@ -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);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+28
-12
@@ -243,33 +243,35 @@ header {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.contribution-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
.contribution-update-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 1400;
|
||||
}
|
||||
|
||||
.contribution-update-hint {
|
||||
position: relative;
|
||||
--contribution-update-arrow-left: 20px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
max-width: 220px;
|
||||
margin-left: 6px;
|
||||
max-width: min(220px, calc(100vw - 24px));
|
||||
padding: 8px 10px;
|
||||
background: color-mix(in srgb, var(--amber) 10%, var(--bg-base));
|
||||
border: 1px solid color-mix(in srgb, var(--amber) 34%, var(--border));
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.contribution-update-hint::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 14px;
|
||||
left: calc(var(--contribution-update-arrow-left) - 6px);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: inherit;
|
||||
@@ -436,7 +438,7 @@ header {
|
||||
|
||||
.run-group {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -1112,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;
|
||||
}
|
||||
@@ -1134,7 +1150,7 @@ header {
|
||||
}
|
||||
|
||||
.hotmail-list-shell.is-collapsed {
|
||||
max-height: 236px;
|
||||
max-height: 176px;
|
||||
}
|
||||
|
||||
.hotmail-list-shell.is-expanded {
|
||||
|
||||
+80
-66
@@ -34,18 +34,9 @@
|
||||
</div>
|
||||
<div class="header-btns">
|
||||
<div class="run-group">
|
||||
<div class="contribution-entry">
|
||||
<button id="btn-contribution-mode" class="btn btn-outline btn-sm btn-contribution-mode" type="button"
|
||||
aria-pressed="false" title="进入贡献模式并打开上传页">贡献/使用</button>
|
||||
<div id="contribution-update-hint" class="contribution-update-hint" aria-live="polite" hidden>
|
||||
<p id="contribution-update-hint-text" class="contribution-update-hint-text">
|
||||
公告 / 使用教程有更新了,可点上方“贡献/使用”查看。
|
||||
</p>
|
||||
<button id="btn-dismiss-contribution-update-hint" class="contribution-update-hint-close" type="button"
|
||||
aria-label="关闭更新提示" title="关闭更新提示">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="运行次数" />
|
||||
<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" 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" />
|
||||
@@ -90,6 +81,16 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="contribution-update-layer" class="contribution-update-layer" hidden>
|
||||
<div id="contribution-update-hint" class="contribution-update-hint" aria-live="polite" hidden>
|
||||
<p id="contribution-update-hint-text" class="contribution-update-hint-text">
|
||||
公告 / 使用教程有更新了,可点上方“贡献/使用”查看。
|
||||
</p>
|
||||
<button id="btn-dismiss-contribution-update-hint" class="contribution-update-hint-close" type="button"
|
||||
aria-label="关闭更新提示" title="关闭更新提示">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="update-section" hidden>
|
||||
<div class="update-card">
|
||||
<div class="update-card-header">
|
||||
@@ -268,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;">
|
||||
@@ -403,6 +409,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>
|
||||
@@ -425,33 +432,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----刷新令牌 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----刷新令牌 name@hotmail.com----password----client-id----refresh-token"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="hotmail-list-shell" class="hotmail-list-shell is-collapsed">
|
||||
@@ -464,31 +475,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="邮箱----密码 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="邮箱----密码 name@2925.com----password"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mail2925-list-shell" class="hotmail-list-shell is-collapsed">
|
||||
@@ -734,6 +747,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>
|
||||
|
||||
+122
-29
@@ -57,6 +57,7 @@ const btnSaveSettings = document.getElementById('btn-save-settings');
|
||||
const btnStop = document.getElementById('btn-stop');
|
||||
const btnReset = document.getElementById('btn-reset');
|
||||
const btnContributionMode = document.getElementById('btn-contribution-mode');
|
||||
const contributionUpdateLayer = document.getElementById('contribution-update-layer');
|
||||
const contributionUpdateHint = document.getElementById('contribution-update-hint');
|
||||
const contributionUpdateHintText = document.getElementById('contribution-update-hint-text');
|
||||
const btnDismissContributionUpdateHint = document.getElementById('btn-dismiss-contribution-update-hint');
|
||||
@@ -97,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');
|
||||
@@ -148,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');
|
||||
@@ -247,8 +252,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';
|
||||
@@ -266,12 +270,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) {
|
||||
@@ -290,31 +306,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;
|
||||
@@ -379,7 +398,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);
|
||||
@@ -591,7 +613,7 @@ const LOG_LEVEL_LABELS = {
|
||||
};
|
||||
|
||||
function usesGeneratedAliasMailProvider(provider, mail2925Mode = getSelectedMail2925Mode()) {
|
||||
return isManagedAliasProvider(provider);
|
||||
return isManagedAliasProvider(provider, mail2925Mode);
|
||||
}
|
||||
|
||||
function parseGmailBaseEmail(rawValue = '') {
|
||||
@@ -880,14 +902,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: '继续',
|
||||
});
|
||||
|
||||
@@ -1162,7 +1180,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() {
|
||||
@@ -2317,9 +2335,40 @@ function getContributionUpdateHintMessage(snapshot = currentContributionContentS
|
||||
return '公告 / 使用教程有更新了,可点上方“贡献/使用”查看。';
|
||||
}
|
||||
|
||||
function positionContributionUpdateHint() {
|
||||
if (!contributionUpdateLayer || !contributionUpdateHint || !btnContributionMode) {
|
||||
return;
|
||||
}
|
||||
if (contributionUpdateLayer.hidden || contributionUpdateHint.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonRect = btnContributionMode.getBoundingClientRect();
|
||||
const viewportWidth = Math.max(document.documentElement?.clientWidth || 0, window.innerWidth || 0);
|
||||
const viewportHeight = Math.max(document.documentElement?.clientHeight || 0, window.innerHeight || 0);
|
||||
const hintWidth = contributionUpdateHint.offsetWidth || 220;
|
||||
const hintHeight = contributionUpdateHint.offsetHeight || 56;
|
||||
const viewportPadding = 12;
|
||||
const gap = 10;
|
||||
|
||||
const maxLeft = Math.max(viewportPadding, viewportWidth - hintWidth - viewportPadding);
|
||||
const left = Math.min(Math.max(viewportPadding, Math.round(buttonRect.left)), maxLeft);
|
||||
const shouldPlaceAbove = (buttonRect.bottom + gap + hintHeight) > (viewportHeight - viewportPadding)
|
||||
&& buttonRect.top > (hintHeight + gap + viewportPadding);
|
||||
const top = shouldPlaceAbove
|
||||
? Math.max(viewportPadding, Math.round(buttonRect.top - hintHeight - gap))
|
||||
: Math.max(viewportPadding, Math.round(buttonRect.bottom + gap));
|
||||
const buttonCenter = Math.round(buttonRect.left + (buttonRect.width / 2));
|
||||
const arrowOffset = Math.min(Math.max(16, buttonCenter - left), Math.max(16, hintWidth - 16));
|
||||
|
||||
contributionUpdateHint.style.left = `${left}px`;
|
||||
contributionUpdateHint.style.top = `${top}px`;
|
||||
contributionUpdateHint.style.setProperty('--contribution-update-arrow-left', `${arrowOffset}px`);
|
||||
}
|
||||
|
||||
function shouldShowContributionUpdateHint(snapshot = currentContributionContentSnapshot) {
|
||||
const promptVersion = String(snapshot?.promptVersion || '').trim();
|
||||
if (!contributionUpdateHint || !contributionUpdateHintText || !btnContributionMode) {
|
||||
if (!contributionUpdateLayer || !contributionUpdateHint || !contributionUpdateHintText || !btnContributionMode) {
|
||||
return false;
|
||||
}
|
||||
if (!promptVersion) {
|
||||
@@ -2335,17 +2384,23 @@ function shouldShowContributionUpdateHint(snapshot = currentContributionContentS
|
||||
}
|
||||
|
||||
function renderContributionUpdateHint(snapshot = currentContributionContentSnapshot) {
|
||||
if (!contributionUpdateHint) {
|
||||
if (!contributionUpdateLayer || !contributionUpdateHint) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visible = shouldShowContributionUpdateHint(snapshot);
|
||||
contributionUpdateLayer.hidden = !visible;
|
||||
contributionUpdateHint.hidden = !visible;
|
||||
if (!visible || !contributionUpdateHintText) {
|
||||
return;
|
||||
}
|
||||
|
||||
contributionUpdateHintText.textContent = getContributionUpdateHintMessage(snapshot);
|
||||
if (typeof window.requestAnimationFrame === 'function') {
|
||||
window.requestAnimationFrame(() => positionContributionUpdateHint());
|
||||
return;
|
||||
}
|
||||
positionContributionUpdateHint();
|
||||
}
|
||||
|
||||
function dismissContributionUpdateHint() {
|
||||
@@ -2524,6 +2579,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;
|
||||
}
|
||||
@@ -2674,13 +2747,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';
|
||||
@@ -2743,8 +2821,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 原邮箱';
|
||||
@@ -2795,7 +2873,9 @@ function updateMailProviderUI() {
|
||||
}
|
||||
if (autoHintText && useMail2925AccountPool) {
|
||||
autoHintText.textContent = getMail2925Accounts().length
|
||||
? '当前已启用 2925 号池模式,步骤 3 会基于下拉框选中的号池邮箱生成别名地址'
|
||||
? (useGeneratedAlias
|
||||
? '当前已启用 2925 号池模式,步骤 3 会基于下拉框选中的号池邮箱生成别名地址'
|
||||
: '当前已启用 2925 号池模式,步骤 4 / 8 遇到登录页时会优先使用下拉框选中的账号自动登录')
|
||||
: '当前已启用 2925 号池模式,请先在下方 2925 账号池中添加账号并选择邮箱';
|
||||
}
|
||||
if (autoHintText && showCloudflareTempEmailReceiveMailbox) {
|
||||
@@ -3123,6 +3203,7 @@ async function fetchGeneratedEmail(options = {}) {
|
||||
generateNew: true,
|
||||
generator: selectEmailGenerator.value,
|
||||
mailProvider: selectMailProvider.value,
|
||||
mail2925Mode: getSelectedMail2925Mode(),
|
||||
...buildManagedAliasBaseEmailPayload(),
|
||||
},
|
||||
});
|
||||
@@ -3182,7 +3263,9 @@ const hotmailManager = window.SidepanelHotmailManager?.createHotmailManager({
|
||||
btnDeleteAllHotmailAccounts,
|
||||
btnHotmailUsageGuide,
|
||||
btnImportHotmailAccounts,
|
||||
btnToggleHotmailForm,
|
||||
btnToggleHotmailList,
|
||||
hotmailFormShell,
|
||||
hotmailAccountsList,
|
||||
hotmailListShell,
|
||||
inputEmail,
|
||||
@@ -3233,14 +3316,15 @@ const mail2925Manager = window.SidepanelMail2925Manager?.createMail2925Manager({
|
||||
},
|
||||
dom: {
|
||||
btnAddMail2925Account,
|
||||
btnCancelMail2925Edit,
|
||||
btnDeleteAllMail2925Accounts,
|
||||
btnImportMail2925Accounts,
|
||||
btnToggleMail2925Form,
|
||||
btnToggleMail2925List,
|
||||
inputMail2925Email,
|
||||
inputMail2925Import,
|
||||
inputMail2925Password,
|
||||
mail2925AccountsList,
|
||||
mail2925FormShell,
|
||||
mail2925ListShell,
|
||||
},
|
||||
helpers: {
|
||||
@@ -3249,6 +3333,7 @@ const mail2925Manager = window.SidepanelMail2925Manager?.createMail2925Manager({
|
||||
getMail2925Accounts,
|
||||
openConfirmModal,
|
||||
refreshManagedAliasBaseEmail: () => {
|
||||
syncMail2925BaseEmailFromCurrentAccount(latestState, { persist: true });
|
||||
setManagedAliasBaseEmailInputForProvider('2925', latestState);
|
||||
},
|
||||
showToast,
|
||||
@@ -3860,7 +3945,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;
|
||||
}
|
||||
@@ -4822,6 +4907,14 @@ document.addEventListener('keydown', (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
positionContributionUpdateHint();
|
||||
});
|
||||
|
||||
document.addEventListener('scroll', () => {
|
||||
positionContributionUpdateHint();
|
||||
}, true);
|
||||
|
||||
// ============================================================
|
||||
// Init
|
||||
// ============================================================
|
||||
|
||||
@@ -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,268 @@
|
||||
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);
|
||||
});
|
||||
@@ -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,72 @@ 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');
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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, '');
|
||||
|
||||
@@ -4,8 +4,12 @@ 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\);/);
|
||||
});
|
||||
|
||||
function extractFunction(name) {
|
||||
@@ -164,7 +168,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 +246,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'),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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, '继续');
|
||||
});
|
||||
@@ -11,6 +11,8 @@ test('sidepanel html keeps a single contribution mode button in header', () => {
|
||||
assert.equal(matches.length, 1);
|
||||
assert.match(html, /id="btn-contribution-mode"[^>]*title="进入贡献模式并打开上传页"/);
|
||||
assert.match(html, />贡献\/使用<\/button>/);
|
||||
assert.match(html, /<\/header>\s*<div id="contribution-update-layer"/);
|
||||
assert.match(html, /id="contribution-update-layer"/);
|
||||
assert.match(html, /id="contribution-update-hint"/);
|
||||
assert.match(html, /id="contribution-update-hint-text"/);
|
||||
assert.match(html, /id="btn-dismiss-contribution-update-hint"/);
|
||||
|
||||
@@ -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 账号/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
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);
|
||||
});
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 账号/);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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']);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -188,6 +188,27 @@ return {
|
||||
assert.strictEqual(snapshot.state, 'phone_verification_page');
|
||||
}
|
||||
|
||||
{
|
||||
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,
|
||||
|
||||
@@ -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/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
+83
-15
@@ -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
|
||||
|
||||
@@ -330,12 +331,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 账号池时才会自动登录,未启用账号池时会直接复用现有停止逻辑结束流程。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 +410,8 @@
|
||||
|
||||
补充:
|
||||
|
||||
- 对 `2925` 而言,Step 8 不再依赖验证码页显示邮箱做收件匹配,而是直接测试所有命中 ChatGPT / OpenAI 过滤条件的邮件。
|
||||
- 对 `2925 provide` 而言,Step 8 仍不依赖验证码页显示邮箱做收件匹配,而是直接测试所有命中 ChatGPT / OpenAI 过滤条件的邮件。
|
||||
- 对 `2925 receive` 而言,Step 8 会把当前目标注册邮箱一并传给 2925 内容脚本;只有当邮件里显式写出了其他邮箱时才会跳过,从而在不破坏历史兼容性的前提下,尽量降低误收验证码的概率。
|
||||
|
||||
### Step 9
|
||||
|
||||
@@ -451,25 +454,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 +513,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 +525,7 @@
|
||||
|
||||
- 本地 helper 除了收信与验证码读取,还提供邮箱记录 JSON 快照同步接口。
|
||||
- 账号运行历史本地同步由独立配置控制,不再绑定 Hotmail 的本地助手模式。
|
||||
- sidepanel 中 Hotmail 账号池的新增表单默认收起,头部通过共享按钮切换“添加账号 / 取消添加”;表单显隐、按钮文案切换、清空与聚焦都复用 `sidepanel/account-pool-ui.js`,不在 Hotmail manager 内重复实现一套。
|
||||
|
||||
### 7.3 LuckMail
|
||||
|
||||
@@ -534,6 +541,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 +551,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 邮箱页:如果仍停留在收件箱则直接复用;如果跳到登录页,则仅在号池模式开启时才自动登录,关闭号池时直接调用现有停止逻辑结束流程
|
||||
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,7 +704,10 @@
|
||||
- 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 次 `重试` 尝试恢复,再重新执行当前轮的 `继续` 点击。
|
||||
## 2026-04-21 2925 邮件时间窗补充
|
||||
|
||||
@@ -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
@@ -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`:测试共享步骤定义模块及侧边栏脚本加载顺序。
|
||||
|
||||
Reference in New Issue
Block a user