feat: 添加登录入口触发器检测逻辑,增强登录状态识别
This commit is contained in:
@@ -114,6 +114,9 @@ const VERIFICATION_CODE_INPUT_SELECTOR = [
|
|||||||
].join(', ');
|
].join(', ');
|
||||||
|
|
||||||
const ONE_TIME_CODE_LOGIN_PATTERN = /使用一次性验证码登录|改用(?:一次性)?验证码(?:登录)?|使用验证码登录|一次性验证码|验证码登录|one[-\s]*time\s*(?:passcode|password|code)|use\s+(?:a\s+)?one[-\s]*time\s*(?:passcode|password|code)(?:\s+instead)?|use\s+(?:a\s+)?code(?:\s+instead)?|sign\s+in\s+with\s+(?:email|code)|email\s+(?:me\s+)?(?:a\s+)?code/i;
|
const ONE_TIME_CODE_LOGIN_PATTERN = /使用一次性验证码登录|改用(?:一次性)?验证码(?:登录)?|使用验证码登录|一次性验证码|验证码登录|one[-\s]*time\s*(?:passcode|password|code)|use\s+(?:a\s+)?one[-\s]*time\s*(?:passcode|password|code)(?:\s+instead)?|use\s+(?:a\s+)?code(?:\s+instead)?|sign\s+in\s+with\s+(?:email|code)|email\s+(?:me\s+)?(?:a\s+)?code/i;
|
||||||
|
const LOGIN_ENTRY_ACTION_PATTERN = /(?:^|\b)(?:log\s*in|sign\s*in|continue\s+(?:with|using)\s+(?:email|chatgpt)|use\s+(?:an?\s+)?email|email\s+address)(?:\b|$)|登录|登陆|邮箱|电子邮件/i;
|
||||||
|
const LOGIN_EXTERNAL_IDP_PATTERN = /google|microsoft|apple|sso|single\s+sign[-\s]*on|企业|工作区|workspace/i;
|
||||||
|
const LOGIN_CODE_ONLY_ACTION_PATTERN = /one[-\s]*time|passcode|use\s+(?:a\s+)?code|验证码|一次性/i;
|
||||||
|
|
||||||
const RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)|resend(?:\s+code)?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive/i;
|
const RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)|resend(?:\s+code)?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive/i;
|
||||||
|
|
||||||
@@ -1719,12 +1722,32 @@ function getLoginSubmitButton({ allowDisabled = false } = {}) {
|
|||||||
}) || null;
|
}) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findLoginEntryTrigger() {
|
||||||
|
const candidates = Array.from(document.querySelectorAll(
|
||||||
|
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
|
||||||
|
)).filter((el) => isVisibleElement(el) && isActionEnabled(el));
|
||||||
|
|
||||||
|
const preferred = candidates.find((el) => {
|
||||||
|
const text = getActionText(el);
|
||||||
|
if (!text || LOGIN_CODE_ONLY_ACTION_PATTERN.test(text) || LOGIN_EXTERNAL_IDP_PATTERN.test(text)) return false;
|
||||||
|
return /continue\s+(?:with|using)\s+email|use\s+(?:an?\s+)?email|email\s+address|邮箱|电子邮件/i.test(text);
|
||||||
|
});
|
||||||
|
if (preferred) return preferred;
|
||||||
|
|
||||||
|
return candidates.find((el) => {
|
||||||
|
const text = getActionText(el);
|
||||||
|
if (!text || LOGIN_CODE_ONLY_ACTION_PATTERN.test(text) || LOGIN_EXTERNAL_IDP_PATTERN.test(text)) return false;
|
||||||
|
return LOGIN_ENTRY_ACTION_PATTERN.test(text);
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
function inspectLoginAuthState() {
|
function inspectLoginAuthState() {
|
||||||
const retryState = getLoginTimeoutErrorPageState();
|
const retryState = getLoginTimeoutErrorPageState();
|
||||||
const verificationTarget = getVerificationCodeTarget();
|
const verificationTarget = getVerificationCodeTarget();
|
||||||
const passwordInput = getLoginPasswordInput();
|
const passwordInput = getLoginPasswordInput();
|
||||||
const emailInput = getLoginEmailInput();
|
const emailInput = getLoginEmailInput();
|
||||||
const switchTrigger = findOneTimeCodeLoginTrigger();
|
const switchTrigger = findOneTimeCodeLoginTrigger();
|
||||||
|
const loginEntryTrigger = findLoginEntryTrigger();
|
||||||
const submitButton = getLoginSubmitButton({ allowDisabled: true });
|
const submitButton = getLoginSubmitButton({ allowDisabled: true });
|
||||||
const verificationVisible = isVerificationPageStillVisible();
|
const verificationVisible = isVerificationPageStillVisible();
|
||||||
const addPhonePage = isAddPhonePageReady();
|
const addPhonePage = isAddPhonePageReady();
|
||||||
@@ -1746,6 +1769,7 @@ function inspectLoginAuthState() {
|
|||||||
emailInput,
|
emailInput,
|
||||||
submitButton,
|
submitButton,
|
||||||
switchTrigger,
|
switchTrigger,
|
||||||
|
loginEntryTrigger,
|
||||||
verificationVisible,
|
verificationVisible,
|
||||||
addPhonePage,
|
addPhonePage,
|
||||||
phoneVerificationPage,
|
phoneVerificationPage,
|
||||||
@@ -1810,6 +1834,13 @@ function inspectLoginAuthState() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loginEntryTrigger) {
|
||||||
|
return {
|
||||||
|
...baseState,
|
||||||
|
state: 'entry_page',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return baseState;
|
return baseState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1828,6 +1859,7 @@ function serializeLoginAuthState(snapshot) {
|
|||||||
hasEmailInput: Boolean(snapshot?.emailInput),
|
hasEmailInput: Boolean(snapshot?.emailInput),
|
||||||
hasSubmitButton: Boolean(snapshot?.submitButton),
|
hasSubmitButton: Boolean(snapshot?.submitButton),
|
||||||
hasSwitchTrigger: Boolean(snapshot?.switchTrigger),
|
hasSwitchTrigger: Boolean(snapshot?.switchTrigger),
|
||||||
|
hasLoginEntryTrigger: Boolean(snapshot?.loginEntryTrigger),
|
||||||
verificationVisible: Boolean(snapshot?.verificationVisible),
|
verificationVisible: Boolean(snapshot?.verificationVisible),
|
||||||
addPhonePage: Boolean(snapshot?.addPhonePage),
|
addPhonePage: Boolean(snapshot?.addPhonePage),
|
||||||
phoneVerificationPage: Boolean(snapshot?.phoneVerificationPage),
|
phoneVerificationPage: Boolean(snapshot?.phoneVerificationPage),
|
||||||
@@ -1849,6 +1881,8 @@ function getLoginAuthStateLabel(snapshot) {
|
|||||||
return '登录超时报错页';
|
return '登录超时报错页';
|
||||||
case 'oauth_consent_page':
|
case 'oauth_consent_page':
|
||||||
return 'OAuth 授权页';
|
return 'OAuth 授权页';
|
||||||
|
case 'entry_page':
|
||||||
|
return '登录入口页';
|
||||||
case 'add_phone_page':
|
case 'add_phone_page':
|
||||||
return '手机号页';
|
return '手机号页';
|
||||||
default:
|
default:
|
||||||
@@ -2816,6 +2850,71 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou
|
|||||||
return transition;
|
return transition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForLoginEntryOpenTransition(timeout = 10000) {
|
||||||
|
const start = Date.now();
|
||||||
|
let snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
|
||||||
|
|
||||||
|
while (Date.now() - start < timeout) {
|
||||||
|
throwIfStopped();
|
||||||
|
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
|
||||||
|
if (snapshot.state !== 'unknown' && snapshot.state !== 'entry_page') {
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
await sleep(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function step6OpenLoginEntry(payload, snapshot) {
|
||||||
|
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
|
||||||
|
const trigger = currentSnapshot.loginEntryTrigger || findLoginEntryTrigger();
|
||||||
|
if (!trigger || !isActionEnabled(trigger)) {
|
||||||
|
return createStep6RecoverableResult('missing_login_entry_trigger', currentSnapshot, {
|
||||||
|
message: '当前登录入口页没有可点击的邮箱登录入口。',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`步骤 7:检测到登录入口页,正在点击 "${getActionText(trigger).slice(0, 80)}"...`);
|
||||||
|
await humanPause(350, 900);
|
||||||
|
simulateClick(trigger);
|
||||||
|
const nextSnapshot = await waitForLoginEntryOpenTransition();
|
||||||
|
|
||||||
|
if (nextSnapshot.state === 'email_page') {
|
||||||
|
return step6LoginFromEmailPage(payload, nextSnapshot);
|
||||||
|
}
|
||||||
|
if (nextSnapshot.state === 'password_page') {
|
||||||
|
return step6LoginFromPasswordPage(payload, nextSnapshot);
|
||||||
|
}
|
||||||
|
if (nextSnapshot.state === 'verification_page') {
|
||||||
|
return finalizeStep6VerificationReady({
|
||||||
|
logLabel: '步骤 7 收尾',
|
||||||
|
loginVerificationRequestedAt: null,
|
||||||
|
via: 'entry_open_verification_page',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (nextSnapshot.state === 'oauth_consent_page') {
|
||||||
|
return createStep6OAuthConsentSuccessResult(nextSnapshot, {
|
||||||
|
via: 'entry_open_oauth_consent_page',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (nextSnapshot.state === 'login_timeout_error_page') {
|
||||||
|
const transition = await createStep6LoginTimeoutRecoveryTransition(
|
||||||
|
'login_timeout_after_entry_open',
|
||||||
|
nextSnapshot,
|
||||||
|
'点击登录入口后进入登录超时报错页。'
|
||||||
|
);
|
||||||
|
if (transition.action === 'done') return transition.result;
|
||||||
|
if (transition.action === 'email') return step6LoginFromEmailPage(payload, transition.snapshot);
|
||||||
|
if (transition.action === 'password') return step6LoginFromPasswordPage(payload, transition.snapshot);
|
||||||
|
return transition.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createStep6RecoverableResult('login_entry_open_stalled', nextSnapshot, {
|
||||||
|
message: '点击登录入口后仍未进入邮箱/密码/验证码页。',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function step6SwitchToOneTimeCodeLogin(payload, snapshot) {
|
async function step6SwitchToOneTimeCodeLogin(payload, snapshot) {
|
||||||
const switchTrigger = snapshot?.switchTrigger || findOneTimeCodeLoginTrigger();
|
const switchTrigger = snapshot?.switchTrigger || findOneTimeCodeLoginTrigger();
|
||||||
if (!switchTrigger || !isActionEnabled(switchTrigger)) {
|
if (!switchTrigger || !isActionEnabled(switchTrigger)) {
|
||||||
@@ -3024,6 +3123,10 @@ async function step6_login(payload) {
|
|||||||
return step6LoginFromPasswordPage(payload, snapshot);
|
return step6LoginFromPasswordPage(payload, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (snapshot.state === 'entry_page') {
|
||||||
|
return step6OpenLoginEntry(payload, snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
throwForStep6FatalState(snapshot);
|
throwForStep6FatalState(snapshot);
|
||||||
throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`);
|
throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ function findOneTimeCodeLoginTrigger() {
|
|||||||
return ${JSON.stringify(overrides.switchTrigger || null)};
|
return ${JSON.stringify(overrides.switchTrigger || null)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findLoginEntryTrigger() {
|
||||||
|
return ${JSON.stringify(overrides.loginEntryTrigger || null)};
|
||||||
|
}
|
||||||
|
|
||||||
function getLoginSubmitButton() {
|
function getLoginSubmitButton() {
|
||||||
return ${JSON.stringify(overrides.submitButton || null)};
|
return ${JSON.stringify(overrides.submitButton || null)};
|
||||||
}
|
}
|
||||||
@@ -226,6 +230,15 @@ return {
|
|||||||
assert.strictEqual(snapshot.state, 'oauth_consent_page', '第六步应保留 oauth_consent_page 状态');
|
assert.strictEqual(snapshot.state, 'oauth_consent_page', '第六步应保留 oauth_consent_page 状态');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const api = createApi({
|
||||||
|
loginEntryTrigger: { id: 'continue-email' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = api.inspectLoginAuthState();
|
||||||
|
assert.strictEqual(snapshot.state, 'entry_page');
|
||||||
|
}
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"),
|
extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"),
|
||||||
'inspectLoginAuthState 应产出 oauth_consent_page 状态'
|
'inspectLoginAuthState 应产出 oauth_consent_page 状态'
|
||||||
|
|||||||
Reference in New Issue
Block a user