feat: 增强2925邮箱处理逻辑,优化账号切换和停止流程,更新相关测试用例

This commit is contained in:
QLHazyCoder
2026-04-20 21:11:08 +08:00
parent 6783cf02b4
commit 20146ce3e9
8 changed files with 154 additions and 12 deletions
+1
View File
@@ -8,3 +8,4 @@
/data/account-run-history.json
.npm-test.log
.omx/
/node_modules
+1 -1
View File
@@ -146,7 +146,7 @@
3. 在“2925 基邮箱”右侧打开 `号池` 开关后,基邮箱输入框会切成下拉框,只能从 2925 账号池中选择邮箱;关闭开关则回退到原来的手填基邮箱路线
4. 可先点 `使用此账号` 让当前别名基邮箱切到这条 2925 账号,再点 `登录` 手动验证网页邮箱登录态
5. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会直接使用当前账号自动登录
6. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间、把当前 2925 账号禁用 24 小时自动切换到下一个可用账号并完成登录,然后直接结束当前尝试
6. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间;如果还有下一个可用账号,就禁用当前账号 24 小时自动切换登录;如果没有下一个可用账号,或当前未启用号池模式,则会直接复用现有“手动暂停 / 停止”逻辑终止自动流程
7. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入
8. `Mail = 2925` 仍然走 Gmail / 2925 共用的别名邮箱链路;实际注册邮箱会基于当前 2925 账号邮箱生成,例如 `name@2925.com -> name123456@2925.com`
+9
View File
@@ -5392,6 +5392,7 @@ const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMa
normalizeMail2925Account,
normalizeMail2925Accounts,
pickMail2925AccountForRun,
requestStop,
reuseOrCreateTab,
sendToMailContentScriptResilient,
setPersistentSettings,
@@ -5453,6 +5454,14 @@ function isMail2925ThreadTerminatedError(error) {
return /^MAIL2925_THREAD_TERMINATED::/.test(message);
}
function isMail2925PoolExhaustedPauseError(error) {
if (typeof mail2925SessionManager !== 'undefined' && mail2925SessionManager?.isMail2925PoolExhaustedPauseError) {
return mail2925SessionManager.isMail2925PoolExhaustedPauseError(error);
}
const message = String(typeof error === 'string' ? error : error?.message || '');
return /^MAIL2925_POOL_EXHAUSTED_PAUSE::/.test(message);
}
const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGeneratedEmailHelpers({
addLog,
buildGeneratedAliasEmail,
+13 -4
View File
@@ -14,6 +14,7 @@
normalizeMail2925Accounts,
pickMail2925AccountForRun,
getState,
requestStop,
reuseOrCreateTab,
sendToMailContentScriptResilient,
setPersistentSettings,
@@ -403,7 +404,12 @@
const state = await getState();
const currentAccount = getCurrentMail2925Account(state);
if (!currentAccount) {
return buildMail2925ThreadTerminatedError(`步骤 ${step}2925 检测到“${reason}”,但当前没有可识别的账号,已结束本次尝试。`);
if (typeof requestStop === 'function') {
await requestStop({
logMessage: `步骤 ${step}2925 检测到“${reason}”,且当前没有可识别账号,已按手动停止逻辑暂停流程。`,
});
}
return new Error('流程已被用户停止。');
}
const disabledUntil = Date.now() + Math.max(1, Number(MAIL2925_LIMIT_COOLDOWN_MS) || (24 * 60 * 60 * 1000));
@@ -426,9 +432,12 @@
if (!nextAccount) {
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
return buildMail2925ThreadTerminatedError(
`步骤 ${step}2925 账号 ${currentAccount.email} 已因“${reason}”禁用 24 小时,且当前没有可切换的下一个账号,本次尝试结束。`
);
if (typeof requestStop === 'function') {
await requestStop({
logMessage: `步骤 ${step}2925 账号 ${currentAccount.email} 已因“${reason}”禁用 24 小时,且当前没有可切换的下一个账号,已按手动停止逻辑暂停流程。`,
});
}
return new Error('流程已被用户停止。');
}
await setCurrentMail2925Account(nextAccount.id);
+2 -4
View File
@@ -420,8 +420,7 @@
throw err;
}
if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) {
const latestState = await getState();
if (latestState?.mail2925UseAccountPool && typeof handleMail2925LimitReachedError === 'function') {
if (typeof handleMail2925LimitReachedError === 'function') {
throw await handleMail2925LimitReachedError(step, err);
}
throw err;
@@ -564,8 +563,7 @@
throw err;
}
if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) {
const latestState = await getState();
if (latestState?.mail2925UseAccountPool && typeof handleMail2925LimitReachedError === 'function') {
if (typeof handleMail2925LimitReachedError === 'function') {
throw await handleMail2925LimitReachedError(step, err);
}
throw err;
+59 -1
View File
@@ -346,6 +346,36 @@ function inspectSignupEntryState() {
};
}
function getSignupEntryStateSummary(snapshot = inspectSignupEntryState()) {
const summary = {
state: snapshot?.state || 'unknown',
url: snapshot?.url || location.href,
hasEmailInput: Boolean(snapshot?.emailInput || getSignupEmailInput()),
hasPasswordInput: Boolean(snapshot?.passwordInput || getSignupPasswordInput()),
};
if (snapshot?.displayedEmail) {
summary.displayedEmail = snapshot.displayedEmail;
}
if (snapshot?.signupTrigger) {
summary.signupTrigger = {
tag: (snapshot.signupTrigger.tagName || '').toLowerCase(),
text: getActionText(snapshot.signupTrigger).slice(0, 80),
};
}
if (snapshot?.continueButton) {
summary.continueButton = {
tag: (snapshot.continueButton.tagName || '').toLowerCase(),
text: getActionText(snapshot.continueButton).slice(0, 80),
enabled: isActionEnabled(snapshot.continueButton),
};
}
return summary;
}
function getSignupEntryDiagnostics() {
const actionCandidates = document.querySelectorAll(
'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
@@ -400,14 +430,24 @@ async function waitForSignupEntryState(options = {}) {
const {
timeout = 15000,
autoOpenEntry = false,
step = 2,
logDiagnostics = false,
} = options;
const start = Date.now();
let lastTriggerClickAt = 0;
let clickAttempts = 0;
let lastState = '';
let slowSnapshotLogged = false;
while (Date.now() - start < timeout) {
throwIfStopped();
const snapshot = inspectSignupEntryState();
if (logDiagnostics && snapshot.state !== lastState) {
lastState = snapshot.state;
log(`步骤 ${step}:注册入口状态切换为 ${snapshot.state},状态快照:${JSON.stringify(getSignupEntryStateSummary(snapshot))}`);
}
if (snapshot.state === 'password_page' || snapshot.state === 'email_entry') {
return snapshot;
}
@@ -419,16 +459,29 @@ async function waitForSignupEntryState(options = {}) {
if (Date.now() - lastTriggerClickAt >= 1500) {
lastTriggerClickAt = Date.now();
clickAttempts += 1;
if (logDiagnostics) {
log(`步骤 ${step}:正在点击官网注册入口(第 ${clickAttempts} 次):"${getActionText(snapshot.signupTrigger).slice(0, 80)}"`);
}
log('步骤 2:正在点击官网注册入口...');
await humanPause(350, 900);
simulateClick(snapshot.signupTrigger);
}
}
if (logDiagnostics && !slowSnapshotLogged && Date.now() - start >= 5000) {
slowSnapshotLogged = true;
log(`步骤 ${step}:等待注册入口超过 5 秒,页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn');
}
await sleep(250);
}
return inspectSignupEntryState();
const finalSnapshot = inspectSignupEntryState();
if (logDiagnostics) {
log(`步骤 ${step}:等待注册入口状态超时,最终状态快照:${JSON.stringify(getSignupEntryStateSummary(finalSnapshot))}`, 'warn');
}
return finalSnapshot;
}
async function ensureSignupEntryReady(timeout = 15000) {
@@ -471,6 +524,8 @@ async function fillSignupEmailAndContinue(email, step) {
const snapshot = await waitForSignupEntryState({
timeout: 20000,
autoOpenEntry: true,
step,
logDiagnostics: step === 2,
});
if (snapshot.state === 'password_page') {
@@ -485,6 +540,9 @@ async function fillSignupEmailAndContinue(email, step) {
}
if (snapshot.state !== 'email_entry' || !snapshot.emailInput) {
if (step === 2) {
log(`步骤 ${step}:未进入邮箱输入页,最终页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn');
}
throw new Error(`步骤 ${step}:未找到可用的邮箱输入入口。URL: ${location.href}`);
}
@@ -92,3 +92,70 @@ test('handleMail2925LimitReachedError disables current account and switches to t
assert.equal(currentAccount.lastError, '子邮箱已达上限邮箱');
assert.ok(currentAccount.disabledUntil > Date.now());
});
test('handleMail2925LimitReachedError requests stop when no next mail2925 account is available', async () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
let currentState = {
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'only', email: 'only@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'only',
};
const events = {
stopCalls: [],
};
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
requestStop: async (options = {}) => {
events.stopCalls.push(options);
},
reuseOrCreateTab: async () => 1,
sendToMailContentScriptResilient: async () => ({ loggedIn: true }),
setPersistentSettings: async (payload) => {
currentState = {
...currentState,
...payload,
};
},
setState: async (updates) => {
currentState = {
...currentState,
...updates,
};
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
const error = await manager.handleMail2925LimitReachedError(
4,
new Error('MAIL2925_LIMIT_REACHED::子邮箱已达上限邮箱')
);
assert.equal(error.message, '流程已被用户停止。');
assert.equal(currentState.currentMail2925AccountId, null);
assert.equal(events.stopCalls.length, 1);
assert.match(events.stopCalls[0].logMessage, /没有可切换的下一个账号/);
});
+2 -2
View File
@@ -527,8 +527,8 @@
1. 用户在 sidepanel 的 2925 账号池中保存 `email / password`
2. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
3. 手动点击 `登录` 或自动流程进入 Step 4 / Step 8 前,只有在号池模式开启时,后台才会确保当前 2925 账号已登录网页邮箱
4. 一旦轮询期间出现“子邮箱已达上限邮箱”,且当前处于号池模式,后台会记录当前时间、把当前账号禁用 24 小时自动切到下一个账号重新登录
5. 切号完成后,后台直接抛出“结束当前尝试”错误;如果 Auto 开启了自动重试,现有控制器会自动进入下一次尝试
4. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先记录当前时间;若当前处于号池模式且还有下一个可用账号,则把当前账号禁用 24 小时自动切到下一个账号重新登录
5. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态
### 7.5 iCloud