fix: stabilize signup verification recovery
- merge the latest dev branch into PR #130 before applying local fixes - detect visible split OTP inputs even when the page exposes numeric single-character fields - stop assuming verification success after repeated auth retry-page recovery failures
This commit is contained in:
@@ -579,9 +579,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 +600,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 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。
|
||||
|
||||
@@ -6564,6 +6564,7 @@ function getMailConfig(state) {
|
||||
}
|
||||
if (provider === '2925') {
|
||||
return {
|
||||
provider: '2925',
|
||||
source: 'mail-2925',
|
||||
url: 'https://2925.com/#/mailList',
|
||||
label: '2925 邮箱',
|
||||
|
||||
+150
-195
@@ -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,
|
||||
@@ -571,13 +528,12 @@
|
||||
timeoutMs: forceRelogin ? 30000 : 25000,
|
||||
retryDelayMs: 800,
|
||||
responseTimeoutMs: forceRelogin ? 30000 : 25000,
|
||||
logMessage: `步骤 0:2925 登录页通信异常,正在等待当前页面重新就绪后继续确认登录态...`,
|
||||
logMessage: '步骤 0:2925 登录页通信异常,正在等待页面恢复...',
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
const stopped = await stopAutoRunForMail2925LoginFailure(
|
||||
`2925:${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'}),已按手动停止逻辑暂停自动流程。`
|
||||
);
|
||||
const message = `2925:${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'})。`;
|
||||
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,36 +98,28 @@
|
||||
}
|
||||
|
||||
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 = ![
|
||||
|
||||
+200
-68
@@ -114,16 +114,24 @@ function isVisibleElement(el) {
|
||||
&& rect.height > 0;
|
||||
}
|
||||
|
||||
function getVisibleSplitVerificationInputs() {
|
||||
return Array.from(document.querySelectorAll('input[maxlength="1"]'))
|
||||
.filter(isVisibleElement);
|
||||
}
|
||||
|
||||
function getVerificationCodeTarget() {
|
||||
const splitInputs = getVisibleSplitVerificationInputs();
|
||||
const codeInput = document.querySelector(VERIFICATION_CODE_INPUT_SELECTOR);
|
||||
if (codeInput && isVisibleElement(codeInput)) {
|
||||
const maxLength = Number(codeInput.getAttribute?.('maxlength') || codeInput.maxLength || 0);
|
||||
if (maxLength === 1 && splitInputs.length >= 6) {
|
||||
return { type: 'split', elements: splitInputs };
|
||||
}
|
||||
return { type: 'single', element: codeInput };
|
||||
}
|
||||
|
||||
const singleInputs = Array.from(document.querySelectorAll('input[maxlength="1"]'))
|
||||
.filter(isVisibleElement);
|
||||
if (singleInputs.length >= 6) {
|
||||
return { type: 'split', elements: singleInputs };
|
||||
if (splitInputs.length >= 6) {
|
||||
return { type: 'split', elements: splitInputs };
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1198,7 +1206,7 @@ function getSignupPasswordTimeoutErrorPageState() {
|
||||
|
||||
function getLoginTimeoutErrorPageState() {
|
||||
return getAuthTimeoutErrorPageState({
|
||||
pathPatterns: [/\/log-in(?:[/?#]|$)/i],
|
||||
pathPatterns: getLoginAuthRetryPathPatterns(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1263,13 +1271,6 @@ function inspectLoginAuthState() {
|
||||
consentReady,
|
||||
};
|
||||
|
||||
if (verificationTarget) {
|
||||
return {
|
||||
...baseState,
|
||||
state: 'verification_page',
|
||||
};
|
||||
}
|
||||
|
||||
if (retryState) {
|
||||
return {
|
||||
...baseState,
|
||||
@@ -1277,6 +1278,13 @@ function inspectLoginAuthState() {
|
||||
};
|
||||
}
|
||||
|
||||
if (verificationTarget) {
|
||||
return {
|
||||
...baseState,
|
||||
state: 'verification_page',
|
||||
};
|
||||
}
|
||||
|
||||
if (addPhonePage) {
|
||||
return {
|
||||
...baseState,
|
||||
@@ -1434,6 +1442,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;
|
||||
@@ -1633,7 +1721,10 @@ async function waitForVerificationSubmitOutcome(step, timeout) {
|
||||
if (retryState?.userAlreadyExistsBlocked) {
|
||||
throw createSignupUserAlreadyExistsError();
|
||||
}
|
||||
if (retryState && recoveryCount < maxRecoveryCount) {
|
||||
if (retryState) {
|
||||
if (recoveryCount >= maxRecoveryCount) {
|
||||
throw new Error(`步骤 ${step}:验证码提交后连续进入认证重试页 ${maxRecoveryCount} 次,页面仍未恢复。URL: ${location.href}`);
|
||||
}
|
||||
recoveryCount += 1;
|
||||
log(`步骤 ${step}:验证码提交后进入认证重试页,正在自动恢复(${recoveryCount}/${maxRecoveryCount})...`, 'warn');
|
||||
await recoverCurrentAuthRetryPage({
|
||||
@@ -1731,6 +1822,26 @@ async function waitForVerificationSubmitButton(codeInput, timeout = 5000) {
|
||||
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();
|
||||
@@ -1766,6 +1877,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();
|
||||
@@ -1778,56 +1890,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++) {
|
||||
const targetInput = singleInputs[i];
|
||||
try {
|
||||
targetInput.focus?.();
|
||||
} catch {}
|
||||
fillInput(singleInputs[i], code[i]);
|
||||
try {
|
||||
targetInput.dispatchEvent(new KeyboardEvent('keyup', { key: code[i], bubbles: true }));
|
||||
} catch {}
|
||||
await sleep(100);
|
||||
}
|
||||
const filled = await waitForSplitVerificationInputsFilled(singleInputs, code, 2500);
|
||||
if (!filled) {
|
||||
const current = Array.from(singleInputs)
|
||||
.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(singleInputs[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;
|
||||
}
|
||||
|
||||
// No input found — check if it's a 405 error and can be recovered
|
||||
if (is405MethodNotAllowedPage() && retry < maxRetries) {
|
||||
log(`步骤 ${step}:未找到验证码输入框且页面出现 405 错误,正在恢复...`, 'warn');
|
||||
@@ -1839,6 +1909,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);
|
||||
}
|
||||
@@ -2108,7 +2223,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) {
|
||||
@@ -2139,8 +2262,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');
|
||||
@@ -2186,8 +2312,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');
|
||||
@@ -2211,8 +2340,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') {
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "多页面自动化",
|
||||
"version": "5.0",
|
||||
"version_name": "Pro5.0",
|
||||
"version": "5.4",
|
||||
"version_name": "Pro5.4",
|
||||
"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 {
|
||||
|
||||
+67
-58
@@ -34,17 +34,8 @@
|
||||
</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>
|
||||
<button id="btn-contribution-mode" class="btn btn-outline btn-sm btn-contribution-mode" type="button"
|
||||
aria-pressed="false" title="进入贡献模式并打开上传页">贡献/使用</button>
|
||||
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="运行次数" />
|
||||
<button id="btn-auto-run" class="btn btn-success" title="自动执行全部步骤">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||
@@ -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">
|
||||
@@ -389,6 +390,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>
|
||||
@@ -411,33 +413,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">
|
||||
@@ -450,31 +456,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">
|
||||
@@ -720,6 +728,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>
|
||||
|
||||
+83
-9
@@ -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');
|
||||
@@ -148,20 +149,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');
|
||||
@@ -291,24 +295,24 @@ function getManagedAliasProviderUiCopy(provider = selectMailProvider.value) {
|
||||
}
|
||||
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;
|
||||
@@ -373,7 +377,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);
|
||||
@@ -2220,9 +2227,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) {
|
||||
@@ -2238,17 +2276,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() {
|
||||
@@ -2427,6 +2471,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;
|
||||
}
|
||||
@@ -3085,7 +3147,9 @@ const hotmailManager = window.SidepanelHotmailManager?.createHotmailManager({
|
||||
btnDeleteAllHotmailAccounts,
|
||||
btnHotmailUsageGuide,
|
||||
btnImportHotmailAccounts,
|
||||
btnToggleHotmailForm,
|
||||
btnToggleHotmailList,
|
||||
hotmailFormShell,
|
||||
hotmailAccountsList,
|
||||
hotmailListShell,
|
||||
inputEmail,
|
||||
@@ -3136,14 +3200,15 @@ const mail2925Manager = window.SidepanelMail2925Manager?.createMail2925Manager({
|
||||
},
|
||||
dom: {
|
||||
btnAddMail2925Account,
|
||||
btnCancelMail2925Edit,
|
||||
btnDeleteAllMail2925Accounts,
|
||||
btnImportMail2925Accounts,
|
||||
btnToggleMail2925Form,
|
||||
btnToggleMail2925List,
|
||||
inputMail2925Email,
|
||||
inputMail2925Import,
|
||||
inputMail2925Password,
|
||||
mail2925AccountsList,
|
||||
mail2925FormShell,
|
||||
mail2925ListShell,
|
||||
},
|
||||
helpers: {
|
||||
@@ -3152,6 +3217,7 @@ const mail2925Manager = window.SidepanelMail2925Manager?.createMail2925Manager({
|
||||
getMail2925Accounts,
|
||||
openConfirmModal,
|
||||
refreshManagedAliasBaseEmail: () => {
|
||||
syncMail2925BaseEmailFromCurrentAccount(latestState, { persist: true });
|
||||
setManagedAliasBaseEmailInputForProvider('2925', latestState);
|
||||
},
|
||||
showToast,
|
||||
@@ -4692,6 +4758,14 @@ document.addEventListener('keydown', (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
positionContributionUpdateHint();
|
||||
});
|
||||
|
||||
document.addEventListener('scroll', () => {
|
||||
positionContributionUpdateHint();
|
||||
}, true);
|
||||
|
||||
// ============================================================
|
||||
// Init
|
||||
// ============================================================
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -8,6 +8,9 @@ const api = new Function('self', `${source}; return self.MultiPageBackgroundStep
|
||||
|
||||
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;
|
||||
|
||||
@@ -15,12 +18,16 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
update: async (tabId, payload) => {
|
||||
tabUpdates.push({ tabId, payload });
|
||||
},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureMail2925MailboxSession: async () => {},
|
||||
ensureMail2925MailboxSession: async () => {
|
||||
ensureCalls += 1;
|
||||
},
|
||||
getMailConfig: () => ({
|
||||
provider: '2925',
|
||||
label: '2925 邮箱',
|
||||
@@ -35,7 +42,9 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
|
||||
resolveVerificationStep: async (_step, _state, _mail, options) => {
|
||||
capturedOptions = options;
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
reuseOrCreateTab: async (source, url) => {
|
||||
tabReuses.push({ source, url });
|
||||
},
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
shouldUseCustomRegistrationEmail: () => false,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||
@@ -46,12 +55,17 @@ 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,
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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, '');
|
||||
|
||||
@@ -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 账号/);
|
||||
});
|
||||
|
||||
@@ -58,6 +58,12 @@ 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',
|
||||
@@ -75,13 +81,20 @@ const submitBtn = {
|
||||
|
||||
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 === 'button[type="submit"], input[type="submit"]') return submitBtn;
|
||||
if (selector === VERIFICATION_CODE_INPUT_SELECTOR) return inputs[0];
|
||||
return null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
@@ -97,7 +110,6 @@ function log(message, level = 'info') { logs.push({ message, level }); }
|
||||
async function waitForLoginVerificationPageReady() {}
|
||||
function is405MethodNotAllowedPage() { return false; }
|
||||
async function handle405ResendError() {}
|
||||
async function waitForElement() { throw new Error('not found'); }
|
||||
function fillInput(el, value) {
|
||||
el.value = value;
|
||||
filledValues.push(value);
|
||||
@@ -110,8 +122,11 @@ 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')}
|
||||
|
||||
|
||||
@@ -101,3 +101,51 @@ return {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -146,6 +146,27 @@ return {
|
||||
assert.strictEqual(snapshot.displayedEmail, 'display.user@example.com');
|
||||
}
|
||||
|
||||
{
|
||||
const api = createApi({
|
||||
pathname: '/email-verification',
|
||||
retryState: {
|
||||
retryEnabled: true,
|
||||
titleMatched: false,
|
||||
detailMatched: false,
|
||||
routeErrorMatched: true,
|
||||
},
|
||||
verificationTarget: { id: 'otp' },
|
||||
verificationVisible: true,
|
||||
});
|
||||
|
||||
const snapshot = api.inspectLoginAuthState();
|
||||
assert.strictEqual(
|
||||
snapshot.state,
|
||||
'login_timeout_error_page',
|
||||
'第七步在 /email-verification 的登录重试页应优先识别为登录超时报错页'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const api = createApi({
|
||||
oauthConsentPage: true,
|
||||
|
||||
@@ -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/
|
||||
);
|
||||
});
|
||||
|
||||
+16
-4
@@ -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,7 +331,7 @@
|
||||
补充行为:
|
||||
|
||||
- `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 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
|
||||
@@ -509,6 +510,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 +522,7 @@
|
||||
|
||||
- 本地 helper 除了收信与验证码读取,还提供邮箱记录 JSON 快照同步接口。
|
||||
- 账号运行历史本地同步由独立配置控制,不再绑定 Hotmail 的本地助手模式。
|
||||
- sidepanel 中 Hotmail 账号池的新增表单默认收起,头部通过共享按钮切换“添加账号 / 取消添加”;表单显隐、按钮文案切换、清空与聚焦都复用 `sidepanel/account-pool-ui.js`,不在 Hotmail manager 内重复实现一套。
|
||||
|
||||
### 7.3 LuckMail
|
||||
|
||||
@@ -534,6 +538,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 +548,19 @@
|
||||
统一承接 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 小时并自动切到下一个账号重新登录
|
||||
3. 手动点击 `登录` 或自动流程进入 Step 4 前,后台会先打开当前 2925 邮箱页:如果仍停留在收件箱则直接复用;如果跳到登录页,则仅在号池模式开启时才自动登录,关闭号池时直接调用现有停止逻辑结束流程
|
||||
4. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先判断是否启用了号池模式:若已启用且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录;若未启用,则直接调用现有停止逻辑结束流程
|
||||
5. 如果登录提交后 20 秒内仍未进入收件箱,且当前正处于自动运行中,则后台会直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态;这类情况常见于图片验证、行为验证或其他阻断登录的中间页
|
||||
6. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态
|
||||
7. sidepanel 中 2925 账号池的新增表单也走与 Hotmail 相同的共享交互:默认收起,头部按钮切换“添加账号 / 取消添加”,操作行右侧提供“批量导入”,保存成功后自动收起并清空。
|
||||
8. 当 2925 号池模式开启时,当前选中的号池邮箱会同步回写到同一个 `mail2925BaseEmail` 字段;因此用户切换号池账号后,即使再次关闭号池模式,也会直接沿用刚才选中的邮箱作为手动基邮箱,无需重新输入。
|
||||
|
||||
### 7.5 iCloud
|
||||
|
||||
@@ -639,7 +648,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 邮件时间窗补充
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
- `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 中复制一套近似状态机
|
||||
|
||||
### 3.3 新增配置项
|
||||
|
||||
|
||||
+6
-5
@@ -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 账号池管理区;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
|
||||
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 2925 号池开关开启时,当前账号变化会同步映射到 2925 基邮箱下拉选择,关闭时则回退到原来的手填基邮箱路线;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,7 @@
|
||||
- `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/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