fix(mail2925): 修复验证码轮询与登录态复用逻辑并清理中文文案,重构 sidepanel:统一 Hotmail 和 2925 号池表单交互
- 抽出共享 account-pool-ui helper - 统一添加账号、取消添加、批量导入交互 - 调整号池列表收起高度与按钮布局 - 补充测试并同步更新项目文档
This commit is contained in:
@@ -6564,6 +6564,7 @@ function getMailConfig(state) {
|
|||||||
}
|
}
|
||||||
if (provider === '2925') {
|
if (provider === '2925') {
|
||||||
return {
|
return {
|
||||||
|
provider: '2925',
|
||||||
source: 'mail-2925',
|
source: 'mail-2925',
|
||||||
url: 'https://2925.com/#/mailList',
|
url: 'https://2925.com/#/mailList',
|
||||||
label: '2925 邮箱',
|
label: '2925 邮箱',
|
||||||
|
|||||||
+140
-201
@@ -6,16 +6,16 @@
|
|||||||
addLog,
|
addLog,
|
||||||
broadcastDataUpdate,
|
broadcastDataUpdate,
|
||||||
chrome,
|
chrome,
|
||||||
|
ensureContentScriptReadyOnTab,
|
||||||
findMail2925Account,
|
findMail2925Account,
|
||||||
getMail2925AccountStatus,
|
getMail2925AccountStatus,
|
||||||
|
getState,
|
||||||
|
isAutoRunLockedState,
|
||||||
isMail2925AccountAvailable,
|
isMail2925AccountAvailable,
|
||||||
MAIL2925_LIMIT_COOLDOWN_MS,
|
MAIL2925_LIMIT_COOLDOWN_MS,
|
||||||
normalizeMail2925Account,
|
normalizeMail2925Account,
|
||||||
normalizeMail2925Accounts,
|
normalizeMail2925Accounts,
|
||||||
pickMail2925AccountForRun,
|
pickMail2925AccountForRun,
|
||||||
getState,
|
|
||||||
isAutoRunLockedState,
|
|
||||||
ensureContentScriptReadyOnTab,
|
|
||||||
requestStop,
|
requestStop,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
sendToContentScriptResilient,
|
sendToContentScriptResilient,
|
||||||
@@ -86,8 +86,10 @@
|
|||||||
|
|
||||||
function isMail2925LimitReachedError(error) {
|
function isMail2925LimitReachedError(error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
|
const normalized = message.toLowerCase();
|
||||||
return message.startsWith(MAIL2925_LIMIT_ERROR_PREFIX)
|
return message.startsWith(MAIL2925_LIMIT_ERROR_PREFIX)
|
||||||
|| /子邮箱.{0,12}已达上限|已达上限邮箱|子邮箱上限|邮箱已达上限/i.test(message);
|
|| normalized.includes('子邮箱已达上限')
|
||||||
|
|| normalized.includes('已达上限邮箱');
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMail2925ThreadTerminatedError(error) {
|
function isMail2925ThreadTerminatedError(error) {
|
||||||
@@ -135,7 +137,7 @@
|
|||||||
try {
|
try {
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
const tabId = Number(state?.tabRegistry?.[MAIL2925_SOURCE]?.tabId || 0);
|
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 '';
|
return '';
|
||||||
}
|
}
|
||||||
const tab = await chrome.tabs.get(tabId);
|
const tab = await chrome.tabs.get(tabId);
|
||||||
@@ -145,6 +147,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 = {}) {
|
async function setCurrentMail2925Account(accountId, options = {}) {
|
||||||
const { logMessage = '', updateLastUsedAt = false } = options;
|
const { logMessage = '', updateLastUsedAt = false } = options;
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
@@ -361,7 +385,7 @@
|
|||||||
origins: MAIL2925_COOKIE_ORIGINS,
|
origins: MAIL2925_COOKIE_ORIGINS,
|
||||||
});
|
});
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Best-effort cleanup only.
|
// 这里只做尽力清理。
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,172 +397,13 @@
|
|||||||
accountId = null,
|
accountId = null,
|
||||||
forceRelogin = false,
|
forceRelogin = false,
|
||||||
actionLabel = '确保 2925 邮箱登录态',
|
actionLabel = '确保 2925 邮箱登录态',
|
||||||
|
allowLoginWhenOnLoginPage = true,
|
||||||
} = options;
|
} = options;
|
||||||
const account = await ensureMail2925AccountForFlow({
|
const account = await ensureMail2925AccountForFlow({
|
||||||
allowAllocate: true,
|
allowAllocate: true,
|
||||||
preferredAccountId: accountId,
|
preferredAccountId: accountId,
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendLoginMessage = typeof sendToContentScriptResilient === 'function'
|
const sendLoginMessage = typeof sendToContentScriptResilient === 'function'
|
||||||
? sendToContentScriptResilient
|
? sendToContentScriptResilient
|
||||||
: async (source, message, runtimeOptions = {}) => sendToMailContentScriptResilient(
|
: async (source, message, runtimeOptions = {}) => sendToMailContentScriptResilient(
|
||||||
@@ -551,9 +416,104 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const buildSuccessPayload = async (result = {}) => ({
|
||||||
|
account: await ensureMail2925AccountForFlow({
|
||||||
|
allowAllocate: false,
|
||||||
|
preferredAccountId: account.id,
|
||||||
|
}),
|
||||||
|
mail: getMail2925MailConfig(),
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
|
||||||
|
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}(forceRelogin=true)`
|
||||||
|
: `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 || 'timeout'}`, matchedLoginTab ? 'info' : 'warn');
|
||||||
|
if (matchedLoginTab?.url) {
|
||||||
|
openedUrl = String(matchedLoginTab.url || '').trim();
|
||||||
|
}
|
||||||
|
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 && !isMail2925LoginUrl(openedUrl)) {
|
||||||
|
await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info');
|
||||||
|
return buildSuccessPayload({
|
||||||
|
loggedIn: true,
|
||||||
|
currentView: 'mailbox',
|
||||||
|
currentUrl: openedUrl,
|
||||||
|
usedExistingSession: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forceRelogin && !allowLoginWhenOnLoginPage) {
|
||||||
|
await failMailboxSession(`2925:${actionLabel}失败,当前页面已跳转到登录页,且当前未启用 2925 账号池,不执行自动登录。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
let result;
|
||||||
try {
|
try {
|
||||||
const beforeSendUrl = await getMail2925CurrentTabUrl();
|
const beforeSendUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl();
|
||||||
await addLog(`2925:发送 ENSURE_MAIL2925_SESSION 前当前地址:${beforeSendUrl || 'unknown'}`, 'info');
|
await addLog(`2925:发送 ENSURE_MAIL2925_SESSION 前当前地址:${beforeSendUrl || 'unknown'}`, 'info');
|
||||||
result = await sendLoginMessage(
|
result = await sendLoginMessage(
|
||||||
MAIL2925_SOURCE,
|
MAIL2925_SOURCE,
|
||||||
@@ -571,13 +531,12 @@
|
|||||||
timeoutMs: forceRelogin ? 30000 : 25000,
|
timeoutMs: forceRelogin ? 30000 : 25000,
|
||||||
retryDelayMs: 800,
|
retryDelayMs: 800,
|
||||||
responseTimeoutMs: forceRelogin ? 30000 : 25000,
|
responseTimeoutMs: forceRelogin ? 30000 : 25000,
|
||||||
logMessage: `步骤 0:2925 登录页通信异常,正在等待当前页面重新就绪后继续确认登录态...`,
|
logMessage: '步骤 0:2925 登录页通信异常,正在等待当前页面重新就绪后继续确认登录态...',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const stopped = await stopAutoRunForMail2925LoginFailure(
|
const message = `2925:${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'})。`;
|
||||||
`2925:${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'}),已按手动停止逻辑暂停自动流程。`
|
const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`);
|
||||||
);
|
|
||||||
if (stopped) {
|
if (stopped) {
|
||||||
throw new Error('流程已被用户停止。');
|
throw new Error('流程已被用户停止。');
|
||||||
}
|
}
|
||||||
@@ -585,27 +544,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
const stopped = await stopAutoRunForMail2925LoginFailure(
|
await failMailboxSession(`2925:${actionLabel}失败(${result.error})。`);
|
||||||
`2925:${actionLabel}失败(${result.error}),已按手动停止逻辑暂停自动流程。`
|
|
||||||
);
|
|
||||||
if (stopped) {
|
|
||||||
throw new Error('流程已被用户停止。');
|
|
||||||
}
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result?.limitReached) {
|
if (result?.limitReached) {
|
||||||
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '2925 子邮箱已达上限邮箱'}`);
|
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '2925 子邮箱已达上限邮箱'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result?.loggedIn) {
|
if (!result?.loggedIn) {
|
||||||
const stopped = await stopAutoRunForMail2925LoginFailure(
|
await failMailboxSession(`2925:${actionLabel}失败,登录后仍未进入收件箱。`);
|
||||||
`2925:${actionLabel}失败(20 秒内未进入收件箱),已按手动停止逻辑暂停自动流程。`
|
|
||||||
);
|
|
||||||
if (stopped) {
|
|
||||||
throw new Error('流程已被用户停止。');
|
|
||||||
}
|
|
||||||
throw new Error(`2925:${actionLabel}失败,当前页面仍未进入收件箱。`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await patchMail2925Account(account.id, {
|
await patchMail2925Account(account.id, {
|
||||||
@@ -614,17 +559,10 @@
|
|||||||
});
|
});
|
||||||
await setState({ currentMail2925AccountId: account.id });
|
await setState({ currentMail2925AccountId: account.id });
|
||||||
broadcastDataUpdate({ currentMail2925AccountId: account.id });
|
broadcastDataUpdate({ currentMail2925AccountId: account.id });
|
||||||
const finalUrl = await getMail2925CurrentTabUrl();
|
const finalUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl();
|
||||||
await addLog(`2925:登录态确认成功,当前地址=${finalUrl || 'unknown'}`, 'ok');
|
await addLog(`2925:登录态确认成功,当前地址=${finalUrl || 'unknown'}`, 'ok');
|
||||||
|
|
||||||
return {
|
return buildSuccessPayload(result);
|
||||||
account: await ensureMail2925AccountForFlow({
|
|
||||||
allowAllocate: false,
|
|
||||||
preferredAccountId: account.id,
|
|
||||||
}),
|
|
||||||
mail: getMail2925MailConfig(),
|
|
||||||
result,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMail2925LimitReachedError(step, error) {
|
async function handleMail2925LimitReachedError(step, error) {
|
||||||
@@ -635,7 +573,7 @@
|
|||||||
if (!currentAccount) {
|
if (!currentAccount) {
|
||||||
if (typeof requestStop === 'function') {
|
if (typeof requestStop === 'function') {
|
||||||
await requestStop({
|
await requestStop({
|
||||||
logMessage: `步骤 ${step}:2925 检测到“${reason}”,且当前没有可识别账号,已按手动停止逻辑暂停流程。`,
|
logMessage: `步骤 ${step}:2925 检测到“${reason}”,但当前没有可识别的账号可供处理。`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return new Error('流程已被用户停止。');
|
return new Error('流程已被用户停止。');
|
||||||
@@ -648,7 +586,7 @@
|
|||||||
lastError: reason,
|
lastError: reason,
|
||||||
});
|
});
|
||||||
await addLog(
|
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'
|
'warn'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -663,7 +601,7 @@
|
|||||||
broadcastDataUpdate({ currentMail2925AccountId: null });
|
broadcastDataUpdate({ currentMail2925AccountId: null });
|
||||||
if (typeof requestStop === 'function') {
|
if (typeof requestStop === 'function') {
|
||||||
await requestStop({
|
await requestStop({
|
||||||
logMessage: `步骤 ${step}:2925 账号 ${currentAccount.email} 已因“${reason}”禁用 24 小时,且当前没有可切换的下一个账号,已按手动停止逻辑暂停流程。`,
|
logMessage: `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,且当前没有可切换的下一个账号。`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return new Error('流程已被用户停止。');
|
return new Error('流程已被用户停止。');
|
||||||
@@ -673,11 +611,12 @@
|
|||||||
await ensureMail2925MailboxSession({
|
await ensureMail2925MailboxSession({
|
||||||
accountId: nextAccount.id,
|
accountId: nextAccount.id,
|
||||||
forceRelogin: true,
|
forceRelogin: true,
|
||||||
|
allowLoginWhenOnLoginPage: true,
|
||||||
actionLabel: `步骤 ${step}:切换 2925 账号`,
|
actionLabel: `步骤 ${step}:切换 2925 账号`,
|
||||||
});
|
});
|
||||||
await addLog(`步骤 ${step}:2925 已自动切换到下一个账号 ${nextAccount.email} 并完成登录,当前尝试将直接结束。`, 'warn');
|
await addLog(`步骤 ${step}:2925 已切换到下一个账号 ${nextAccount.email}。`, 'warn');
|
||||||
return buildMail2925ThreadTerminatedError(
|
return buildMail2925ThreadTerminatedError(
|
||||||
`步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”并已禁用 24 小时,已切换到 ${nextAccount.email},当前尝试结束,等待自动重试进入下一次尝试。`
|
`步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,已切换到 ${nextAccount.email},当前尝试结束,等待下一次重试。`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
chrome,
|
chrome,
|
||||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||||
confirmCustomVerificationStepBypass,
|
confirmCustomVerificationStepBypass,
|
||||||
ensureMail2925MailboxSession,
|
|
||||||
ensureStep8VerificationPageReady,
|
ensureStep8VerificationPageReady,
|
||||||
getOAuthFlowRemainingMs,
|
getOAuthFlowRemainingMs,
|
||||||
getOAuthFlowStepTimeoutMs,
|
getOAuthFlowStepTimeoutMs,
|
||||||
@@ -58,6 +57,28 @@
|
|||||||
return String(value || '').trim().toLowerCase();
|
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) {
|
async function runStep8Attempt(state) {
|
||||||
const mail = getMailConfig(state);
|
const mail = getMailConfig(state);
|
||||||
if (mail.error) throw new Error(mail.error);
|
if (mail.error) throw new Error(mail.error);
|
||||||
@@ -111,33 +132,11 @@
|
|||||||
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
|
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
|
||||||
) {
|
) {
|
||||||
await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`);
|
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 {
|
} else {
|
||||||
await addLog(`步骤 8:正在打开${mail.label}...`);
|
await addLog(`步骤 8:正在打开${mail.label}...`);
|
||||||
|
await focusOrOpenMailTab(mail);
|
||||||
const alive = await isTabAlive(mail.source);
|
if (mail.provider === '2925') {
|
||||||
if (alive) {
|
await addLog(`步骤 8:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,15 +24,39 @@
|
|||||||
throwIfStopped,
|
throwIfStopped,
|
||||||
} = deps;
|
} = 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) {
|
async function executeStep4(state) {
|
||||||
const mail = getMailConfig(state);
|
const mail = getMailConfig(state);
|
||||||
if (mail.error) throw new Error(mail.error);
|
if (mail.error) throw new Error(mail.error);
|
||||||
|
|
||||||
const stepStartedAt = Date.now();
|
const stepStartedAt = Date.now();
|
||||||
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
||||||
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||||
: stepStartedAt;
|
: stepStartedAt;
|
||||||
const verificationSessionKey = `4:${stepStartedAt}`;
|
const verificationSessionKey = `4:${stepStartedAt}`;
|
||||||
const signupTabId = await getTabId('signup-page');
|
const signupTabId = await getTabId('signup-page');
|
||||||
|
|
||||||
if (!signupTabId) {
|
if (!signupTabId) {
|
||||||
throw new Error('认证页面标签页已关闭,无法继续步骤 4。');
|
throw new Error('认证页面标签页已关闭,无法继续步骤 4。');
|
||||||
}
|
}
|
||||||
@@ -40,6 +64,7 @@
|
|||||||
await chrome.tabs.update(signupTabId, { active: true });
|
await chrome.tabs.update(signupTabId, { active: true });
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
|
await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
|
||||||
|
|
||||||
const prepareResult = await sendToContentScriptResilient(
|
const prepareResult = await sendToContentScriptResilient(
|
||||||
'signup-page',
|
'signup-page',
|
||||||
{
|
{
|
||||||
@@ -73,36 +98,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
throwIfStopped();
|
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} 轮询验证码...`);
|
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
|
||||||
} else if (mail.provider === '2925') {
|
} else if (mail.provider === '2925') {
|
||||||
if (state?.mail2925UseAccountPool && typeof ensureMail2925MailboxSession === 'function') {
|
await addLog(`步骤 4:正在打开${mail.label}...`);
|
||||||
|
if (typeof ensureMail2925MailboxSession === 'function') {
|
||||||
await ensureMail2925MailboxSession({
|
await ensureMail2925MailboxSession({
|
||||||
accountId: state.currentMail2925AccountId || null,
|
accountId: state.currentMail2925AccountId || null,
|
||||||
|
forceRelogin: false,
|
||||||
|
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
|
||||||
actionLabel: '步骤 4:确认 2925 邮箱登录态',
|
actionLabel: '步骤 4:确认 2925 邮箱登录态',
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await focusOrOpenMailTab(mail);
|
||||||
}
|
}
|
||||||
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
|
await addLog(`步骤 4:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
|
||||||
} else {
|
} else {
|
||||||
await addLog(`步骤 4:正在打开${mail.label}...`);
|
await addLog(`步骤 4:正在打开${mail.label}...`);
|
||||||
|
await 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,
|
|
||||||
});
|
|
||||||
} 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 resolveVerificationStep(4, state, mail, {
|
await resolveVerificationStep(4, state, mail, {
|
||||||
|
|||||||
@@ -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 expandedStorageKey = constants.expandedStorageKey || 'multipage-hotmail-list-expanded';
|
||||||
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
|
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
|
||||||
const copyIcon = constants.copyIcon || '';
|
const copyIcon = constants.copyIcon || '';
|
||||||
|
const createAccountPoolFormController = globalScope.SidepanelAccountPoolUi?.createAccountPoolFormController;
|
||||||
|
|
||||||
let actionInFlight = false;
|
let actionInFlight = false;
|
||||||
let listExpanded = false;
|
let listExpanded = false;
|
||||||
@@ -177,6 +178,25 @@
|
|||||||
dom.inputHotmailRefreshToken.value = '';
|
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() {
|
function renderHotmailAccounts() {
|
||||||
if (!dom.hotmailAccountsList) return;
|
if (!dom.hotmailAccountsList) return;
|
||||||
const latestState = state.getLatestState();
|
const latestState = state.getLatestState();
|
||||||
@@ -318,7 +338,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
helpers.showToast(`已保存 Hotmail 账号 ${email}`, 'success', 1800);
|
helpers.showToast(`已保存 Hotmail 账号 ${email}`, 'success', 1800);
|
||||||
clearHotmailForm();
|
formController.setVisible(false, { clearForm: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
helpers.showToast(`保存 Hotmail 账号失败:${err.message}`, 'error');
|
helpers.showToast(`保存 Hotmail 账号失败:${err.message}`, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -474,6 +494,14 @@
|
|||||||
setHotmailListExpanded(!listExpanded);
|
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 () => {
|
dom.btnHotmailUsageGuide?.addEventListener('click', async () => {
|
||||||
await helpers.openConfirmModal({
|
await helpers.openConfirmModal({
|
||||||
title: '使用教程',
|
title: '使用教程',
|
||||||
@@ -514,6 +542,7 @@
|
|||||||
dom.btnAddHotmailAccount?.addEventListener('click', handleAddHotmailAccount);
|
dom.btnAddHotmailAccount?.addEventListener('click', handleAddHotmailAccount);
|
||||||
dom.btnImportHotmailAccounts?.addEventListener('click', handleImportHotmailAccounts);
|
dom.btnImportHotmailAccounts?.addEventListener('click', handleImportHotmailAccounts);
|
||||||
dom.hotmailAccountsList?.addEventListener('click', handleAccountListClick);
|
dom.hotmailAccountsList?.addEventListener('click', handleAccountListClick);
|
||||||
|
formController.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
const expandedStorageKey = constants.expandedStorageKey || 'multipage-mail2925-list-expanded';
|
const expandedStorageKey = constants.expandedStorageKey || 'multipage-mail2925-list-expanded';
|
||||||
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
|
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
|
||||||
const copyIcon = constants.copyIcon || '';
|
const copyIcon = constants.copyIcon || '';
|
||||||
|
const createAccountPoolFormController = globalScope.SidepanelAccountPoolUi?.createAccountPoolFormController;
|
||||||
|
|
||||||
let actionInFlight = false;
|
let actionInFlight = false;
|
||||||
let listExpanded = false;
|
let listExpanded = false;
|
||||||
let editingAccountId = '';
|
let editingAccountId = '';
|
||||||
let formVisible = false;
|
|
||||||
|
|
||||||
function getMail2925Accounts(currentState = state.getLatestState()) {
|
function getMail2925Accounts(currentState = state.getLatestState()) {
|
||||||
return helpers.getMail2925Accounts(currentState);
|
return helpers.getMail2925Accounts(currentState);
|
||||||
@@ -119,34 +119,24 @@
|
|||||||
if (dom.inputMail2925Password) dom.inputMail2925Password.value = '';
|
if (dom.inputMail2925Password) dom.inputMail2925Password.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncMail2925FormUi() {
|
const formController = typeof createAccountPoolFormController === 'function'
|
||||||
if (dom.mail2925FormShell) {
|
? createAccountPoolFormController({
|
||||||
dom.mail2925FormShell.hidden = !formVisible;
|
formShell: dom.mail2925FormShell,
|
||||||
}
|
toggleButton: dom.btnToggleMail2925Form,
|
||||||
if (dom.btnToggleMail2925Form) {
|
hiddenLabel: '添加账号',
|
||||||
dom.btnToggleMail2925Form.textContent = formVisible ? '取消添加' : '添加账号';
|
visibleLabel: '取消添加',
|
||||||
dom.btnToggleMail2925Form.setAttribute('aria-expanded', String(formVisible));
|
onClear: () => {
|
||||||
}
|
stopEditingAccount({ clearForm: true });
|
||||||
}
|
},
|
||||||
|
onFocus: () => {
|
||||||
function setMail2925FormVisible(visible, options = {}) {
|
dom.inputMail2925Email?.focus?.();
|
||||||
const {
|
},
|
||||||
clearForm = false,
|
})
|
||||||
focusField = false,
|
: {
|
||||||
} = options;
|
isVisible: () => false,
|
||||||
|
setVisible() {},
|
||||||
formVisible = Boolean(visible);
|
sync() {},
|
||||||
if (!formVisible && clearForm) {
|
};
|
||||||
stopEditingAccount({ clearForm: true });
|
|
||||||
} else if (clearForm) {
|
|
||||||
clearMail2925Form();
|
|
||||||
}
|
|
||||||
|
|
||||||
syncMail2925FormUi();
|
|
||||||
if (formVisible && focusField) {
|
|
||||||
dom.inputMail2925Email?.focus?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncEditUi() {
|
function syncEditUi() {
|
||||||
if (dom.btnAddMail2925Account) {
|
if (dom.btnAddMail2925Account) {
|
||||||
@@ -159,7 +149,7 @@
|
|||||||
editingAccountId = account.id;
|
editingAccountId = account.id;
|
||||||
if (dom.inputMail2925Email) dom.inputMail2925Email.value = String(account.email || '').trim();
|
if (dom.inputMail2925Email) dom.inputMail2925Email.value = String(account.email || '').trim();
|
||||||
if (dom.inputMail2925Password) dom.inputMail2925Password.value = String(account.password || '');
|
if (dom.inputMail2925Password) dom.inputMail2925Password.value = String(account.password || '');
|
||||||
setMail2925FormVisible(true, { focusField: false });
|
formController.setVisible(true, { focusField: false });
|
||||||
syncEditUi();
|
syncEditUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +252,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyMail2925AccountMutation(response.account);
|
applyMail2925AccountMutation(response.account);
|
||||||
setMail2925FormVisible(false, { clearForm: true });
|
formController.setVisible(false, { clearForm: true });
|
||||||
helpers.showToast(
|
helpers.showToast(
|
||||||
updatingExisting
|
updatingExisting
|
||||||
? `已更新 2925 账号 ${email}`
|
? `已更新 2925 账号 ${email}`
|
||||||
@@ -360,7 +350,7 @@
|
|||||||
mail2925Accounts: [],
|
mail2925Accounts: [],
|
||||||
currentMail2925AccountId: null,
|
currentMail2925AccountId: null,
|
||||||
});
|
});
|
||||||
setMail2925FormVisible(false, { clearForm: true });
|
formController.setVisible(false, { clearForm: true });
|
||||||
refreshManagedAliasBaseEmail();
|
refreshManagedAliasBaseEmail();
|
||||||
renderMail2925Accounts();
|
renderMail2925Accounts();
|
||||||
helpers.showToast(`已删除全部 ${response.deletedCount || 0} 个 2925 账号`, 'success', 2200);
|
helpers.showToast(`已删除全部 ${response.deletedCount || 0} 个 2925 账号`, 'success', 2200);
|
||||||
@@ -488,7 +478,7 @@
|
|||||||
}
|
}
|
||||||
state.syncLatestState(nextState);
|
state.syncLatestState(nextState);
|
||||||
if (editingAccountId === accountId) {
|
if (editingAccountId === accountId) {
|
||||||
setMail2925FormVisible(false, { clearForm: true });
|
formController.setVisible(false, { clearForm: true });
|
||||||
}
|
}
|
||||||
refreshManagedAliasBaseEmail();
|
refreshManagedAliasBaseEmail();
|
||||||
renderMail2925Accounts();
|
renderMail2925Accounts();
|
||||||
@@ -508,11 +498,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
dom.btnToggleMail2925Form?.addEventListener('click', () => {
|
dom.btnToggleMail2925Form?.addEventListener('click', () => {
|
||||||
if (formVisible) {
|
if (formController.isVisible()) {
|
||||||
setMail2925FormVisible(false, { clearForm: true });
|
formController.setVisible(false, { clearForm: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMail2925FormVisible(true, { clearForm: !editingAccountId, focusField: true });
|
formController.setVisible(true, { clearForm: !editingAccountId, focusField: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
dom.btnDeleteAllMail2925Accounts?.addEventListener('click', async () => {
|
dom.btnDeleteAllMail2925Accounts?.addEventListener('click', async () => {
|
||||||
@@ -532,7 +522,7 @@
|
|||||||
dom.btnImportMail2925Accounts?.addEventListener('click', handleImportMail2925Accounts);
|
dom.btnImportMail2925Accounts?.addEventListener('click', handleImportMail2925Accounts);
|
||||||
dom.mail2925AccountsList?.addEventListener('click', handleAccountListClick);
|
dom.mail2925AccountsList?.addEventListener('click', handleAccountListClick);
|
||||||
syncEditUi();
|
syncEditUi();
|
||||||
syncMail2925FormUi();
|
formController.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1112,17 +1112,17 @@ header {
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail2925-form-shell {
|
.account-pool-form-shell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail2925-actions-inline {
|
.account-pool-actions-inline {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail2925-import-action {
|
.account-pool-import-action {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1148,7 +1148,7 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hotmail-list-shell.is-collapsed {
|
.hotmail-list-shell.is-collapsed {
|
||||||
max-height: 236px;
|
max-height: 176px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotmail-list-shell.is-expanded {
|
.hotmail-list-shell.is-expanded {
|
||||||
|
|||||||
+36
-30
@@ -389,6 +389,7 @@
|
|||||||
<span class="section-label">Hotmail 账号池</span>
|
<span class="section-label">Hotmail 账号池</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-mini-actions">
|
<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-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-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>
|
<button id="btn-delete-all-hotmail-accounts" class="btn btn-ghost btn-xs" type="button">全部删除</button>
|
||||||
@@ -411,33 +412,37 @@
|
|||||||
<span class="data-label">本地助手</span>
|
<span class="data-label">本地助手</span>
|
||||||
<input type="text" id="input-hotmail-local-base-url" class="data-input mono" placeholder="http://127.0.0.1:17373" />
|
<input type="text" id="input-hotmail-local-base-url" class="data-input mono" placeholder="http://127.0.0.1:17373" />
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div id="hotmail-form-shell" class="account-pool-form-shell" hidden>
|
||||||
<span class="data-label">邮箱</span>
|
<div class="data-row">
|
||||||
<input type="text" id="input-hotmail-email" class="data-input" placeholder="name@hotmail.com" />
|
<span class="data-label">邮箱</span>
|
||||||
</div>
|
<input type="text" id="input-hotmail-email" class="data-input" placeholder="name@hotmail.com" />
|
||||||
<div class="data-row">
|
</div>
|
||||||
<span class="data-label">客户端 ID</span>
|
<div class="data-row">
|
||||||
<input type="text" id="input-hotmail-client-id" class="data-input mono" placeholder="微软应用客户端 ID" />
|
<span class="data-label">客户端 ID</span>
|
||||||
</div>
|
<input type="text" id="input-hotmail-client-id" class="data-input mono" placeholder="微软应用客户端 ID" />
|
||||||
<div class="data-row">
|
</div>
|
||||||
<span class="data-label">邮箱密码</span>
|
<div class="data-row">
|
||||||
<input type="password" id="input-hotmail-password" class="data-input" placeholder="可选,仅用于记录" />
|
<span class="data-label">邮箱密码</span>
|
||||||
</div>
|
<input type="password" id="input-hotmail-password" class="data-input" placeholder="可选,仅用于记录" />
|
||||||
<div class="data-row">
|
</div>
|
||||||
<span class="data-label">刷新令牌</span>
|
<div class="data-row">
|
||||||
<input type="password" id="input-hotmail-refresh-token" class="data-input mono"
|
<span class="data-label">刷新令牌</span>
|
||||||
placeholder="必填,粘贴刷新令牌(refresh token)" />
|
<input type="password" id="input-hotmail-refresh-token" class="data-input mono"
|
||||||
</div>
|
placeholder="必填,粘贴刷新令牌(refresh token)" />
|
||||||
<div class="data-row hotmail-actions-row">
|
</div>
|
||||||
<span class="data-label"></span>
|
<div class="data-row hotmail-actions-row">
|
||||||
<button id="btn-add-hotmail-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
|
<span class="data-label"></span>
|
||||||
</div>
|
<div class="data-inline account-pool-actions-inline">
|
||||||
<div class="data-row hotmail-import-row">
|
<button id="btn-add-hotmail-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
|
||||||
<span class="data-label">批量导入</span>
|
<button id="btn-import-hotmail-accounts" class="btn btn-outline btn-sm account-pool-import-action" type="button">批量导入</button>
|
||||||
<div class="hotmail-import-box">
|
</div>
|
||||||
<textarea id="input-hotmail-import" class="data-textarea mono"
|
</div>
|
||||||
placeholder="账号----密码----客户端ID----刷新令牌 name@hotmail.com----password----client-id----refresh-token"></textarea>
|
<div class="data-row hotmail-import-row">
|
||||||
<button id="btn-import-hotmail-accounts" class="btn btn-outline btn-sm" type="button">导入</button>
|
<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>
|
</div>
|
||||||
<div id="hotmail-list-shell" class="hotmail-list-shell is-collapsed">
|
<div id="hotmail-list-shell" class="hotmail-list-shell is-collapsed">
|
||||||
@@ -455,7 +460,7 @@
|
|||||||
<button id="btn-toggle-mail2925-list" class="btn btn-ghost btn-xs" type="button" aria-expanded="false">展开列表</button>
|
<button id="btn-toggle-mail2925-list" class="btn btn-ghost btn-xs" type="button" aria-expanded="false">展开列表</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="mail2925-form-shell" class="mail2925-form-shell" hidden>
|
<div id="mail2925-form-shell" class="account-pool-form-shell" hidden>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">邮箱</span>
|
<span class="data-label">邮箱</span>
|
||||||
<input type="text" id="input-mail2925-email" class="data-input" placeholder="name@2925.com" />
|
<input type="text" id="input-mail2925-email" class="data-input" placeholder="name@2925.com" />
|
||||||
@@ -466,9 +471,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="data-row hotmail-actions-row">
|
<div class="data-row hotmail-actions-row">
|
||||||
<span class="data-label"></span>
|
<span class="data-label"></span>
|
||||||
<div class="data-inline mail2925-actions-inline">
|
<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-add-mail2925-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
|
||||||
<button id="btn-import-mail2925-accounts" class="btn btn-outline btn-sm mail2925-import-action" type="button">批量导入</button>
|
<button id="btn-import-mail2925-accounts" class="btn btn-outline btn-sm account-pool-import-action" type="button">批量导入</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row hotmail-import-row">
|
<div class="data-row hotmail-import-row">
|
||||||
@@ -722,6 +727,7 @@
|
|||||||
<script src="../data/step-definitions.js"></script>
|
<script src="../data/step-definitions.js"></script>
|
||||||
<script src="update-service.js"></script>
|
<script src="update-service.js"></script>
|
||||||
<script src="contribution-content-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="hotmail-manager.js"></script>
|
||||||
<script src="mail-2925-manager.js"></script>
|
<script src="mail-2925-manager.js"></script>
|
||||||
<script src="icloud-manager.js"></script>
|
<script src="icloud-manager.js"></script>
|
||||||
|
|||||||
@@ -148,10 +148,12 @@ const inputHotmailRefreshToken = document.getElementById('input-hotmail-refresh-
|
|||||||
const inputHotmailImport = document.getElementById('input-hotmail-import');
|
const inputHotmailImport = document.getElementById('input-hotmail-import');
|
||||||
const btnAddHotmailAccount = document.getElementById('btn-add-hotmail-account');
|
const btnAddHotmailAccount = document.getElementById('btn-add-hotmail-account');
|
||||||
const btnImportHotmailAccounts = document.getElementById('btn-import-hotmail-accounts');
|
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 btnHotmailUsageGuide = document.getElementById('btn-hotmail-usage-guide');
|
||||||
const btnClearUsedHotmailAccounts = document.getElementById('btn-clear-used-hotmail-accounts');
|
const btnClearUsedHotmailAccounts = document.getElementById('btn-clear-used-hotmail-accounts');
|
||||||
const btnDeleteAllHotmailAccounts = document.getElementById('btn-delete-all-hotmail-accounts');
|
const btnDeleteAllHotmailAccounts = document.getElementById('btn-delete-all-hotmail-accounts');
|
||||||
const btnToggleHotmailList = document.getElementById('btn-toggle-hotmail-list');
|
const btnToggleHotmailList = document.getElementById('btn-toggle-hotmail-list');
|
||||||
|
const hotmailFormShell = document.getElementById('hotmail-form-shell');
|
||||||
const hotmailListShell = document.getElementById('hotmail-list-shell');
|
const hotmailListShell = document.getElementById('hotmail-list-shell');
|
||||||
const hotmailAccountsList = document.getElementById('hotmail-accounts-list');
|
const hotmailAccountsList = document.getElementById('hotmail-accounts-list');
|
||||||
const inputMail2925Email = document.getElementById('input-mail2925-email');
|
const inputMail2925Email = document.getElementById('input-mail2925-email');
|
||||||
@@ -3086,7 +3088,9 @@ const hotmailManager = window.SidepanelHotmailManager?.createHotmailManager({
|
|||||||
btnDeleteAllHotmailAccounts,
|
btnDeleteAllHotmailAccounts,
|
||||||
btnHotmailUsageGuide,
|
btnHotmailUsageGuide,
|
||||||
btnImportHotmailAccounts,
|
btnImportHotmailAccounts,
|
||||||
|
btnToggleHotmailForm,
|
||||||
btnToggleHotmailList,
|
btnToggleHotmailList,
|
||||||
|
hotmailFormShell,
|
||||||
hotmailAccountsList,
|
hotmailAccountsList,
|
||||||
hotmailListShell,
|
hotmailListShell,
|
||||||
inputEmail,
|
inputEmail,
|
||||||
|
|||||||
@@ -143,3 +143,32 @@ return { getMailConfig };
|
|||||||
navigateOnReuse: true,
|
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,207 @@
|
|||||||
|
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 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);
|
||||||
|
});
|
||||||
@@ -78,5 +78,5 @@ test('ensureMail2925MailboxSession waits 3 seconds before and after opening logi
|
|||||||
|
|
||||||
assert.deepStrictEqual(events.openedUrls, ['https://2925.com/login/']);
|
assert.deepStrictEqual(events.openedUrls, ['https://2925.com/login/']);
|
||||||
assert.deepStrictEqual(events.sleeps, [3000, 3000]);
|
assert.deepStrictEqual(events.sleeps, [3000, 3000]);
|
||||||
assert.equal(events.readyCalls, 1);
|
assert.equal(events.readyCalls, 2);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -223,5 +223,5 @@ test('ensureMail2925MailboxSession requests stop when auto run is active and log
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(events.stopCalls.length, 1);
|
assert.equal(events.stopCalls.length, 1);
|
||||||
assert.match(events.stopCalls[0].logMessage, /20 秒内未进入收件箱/);
|
assert.match(events.stopCalls[0].logMessage, /登录后仍未进入收件箱/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 () => {
|
test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling', async () => {
|
||||||
let capturedOptions = null;
|
let capturedOptions = null;
|
||||||
|
let ensureCalls = 0;
|
||||||
|
const tabUpdates = [];
|
||||||
|
const tabReuses = [];
|
||||||
const realDateNow = Date.now;
|
const realDateNow = Date.now;
|
||||||
Date.now = () => 700000;
|
Date.now = () => 700000;
|
||||||
|
|
||||||
@@ -15,12 +18,16 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
|
|||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
chrome: {
|
chrome: {
|
||||||
tabs: {
|
tabs: {
|
||||||
update: async () => {},
|
update: async (tabId, payload) => {
|
||||||
|
tabUpdates.push({ tabId, payload });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
completeStepFromBackground: async () => {},
|
completeStepFromBackground: async () => {},
|
||||||
confirmCustomVerificationStepBypass: async () => {},
|
confirmCustomVerificationStepBypass: async () => {},
|
||||||
ensureMail2925MailboxSession: async () => {},
|
ensureMail2925MailboxSession: async () => {
|
||||||
|
ensureCalls += 1;
|
||||||
|
},
|
||||||
getMailConfig: () => ({
|
getMailConfig: () => ({
|
||||||
provider: '2925',
|
provider: '2925',
|
||||||
label: '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) => {
|
resolveVerificationStep: async (_step, _state, _mail, options) => {
|
||||||
capturedOptions = options;
|
capturedOptions = options;
|
||||||
},
|
},
|
||||||
reuseOrCreateTab: async () => {},
|
reuseOrCreateTab: async (source, url) => {
|
||||||
|
tabReuses.push({ source, url });
|
||||||
|
},
|
||||||
sendToContentScriptResilient: async () => ({}),
|
sendToContentScriptResilient: async () => ({}),
|
||||||
shouldUseCustomRegistrationEmail: () => false,
|
shouldUseCustomRegistrationEmail: () => false,
|
||||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
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({
|
await executor.executeStep4({
|
||||||
email: 'user@example.com',
|
email: 'user@example.com',
|
||||||
password: 'secret',
|
password: 'secret',
|
||||||
mail2925UseAccountPool: false,
|
mail2925UseAccountPool: true,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
Date.now = realDateNow;
|
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.filterAfterTimestamp, 100000);
|
||||||
assert.equal(capturedOptions.resendIntervalMs, 0);
|
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 () => {
|
test('step 8 uses a fixed 10-minute lookback window and disables resend interval for 2925 mailbox polling', async () => {
|
||||||
let capturedOptions = null;
|
let capturedOptions = null;
|
||||||
|
let ensureCalls = 0;
|
||||||
|
const tabUpdates = [];
|
||||||
|
const tabReuses = [];
|
||||||
const realDateNow = Date.now;
|
const realDateNow = Date.now;
|
||||||
Date.now = () => 900000;
|
Date.now = () => 900000;
|
||||||
|
|
||||||
@@ -98,11 +101,16 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
|
|||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
chrome: {
|
chrome: {
|
||||||
tabs: {
|
tabs: {
|
||||||
update: async () => {},
|
update: async (tabId, payload) => {
|
||||||
|
tabUpdates.push({ tabId, payload });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||||
confirmCustomVerificationStepBypass: async () => {},
|
confirmCustomVerificationStepBypass: async () => {},
|
||||||
|
ensureMail2925MailboxSession: async () => {
|
||||||
|
ensureCalls += 1;
|
||||||
|
},
|
||||||
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }),
|
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }),
|
||||||
rerunStep7ForStep8Recovery: async () => {},
|
rerunStep7ForStep8Recovery: async () => {},
|
||||||
getOAuthFlowRemainingMs: async () => 8000,
|
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) => {
|
resolveVerificationStep: async (_step, _state, _mail, options) => {
|
||||||
capturedOptions = options;
|
capturedOptions = options;
|
||||||
},
|
},
|
||||||
reuseOrCreateTab: async () => {},
|
reuseOrCreateTab: async (source, url) => {
|
||||||
|
tabReuses.push({ source, url });
|
||||||
|
},
|
||||||
setState: async () => {},
|
setState: async () => {},
|
||||||
setStepStatus: async () => {},
|
setStepStatus: async () => {},
|
||||||
shouldUseCustomRegistrationEmail: () => false,
|
shouldUseCustomRegistrationEmail: () => false,
|
||||||
@@ -137,11 +147,18 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
|
|||||||
email: 'user@example.com',
|
email: 'user@example.com',
|
||||||
password: 'secret',
|
password: 'secret',
|
||||||
oauthUrl: 'https://oauth.example/latest',
|
oauthUrl: 'https://oauth.example/latest',
|
||||||
|
mail2925UseAccountPool: true,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
Date.now = realDateNow;
|
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.filterAfterTimestamp, 300000);
|
||||||
assert.equal(capturedOptions.resendIntervalMs, 0);
|
assert.equal(capturedOptions.resendIntervalMs, 0);
|
||||||
assert.equal(capturedOptions.targetEmail, '');
|
assert.equal(capturedOptions.targetEmail, '');
|
||||||
|
|||||||
@@ -2,19 +2,74 @@ const test = require('node:test');
|
|||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const fs = require('node:fs');
|
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', () => {
|
test('sidepanel loads hotmail manager before sidepanel bootstrap', () => {
|
||||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
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 hotmailManagerIndex = html.indexOf('<script src="hotmail-manager.js"></script>');
|
||||||
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
|
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
|
||||||
|
|
||||||
|
assert.notEqual(helperIndex, -1);
|
||||||
assert.notEqual(hotmailManagerIndex, -1);
|
assert.notEqual(hotmailManagerIndex, -1);
|
||||||
assert.notEqual(sidepanelIndex, -1);
|
assert.notEqual(sidepanelIndex, -1);
|
||||||
|
assert.ok(helperIndex < hotmailManagerIndex);
|
||||||
assert.ok(hotmailManagerIndex < sidepanelIndex);
|
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', () => {
|
test('hotmail manager exposes a factory and renders empty state', () => {
|
||||||
const source = fs.readFileSync('sidepanel/hotmail-manager.js', 'utf8');
|
const source = fs.readFileSync('sidepanel/hotmail-manager.js', 'utf8');
|
||||||
const windowObject = {};
|
const windowObject = {
|
||||||
|
SidepanelAccountPoolUi: createAccountPoolUiStub(),
|
||||||
|
};
|
||||||
const localStorageMock = {
|
const localStorageMock = {
|
||||||
getItem() {
|
getItem() {
|
||||||
return null;
|
return null;
|
||||||
@@ -77,3 +132,199 @@ test('hotmail manager exposes a factory and renders empty state', () => {
|
|||||||
manager.renderHotmailAccounts();
|
manager.renderHotmailAccounts();
|
||||||
assert.match(hotmailAccountsList.innerHTML, /还没有 Hotmail 账号/);
|
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 账号/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,6 +2,49 @@ const test = require('node:test');
|
|||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const fs = require('node:fs');
|
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 html contains collapsible mail2925 form controls', () => {
|
test('sidepanel html contains collapsible mail2925 form controls', () => {
|
||||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||||
assert.match(html, /id="btn-toggle-mail2925-form"/);
|
assert.match(html, /id="btn-toggle-mail2925-form"/);
|
||||||
@@ -11,7 +54,9 @@ test('sidepanel html contains collapsible mail2925 form controls', () => {
|
|||||||
|
|
||||||
test('mail2925 manager renders edit action for existing accounts', () => {
|
test('mail2925 manager renders edit action for existing accounts', () => {
|
||||||
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
|
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
|
||||||
const windowObject = {};
|
const windowObject = {
|
||||||
|
SidepanelAccountPoolUi: createAccountPoolUiStub(),
|
||||||
|
};
|
||||||
const localStorageMock = {
|
const localStorageMock = {
|
||||||
getItem() {
|
getItem() {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,13 +2,59 @@ const test = require('node:test');
|
|||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const fs = require('node:fs');
|
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', () => {
|
test('sidepanel loads mail2925 manager before sidepanel bootstrap', () => {
|
||||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
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 managerIndex = html.indexOf('<script src="mail-2925-manager.js"></script>');
|
||||||
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
|
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
|
||||||
|
|
||||||
|
assert.notEqual(helperIndex, -1);
|
||||||
assert.notEqual(managerIndex, -1);
|
assert.notEqual(managerIndex, -1);
|
||||||
assert.notEqual(sidepanelIndex, -1);
|
assert.notEqual(sidepanelIndex, -1);
|
||||||
|
assert.ok(helperIndex < managerIndex);
|
||||||
assert.ok(managerIndex < sidepanelIndex);
|
assert.ok(managerIndex < sidepanelIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -22,9 +68,16 @@ test('sidepanel html contains mail2925 pool toggle and selector controls', () =>
|
|||||||
assert.doesNotMatch(html, /id="btn-cancel-mail2925-edit"/);
|
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', () => {
|
test('mail2925 manager exposes a factory and renders empty state', () => {
|
||||||
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
|
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
|
||||||
const windowObject = {};
|
const windowObject = {
|
||||||
|
SidepanelAccountPoolUi: createAccountPoolUiStub(),
|
||||||
|
};
|
||||||
const localStorageMock = {
|
const localStorageMock = {
|
||||||
getItem() {
|
getItem() {
|
||||||
return null;
|
return null;
|
||||||
@@ -101,7 +154,9 @@ test('mail2925 manager exposes a factory and renders empty state', () => {
|
|||||||
|
|
||||||
test('mail2925 manager toggles form container from header button', () => {
|
test('mail2925 manager toggles form container from header button', () => {
|
||||||
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
|
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
|
||||||
const windowObject = {};
|
const windowObject = {
|
||||||
|
SidepanelAccountPoolUi: createAccountPoolUiStub(),
|
||||||
|
};
|
||||||
const localStorageMock = {
|
const localStorageMock = {
|
||||||
getItem() {
|
getItem() {
|
||||||
return null;
|
return null;
|
||||||
@@ -179,7 +234,9 @@ test('mail2925 manager toggles form container from header button', () => {
|
|||||||
|
|
||||||
test('mail2925 manager hides form after save succeeds', async () => {
|
test('mail2925 manager hides form after save succeeds', async () => {
|
||||||
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
|
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
|
||||||
const windowObject = {};
|
const windowObject = {
|
||||||
|
SidepanelAccountPoolUi: createAccountPoolUiStub(),
|
||||||
|
};
|
||||||
const localStorageMock = {
|
const localStorageMock = {
|
||||||
getItem() {
|
getItem() {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+11
-3
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
### 2.1 Sidepanel
|
### 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 初始化和点击“自动”按钮前刷新一次贡献站公开内容摘要;如果刷新失败,不阻塞主自动流程
|
- 在 sidepanel 初始化和点击“自动”按钮前刷新一次贡献站公开内容摘要;如果刷新失败,不阻塞主自动流程
|
||||||
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表
|
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表
|
||||||
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Pro` 与 legacy `v` 两个版本族,排序时优先保持版本族语义一致,同时会在读取缓存后重新排序,避免旧缓存把 `v` 版本误显示为比 `Pro` 更新
|
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Pro` 与 legacy `v` 两个版本族,排序时优先保持版本族语义一致,同时会在读取缓存后重新排序,避免旧缓存把 `v` 版本误显示为比 `Pro` 更新
|
||||||
|
- 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作
|
||||||
|
|
||||||
### 2.2 Background Service Worker
|
### 2.2 Background Service Worker
|
||||||
|
|
||||||
@@ -330,7 +331,7 @@
|
|||||||
补充行为:
|
补充行为:
|
||||||
|
|
||||||
- `2925` provider 会关闭 Step 4 / 8 的自动重发间隔 25 秒节流;每次“重新发送验证码”之间,会在邮箱页内部执行一轮固定 15 次的刷新轮询,不再因 OAuth 剩余时间预算而缩短。
|
- `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` 当前既不依赖时间窗,也不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中的匹配邮件。
|
- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 当前既不依赖时间窗,也不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中的匹配邮件。
|
||||||
- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
|
- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
|
||||||
- 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
|
- 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
|
||||||
@@ -509,6 +510,8 @@
|
|||||||
- [hotmail-utils.js](c:/Users/projectf/Downloads/codex注册扩展/hotmail-utils.js)
|
- [hotmail-utils.js](c:/Users/projectf/Downloads/codex注册扩展/hotmail-utils.js)
|
||||||
- [microsoft-email.js](c:/Users/projectf/Downloads/codex注册扩展/microsoft-email.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)
|
- [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 快照同步接口。
|
- 本地 helper 除了收信与验证码读取,还提供邮箱记录 JSON 快照同步接口。
|
||||||
- 账号运行历史本地同步由独立配置控制,不再绑定 Hotmail 的本地助手模式。
|
- 账号运行历史本地同步由独立配置控制,不再绑定 Hotmail 的本地助手模式。
|
||||||
|
- sidepanel 中 Hotmail 账号池的新增表单默认收起,头部通过共享按钮切换“添加账号 / 取消添加”;表单显隐、按钮文案切换、清空与聚焦都复用 `sidepanel/account-pool-ui.js`,不在 Hotmail manager 内重复实现一套。
|
||||||
|
|
||||||
### 7.3 LuckMail
|
### 7.3 LuckMail
|
||||||
|
|
||||||
@@ -534,6 +538,7 @@
|
|||||||
- [mail2925-utils.js](c:/Users/projectf/Downloads/codex注册扩展/mail2925-utils.js)
|
- [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)
|
- [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/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,18 @@
|
|||||||
统一承接 2925 账号池的持久化、当前账号切换、cookie 清理登出、网页登录态确认、自动登录,以及命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
|
统一承接 2925 账号池的持久化、当前账号切换、cookie 清理登出、网页登录态确认、自动登录,以及命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
|
||||||
- `sidepanel/mail-2925-manager.js`
|
- `sidepanel/mail-2925-manager.js`
|
||||||
负责 2925 账号池的新增、导入、切换、手动登录、禁用、清冷却与删除。
|
负责 2925 账号池的新增、导入、切换、手动登录、禁用、清冷却与删除。
|
||||||
|
- `sidepanel/account-pool-ui.js`
|
||||||
|
负责 Hotmail / 2925 账号池共用的新增表单显隐、头部按钮文案切换、清空表单与首字段聚焦;2925 manager 不再单独维护另一套表单开关状态机。
|
||||||
|
|
||||||
链路:
|
链路:
|
||||||
|
|
||||||
1. 用户在 sidepanel 的 2925 账号池中保存 `email / password`
|
1. 用户在 sidepanel 的 2925 账号池中保存 `email / password`
|
||||||
2. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
|
2. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
|
||||||
3. 手动点击 `登录` 或自动流程进入 Step 4 / Step 8 前,只有在号池模式开启时,后台才会确保当前 2925 账号已登录网页邮箱;登录动作提交后,内容脚本会在 20 秒内轮询“是否已进入收件箱”
|
3. 手动点击 `登录` 或自动流程进入 Step 4 前,后台会先打开当前 2925 邮箱页:如果仍停留在收件箱则直接复用;如果跳到登录页,则仅在号池模式开启时才自动登录,关闭号池时直接停止
|
||||||
4. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先记录当前时间;若当前处于号池模式且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录
|
4. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先记录当前时间;若当前处于号池模式且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录
|
||||||
5. 如果登录提交后 20 秒内仍未进入收件箱,且当前正处于自动运行中,则后台会直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态;这类情况常见于图片验证、行为验证或其他阻断登录的中间页
|
5. 如果登录提交后 20 秒内仍未进入收件箱,且当前正处于自动运行中,则后台会直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态;这类情况常见于图片验证、行为验证或其他阻断登录的中间页
|
||||||
6. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态
|
6. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态
|
||||||
|
7. sidepanel 中 2925 账号池的新增表单也走与 Hotmail 相同的共享交互:默认收起,头部按钮切换“添加账号 / 取消添加”,操作行右侧提供“批量导入”,保存成功后自动收起并清空。
|
||||||
|
|
||||||
### 7.5 iCloud
|
### 7.5 iCloud
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
- `background/generated-email-helpers.js` 只负责调度,不应再次复制 Gmail / 2925 规则
|
- `background/generated-email-helpers.js` 只负责调度,不应再次复制 Gmail / 2925 规则
|
||||||
- `background/signup-flow-helpers.js` 只负责“复用已有邮箱还是重新生成”的流程决策
|
- `background/signup-flow-helpers.js` 只负责“复用已有邮箱还是重新生成”的流程决策
|
||||||
- `sidepanel/sidepanel.js` 只负责 UI 接线、校验触发和状态同步
|
- `sidepanel/sidepanel.js` 只负责 UI 接线、校验触发和状态同步
|
||||||
|
- Hotmail / 2925 账号池这类跨 provider 的 sidepanel 表单显隐、头部按钮文案切换与共用操作行,应优先收敛到共享 UI helper(当前为 `sidepanel/account-pool-ui.js`),不要在各自 manager 中复制一套近似状态机
|
||||||
|
|
||||||
### 3.3 新增配置项
|
### 3.3 新增配置项
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -112,6 +112,7 @@
|
|||||||
|
|
||||||
## `sidepanel/`
|
## `sidepanel/`
|
||||||
|
|
||||||
|
- `sidepanel/account-pool-ui.js`:侧边栏号池表单共享 UI helper,负责 Hotmail / 2925 账号池新增表单的显隐、头部“添加账号/取消添加”按钮文案切换、清空表单与首字段聚焦。
|
||||||
- `sidepanel/hotmail-manager.js`:侧边栏 Hotmail 账号池管理器,负责列表、导入、验证、测试收信和批量操作。
|
- `sidepanel/hotmail-manager.js`:侧边栏 Hotmail 账号池管理器,负责列表、导入、验证、测试收信和批量操作。
|
||||||
- `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。
|
- `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。
|
||||||
- `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
|
- `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
|
||||||
@@ -119,9 +120,9 @@
|
|||||||
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。
|
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。
|
||||||
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
|
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
|
||||||
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行的禁用与隐藏。
|
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行的禁用与隐藏。
|
||||||
- `sidepanel/sidepanel.css`:侧边栏样式文件。
|
- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行与统一的收起态列表高度样式。
|
||||||
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,别名基邮箱行右侧会显示“号池”开关,开启后输入框切换为号池邮箱下拉选择,并显示 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 基邮箱下拉选择,关闭时则回退到原来的手填基邮箱路线;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示。
|
- `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` 双版本族排序、缓存读取与版本展示。
|
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。
|
||||||
|
|
||||||
## `tests/`
|
## `tests/`
|
||||||
@@ -171,7 +172,7 @@
|
|||||||
- `tests/mail2925-utils.test.js`:测试 2925 账号池工具层的账号归一化、冷却期判断、可用账号挑选与批量导入解析。
|
- `tests/mail2925-utils.test.js`:测试 2925 账号池工具层的账号归一化、冷却期判断、可用账号挑选与批量导入解析。
|
||||||
- `tests/managed-alias-utils.test.js`:覆盖 Gmail / 2925 共享别名工具的解析、生成、兼容性判断。
|
- `tests/managed-alias-utils.test.js`:覆盖 Gmail / 2925 共享别名工具的解析、生成、兼容性判断。
|
||||||
- `tests/microsoft-email.test.js`:测试 Microsoft 邮件拉取与验证码提取逻辑。
|
- `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-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。
|
||||||
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏邮箱记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
|
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏邮箱记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
|
||||||
- `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。
|
- `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。
|
||||||
@@ -180,7 +181,7 @@
|
|||||||
- `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。
|
- `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。
|
||||||
- `tests/sidepanel-icloud-provider.test.js`:测试侧边栏 iCloud 登录地址解析逻辑。
|
- `tests/sidepanel-icloud-provider.test.js`:测试侧边栏 iCloud 登录地址解析逻辑。
|
||||||
- `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。
|
- `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-entry-diagnostics.test.js`:测试注册入口诊断快照输出。
|
||||||
- `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
|
- `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
|
||||||
- `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。
|
- `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。
|
||||||
|
|||||||
Reference in New Issue
Block a user