feat: 增强2925邮箱处理逻辑,优化账号切换和停止流程,更新相关测试用例
This commit is contained in:
@@ -8,3 +8,4 @@
|
||||
/data/account-run-history.json
|
||||
.npm-test.log
|
||||
.omx/
|
||||
/node_modules
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user