feat: 优化2925账号池表单,修复步骤7无法识别重试页面bug
This commit is contained in:
@@ -579,9 +579,11 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
|
||||
- 已刷新到最新 OAuth 链接
|
||||
- 认证页已经真正进入登录验证码页面
|
||||
- 在真正把 Step 7 记为完成前,还会再做一轮收尾确认;如果页面只是短暂进入登录验证码页、随后又掉进登录重试页,则不会直接进入 Step 8,而是先按共享恢复逻辑处理并重跑 Step 7
|
||||
- 如遇登录超时报错,会先尝试通过共享恢复逻辑最多自动点击 5 次认证页上的 `重试` 恢复当前页面;若仍未恢复,再按既有逻辑重跑整个 Step 7
|
||||
- 如遇登录页长时间停滞,会由后台刷新 OAuth 后重跑整个 Step 7
|
||||
- 如果重试页内容中出现 `max_check_attempts`,会立刻完全停止流程,并在侧边栏复用现有确认弹窗提示这是 Cloudflare 风控拦截,确认按钮显示为“我知道了”
|
||||
- Step 8 不负责替代 Step 7 的收尾确认;它默认消费的是“已经由 Step 7 确认稳定进入”的登录验证码页,只在后台入口做防御性状态兜底
|
||||
|
||||
支持:
|
||||
|
||||
@@ -598,7 +600,7 @@ Step 8 默认要求当前认证页已经处于登录验证码页。
|
||||
- 打开邮箱并轮询登录验证码
|
||||
- 填写并提交登录验证码
|
||||
- 验证码链路失败后按有限次数回退到 Step 7
|
||||
- 如果进入登录超时报错/重试页,会直接报错并回到 Step 7,不会在 Step 8 内部点击 `重试`
|
||||
- 如果进入登录超时报错/重试页,包括 `/email-verification` 上的 `405 / Route Error` 登录重试页,会直接报错并回到 Step 7,不会在 Step 8 内部点击 `重试`
|
||||
- 如果重试页内容中出现 `max_check_attempts`,会直接完全停止整个流程,并复用现有确认弹窗提醒先等待 15 到 30 分钟或更换浏览器,确认按钮显示为“我知道了”
|
||||
|
||||
与 Step 4 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。
|
||||
|
||||
+112
-15
@@ -1198,7 +1198,7 @@ function getSignupPasswordTimeoutErrorPageState() {
|
||||
|
||||
function getLoginTimeoutErrorPageState() {
|
||||
return getAuthTimeoutErrorPageState({
|
||||
pathPatterns: [/\/log-in(?:[/?#]|$)/i],
|
||||
pathPatterns: getLoginAuthRetryPathPatterns(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1263,13 +1263,6 @@ function inspectLoginAuthState() {
|
||||
consentReady,
|
||||
};
|
||||
|
||||
if (verificationTarget) {
|
||||
return {
|
||||
...baseState,
|
||||
state: 'verification_page',
|
||||
};
|
||||
}
|
||||
|
||||
if (retryState) {
|
||||
return {
|
||||
...baseState,
|
||||
@@ -1277,6 +1270,13 @@ function inspectLoginAuthState() {
|
||||
};
|
||||
}
|
||||
|
||||
if (verificationTarget) {
|
||||
return {
|
||||
...baseState,
|
||||
state: 'verification_page',
|
||||
};
|
||||
}
|
||||
|
||||
if (addPhonePage) {
|
||||
return {
|
||||
...baseState,
|
||||
@@ -1434,6 +1434,86 @@ async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, messag
|
||||
});
|
||||
}
|
||||
|
||||
async function finalizeStep6VerificationReady(options = {}) {
|
||||
const {
|
||||
logLabel = '步骤 7 收尾',
|
||||
loginVerificationRequestedAt = null,
|
||||
timeout = 12000,
|
||||
via = 'verification_page_ready',
|
||||
} = options;
|
||||
const start = Date.now();
|
||||
const maxRounds = 3;
|
||||
const settleDelayMs = 3000;
|
||||
let round = 0;
|
||||
|
||||
while (Date.now() - start < timeout && round < maxRounds) {
|
||||
throwIfStopped();
|
||||
round += 1;
|
||||
log(`${logLabel}:确认页面是否稳定停留在登录验证码阶段(第 ${round}/${maxRounds} 轮,先等待 3 秒)...`, 'info');
|
||||
await sleep(settleDelayMs);
|
||||
|
||||
const rawSnapshot = inspectLoginAuthState();
|
||||
const snapshot = normalizeStep6Snapshot(rawSnapshot);
|
||||
|
||||
if (snapshot.state === 'verification_page') {
|
||||
log(`${logLabel}:登录验证码页面已稳定就绪。`, 'ok');
|
||||
return createStep6SuccessResult(snapshot, {
|
||||
via,
|
||||
loginVerificationRequestedAt,
|
||||
});
|
||||
}
|
||||
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn');
|
||||
return createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'登录验证码页面准备就绪前进入登录超时报错页。'
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.state === 'password_page' || snapshot.state === 'email_page') {
|
||||
return createStep6RecoverableResult('verification_page_unstable', snapshot, {
|
||||
message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 7。`,
|
||||
loginVerificationRequestedAt,
|
||||
});
|
||||
}
|
||||
|
||||
if (snapshot.state === 'add_phone_page') {
|
||||
throw new Error(`登录验证码页面准备过程中页面进入手机号页面。URL: ${snapshot.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
const rawSnapshot = inspectLoginAuthState();
|
||||
const snapshot = normalizeStep6Snapshot(rawSnapshot);
|
||||
if (snapshot.state === 'verification_page') {
|
||||
log(`${logLabel}:登录验证码页面已稳定就绪。`, 'ok');
|
||||
return createStep6SuccessResult(snapshot, {
|
||||
via,
|
||||
loginVerificationRequestedAt,
|
||||
});
|
||||
}
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn');
|
||||
return createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'登录验证码页面准备就绪前进入登录超时报错页。'
|
||||
);
|
||||
}
|
||||
if (snapshot.state === 'password_page' || snapshot.state === 'email_page') {
|
||||
return createStep6RecoverableResult('verification_page_unstable', snapshot, {
|
||||
message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 7。`,
|
||||
loginVerificationRequestedAt,
|
||||
});
|
||||
}
|
||||
|
||||
return createStep6RecoverableResult('verification_page_finalize_unknown', snapshot, {
|
||||
message: '登录验证码页面状态在收尾确认阶段未稳定,准备重新执行步骤 7。',
|
||||
loginVerificationRequestedAt,
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeStep6Snapshot(snapshot) {
|
||||
if (snapshot?.state !== 'oauth_consent_page') {
|
||||
return snapshot;
|
||||
@@ -1999,7 +2079,15 @@ async function step6SwitchToOneTimeCodeLogin(snapshot) {
|
||||
simulateClick(switchTrigger);
|
||||
log('步骤 7:已点击一次性验证码登录');
|
||||
await sleep(1200);
|
||||
return waitForStep6SwitchTransition(loginVerificationRequestedAt);
|
||||
const result = await waitForStep6SwitchTransition(loginVerificationRequestedAt);
|
||||
if (result?.step6Outcome === 'success') {
|
||||
return finalizeStep6VerificationReady({
|
||||
logLabel: '步骤 7 收尾',
|
||||
loginVerificationRequestedAt: result.loginVerificationRequestedAt || loginVerificationRequestedAt,
|
||||
via: result.via || 'switch_to_one_time_code_login',
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function step6LoginFromPasswordPage(payload, snapshot) {
|
||||
@@ -2030,8 +2118,11 @@ async function step6LoginFromPasswordPage(payload, snapshot) {
|
||||
|
||||
const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt);
|
||||
if (transition.action === 'done') {
|
||||
log('步骤 7:已进入登录验证码页面。', 'ok');
|
||||
return transition.result;
|
||||
return finalizeStep6VerificationReady({
|
||||
logLabel: '步骤 7 收尾',
|
||||
loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || passwordSubmittedAt,
|
||||
via: transition.result.via || 'password_submit',
|
||||
});
|
||||
}
|
||||
if (transition.action === 'recoverable') {
|
||||
log(`步骤 7:${transition.result.message || '提交密码后仍未进入登录验证码页面,准备重新执行步骤 7。'}`, 'warn');
|
||||
@@ -2077,8 +2168,11 @@ async function step6LoginFromEmailPage(payload, snapshot) {
|
||||
|
||||
const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt);
|
||||
if (transition.action === 'done') {
|
||||
log('步骤 7:已进入登录验证码页面。', 'ok');
|
||||
return transition.result;
|
||||
return finalizeStep6VerificationReady({
|
||||
logLabel: '步骤 7 收尾',
|
||||
loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || emailSubmittedAt,
|
||||
via: transition.result.via || 'email_submit',
|
||||
});
|
||||
}
|
||||
if (transition.action === 'recoverable') {
|
||||
log(`步骤 7:${transition.result.message || '提交邮箱后仍未进入目标页面,准备重新执行步骤 7。'}`, 'warn');
|
||||
@@ -2102,8 +2196,11 @@ async function step6_login(payload) {
|
||||
const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000));
|
||||
|
||||
if (snapshot.state === 'verification_page') {
|
||||
log('步骤 7:登录验证码页面已就绪。', 'ok');
|
||||
return createStep6SuccessResult(snapshot, { via: 'already_on_verification_page' });
|
||||
return finalizeStep6VerificationReady({
|
||||
logLabel: '步骤 7 收尾',
|
||||
loginVerificationRequestedAt: null,
|
||||
via: 'already_on_verification_page',
|
||||
});
|
||||
}
|
||||
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
let actionInFlight = false;
|
||||
let listExpanded = false;
|
||||
let editingAccountId = '';
|
||||
let formVisible = false;
|
||||
|
||||
function getMail2925Accounts(currentState = state.getLatestState()) {
|
||||
return helpers.getMail2925Accounts(currentState);
|
||||
@@ -118,13 +119,39 @@
|
||||
if (dom.inputMail2925Password) dom.inputMail2925Password.value = '';
|
||||
}
|
||||
|
||||
function syncMail2925FormUi() {
|
||||
if (dom.mail2925FormShell) {
|
||||
dom.mail2925FormShell.hidden = !formVisible;
|
||||
}
|
||||
if (dom.btnToggleMail2925Form) {
|
||||
dom.btnToggleMail2925Form.textContent = formVisible ? '取消添加' : '添加账号';
|
||||
dom.btnToggleMail2925Form.setAttribute('aria-expanded', String(formVisible));
|
||||
}
|
||||
}
|
||||
|
||||
function setMail2925FormVisible(visible, options = {}) {
|
||||
const {
|
||||
clearForm = false,
|
||||
focusField = false,
|
||||
} = options;
|
||||
|
||||
formVisible = Boolean(visible);
|
||||
if (!formVisible && clearForm) {
|
||||
stopEditingAccount({ clearForm: true });
|
||||
} else if (clearForm) {
|
||||
clearMail2925Form();
|
||||
}
|
||||
|
||||
syncMail2925FormUi();
|
||||
if (formVisible && focusField) {
|
||||
dom.inputMail2925Email?.focus?.();
|
||||
}
|
||||
}
|
||||
|
||||
function syncEditUi() {
|
||||
if (dom.btnAddMail2925Account) {
|
||||
dom.btnAddMail2925Account.textContent = editingAccountId ? '保存修改' : '添加账号';
|
||||
}
|
||||
if (dom.btnCancelMail2925Edit) {
|
||||
dom.btnCancelMail2925Edit.style.display = editingAccountId ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function startEditingAccount(account) {
|
||||
@@ -132,6 +159,7 @@
|
||||
editingAccountId = account.id;
|
||||
if (dom.inputMail2925Email) dom.inputMail2925Email.value = String(account.email || '').trim();
|
||||
if (dom.inputMail2925Password) dom.inputMail2925Password.value = String(account.password || '');
|
||||
setMail2925FormVisible(true, { focusField: false });
|
||||
syncEditUi();
|
||||
}
|
||||
|
||||
@@ -234,7 +262,7 @@
|
||||
}
|
||||
|
||||
applyMail2925AccountMutation(response.account);
|
||||
stopEditingAccount();
|
||||
setMail2925FormVisible(false, { clearForm: true });
|
||||
helpers.showToast(
|
||||
updatingExisting
|
||||
? `已更新 2925 账号 ${email}`
|
||||
@@ -332,7 +360,7 @@
|
||||
mail2925Accounts: [],
|
||||
currentMail2925AccountId: null,
|
||||
});
|
||||
stopEditingAccount();
|
||||
setMail2925FormVisible(false, { clearForm: true });
|
||||
refreshManagedAliasBaseEmail();
|
||||
renderMail2925Accounts();
|
||||
helpers.showToast(`已删除全部 ${response.deletedCount || 0} 个 2925 账号`, 'success', 2200);
|
||||
@@ -460,7 +488,7 @@
|
||||
}
|
||||
state.syncLatestState(nextState);
|
||||
if (editingAccountId === accountId) {
|
||||
stopEditingAccount();
|
||||
setMail2925FormVisible(false, { clearForm: true });
|
||||
}
|
||||
refreshManagedAliasBaseEmail();
|
||||
renderMail2925Accounts();
|
||||
@@ -479,6 +507,14 @@
|
||||
setMail2925ListExpanded(!listExpanded);
|
||||
});
|
||||
|
||||
dom.btnToggleMail2925Form?.addEventListener('click', () => {
|
||||
if (formVisible) {
|
||||
setMail2925FormVisible(false, { clearForm: true });
|
||||
return;
|
||||
}
|
||||
setMail2925FormVisible(true, { clearForm: !editingAccountId, focusField: true });
|
||||
});
|
||||
|
||||
dom.btnDeleteAllMail2925Accounts?.addEventListener('click', async () => {
|
||||
if (actionInFlight) return;
|
||||
actionInFlight = true;
|
||||
@@ -493,12 +529,10 @@
|
||||
});
|
||||
|
||||
dom.btnAddMail2925Account?.addEventListener('click', handleAddMail2925Account);
|
||||
dom.btnCancelMail2925Edit?.addEventListener('click', () => {
|
||||
stopEditingAccount();
|
||||
});
|
||||
dom.btnImportMail2925Accounts?.addEventListener('click', handleImportMail2925Accounts);
|
||||
dom.mail2925AccountsList?.addEventListener('click', handleAccountListClick);
|
||||
syncEditUi();
|
||||
syncMail2925FormUi();
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1112,6 +1112,20 @@ header {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.mail2925-form-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mail2925-actions-inline {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mail2925-import-action {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.hotmail-import-row {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
+22
-20
@@ -450,31 +450,33 @@
|
||||
<span class="section-label">2925 账号池</span>
|
||||
</div>
|
||||
<div class="section-mini-actions">
|
||||
<button id="btn-toggle-mail2925-form" class="btn btn-outline btn-xs" type="button" aria-expanded="false">添加账号</button>
|
||||
<button id="btn-delete-all-mail2925-accounts" class="btn btn-ghost btn-xs" type="button">全部删除</button>
|
||||
<button id="btn-toggle-mail2925-list" class="btn btn-ghost btn-xs" type="button" aria-expanded="false">展开列表</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">邮箱</span>
|
||||
<input type="text" id="input-mail2925-email" class="data-input" placeholder="name@2925.com" />
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">密码</span>
|
||||
<input type="password" id="input-mail2925-password" class="data-input" placeholder="2925 登录密码" />
|
||||
</div>
|
||||
<div class="data-row hotmail-actions-row">
|
||||
<span class="data-label"></span>
|
||||
<div class="data-inline">
|
||||
<button id="btn-add-mail2925-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
|
||||
<button id="btn-cancel-mail2925-edit" class="btn btn-outline btn-sm" type="button" style="display:none;">取消编辑</button>
|
||||
<div id="mail2925-form-shell" class="mail2925-form-shell" hidden>
|
||||
<div class="data-row">
|
||||
<span class="data-label">邮箱</span>
|
||||
<input type="text" id="input-mail2925-email" class="data-input" placeholder="name@2925.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row hotmail-import-row">
|
||||
<span class="data-label">批量导入</span>
|
||||
<div class="hotmail-import-box">
|
||||
<textarea id="input-mail2925-import" class="data-textarea mono"
|
||||
placeholder="邮箱----密码 name@2925.com----password"></textarea>
|
||||
<button id="btn-import-mail2925-accounts" class="btn btn-outline btn-sm" type="button">导入</button>
|
||||
<div class="data-row">
|
||||
<span class="data-label">密码</span>
|
||||
<input type="password" id="input-mail2925-password" class="data-input" placeholder="2925 登录密码" />
|
||||
</div>
|
||||
<div class="data-row hotmail-actions-row">
|
||||
<span class="data-label"></span>
|
||||
<div class="data-inline mail2925-actions-inline">
|
||||
<button id="btn-add-mail2925-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
|
||||
<button id="btn-import-mail2925-accounts" class="btn btn-outline btn-sm mail2925-import-action" type="button">批量导入</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row hotmail-import-row">
|
||||
<span class="data-label">批量导入</span>
|
||||
<div class="hotmail-import-box">
|
||||
<textarea id="input-mail2925-import" class="data-textarea mono"
|
||||
placeholder="邮箱----密码 name@2925.com----password"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mail2925-list-shell" class="hotmail-list-shell is-collapsed">
|
||||
|
||||
@@ -158,10 +158,11 @@ const inputMail2925Email = document.getElementById('input-mail2925-email');
|
||||
const inputMail2925Password = document.getElementById('input-mail2925-password');
|
||||
const inputMail2925Import = document.getElementById('input-mail2925-import');
|
||||
const btnAddMail2925Account = document.getElementById('btn-add-mail2925-account');
|
||||
const btnCancelMail2925Edit = document.getElementById('btn-cancel-mail2925-edit');
|
||||
const btnToggleMail2925Form = document.getElementById('btn-toggle-mail2925-form');
|
||||
const btnImportMail2925Accounts = document.getElementById('btn-import-mail2925-accounts');
|
||||
const btnDeleteAllMail2925Accounts = document.getElementById('btn-delete-all-mail2925-accounts');
|
||||
const btnToggleMail2925List = document.getElementById('btn-toggle-mail2925-list');
|
||||
const mail2925FormShell = document.getElementById('mail2925-form-shell');
|
||||
const mail2925ListShell = document.getElementById('mail2925-list-shell');
|
||||
const mail2925AccountsList = document.getElementById('mail2925-accounts-list');
|
||||
const inputLuckmailApiKey = document.getElementById('input-luckmail-api-key');
|
||||
@@ -3136,14 +3137,15 @@ const mail2925Manager = window.SidepanelMail2925Manager?.createMail2925Manager({
|
||||
},
|
||||
dom: {
|
||||
btnAddMail2925Account,
|
||||
btnCancelMail2925Edit,
|
||||
btnDeleteAllMail2925Accounts,
|
||||
btnImportMail2925Accounts,
|
||||
btnToggleMail2925Form,
|
||||
btnToggleMail2925List,
|
||||
inputMail2925Email,
|
||||
inputMail2925Import,
|
||||
inputMail2925Password,
|
||||
mail2925AccountsList,
|
||||
mail2925FormShell,
|
||||
mail2925ListShell,
|
||||
},
|
||||
helpers: {
|
||||
|
||||
@@ -2,9 +2,11 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('sidepanel html contains cancel edit button for mail2925 form', () => {
|
||||
test('sidepanel html contains collapsible mail2925 form controls', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
assert.match(html, /id="btn-cancel-mail2925-edit"/);
|
||||
assert.match(html, /id="btn-toggle-mail2925-form"/);
|
||||
assert.match(html, /id="mail2925-form-shell"/);
|
||||
assert.doesNotMatch(html, /id="btn-cancel-mail2925-edit"/);
|
||||
});
|
||||
|
||||
test('mail2925 manager renders edit action for existing accounts', () => {
|
||||
@@ -43,14 +45,15 @@ test('mail2925 manager renders edit action for existing accounts', () => {
|
||||
},
|
||||
dom: {
|
||||
btnAddMail2925Account: { textContent: '', disabled: false, addEventListener() {} },
|
||||
btnCancelMail2925Edit: { style: { display: 'none' }, addEventListener() {} },
|
||||
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
|
||||
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
|
||||
btnToggleMail2925Form: { textContent: '', setAttribute() {}, addEventListener() {} },
|
||||
btnToggleMail2925List: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
|
||||
inputMail2925Email: { value: '' },
|
||||
inputMail2925Import: { value: '' },
|
||||
inputMail2925Password: { value: '' },
|
||||
mail2925AccountsList,
|
||||
mail2925FormShell: { hidden: true },
|
||||
mail2925ListShell: { classList: { toggle() {} } },
|
||||
},
|
||||
helpers: {
|
||||
|
||||
@@ -17,7 +17,9 @@ test('sidepanel html contains mail2925 pool toggle and selector controls', () =>
|
||||
|
||||
assert.match(html, /id="input-mail2925-use-account-pool"/);
|
||||
assert.match(html, /id="select-mail2925-pool-account"/);
|
||||
assert.match(html, /id="btn-cancel-mail2925-edit"/);
|
||||
assert.match(html, /id="btn-toggle-mail2925-form"/);
|
||||
assert.match(html, /id="mail2925-form-shell"/);
|
||||
assert.doesNotMatch(html, /id="btn-cancel-mail2925-edit"/);
|
||||
});
|
||||
|
||||
test('mail2925 manager exposes a factory and renders empty state', () => {
|
||||
@@ -38,6 +40,12 @@ test('mail2925 manager exposes a factory and renders empty state', () => {
|
||||
assert.equal(typeof api?.createMail2925Manager, 'function');
|
||||
|
||||
const mail2925AccountsList = { innerHTML: '', addEventListener() {} };
|
||||
const formToggleButton = {
|
||||
textContent: '',
|
||||
disabled: false,
|
||||
setAttribute() {},
|
||||
addEventListener() {},
|
||||
};
|
||||
const toggleButton = {
|
||||
textContent: '',
|
||||
disabled: false,
|
||||
@@ -55,11 +63,13 @@ test('mail2925 manager exposes a factory and renders empty state', () => {
|
||||
btnAddMail2925Account: { disabled: false, addEventListener() {} },
|
||||
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
|
||||
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
|
||||
btnToggleMail2925Form: formToggleButton,
|
||||
btnToggleMail2925List: toggleButton,
|
||||
inputMail2925Email: { value: '' },
|
||||
inputMail2925Import: { value: '' },
|
||||
inputMail2925Password: { value: '' },
|
||||
mail2925AccountsList,
|
||||
mail2925FormShell: { hidden: true },
|
||||
mail2925ListShell: { classList: noopClassList },
|
||||
},
|
||||
helpers: {
|
||||
@@ -88,3 +98,188 @@ test('mail2925 manager exposes a factory and renders empty state', () => {
|
||||
manager.renderMail2925Accounts();
|
||||
assert.match(mail2925AccountsList.innerHTML, /还没有 2925 账号/);
|
||||
});
|
||||
|
||||
test('mail2925 manager toggles form container from header button', () => {
|
||||
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
|
||||
const windowObject = {};
|
||||
const localStorageMock = {
|
||||
getItem() {
|
||||
return null;
|
||||
},
|
||||
setItem() {},
|
||||
};
|
||||
|
||||
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelMail2925Manager;`)(
|
||||
windowObject,
|
||||
localStorageMock
|
||||
);
|
||||
|
||||
const clickHandlers = {};
|
||||
const formToggleButton = {
|
||||
textContent: '',
|
||||
disabled: false,
|
||||
setAttribute(name, value) {
|
||||
this[name] = value;
|
||||
},
|
||||
addEventListener(type, handler) {
|
||||
clickHandlers[type] = handler;
|
||||
},
|
||||
};
|
||||
const formShell = { hidden: true };
|
||||
|
||||
const manager = api.createMail2925Manager({
|
||||
state: {
|
||||
getLatestState: () => ({ currentMail2925AccountId: null, mail2925Accounts: [] }),
|
||||
syncLatestState() {},
|
||||
},
|
||||
dom: {
|
||||
btnAddMail2925Account: { textContent: '', disabled: false, addEventListener() {} },
|
||||
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
|
||||
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
|
||||
btnToggleMail2925Form: formToggleButton,
|
||||
btnToggleMail2925List: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
|
||||
inputMail2925Email: { value: '', focus() { this.focused = true; } },
|
||||
inputMail2925Import: { value: '' },
|
||||
inputMail2925Password: { value: '' },
|
||||
mail2925AccountsList: { innerHTML: '', addEventListener() {} },
|
||||
mail2925FormShell: formShell,
|
||||
mail2925ListShell: { classList: { toggle() {} } },
|
||||
},
|
||||
helpers: {
|
||||
getMail2925Accounts: () => [],
|
||||
escapeHtml: (value) => String(value || ''),
|
||||
showToast() {},
|
||||
openConfirmModal: async () => true,
|
||||
copyTextToClipboard: async () => {},
|
||||
refreshManagedAliasBaseEmail() {},
|
||||
},
|
||||
runtime: {
|
||||
sendMessage: async () => ({}),
|
||||
},
|
||||
constants: {
|
||||
copyIcon: '',
|
||||
displayTimeZone: 'Asia/Shanghai',
|
||||
expandedStorageKey: 'multipage-mail2925-list-expanded',
|
||||
},
|
||||
mail2925Utils: {},
|
||||
});
|
||||
|
||||
manager.bindMail2925Events();
|
||||
assert.equal(formShell.hidden, true);
|
||||
assert.equal(formToggleButton.textContent, '添加账号');
|
||||
|
||||
clickHandlers.click();
|
||||
assert.equal(formShell.hidden, false);
|
||||
assert.equal(formToggleButton.textContent, '取消添加');
|
||||
|
||||
clickHandlers.click();
|
||||
assert.equal(formShell.hidden, true);
|
||||
assert.equal(formToggleButton.textContent, '添加账号');
|
||||
});
|
||||
|
||||
test('mail2925 manager hides form after save succeeds', async () => {
|
||||
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
|
||||
const windowObject = {};
|
||||
const localStorageMock = {
|
||||
getItem() {
|
||||
return null;
|
||||
},
|
||||
setItem() {},
|
||||
};
|
||||
|
||||
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelMail2925Manager;`)(
|
||||
windowObject,
|
||||
localStorageMock
|
||||
);
|
||||
|
||||
let latestState = { currentMail2925AccountId: null, mail2925Accounts: [] };
|
||||
const handlers = {};
|
||||
const formToggleButton = {
|
||||
textContent: '',
|
||||
disabled: false,
|
||||
setAttribute() {},
|
||||
addEventListener(type, handler) {
|
||||
if (type === 'click') handlers.toggle = handler;
|
||||
},
|
||||
};
|
||||
const addButton = {
|
||||
textContent: '',
|
||||
disabled: false,
|
||||
addEventListener(type, handler) {
|
||||
if (type === 'click') handlers.add = handler;
|
||||
},
|
||||
};
|
||||
const formShell = { hidden: true };
|
||||
const inputMail2925Email = { value: '', focus() {} };
|
||||
const inputMail2925Password = { value: '' };
|
||||
const toastMessages = [];
|
||||
|
||||
const manager = api.createMail2925Manager({
|
||||
state: {
|
||||
getLatestState: () => latestState,
|
||||
syncLatestState(patch) {
|
||||
latestState = { ...latestState, ...patch };
|
||||
},
|
||||
},
|
||||
dom: {
|
||||
btnAddMail2925Account: addButton,
|
||||
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
|
||||
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
|
||||
btnToggleMail2925Form: formToggleButton,
|
||||
btnToggleMail2925List: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
|
||||
inputMail2925Email,
|
||||
inputMail2925Import: { value: '' },
|
||||
inputMail2925Password,
|
||||
mail2925AccountsList: { innerHTML: '', addEventListener() {} },
|
||||
mail2925FormShell: formShell,
|
||||
mail2925ListShell: { classList: { toggle() {} } },
|
||||
},
|
||||
helpers: {
|
||||
getMail2925Accounts: (state) => state.mail2925Accounts || [],
|
||||
escapeHtml: (value) => String(value || ''),
|
||||
showToast(message) {
|
||||
toastMessages.push(message);
|
||||
},
|
||||
openConfirmModal: async () => true,
|
||||
copyTextToClipboard: async () => {},
|
||||
refreshManagedAliasBaseEmail() {},
|
||||
},
|
||||
runtime: {
|
||||
sendMessage: async () => ({
|
||||
account: {
|
||||
id: 'acc-1',
|
||||
email: 'demo@2925.com',
|
||||
password: 'secret',
|
||||
enabled: true,
|
||||
lastLoginAt: 0,
|
||||
lastUsedAt: 0,
|
||||
lastLimitAt: 0,
|
||||
disabledUntil: 0,
|
||||
lastError: '',
|
||||
},
|
||||
}),
|
||||
},
|
||||
constants: {
|
||||
copyIcon: '',
|
||||
displayTimeZone: 'Asia/Shanghai',
|
||||
expandedStorageKey: 'multipage-mail2925-list-expanded',
|
||||
},
|
||||
mail2925Utils: {
|
||||
upsertMail2925AccountInList: (accounts, nextAccount) => accounts.concat(nextAccount),
|
||||
},
|
||||
});
|
||||
|
||||
manager.bindMail2925Events();
|
||||
handlers.toggle();
|
||||
inputMail2925Email.value = 'demo@2925.com';
|
||||
inputMail2925Password.value = 'secret';
|
||||
|
||||
await handlers.add();
|
||||
|
||||
assert.equal(formShell.hidden, true);
|
||||
assert.equal(formToggleButton.textContent, '添加账号');
|
||||
assert.equal(addButton.textContent, '添加账号');
|
||||
assert.equal(inputMail2925Email.value, '');
|
||||
assert.equal(inputMail2925Password.value, '');
|
||||
assert.match(toastMessages.at(-1) || '', /已保存 2925 账号/);
|
||||
});
|
||||
|
||||
@@ -146,6 +146,27 @@ return {
|
||||
assert.strictEqual(snapshot.displayedEmail, 'display.user@example.com');
|
||||
}
|
||||
|
||||
{
|
||||
const api = createApi({
|
||||
pathname: '/email-verification',
|
||||
retryState: {
|
||||
retryEnabled: true,
|
||||
titleMatched: false,
|
||||
detailMatched: false,
|
||||
routeErrorMatched: true,
|
||||
},
|
||||
verificationTarget: { id: 'otp' },
|
||||
verificationVisible: true,
|
||||
});
|
||||
|
||||
const snapshot = api.inspectLoginAuthState();
|
||||
assert.strictEqual(
|
||||
snapshot.state,
|
||||
'login_timeout_error_page',
|
||||
'第七步在 /email-verification 的登录重试页应优先识别为登录超时报错页'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const api = createApi({
|
||||
oauthConsentPage: true,
|
||||
|
||||
@@ -103,3 +103,111 @@ return {
|
||||
assert.equal(result.state, 'login_timeout_error_page');
|
||||
assert.equal(result.message, '当前页面处于登录超时报错页。');
|
||||
});
|
||||
|
||||
test('step 7 finalize converts verification page that falls into retry page into recoverable result', async () => {
|
||||
const api = new Function(`
|
||||
const logs = [];
|
||||
let recoverCalls = 0;
|
||||
let currentState = 'verification_page';
|
||||
|
||||
const location = {
|
||||
href: 'https://auth.openai.com/email-verification',
|
||||
};
|
||||
|
||||
function inspectLoginAuthState() {
|
||||
return {
|
||||
state: currentState,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
async function recoverCurrentAuthRetryPage() {
|
||||
recoverCalls += 1;
|
||||
return { recovered: true };
|
||||
}
|
||||
|
||||
async function sleep() {
|
||||
currentState = 'login_timeout_error_page';
|
||||
}
|
||||
|
||||
function log(message, level = 'info') {
|
||||
logs.push({ message, level });
|
||||
}
|
||||
|
||||
function throwIfStopped() {}
|
||||
|
||||
function getLoginAuthStateLabel(snapshot) {
|
||||
switch (snapshot?.state) {
|
||||
case 'verification_page':
|
||||
return '登录验证码页';
|
||||
case 'login_timeout_error_page':
|
||||
return '登录超时报错页';
|
||||
default:
|
||||
return '未知页面';
|
||||
}
|
||||
}
|
||||
|
||||
${extractFunction('createStep6SuccessResult')}
|
||||
${extractFunction('createStep6RecoverableResult')}
|
||||
${extractFunction('normalizeStep6Snapshot')}
|
||||
${extractFunction('createStep6LoginTimeoutRecoverableResult')}
|
||||
${extractFunction('finalizeStep6VerificationReady')}
|
||||
|
||||
return {
|
||||
async run() {
|
||||
return finalizeStep6VerificationReady({
|
||||
logLabel: '步骤 7 收尾',
|
||||
loginVerificationRequestedAt: 123,
|
||||
via: 'password_submit',
|
||||
});
|
||||
},
|
||||
snapshot() {
|
||||
return { logs, recoverCalls };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.run();
|
||||
const snapshot = api.snapshot();
|
||||
|
||||
assert.equal(snapshot.recoverCalls, 1);
|
||||
assert.equal(result.step6Outcome, 'recoverable');
|
||||
assert.equal(result.reason, 'login_timeout_error_page');
|
||||
assert.equal(result.state, 'login_timeout_error_page');
|
||||
assert.equal(result.message, '登录验证码页面准备就绪前进入登录超时报错页。');
|
||||
});
|
||||
|
||||
test('waitForLoginVerificationPageReady reports login timeout page without step8 restart prefix', async () => {
|
||||
const api = new Function(`
|
||||
const location = {
|
||||
href: 'https://auth.openai.com/email-verification',
|
||||
};
|
||||
|
||||
function inspectLoginAuthState() {
|
||||
return {
|
||||
state: 'login_timeout_error_page',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
function throwIfStopped() {}
|
||||
async function sleep() {}
|
||||
|
||||
function getLoginAuthStateLabel(snapshot) {
|
||||
return snapshot?.state === 'login_timeout_error_page' ? '登录超时报错页' : '未知页面';
|
||||
}
|
||||
|
||||
${extractFunction('waitForLoginVerificationPageReady')}
|
||||
|
||||
return {
|
||||
run() {
|
||||
return waitForLoginVerificationPageReady(10);
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
await assert.rejects(
|
||||
() => api.run(),
|
||||
/当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:登录超时报错页。URL: https:\/\/auth\.openai\.com\/email-verification/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -639,7 +639,10 @@
|
||||
- Step 4 在等待注册验证码页时,如果命中认证页 `Try again / 重试` 页,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续回到密码页重提和验证码页确认流程。
|
||||
- 但如果 Step 4 的认证重试页正文中出现 `user_already_exists`,则会直接视为“当前用户已存在”:不点击 `重试`,不再回到步骤 1 重开当前轮,而是立即结束当前轮;开启自动重试时直接进入下一轮。
|
||||
- Step 7 在识别到登录超时报错页时,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复当前页面;若仍未恢复,则按原有可恢复失败逻辑重跑 Step 7。
|
||||
- Step 7 在首次识别到登录验证码页后,不会立刻把步骤判定为成功;还会额外做一轮“收尾确认”,确保页面稳定停留在登录验证码页。如果只是短暂进入验证码页、随后又掉进登录重试页,则会先走共享恢复逻辑,再按既有可恢复失败逻辑重跑 Step 7。
|
||||
- Step 7 的这轮收尾确认是主要责任边界;Step 8 默认建立在“登录验证码页已经由 Step 7 稳定确认”的前提上,只在后台入口保留防御性回退判断,不替代 Step 7 收尾。
|
||||
- Step 8 如果发现认证页已经进入登录超时报错/重试页,会直接报错并回到 Step 7 重新开始,而不是在 Step 8 内部点击 `重试`。
|
||||
- Step 8 的登录重试页判定也覆盖 `/email-verification` 上的 `405 / Route Error`,避免这类页面被误当成普通未知页。
|
||||
- 任意认证页重试页如果正文中出现 `max_check_attempts`,会被视为 Cloudflare 风控触发:后台立刻完全停止流程,侧边栏会复用现有确认弹窗提示等待 15~30 分钟后再试,避免继续刷新或反复重试加重风控,确认按钮显示为“我知道了”。
|
||||
- Step 9 在点击 OAuth 同意页 `继续` 后,会额外检查是否进入认证页重试页;若命中则先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再重新执行当前轮的 `继续` 点击。
|
||||
## 2026-04-21 2925 邮件时间窗补充
|
||||
|
||||
Reference in New Issue
Block a user