feat: 增强2925邮箱处理逻辑,优化账号切换和停止流程,更新相关测试用例
This commit is contained in:
@@ -8,3 +8,4 @@
|
|||||||
/data/account-run-history.json
|
/data/account-run-history.json
|
||||||
.npm-test.log
|
.npm-test.log
|
||||||
.omx/
|
.omx/
|
||||||
|
/node_modules
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
3. 在“2925 基邮箱”右侧打开 `号池` 开关后,基邮箱输入框会切成下拉框,只能从 2925 账号池中选择邮箱;关闭开关则回退到原来的手填基邮箱路线
|
3. 在“2925 基邮箱”右侧打开 `号池` 开关后,基邮箱输入框会切成下拉框,只能从 2925 账号池中选择邮箱;关闭开关则回退到原来的手填基邮箱路线
|
||||||
4. 可先点 `使用此账号` 让当前别名基邮箱切到这条 2925 账号,再点 `登录` 手动验证网页邮箱登录态
|
4. 可先点 `使用此账号` 让当前别名基邮箱切到这条 2925 账号,再点 `登录` 手动验证网页邮箱登录态
|
||||||
5. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会直接使用当前账号自动登录
|
5. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会直接使用当前账号自动登录
|
||||||
6. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间、把当前 2925 账号禁用 24 小时、自动切换到下一个可用账号并完成登录,然后直接结束当前尝试
|
6. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间;如果还有下一个可用账号,就禁用当前账号 24 小时并自动切换登录;如果没有下一个可用账号,或当前未启用号池模式,则会直接复用现有“手动暂停 / 停止”逻辑终止自动流程
|
||||||
7. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入
|
7. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入
|
||||||
8. `Mail = 2925` 仍然走 Gmail / 2925 共用的别名邮箱链路;实际注册邮箱会基于当前 2925 账号邮箱生成,例如 `name@2925.com -> name123456@2925.com`
|
8. `Mail = 2925` 仍然走 Gmail / 2925 共用的别名邮箱链路;实际注册邮箱会基于当前 2925 账号邮箱生成,例如 `name@2925.com -> name123456@2925.com`
|
||||||
|
|
||||||
|
|||||||
@@ -5392,6 +5392,7 @@ const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMa
|
|||||||
normalizeMail2925Account,
|
normalizeMail2925Account,
|
||||||
normalizeMail2925Accounts,
|
normalizeMail2925Accounts,
|
||||||
pickMail2925AccountForRun,
|
pickMail2925AccountForRun,
|
||||||
|
requestStop,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
sendToMailContentScriptResilient,
|
sendToMailContentScriptResilient,
|
||||||
setPersistentSettings,
|
setPersistentSettings,
|
||||||
@@ -5453,6 +5454,14 @@ function isMail2925ThreadTerminatedError(error) {
|
|||||||
return /^MAIL2925_THREAD_TERMINATED::/.test(message);
|
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({
|
const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGeneratedEmailHelpers({
|
||||||
addLog,
|
addLog,
|
||||||
buildGeneratedAliasEmail,
|
buildGeneratedAliasEmail,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
normalizeMail2925Accounts,
|
normalizeMail2925Accounts,
|
||||||
pickMail2925AccountForRun,
|
pickMail2925AccountForRun,
|
||||||
getState,
|
getState,
|
||||||
|
requestStop,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
sendToMailContentScriptResilient,
|
sendToMailContentScriptResilient,
|
||||||
setPersistentSettings,
|
setPersistentSettings,
|
||||||
@@ -403,7 +404,12 @@
|
|||||||
const state = await getState();
|
const state = await getState();
|
||||||
const currentAccount = getCurrentMail2925Account(state);
|
const currentAccount = getCurrentMail2925Account(state);
|
||||||
if (!currentAccount) {
|
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));
|
const disabledUntil = Date.now() + Math.max(1, Number(MAIL2925_LIMIT_COOLDOWN_MS) || (24 * 60 * 60 * 1000));
|
||||||
@@ -426,9 +432,12 @@
|
|||||||
if (!nextAccount) {
|
if (!nextAccount) {
|
||||||
await setState({ currentMail2925AccountId: null });
|
await setState({ currentMail2925AccountId: null });
|
||||||
broadcastDataUpdate({ currentMail2925AccountId: null });
|
broadcastDataUpdate({ currentMail2925AccountId: null });
|
||||||
return buildMail2925ThreadTerminatedError(
|
if (typeof requestStop === 'function') {
|
||||||
`步骤 ${step}:2925 账号 ${currentAccount.email} 已因“${reason}”禁用 24 小时,且当前没有可切换的下一个账号,本次尝试结束。`
|
await requestStop({
|
||||||
);
|
logMessage: `步骤 ${step}:2925 账号 ${currentAccount.email} 已因“${reason}”禁用 24 小时,且当前没有可切换的下一个账号,已按手动停止逻辑暂停流程。`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new Error('流程已被用户停止。');
|
||||||
}
|
}
|
||||||
|
|
||||||
await setCurrentMail2925Account(nextAccount.id);
|
await setCurrentMail2925Account(nextAccount.id);
|
||||||
|
|||||||
@@ -420,8 +420,7 @@
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) {
|
if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) {
|
||||||
const latestState = await getState();
|
if (typeof handleMail2925LimitReachedError === 'function') {
|
||||||
if (latestState?.mail2925UseAccountPool && typeof handleMail2925LimitReachedError === 'function') {
|
|
||||||
throw await handleMail2925LimitReachedError(step, err);
|
throw await handleMail2925LimitReachedError(step, err);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
@@ -564,8 +563,7 @@
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) {
|
if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) {
|
||||||
const latestState = await getState();
|
if (typeof handleMail2925LimitReachedError === 'function') {
|
||||||
if (latestState?.mail2925UseAccountPool && typeof handleMail2925LimitReachedError === 'function') {
|
|
||||||
throw await handleMail2925LimitReachedError(step, err);
|
throw await handleMail2925LimitReachedError(step, err);
|
||||||
}
|
}
|
||||||
throw 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() {
|
function getSignupEntryDiagnostics() {
|
||||||
const actionCandidates = document.querySelectorAll(
|
const actionCandidates = document.querySelectorAll(
|
||||||
'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
|
'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
|
||||||
@@ -400,14 +430,24 @@ async function waitForSignupEntryState(options = {}) {
|
|||||||
const {
|
const {
|
||||||
timeout = 15000,
|
timeout = 15000,
|
||||||
autoOpenEntry = false,
|
autoOpenEntry = false,
|
||||||
|
step = 2,
|
||||||
|
logDiagnostics = false,
|
||||||
} = options;
|
} = options;
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
let lastTriggerClickAt = 0;
|
let lastTriggerClickAt = 0;
|
||||||
|
let clickAttempts = 0;
|
||||||
|
let lastState = '';
|
||||||
|
let slowSnapshotLogged = false;
|
||||||
|
|
||||||
while (Date.now() - start < timeout) {
|
while (Date.now() - start < timeout) {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
const snapshot = inspectSignupEntryState();
|
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') {
|
if (snapshot.state === 'password_page' || snapshot.state === 'email_entry') {
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
@@ -419,16 +459,29 @@ async function waitForSignupEntryState(options = {}) {
|
|||||||
|
|
||||||
if (Date.now() - lastTriggerClickAt >= 1500) {
|
if (Date.now() - lastTriggerClickAt >= 1500) {
|
||||||
lastTriggerClickAt = Date.now();
|
lastTriggerClickAt = Date.now();
|
||||||
|
clickAttempts += 1;
|
||||||
|
if (logDiagnostics) {
|
||||||
|
log(`步骤 ${step}:正在点击官网注册入口(第 ${clickAttempts} 次):"${getActionText(snapshot.signupTrigger).slice(0, 80)}"`);
|
||||||
|
}
|
||||||
log('步骤 2:正在点击官网注册入口...');
|
log('步骤 2:正在点击官网注册入口...');
|
||||||
await humanPause(350, 900);
|
await humanPause(350, 900);
|
||||||
simulateClick(snapshot.signupTrigger);
|
simulateClick(snapshot.signupTrigger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (logDiagnostics && !slowSnapshotLogged && Date.now() - start >= 5000) {
|
||||||
|
slowSnapshotLogged = true;
|
||||||
|
log(`步骤 ${step}:等待注册入口超过 5 秒,页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn');
|
||||||
|
}
|
||||||
|
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
}
|
}
|
||||||
|
|
||||||
return inspectSignupEntryState();
|
const finalSnapshot = inspectSignupEntryState();
|
||||||
|
if (logDiagnostics) {
|
||||||
|
log(`步骤 ${step}:等待注册入口状态超时,最终状态快照:${JSON.stringify(getSignupEntryStateSummary(finalSnapshot))}`, 'warn');
|
||||||
|
}
|
||||||
|
return finalSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureSignupEntryReady(timeout = 15000) {
|
async function ensureSignupEntryReady(timeout = 15000) {
|
||||||
@@ -471,6 +524,8 @@ async function fillSignupEmailAndContinue(email, step) {
|
|||||||
const snapshot = await waitForSignupEntryState({
|
const snapshot = await waitForSignupEntryState({
|
||||||
timeout: 20000,
|
timeout: 20000,
|
||||||
autoOpenEntry: true,
|
autoOpenEntry: true,
|
||||||
|
step,
|
||||||
|
logDiagnostics: step === 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (snapshot.state === 'password_page') {
|
if (snapshot.state === 'password_page') {
|
||||||
@@ -485,6 +540,9 @@ async function fillSignupEmailAndContinue(email, step) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.state !== 'email_entry' || !snapshot.emailInput) {
|
if (snapshot.state !== 'email_entry' || !snapshot.emailInput) {
|
||||||
|
if (step === 2) {
|
||||||
|
log(`步骤 ${step}:未进入邮箱输入页,最终页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn');
|
||||||
|
}
|
||||||
throw new Error(`步骤 ${step}:未找到可用的邮箱输入入口。URL: ${location.href}`);
|
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.equal(currentAccount.lastError, '子邮箱已达上限邮箱');
|
||||||
assert.ok(currentAccount.disabledUntil > Date.now());
|
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`
|
1. 用户在 sidepanel 的 2925 账号池中保存 `email / password`
|
||||||
2. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
|
2. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
|
||||||
3. 手动点击 `登录` 或自动流程进入 Step 4 / Step 8 前,只有在号池模式开启时,后台才会确保当前 2925 账号已登录网页邮箱
|
3. 手动点击 `登录` 或自动流程进入 Step 4 / Step 8 前,只有在号池模式开启时,后台才会确保当前 2925 账号已登录网页邮箱
|
||||||
4. 一旦轮询期间出现“子邮箱已达上限邮箱”,且当前处于号池模式,后台会记录当前时间、把当前账号禁用 24 小时、自动切到下一个账号并重新登录
|
4. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先记录当前时间;若当前处于号池模式且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录
|
||||||
5. 切号完成后,后台直接抛出“结束当前尝试”错误;如果 Auto 开启了自动重试,现有控制器会自动进入下一次尝试
|
5. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态
|
||||||
|
|
||||||
### 7.5 iCloud
|
### 7.5 iCloud
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user