Merge remote-tracking branch 'origin/dev' into codex-luckmail
This commit is contained in:
@@ -492,13 +492,27 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
|
||||
在登录前会先重新获取一遍最新的 CPA OAuth 链接,再使用刚注册的账号登录。
|
||||
|
||||
当前 Step 6 的完成标准不是“邮箱/密码已提交”,而是:
|
||||
|
||||
- 认证页已经真正进入登录验证码页面
|
||||
- 如遇登录超时报错或登录页长时间停滞,会由后台刷新 OAuth 后重跑整个 Step 6
|
||||
|
||||
支持:
|
||||
|
||||
- 邮箱 + 密码登录
|
||||
- 提交后进入验证码验证流程
|
||||
- 必要时切换到一次性验证码登录
|
||||
- 直到登录验证码页就绪才算步骤完成
|
||||
|
||||
### Step 7: Get Login Code
|
||||
|
||||
Step 7 默认要求当前认证页已经处于登录验证码页。
|
||||
|
||||
它只负责:
|
||||
|
||||
- 打开邮箱并轮询登录验证码
|
||||
- 填写并提交登录验证码
|
||||
- 验证码链路失败后按有限次数回退到 Step 6
|
||||
|
||||
与 Step 4 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。
|
||||
|
||||
### Step 8: Manual OAuth Confirm
|
||||
|
||||
+172
-110
@@ -3249,6 +3249,10 @@ function getContentScriptResponseTimeoutMs(message) {
|
||||
return 30000;
|
||||
}
|
||||
|
||||
if (message.type === 'EXECUTE_STEP' && Number(message.step) === 6) {
|
||||
return 75000;
|
||||
}
|
||||
|
||||
if (message.type === 'POLL_EMAIL') {
|
||||
const maxAttempts = Math.max(1, Number(message.payload?.maxAttempts) || 1);
|
||||
const intervalMs = Math.max(0, Number(message.payload?.intervalMs) || 0);
|
||||
@@ -3733,50 +3737,23 @@ function isVerificationMailPollingError(error) {
|
||||
return /未在 .*邮箱中找到新的匹配邮件|未在 Hotmail 收件箱中找到新的匹配验证码|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|did not respond in \d+s/i.test(message);
|
||||
}
|
||||
|
||||
const STEP7_RESTART_FROM_STEP6_ERROR_CODE = 'STEP7_RESTART_FROM_STEP6';
|
||||
const STEP7_RESTART_FROM_STEP6_MARKER_PATTERN = /^STEP7_RESTART_FROM_STEP6::([^:]+)::(.*)$/;
|
||||
|
||||
function createStep7RestartFromStep6Error(details = {}) {
|
||||
const { reason = 'unknown', url = '' } = details || {};
|
||||
const reasonLabel = reason === 'login_timeout_error_page'
|
||||
? '检测到登录页超时报错'
|
||||
: '步骤 7 请求回到步骤 6';
|
||||
const error = new Error(`步骤 7:${reasonLabel}。${url ? `URL: ${url}` : ''}`.trim());
|
||||
error.code = STEP7_RESTART_FROM_STEP6_ERROR_CODE;
|
||||
error.restartReason = reason;
|
||||
error.restartUrl = url;
|
||||
return error;
|
||||
}
|
||||
|
||||
function parseStep7RestartFromStep6Marker(message) {
|
||||
const normalized = getErrorMessage(message);
|
||||
const match = normalized.match(STEP7_RESTART_FROM_STEP6_MARKER_PATTERN);
|
||||
if (!match) {
|
||||
return null;
|
||||
function getLoginAuthStateLabel(state) {
|
||||
switch (state) {
|
||||
case 'verification_page':
|
||||
return '登录验证码页';
|
||||
case 'password_page':
|
||||
return '密码页';
|
||||
case 'email_page':
|
||||
return '邮箱输入页';
|
||||
case 'login_timeout_error_page':
|
||||
return '登录超时报错页';
|
||||
case 'oauth_consent_page':
|
||||
return 'OAuth 授权页';
|
||||
case 'add_phone_page':
|
||||
return '手机号页';
|
||||
default:
|
||||
return '未知页面';
|
||||
}
|
||||
|
||||
return {
|
||||
reason: match[1] || 'unknown',
|
||||
url: match[2] || '',
|
||||
};
|
||||
}
|
||||
|
||||
function getStep7RestartFromStep6Error(result) {
|
||||
if (result?.restartFromStep6) {
|
||||
return createStep7RestartFromStep6Error(result);
|
||||
}
|
||||
|
||||
const parsed = parseStep7RestartFromStep6Marker(result?.error);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createStep7RestartFromStep6Error(parsed);
|
||||
}
|
||||
|
||||
function isStep7RestartFromStep6Error(error) {
|
||||
return error?.code === STEP7_RESTART_FROM_STEP6_ERROR_CODE
|
||||
|| Boolean(parseStep7RestartFromStep6Marker(error));
|
||||
}
|
||||
|
||||
function isRestartCurrentAttemptError(error) {
|
||||
@@ -4816,8 +4793,8 @@ async function handleStepData(step, payload) {
|
||||
const stepWaiters = new Map();
|
||||
let resumeWaiter = null;
|
||||
const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000;
|
||||
const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([4, 7, 8]);
|
||||
const STEP_COMPLETION_SIGNAL_STEPS = new Set([1, 2, 3, 5, 6, 9]);
|
||||
const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([4, 6, 7, 8]);
|
||||
const STEP_COMPLETION_SIGNAL_STEPS = new Set([1, 2, 3, 5, 9]);
|
||||
|
||||
function waitForStepComplete(step, timeoutMs = 120000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -6652,17 +6629,15 @@ async function requestVerificationCodeResend(step) {
|
||||
payload: {},
|
||||
});
|
||||
|
||||
if (step === 7) {
|
||||
const restartError = getStep7RestartFromStep6Error(result);
|
||||
if (restartError) {
|
||||
throw restartError;
|
||||
}
|
||||
}
|
||||
|
||||
if (result && result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
const requestedAt = Date.now();
|
||||
if (step === 7) {
|
||||
await setState({ loginVerificationRequestedAt: requestedAt });
|
||||
}
|
||||
|
||||
const currentState = await getState();
|
||||
if (currentState.mailProvider === '2925') {
|
||||
const mailTabId = await getTabId('mail-2925');
|
||||
@@ -6672,16 +6647,18 @@ async function requestVerificationCodeResend(step) {
|
||||
}
|
||||
}
|
||||
|
||||
return Date.now();
|
||||
return requestedAt;
|
||||
}
|
||||
|
||||
async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
|
||||
const { onResendRequestedAt, ...cleanPollOverrides } = pollOverrides;
|
||||
|
||||
if (mail.provider === HOTMAIL_PROVIDER) {
|
||||
const hotmailPollConfig = getHotmailVerificationPollConfig(step);
|
||||
return pollHotmailVerificationCode(step, state, {
|
||||
...getVerificationPollPayload(step, state),
|
||||
...hotmailPollConfig,
|
||||
...pollOverrides,
|
||||
...cleanPollOverrides,
|
||||
});
|
||||
}
|
||||
if (mail.provider === LUCKMAIL_PROVIDER) {
|
||||
@@ -6711,17 +6688,23 @@ async function pollFreshVerificationCode(step, state, mail, pollOverrides = {})
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
const filterAfterTimestamp = pollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
|
||||
let filterAfterTimestamp = cleanPollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
|
||||
const maxRounds = pollOverrides.maxRounds || VERIFICATION_POLL_MAX_ROUNDS;
|
||||
|
||||
for (let round = 1; round <= maxRounds; round++) {
|
||||
throwIfStopped();
|
||||
if (round > 1) {
|
||||
await requestVerificationCodeResend(step);
|
||||
const requestedAt = await requestVerificationCodeResend(step);
|
||||
if (typeof onResendRequestedAt === 'function') {
|
||||
const nextFilterAfterTimestamp = await onResendRequestedAt(requestedAt);
|
||||
if (nextFilterAfterTimestamp !== undefined) {
|
||||
filterAfterTimestamp = nextFilterAfterTimestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const payload = getVerificationPollPayload(step, state, {
|
||||
...pollOverrides,
|
||||
...cleanPollOverrides,
|
||||
filterAfterTimestamp,
|
||||
excludeCodes: [...rejectedCodes],
|
||||
});
|
||||
@@ -6783,10 +6766,14 @@ async function pollFreshVerificationCodeWithResendInterval(step, state, mail, po
|
||||
maxRounds: _ignoredMaxRounds,
|
||||
resendIntervalMs: _ignoredResendIntervalMs,
|
||||
lastResendAt: _ignoredLastResendAt,
|
||||
onResendRequestedAt: _ignoredOnResendRequestedAt,
|
||||
...payloadOverrides
|
||||
} = pollOverrides;
|
||||
const onResendRequestedAt = typeof pollOverrides.onResendRequestedAt === 'function'
|
||||
? pollOverrides.onResendRequestedAt
|
||||
: null;
|
||||
let lastError = null;
|
||||
const filterAfterTimestamp = payloadOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
|
||||
let filterAfterTimestamp = payloadOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
|
||||
const maxRounds = pollOverrides.maxRounds || VERIFICATION_POLL_MAX_ROUNDS;
|
||||
const resendIntervalMs = Math.max(0, Number(pollOverrides.resendIntervalMs) || 0);
|
||||
let lastResendAt = Number(pollOverrides.lastResendAt) || 0;
|
||||
@@ -6795,6 +6782,12 @@ async function pollFreshVerificationCodeWithResendInterval(step, state, mail, po
|
||||
throwIfStopped();
|
||||
if (round > 1) {
|
||||
lastResendAt = await requestVerificationCodeResend(step);
|
||||
if (onResendRequestedAt) {
|
||||
const nextFilterAfterTimestamp = await onResendRequestedAt(lastResendAt);
|
||||
if (nextFilterAfterTimestamp !== undefined) {
|
||||
filterAfterTimestamp = nextFilterAfterTimestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
@@ -6886,13 +6879,6 @@ async function submitVerificationCode(step, code) {
|
||||
payload: { code },
|
||||
});
|
||||
|
||||
if (step === 7) {
|
||||
const restartError = getStep7RestartFromStep6Error(result);
|
||||
if (restartError) {
|
||||
throw restartError;
|
||||
}
|
||||
}
|
||||
|
||||
if (result && result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
@@ -6911,7 +6897,7 @@ async function resolveVerificationStep(step, state, mail, options = {}) {
|
||||
rejectedCodes.add(state[stateKey]);
|
||||
}
|
||||
|
||||
const nextFilterAfterTimestamp = options.filterAfterTimestamp ?? null;
|
||||
let nextFilterAfterTimestamp = options.filterAfterTimestamp ?? null;
|
||||
const requestFreshCodeFirst = options.requestFreshCodeFirst !== undefined
|
||||
? Boolean(options.requestFreshCodeFirst)
|
||||
: (hotmailPollConfig?.requestFreshCodeFirst ?? false);
|
||||
@@ -6919,12 +6905,30 @@ async function resolveVerificationStep(step, state, mail, options = {}) {
|
||||
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
|
||||
let lastResendAt = Number(options.lastResendAt) || 0;
|
||||
|
||||
const updateFilterAfterTimestampForStep7 = async (requestedAt) => {
|
||||
if (step !== 7 || !requestedAt) {
|
||||
return nextFilterAfterTimestamp;
|
||||
}
|
||||
|
||||
if (mail.provider === HOTMAIL_PROVIDER) {
|
||||
nextFilterAfterTimestamp = getHotmailVerificationRequestTimestamp(7, {
|
||||
...state,
|
||||
loginVerificationRequestedAt: requestedAt,
|
||||
});
|
||||
} else {
|
||||
nextFilterAfterTimestamp = Math.max(0, Number(requestedAt) - 60000);
|
||||
}
|
||||
|
||||
return nextFilterAfterTimestamp;
|
||||
};
|
||||
|
||||
if (requestFreshCodeFirst) {
|
||||
try {
|
||||
lastResendAt = await requestVerificationCodeResend(step);
|
||||
await updateFilterAfterTimestampForStep7(lastResendAt);
|
||||
await addLog(`步骤 ${step}:已先请求一封新的${getVerificationCodeLabel(step)}验证码,再开始轮询邮箱。`, 'warn');
|
||||
} catch (err) {
|
||||
if (isStopError(err) || (step === 7 && isStep7RestartFromStep6Error(err))) {
|
||||
if (isStopError(err)) {
|
||||
throw err;
|
||||
}
|
||||
await addLog(`步骤 ${step}:首次重新获取验证码失败:${err.message},将继续使用当前时间窗口轮询。`, 'warn');
|
||||
@@ -6945,6 +6949,7 @@ async function resolveVerificationStep(step, state, mail, options = {}) {
|
||||
filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined,
|
||||
resendIntervalMs,
|
||||
lastResendAt,
|
||||
onResendRequestedAt: updateFilterAfterTimestampForStep7,
|
||||
});
|
||||
lastResendAt = Number(result?.lastResendAt) || lastResendAt;
|
||||
|
||||
@@ -6973,6 +6978,7 @@ async function resolveVerificationStep(step, state, mail, options = {}) {
|
||||
}
|
||||
|
||||
lastResendAt = await requestVerificationCodeResend(step);
|
||||
await updateFilterAfterTimestampForStep7(lastResendAt);
|
||||
await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn');
|
||||
continue;
|
||||
}
|
||||
@@ -7020,9 +7026,6 @@ async function executeStep4(state) {
|
||||
if (prepareResult && prepareResult.error) {
|
||||
throw new Error(prepareResult.error);
|
||||
}
|
||||
if (prepareResult?.verificationRequestedAt) {
|
||||
await setState({ loginVerificationRequestedAt: prepareResult.verificationRequestedAt });
|
||||
}
|
||||
if (prepareResult?.alreadyVerified) {
|
||||
await completeStepFromBackground(4, {});
|
||||
return;
|
||||
@@ -7086,7 +7089,7 @@ async function executeStep5(state) {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step 6: Login ChatGPT (Background opens tab, chatgpt.js handles login)
|
||||
// Step 6: Login and ensure the auth page reaches the login verification page
|
||||
// ============================================================
|
||||
|
||||
async function refreshOAuthUrlBeforeStep6(state) {
|
||||
@@ -7107,28 +7110,110 @@ async function refreshOAuthUrlBeforeStep6(state) {
|
||||
return latestState.oauthUrl;
|
||||
}
|
||||
|
||||
function isStep6SuccessResult(result) {
|
||||
return result?.step6Outcome === 'success';
|
||||
}
|
||||
|
||||
function isStep6RecoverableResult(result) {
|
||||
return result?.step6Outcome === 'recoverable';
|
||||
}
|
||||
|
||||
async function getLoginAuthStateFromContent() {
|
||||
const result = await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
{
|
||||
type: 'GET_LOGIN_AUTH_STATE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
},
|
||||
{
|
||||
timeoutMs: 15000,
|
||||
retryDelayMs: 600,
|
||||
logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续确认验证码页状态...',
|
||||
}
|
||||
);
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function ensureStep7VerificationPageReady() {
|
||||
const pageState = await getLoginAuthStateFromContent();
|
||||
if (pageState.state === 'verification_page') {
|
||||
return pageState;
|
||||
}
|
||||
|
||||
const stateLabel = getLoginAuthStateLabel(pageState.state);
|
||||
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
|
||||
throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 6。当前状态:${stateLabel}.${urlPart}`.trim());
|
||||
}
|
||||
|
||||
async function executeStep6(state) {
|
||||
if (!state.email) {
|
||||
throw new Error('缺少邮箱地址,请先完成步骤 3。');
|
||||
}
|
||||
let attempt = 0;
|
||||
|
||||
const oauthUrl = await refreshOAuthUrlBeforeStep6(state);
|
||||
while (true) {
|
||||
throwIfStopped();
|
||||
attempt += 1;
|
||||
const currentState = attempt === 1 ? state : await getState();
|
||||
const password = currentState.password || currentState.customPassword || '';
|
||||
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
|
||||
|
||||
await addLog('步骤 6:正在打开最新 OAuth 链接并登录...');
|
||||
// Reuse the signup-page tab — navigate it to the OAuth URL
|
||||
await reuseOrCreateTab('signup-page', oauthUrl);
|
||||
if (attempt === 1) {
|
||||
await addLog('步骤 6:正在打开最新 OAuth 链接并登录...');
|
||||
} else {
|
||||
await addLog(`步骤 6:上一轮登录未进入验证码页,正在重新发起第 ${attempt} 轮登录尝试...`, 'warn');
|
||||
}
|
||||
|
||||
// signup-page.js will inject (same auth.openai.com domain) and handle login
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 6,
|
||||
source: 'background',
|
||||
payload: { email: state.email, password: state.password },
|
||||
});
|
||||
await reuseOrCreateTab('signup-page', oauthUrl);
|
||||
|
||||
const result = await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
{
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 6,
|
||||
source: 'background',
|
||||
payload: {
|
||||
email: currentState.email,
|
||||
password,
|
||||
},
|
||||
},
|
||||
{
|
||||
timeoutMs: 180000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 6:认证页正在切换,等待页面重新就绪后继续登录...',
|
||||
}
|
||||
);
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
if (isStep6SuccessResult(result)) {
|
||||
await completeStepFromBackground(6, {
|
||||
loginVerificationRequestedAt: result.loginVerificationRequestedAt || null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isStep6RecoverableResult(result)) {
|
||||
const reasonMessage = result.message
|
||||
|| `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 6。`;
|
||||
await addLog(`步骤 6:${reasonMessage}`, 'warn');
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error('步骤 6:认证页未返回可识别的登录结果。');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step 7: Get Login Verification Code (qq-mail.js polls, then fills in chatgpt.js)
|
||||
// Step 7: Poll login verification mail and submit the login code
|
||||
// ============================================================
|
||||
|
||||
async function runStep7Attempt(state) {
|
||||
@@ -7147,22 +7232,8 @@ async function runStep7Attempt(state) {
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
await addLog('步骤 7:正在准备认证页,必要时切换到一次性验证码登录...');
|
||||
const prepareResult = await sendToContentScript('signup-page', {
|
||||
type: 'PREPARE_LOGIN_CODE',
|
||||
step: 7,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
const restartError = getStep7RestartFromStep6Error(prepareResult);
|
||||
if (restartError) {
|
||||
throw restartError;
|
||||
}
|
||||
|
||||
if (prepareResult && prepareResult.error) {
|
||||
throw new Error(prepareResult.error);
|
||||
}
|
||||
await ensureStep7VerificationPageReady();
|
||||
await addLog('步骤 7:登录验证码页面已就绪,开始获取验证码。', 'info');
|
||||
|
||||
if (shouldUseCustomRegistrationEmail(state)) {
|
||||
await confirmCustomVerificationStepBypass(7);
|
||||
@@ -7195,18 +7266,16 @@ async function runStep7Attempt(state) {
|
||||
}
|
||||
|
||||
await resolveVerificationStep(7, state, mail, {
|
||||
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt,
|
||||
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
|
||||
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : Math.max(0, stepStartedAt - 60000),
|
||||
requestFreshCodeFirst: false,
|
||||
resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
});
|
||||
}
|
||||
|
||||
async function rerunStep6ForStep7Recovery() {
|
||||
const currentState = await getState();
|
||||
const waitForStep6 = waitForStepComplete(6, 120000);
|
||||
await addLog('步骤 7:正在回到步骤 6,重新发起登录验证码流程...', 'warn');
|
||||
await executeStep6(currentState);
|
||||
await waitForStep6;
|
||||
await sleepWithStop(3000);
|
||||
}
|
||||
|
||||
@@ -7220,13 +7289,6 @@ async function executeStep7(state) {
|
||||
await runStep7Attempt(currentState);
|
||||
return;
|
||||
} catch (err) {
|
||||
if (isStep7RestartFromStep6Error(err)) {
|
||||
await addLog('步骤 7:检测到登录页超时报错,准备从步骤 6 重新开始...', 'warn');
|
||||
await rerunStep6ForStep7Recovery();
|
||||
currentState = await getState();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isVerificationMailPollingError(err)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
+567
-144
@@ -11,7 +11,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
|| message.type === 'STEP8_FIND_AND_CLICK'
|
||||
|| message.type === 'STEP8_GET_STATE'
|
||||
|| message.type === 'STEP8_TRIGGER_CONTINUE'
|
||||
|| message.type === 'PREPARE_LOGIN_CODE'
|
||||
|| message.type === 'GET_LOGIN_AUTH_STATE'
|
||||
|| message.type === 'PREPARE_SIGNUP_VERIFICATION'
|
||||
|| message.type === 'RESEND_VERIFICATION_CODE'
|
||||
) {
|
||||
@@ -52,10 +52,10 @@ async function handleCommand(message) {
|
||||
case 'FILL_CODE':
|
||||
// Step 4 = signup code, Step 7 = login code (same handler)
|
||||
return await fillVerificationCode(message.step, message.payload);
|
||||
case 'GET_LOGIN_AUTH_STATE':
|
||||
return serializeLoginAuthState(inspectLoginAuthState());
|
||||
case 'PREPARE_SIGNUP_VERIFICATION':
|
||||
return await prepareSignupVerificationFlow(message.payload);
|
||||
case 'PREPARE_LOGIN_CODE':
|
||||
return await prepareLoginCodeFlow();
|
||||
case 'RESEND_VERIFICATION_CODE':
|
||||
return await resendVerificationCode(message.step);
|
||||
case 'STEP8_FIND_AND_CLICK':
|
||||
@@ -176,86 +176,9 @@ function isEmailVerificationPage() {
|
||||
return /\/email-verification(?:[/?#]|$)/i.test(location.pathname || '');
|
||||
}
|
||||
|
||||
async function prepareLoginCodeFlow(timeout = 15000) {
|
||||
const readyTarget = getVerificationCodeTarget();
|
||||
if (readyTarget) {
|
||||
log('步骤 7:验证码输入框已就绪。');
|
||||
return { ready: true, mode: readyTarget.type };
|
||||
}
|
||||
|
||||
if (isEmailVerificationPage() && isVerificationPageStillVisible()) {
|
||||
log('步骤 7:已进入邮箱验证码页面,正在等待验证码输入框或重发入口稳定。');
|
||||
return { ready: true, mode: 'verification_page' };
|
||||
}
|
||||
|
||||
const initialRestartSignal = getStep7RestartFromStep6Signal();
|
||||
if (initialRestartSignal) {
|
||||
log('步骤 7:检测到登录页超时报错,准备回到步骤 6 重新发起登录验证码流程...', 'warn');
|
||||
return initialRestartSignal;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
let switchClickCount = 0;
|
||||
let lastSwitchAttemptAt = 0;
|
||||
let loggedPasswordPage = false;
|
||||
let loggedVerificationPage = false;
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
|
||||
const target = getVerificationCodeTarget();
|
||||
if (target) {
|
||||
log('步骤 7:验证码页面已就绪。');
|
||||
return { ready: true, mode: target.type };
|
||||
}
|
||||
|
||||
if (isEmailVerificationPage() && isVerificationPageStillVisible()) {
|
||||
if (!loggedVerificationPage) {
|
||||
loggedVerificationPage = true;
|
||||
log('步骤 7:页面已进入邮箱验证码流程,继续等待验证码输入框渲染...');
|
||||
}
|
||||
await sleep(250);
|
||||
continue;
|
||||
}
|
||||
|
||||
const restartSignal = getStep7RestartFromStep6Signal();
|
||||
if (restartSignal) {
|
||||
log('步骤 7:检测到登录页超时报错,准备回到步骤 6 重新发起登录验证码流程...', 'warn');
|
||||
return restartSignal;
|
||||
}
|
||||
|
||||
const passwordInput = document.querySelector('input[type="password"]');
|
||||
const switchTrigger = findOneTimeCodeLoginTrigger();
|
||||
|
||||
if (switchTrigger && (switchClickCount === 0 || Date.now() - lastSwitchAttemptAt > 1500)) {
|
||||
switchClickCount += 1;
|
||||
lastSwitchAttemptAt = Date.now();
|
||||
loggedPasswordPage = false;
|
||||
log('步骤 7:检测到密码页,正在切换到一次性验证码登录...');
|
||||
await humanPause(350, 900);
|
||||
const verificationRequestedAt = Date.now();
|
||||
simulateClick(switchTrigger);
|
||||
await sleep(1200);
|
||||
return { ready: true, mode: 'verification_switch', verificationRequestedAt };
|
||||
}
|
||||
|
||||
if (passwordInput && !loggedPasswordPage) {
|
||||
loggedPasswordPage = true;
|
||||
log('步骤 7:正在等待密码页上的一次性验证码登录入口...');
|
||||
}
|
||||
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
throw new Error('无法切换到一次性验证码验证页面。URL: ' + location.href);
|
||||
}
|
||||
|
||||
async function resendVerificationCode(step, timeout = 45000) {
|
||||
if (step === 7) {
|
||||
const prepareResult = await prepareLoginCodeFlow();
|
||||
if (prepareResult?.restartFromStep6) {
|
||||
return prepareResult;
|
||||
}
|
||||
await waitForLoginVerificationPageReady();
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
@@ -730,26 +653,248 @@ function getLoginTimeoutErrorPageState() {
|
||||
});
|
||||
}
|
||||
|
||||
function isSignupPasswordErrorPage() {
|
||||
return Boolean(getSignupPasswordTimeoutErrorPageState());
|
||||
function getLoginEmailInput() {
|
||||
const input = document.querySelector(
|
||||
'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]'
|
||||
);
|
||||
return input && isVisibleElement(input) ? input : null;
|
||||
}
|
||||
|
||||
function buildStep7RestartFromStep6Marker(reason, url = location.href) {
|
||||
return `STEP7_RESTART_FROM_STEP6::${reason || 'unknown'}::${url || ''}`;
|
||||
function getLoginPasswordInput() {
|
||||
const input = document.querySelector('input[type="password"]');
|
||||
return input && isVisibleElement(input) ? input : null;
|
||||
}
|
||||
|
||||
function getStep7RestartFromStep6Signal() {
|
||||
const timeoutPage = getLoginTimeoutErrorPageState();
|
||||
if (!timeoutPage) {
|
||||
return null;
|
||||
function getLoginSubmitButton({ allowDisabled = false } = {}) {
|
||||
const direct = document.querySelector('button[type="submit"], input[type="submit"]');
|
||||
if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
return {
|
||||
error: buildStep7RestartFromStep6Marker('login_timeout_error_page', timeoutPage.url),
|
||||
restartFromStep6: true,
|
||||
reason: 'login_timeout_error_page',
|
||||
url: timeoutPage.url,
|
||||
const candidates = document.querySelectorAll(
|
||||
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
|
||||
);
|
||||
return Array.from(candidates).find((el) => {
|
||||
if (!isVisibleElement(el) || (!allowDisabled && !isActionEnabled(el))) return false;
|
||||
const text = getActionText(el);
|
||||
if (!text || ONE_TIME_CODE_LOGIN_PATTERN.test(text)) return false;
|
||||
return /continue|next|submit|sign\s*in|log\s*in|继续|下一步|登录/i.test(text);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function inspectLoginAuthState() {
|
||||
const retryState = getLoginTimeoutErrorPageState();
|
||||
const verificationTarget = getVerificationCodeTarget();
|
||||
const passwordInput = getLoginPasswordInput();
|
||||
const emailInput = getLoginEmailInput();
|
||||
const switchTrigger = findOneTimeCodeLoginTrigger();
|
||||
const submitButton = getLoginSubmitButton({ allowDisabled: true });
|
||||
const verificationVisible = isVerificationPageStillVisible();
|
||||
const addPhonePage = isAddPhonePageReady();
|
||||
const consentReady = isStep8Ready();
|
||||
const oauthConsentPage = isOAuthConsentPage();
|
||||
const baseState = {
|
||||
state: 'unknown',
|
||||
url: location.href,
|
||||
path: location.pathname || '',
|
||||
retryButton: retryState?.retryButton || null,
|
||||
retryEnabled: Boolean(retryState?.retryEnabled),
|
||||
titleMatched: Boolean(retryState?.titleMatched),
|
||||
detailMatched: Boolean(retryState?.detailMatched),
|
||||
verificationTarget,
|
||||
passwordInput,
|
||||
emailInput,
|
||||
submitButton,
|
||||
switchTrigger,
|
||||
verificationVisible,
|
||||
addPhonePage,
|
||||
oauthConsentPage,
|
||||
consentReady,
|
||||
};
|
||||
|
||||
if (verificationTarget || verificationVisible) {
|
||||
return {
|
||||
...baseState,
|
||||
state: 'verification_page',
|
||||
};
|
||||
}
|
||||
|
||||
if (retryState) {
|
||||
return {
|
||||
...baseState,
|
||||
state: 'login_timeout_error_page',
|
||||
};
|
||||
}
|
||||
|
||||
if (addPhonePage) {
|
||||
return {
|
||||
...baseState,
|
||||
state: 'add_phone_page',
|
||||
};
|
||||
}
|
||||
|
||||
if (oauthConsentPage) {
|
||||
return {
|
||||
...baseState,
|
||||
state: 'oauth_consent_page',
|
||||
};
|
||||
}
|
||||
|
||||
if (passwordInput || switchTrigger) {
|
||||
return {
|
||||
...baseState,
|
||||
state: 'password_page',
|
||||
};
|
||||
}
|
||||
|
||||
if (emailInput) {
|
||||
return {
|
||||
...baseState,
|
||||
state: 'email_page',
|
||||
};
|
||||
}
|
||||
|
||||
return baseState;
|
||||
}
|
||||
|
||||
function serializeLoginAuthState(snapshot) {
|
||||
return {
|
||||
state: snapshot?.state || 'unknown',
|
||||
url: snapshot?.url || location.href,
|
||||
path: snapshot?.path || location.pathname || '',
|
||||
retryEnabled: Boolean(snapshot?.retryEnabled),
|
||||
titleMatched: Boolean(snapshot?.titleMatched),
|
||||
detailMatched: Boolean(snapshot?.detailMatched),
|
||||
hasVerificationTarget: Boolean(snapshot?.verificationTarget),
|
||||
hasPasswordInput: Boolean(snapshot?.passwordInput),
|
||||
hasEmailInput: Boolean(snapshot?.emailInput),
|
||||
hasSubmitButton: Boolean(snapshot?.submitButton),
|
||||
hasSwitchTrigger: Boolean(snapshot?.switchTrigger),
|
||||
verificationVisible: Boolean(snapshot?.verificationVisible),
|
||||
addPhonePage: Boolean(snapshot?.addPhonePage),
|
||||
oauthConsentPage: Boolean(snapshot?.oauthConsentPage),
|
||||
consentReady: Boolean(snapshot?.consentReady),
|
||||
};
|
||||
}
|
||||
|
||||
function getLoginAuthStateLabel(snapshot) {
|
||||
switch (snapshot?.state) {
|
||||
case 'verification_page':
|
||||
return '登录验证码页';
|
||||
case 'password_page':
|
||||
return '密码页';
|
||||
case 'email_page':
|
||||
return '邮箱输入页';
|
||||
case 'login_timeout_error_page':
|
||||
return '登录超时报错页';
|
||||
case 'oauth_consent_page':
|
||||
return 'OAuth 授权页';
|
||||
case 'add_phone_page':
|
||||
return '手机号页';
|
||||
default:
|
||||
return '未知页面';
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForKnownLoginAuthState(timeout = 15000) {
|
||||
const start = Date.now();
|
||||
let snapshot = inspectLoginAuthState();
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
snapshot = inspectLoginAuthState();
|
||||
if (snapshot.state !== 'unknown') {
|
||||
return snapshot;
|
||||
}
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
async function waitForLoginVerificationPageReady(timeout = 10000) {
|
||||
const start = Date.now();
|
||||
let snapshot = inspectLoginAuthState();
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
snapshot = inspectLoginAuthState();
|
||||
if (snapshot.state === 'verification_page') {
|
||||
return snapshot;
|
||||
}
|
||||
if (snapshot.state !== 'unknown') {
|
||||
break;
|
||||
}
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`当前未进入登录验证码页面,请先重新完成步骤 6。当前状态:${getLoginAuthStateLabel(snapshot)}。URL: ${snapshot?.url || location.href}`
|
||||
);
|
||||
}
|
||||
|
||||
function createStep6SuccessResult(snapshot, options = {}) {
|
||||
return {
|
||||
step6Outcome: 'success',
|
||||
state: snapshot?.state || 'verification_page',
|
||||
url: snapshot?.url || location.href,
|
||||
via: options.via || '',
|
||||
loginVerificationRequestedAt: options.loginVerificationRequestedAt || null,
|
||||
};
|
||||
}
|
||||
|
||||
function createStep6RecoverableResult(reason, snapshot, options = {}) {
|
||||
return {
|
||||
step6Outcome: 'recoverable',
|
||||
reason,
|
||||
state: snapshot?.state || 'unknown',
|
||||
url: snapshot?.url || location.href,
|
||||
message: options.message || '',
|
||||
loginVerificationRequestedAt: options.loginVerificationRequestedAt || null,
|
||||
};
|
||||
}
|
||||
|
||||
function throwForStep6FatalState(snapshot) {
|
||||
switch (snapshot?.state) {
|
||||
case 'oauth_consent_page':
|
||||
throw new Error(`当前页面已进入 OAuth 授权页,未经过登录验证码页,无法完成步骤 6。URL: ${snapshot.url}`);
|
||||
case 'add_phone_page':
|
||||
throw new Error(`当前页面已进入手机号页面,未经过登录验证码页,无法完成步骤 6。URL: ${snapshot.url}`);
|
||||
case 'unknown':
|
||||
throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerLoginSubmitAction(button, fallbackField) {
|
||||
const form = button?.form || fallbackField?.form || button?.closest?.('form') || fallbackField?.closest?.('form') || null;
|
||||
|
||||
await humanPause(400, 1100);
|
||||
if (button && isActionEnabled(button)) {
|
||||
simulateClick(button);
|
||||
return;
|
||||
}
|
||||
|
||||
if (form && typeof form.requestSubmit === 'function') {
|
||||
if (button && button.form === form) {
|
||||
form.requestSubmit(button);
|
||||
} else {
|
||||
form.requestSubmit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (button && typeof button.click === 'function') {
|
||||
button.click();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('未找到可用的登录提交按钮。URL: ' + location.href);
|
||||
}
|
||||
|
||||
function isSignupPasswordErrorPage() {
|
||||
return Boolean(getSignupPasswordTimeoutErrorPageState());
|
||||
}
|
||||
|
||||
function isSignupEmailAlreadyExistsPage() {
|
||||
@@ -922,10 +1067,7 @@ async function fillVerificationCode(step, payload) {
|
||||
log(`步骤 ${step}:正在填写验证码:${code}`);
|
||||
|
||||
if (step === 7) {
|
||||
const prepareResult = await prepareLoginCodeFlow();
|
||||
if (prepareResult?.restartFromStep6) {
|
||||
return prepareResult;
|
||||
}
|
||||
await waitForLoginVerificationPageReady();
|
||||
}
|
||||
|
||||
// Find code input — could be a single input or multiple separate inputs
|
||||
@@ -986,63 +1128,344 @@ async function fillVerificationCode(step, payload) {
|
||||
// Step 6: Login with registered account (on OAuth auth page)
|
||||
// ============================================================
|
||||
|
||||
async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000) {
|
||||
const start = Date.now();
|
||||
let snapshot = inspectLoginAuthState();
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
snapshot = inspectLoginAuthState();
|
||||
|
||||
if (snapshot.state === 'verification_page') {
|
||||
return {
|
||||
action: 'done',
|
||||
result: createStep6SuccessResult(snapshot, {
|
||||
via: 'email_submit',
|
||||
loginVerificationRequestedAt: emailSubmittedAt,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (snapshot.state === 'password_page') {
|
||||
return { action: 'password', snapshot };
|
||||
}
|
||||
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '提交邮箱后进入登录超时报错页。',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
throw new Error(`提交邮箱后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
}
|
||||
|
||||
if (snapshot.state === 'add_phone_page') {
|
||||
throw new Error(`提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
}
|
||||
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
snapshot = inspectLoginAuthState();
|
||||
if (snapshot.state === 'verification_page') {
|
||||
return {
|
||||
action: 'done',
|
||||
result: createStep6SuccessResult(snapshot, {
|
||||
via: 'email_submit',
|
||||
loginVerificationRequestedAt: emailSubmittedAt,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (snapshot.state === 'password_page') {
|
||||
return { action: 'password', snapshot };
|
||||
}
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '提交邮箱后进入登录超时报错页。',
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
throw new Error(`提交邮箱后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
}
|
||||
if (snapshot.state === 'add_phone_page') {
|
||||
throw new Error(`提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
}
|
||||
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: createStep6RecoverableResult('email_submit_stalled', snapshot, {
|
||||
message: '提交邮箱后长时间未进入密码页或登录验证码页。',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout = 10000) {
|
||||
const start = Date.now();
|
||||
let snapshot = inspectLoginAuthState();
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
snapshot = inspectLoginAuthState();
|
||||
|
||||
if (snapshot.state === 'verification_page') {
|
||||
return {
|
||||
action: 'done',
|
||||
result: createStep6SuccessResult(snapshot, {
|
||||
via: 'password_submit',
|
||||
loginVerificationRequestedAt: passwordSubmittedAt,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '提交密码后进入登录超时报错页。',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
throw new Error(`提交密码后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
}
|
||||
|
||||
if (snapshot.state === 'add_phone_page') {
|
||||
throw new Error(`提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
}
|
||||
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
snapshot = inspectLoginAuthState();
|
||||
if (snapshot.state === 'verification_page') {
|
||||
return {
|
||||
action: 'done',
|
||||
result: createStep6SuccessResult(snapshot, {
|
||||
via: 'password_submit',
|
||||
loginVerificationRequestedAt: passwordSubmittedAt,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '提交密码后进入登录超时报错页。',
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
throw new Error(`提交密码后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
}
|
||||
if (snapshot.state === 'add_phone_page') {
|
||||
throw new Error(`提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
}
|
||||
if (snapshot.state === 'password_page' && snapshot.switchTrigger) {
|
||||
return { action: 'switch', snapshot };
|
||||
}
|
||||
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: createStep6RecoverableResult('password_submit_stalled', snapshot, {
|
||||
message: '提交密码后仍未进入登录验证码页。',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeout = 10000) {
|
||||
const start = Date.now();
|
||||
let snapshot = inspectLoginAuthState();
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
snapshot = inspectLoginAuthState();
|
||||
|
||||
if (snapshot.state === 'verification_page') {
|
||||
return createStep6SuccessResult(snapshot, {
|
||||
via: 'switch_to_one_time_code_login',
|
||||
loginVerificationRequestedAt,
|
||||
});
|
||||
}
|
||||
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '切换到一次性验证码登录后进入登录超时报错页。',
|
||||
});
|
||||
}
|
||||
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
throw new Error(`切换到一次性验证码登录后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
}
|
||||
|
||||
if (snapshot.state === 'add_phone_page') {
|
||||
throw new Error(`切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
}
|
||||
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
snapshot = inspectLoginAuthState();
|
||||
if (snapshot.state === 'verification_page') {
|
||||
return createStep6SuccessResult(snapshot, {
|
||||
via: 'switch_to_one_time_code_login',
|
||||
loginVerificationRequestedAt,
|
||||
});
|
||||
}
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '切换到一次性验证码登录后进入登录超时报错页。',
|
||||
});
|
||||
}
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
throw new Error(`切换到一次性验证码登录后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
}
|
||||
if (snapshot.state === 'add_phone_page') {
|
||||
throw new Error(`切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
}
|
||||
|
||||
return createStep6RecoverableResult('one_time_code_switch_stalled', snapshot, {
|
||||
message: '点击一次性验证码登录后仍未进入登录验证码页。',
|
||||
});
|
||||
}
|
||||
|
||||
async function step6SwitchToOneTimeCodeLogin(snapshot) {
|
||||
const switchTrigger = snapshot?.switchTrigger || findOneTimeCodeLoginTrigger();
|
||||
if (!switchTrigger || !isActionEnabled(switchTrigger)) {
|
||||
return createStep6RecoverableResult('missing_one_time_code_trigger', inspectLoginAuthState(), {
|
||||
message: '当前登录页没有可用的一次性验证码登录入口。',
|
||||
});
|
||||
}
|
||||
|
||||
log('步骤 6:已检测到一次性验证码登录入口,准备切换...');
|
||||
const loginVerificationRequestedAt = Date.now();
|
||||
await humanPause(350, 900);
|
||||
simulateClick(switchTrigger);
|
||||
log('步骤 6:已点击一次性验证码登录');
|
||||
await sleep(1200);
|
||||
return waitForStep6SwitchTransition(loginVerificationRequestedAt);
|
||||
}
|
||||
|
||||
async function step6LoginFromPasswordPage(payload, snapshot) {
|
||||
const currentSnapshot = snapshot || inspectLoginAuthState();
|
||||
|
||||
if (currentSnapshot.passwordInput) {
|
||||
if (!payload.password) {
|
||||
throw new Error('登录时缺少密码,步骤 6 无法继续。');
|
||||
}
|
||||
|
||||
log('步骤 6:已进入密码页,准备填写密码...');
|
||||
await humanPause(550, 1450);
|
||||
fillInput(currentSnapshot.passwordInput, payload.password);
|
||||
log('步骤 6:已填写密码');
|
||||
|
||||
await sleep(500);
|
||||
const passwordSubmittedAt = Date.now();
|
||||
await triggerLoginSubmitAction(currentSnapshot.submitButton, currentSnapshot.passwordInput);
|
||||
log('步骤 6:已提交密码');
|
||||
|
||||
const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt);
|
||||
if (transition.action === 'done') {
|
||||
log('步骤 6:已进入登录验证码页面。', 'ok');
|
||||
return transition.result;
|
||||
}
|
||||
if (transition.action === 'recoverable') {
|
||||
log(`步骤 6:${transition.result.message || '提交密码后仍未进入登录验证码页面,准备重新执行步骤 6。'}`, 'warn');
|
||||
return transition.result;
|
||||
}
|
||||
if (transition.action === 'switch') {
|
||||
return step6SwitchToOneTimeCodeLogin(transition.snapshot);
|
||||
}
|
||||
|
||||
return createStep6RecoverableResult('password_submit_unknown', inspectLoginAuthState(), {
|
||||
message: '提交密码后未得到可用的下一步状态。',
|
||||
});
|
||||
}
|
||||
|
||||
if (currentSnapshot.switchTrigger) {
|
||||
return step6SwitchToOneTimeCodeLogin(currentSnapshot);
|
||||
}
|
||||
|
||||
return createStep6RecoverableResult('password_page_unactionable', currentSnapshot, {
|
||||
message: '当前停留在登录页,但没有可提交密码的输入框,也没有一次性验证码登录入口。',
|
||||
});
|
||||
}
|
||||
|
||||
async function step6LoginFromEmailPage(payload, snapshot) {
|
||||
const currentSnapshot = snapshot || inspectLoginAuthState();
|
||||
const emailInput = currentSnapshot.emailInput || getLoginEmailInput();
|
||||
if (!emailInput) {
|
||||
throw new Error('在登录页未找到邮箱输入框。URL: ' + location.href);
|
||||
}
|
||||
|
||||
if ((emailInput.value || '').trim() !== payload.email) {
|
||||
await humanPause(500, 1400);
|
||||
fillInput(emailInput, payload.email);
|
||||
log('步骤 6:已填写邮箱');
|
||||
} else {
|
||||
log('步骤 6:邮箱已在输入框中,准备提交...');
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
const emailSubmittedAt = Date.now();
|
||||
await triggerLoginSubmitAction(currentSnapshot.submitButton, emailInput);
|
||||
log('步骤 6:已提交邮箱');
|
||||
|
||||
const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt);
|
||||
if (transition.action === 'done') {
|
||||
log('步骤 6:已进入登录验证码页面。', 'ok');
|
||||
return transition.result;
|
||||
}
|
||||
if (transition.action === 'recoverable') {
|
||||
log(`步骤 6:${transition.result.message || '提交邮箱后仍未进入目标页面,准备重新执行步骤 6。'}`, 'warn');
|
||||
return transition.result;
|
||||
}
|
||||
if (transition.action === 'password') {
|
||||
return step6LoginFromPasswordPage(payload, transition.snapshot);
|
||||
}
|
||||
|
||||
return createStep6RecoverableResult('email_submit_unknown', inspectLoginAuthState(), {
|
||||
message: '提交邮箱后未得到可用的下一步状态。',
|
||||
});
|
||||
}
|
||||
|
||||
async function step6_login(payload) {
|
||||
const { email, password } = payload;
|
||||
const { email } = payload;
|
||||
if (!email) throw new Error('登录时缺少邮箱地址。');
|
||||
|
||||
log(`步骤 6:正在使用 ${email} 登录...`);
|
||||
|
||||
// Wait for email input on the auth page
|
||||
let emailInput = null;
|
||||
try {
|
||||
emailInput = await waitForElement(
|
||||
'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]',
|
||||
15000
|
||||
);
|
||||
} catch {
|
||||
throw new Error('在登录页未找到邮箱输入框。URL: ' + location.href);
|
||||
const snapshot = await waitForKnownLoginAuthState(15000);
|
||||
|
||||
if (snapshot.state === 'verification_page') {
|
||||
log('步骤 6:登录验证码页面已就绪。', 'ok');
|
||||
return createStep6SuccessResult(snapshot, { via: 'already_on_verification_page' });
|
||||
}
|
||||
|
||||
await humanPause(500, 1400);
|
||||
fillInput(emailInput, email);
|
||||
log('步骤 6:邮箱已填写');
|
||||
|
||||
// Submit email
|
||||
await sleep(500);
|
||||
const submitBtn1 = document.querySelector('button[type="submit"]')
|
||||
|| await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
|
||||
if (submitBtn1) {
|
||||
await humanPause(400, 1100);
|
||||
simulateClick(submitBtn1);
|
||||
log('步骤 6:邮箱已提交');
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
log('步骤 6:检测到登录超时报错,准备重新执行步骤 6。', 'warn');
|
||||
return createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '当前页面处于登录超时报错页。',
|
||||
});
|
||||
}
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
// Check for password field
|
||||
const passwordInput = document.querySelector('input[type="password"]');
|
||||
if (passwordInput) {
|
||||
log('步骤 6:已找到密码输入框,正在填写密码...');
|
||||
await humanPause(550, 1450);
|
||||
fillInput(passwordInput, password);
|
||||
|
||||
await sleep(500);
|
||||
const submitBtn2 = document.querySelector('button[type="submit"]')
|
||||
|| await waitForElementByText('button', /continue|log\s*in|submit|sign\s*in|登录|继续/i, 5000).catch(() => null);
|
||||
// Report complete BEFORE submit in case page navigates
|
||||
reportComplete(6, { needsOTP: true });
|
||||
|
||||
if (submitBtn2) {
|
||||
await humanPause(450, 1200);
|
||||
simulateClick(submitBtn2);
|
||||
log('步骤 6:密码已提交,可能还需要验证码(步骤 7)');
|
||||
}
|
||||
return;
|
||||
if (snapshot.state === 'email_page') {
|
||||
return step6LoginFromEmailPage(payload, snapshot);
|
||||
}
|
||||
|
||||
// No password field — OTP flow
|
||||
log('步骤 6:未发现密码输入框,可能进入验证码流程或自动跳转。');
|
||||
reportComplete(6, { needsOTP: true });
|
||||
if (snapshot.state === 'password_page') {
|
||||
return step6LoginFromPasswordPage(payload, snapshot);
|
||||
}
|
||||
|
||||
throwForStep6FatalState(snapshot);
|
||||
throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
+69
-32
@@ -26,6 +26,7 @@
|
||||
10. 默认不编译测试。处理完成后提醒用户自行测试。
|
||||
11. 任何 GitHub 评论、回复、感谢留言发出后,AI 都必须立即再读一遍线上实际内容,确认正文不是乱码、不是 `?`、不是编码异常;如果发现异常,必须立刻修正后再继续后续流程。
|
||||
12. 如果流程执行过程中,PR 的实时目标分支被别人改掉,或者不再是 `dev`,AI 必须停止当前合并流程,重新拉取信息后再决定下一步。
|
||||
13. 进入“阶段 2:分析与审查”时,AI 必须先给当前 PR 添加 `审查中` 标签,并在开始正式审查前确认该标签已经在线上生效。
|
||||
|
||||
## 输入要求
|
||||
|
||||
@@ -53,7 +54,7 @@ AI 必须先执行下面这些动作,不能跳步:
|
||||
2. 拉取 PR 元数据:
|
||||
|
||||
```powershell
|
||||
gh pr view <PR_NUMBER> --repo <OWNER/REPO> --json number,title,body,author,baseRefName,headRefName,headRepository,headRepositoryOwner,changedFiles,additions,deletions,commits,files,isDraft,mergeStateStatus,mergeable,state,url
|
||||
gh pr view <PR_NUMBER> --repo <OWNER/REPO> --json number,title,body,author,baseRefName,headRefName,headRepository,headRepositoryOwner,changedFiles,additions,deletions,commits,files,labels,isDraft,mergeStateStatus,mergeable,state,url
|
||||
```
|
||||
|
||||
3. 先根据实时 `baseRefName` 处理目标分支:
|
||||
@@ -107,27 +108,37 @@ git worktree add --detach $TempWorktree origin/pr-<PR_NUMBER>
|
||||
|
||||
AI 分析时,必须完整执行下面这些要求:
|
||||
|
||||
1. 不能只看 PR 描述,必须结合真实 diff 和仓库现有代码上下文一起分析。
|
||||
2. 如果阶段 1 发生过 `master -> dev` 自动转向,分析时只能基于转向后的最新 diff,不得继续引用转向前的 diff 结论。
|
||||
3. 对于高风险文件,必须继续读取改动函数周边逻辑,确认消息流、状态流、配置项、回调流程、页面跳转流程是否一致。
|
||||
4. 必须分析并输出:
|
||||
1. 审查一开始必须先执行:
|
||||
|
||||
```powershell
|
||||
gh pr edit <PR_NUMBER> --add-label "审查中" --repo <OWNER/REPO>
|
||||
```
|
||||
|
||||
补充要求:
|
||||
|
||||
- 打标后必须立刻重新读取一次实时 PR 元数据,确认 `labels` 中已经出现 `审查中`。
|
||||
- 如果仓库里没有 `审查中` 标签、当前账号无权限打标,或者命令执行失败,必须立刻告知用户,不能假装已经开始审查。
|
||||
2. 不能只看 PR 描述,必须结合真实 diff 和仓库现有代码上下文一起分析。
|
||||
3. 如果阶段 1 发生过 `master -> dev` 自动转向,分析时只能基于转向后的最新 diff,不得继续引用转向前的 diff 结论。
|
||||
4. 对于高风险文件,必须继续读取改动函数周边逻辑,确认消息流、状态流、配置项、回调流程、页面跳转流程是否一致。
|
||||
5. 必须分析并输出:
|
||||
- 这个 PR 改了什么
|
||||
- 这些改动是否合理
|
||||
- 是否存在 bug / 风险 / 逻辑冲突
|
||||
- PR 当前是否可直接合并,例如 `mergeable`、`mergeStateStatus`
|
||||
5. 如果发现问题,结论必须按严重级别排序,优先写真正会影响功能、合并或后续维护的问题。
|
||||
6. 如果没有发现明确问题,也要说明剩余风险,例如:
|
||||
6. 如果发现问题,结论必须按严重级别排序,优先写真正会影响功能、合并或后续维护的问题。
|
||||
7. 如果没有发现明确问题,也要说明剩余风险,例如:
|
||||
- 未运行测试
|
||||
- 需要人工验证真实业务接口
|
||||
- 当前仅完成静态分析
|
||||
7. 在准备进入后续合并步骤前,必须再拉取一次实时 PR 元数据,确认:
|
||||
8. 在准备进入后续合并步骤前,必须再拉取一次实时 PR 元数据,确认:
|
||||
- PR 仍然是打开状态
|
||||
- PR 不是 draft
|
||||
- `baseRefName` 仍然是 `dev`
|
||||
|
||||
## 分析后评论规则
|
||||
|
||||
### 有问题时
|
||||
### 有问题 / 审核不通过时
|
||||
|
||||
如果发现需要作者或维护者注意的问题,AI 必须自动发 PR 评论,并且评论格式必须固定如下:
|
||||
|
||||
@@ -144,6 +155,10 @@ AI 分析时,必须完整执行下面这些要求:
|
||||
3. 正文内容要直接写问题,不要再套娃解释“下面是分析结果”。
|
||||
4. 评论内容必须基于实际检查到的问题,不能编。
|
||||
5. 评论发出后,必须立刻回读该评论的线上正文,确认不是乱码;如果有乱码,必须先修正评论,再继续后续动作。
|
||||
6. 如果问题能够明确定位到某个改动文件、代码块或行号,AI 应优先在对应文件的代码 diff / 代码附件上追加行级评论,直接指出问题和期望修改方式。
|
||||
7. 如果结论是“当前不能通过审查”,AI 必须明确写出“需要修改后再继续”,不能只写模糊提醒。
|
||||
8. 如果当前环境支持正式 PR review,且问题足以阻止合并,优先使用带“要求更改(Request changes)”语义的审查,而不是只留普通闲聊式评论。
|
||||
9. 行级评论是对总评论的补充,不得用零散行级评论替代总评论结论。
|
||||
|
||||
### 没问题时
|
||||
|
||||
@@ -158,49 +173,52 @@ AI 分析时,必须完整执行下面这些要求:
|
||||
如果 PR 有问题,但用户明确要求:
|
||||
|
||||
- 不等待 PR 作者修复
|
||||
- 由当前 AI 直接本地处理冲突和问题
|
||||
- 由当前 AI 直接修改 PR 分支代码,处理冲突和问题
|
||||
- 处理完成后继续合并
|
||||
|
||||
则必须进入下面的流程。
|
||||
|
||||
### 阶段 3:临时工作区合并
|
||||
### 阶段 3:临时工作区修复 PR 分支
|
||||
|
||||
禁止在用户当前工作区直接乱合并。
|
||||
|
||||
必须先创建临时 `worktree`,再在临时目录内处理:
|
||||
必须先创建临时 `worktree`,再在临时目录内处理 PR 分支:
|
||||
|
||||
```powershell
|
||||
git fetch origin dev
|
||||
git fetch origin pull/<PR_NUMBER>/head:refs/remotes/origin/pr-<PR_NUMBER>
|
||||
git worktree add <TEMP_WORKTREE> -b pr-<PR_NUMBER>-merge origin/dev
|
||||
git worktree add <TEMP_WORKTREE> -b pr-<PR_NUMBER>-fix origin/pr-<PR_NUMBER>
|
||||
```
|
||||
|
||||
进入临时目录后再执行:
|
||||
进入临时目录后,先把最新 `dev` 合到 PR 分支里,显式暴露冲突:
|
||||
|
||||
```powershell
|
||||
git merge --no-ff --no-commit origin/pr-<PR_NUMBER>
|
||||
git merge --no-ff --no-commit origin/dev
|
||||
```
|
||||
|
||||
处理规则:
|
||||
|
||||
1. 如果阶段 1 发生过 `master -> dev` 自动转向,本阶段必须以 `dev` 为唯一合并基准,不允许再回到 `master` 做本地吸收。
|
||||
2. 如果出现冲突,先解决冲突,再继续检查相关联逻辑。
|
||||
2. 如果出现冲突,先在 PR 分支上解决冲突,再继续检查相关联逻辑。
|
||||
3. 不能只消掉冲突标记就结束,必须继续看是否有设计冲突、状态字段不一致、调用链断裂、配置项名不一致、回调逻辑互相打架的问题。
|
||||
4. 如果 PR 本身逻辑有 bug,而用户又明确要求继续合并,AI 需要直接在本地修正。
|
||||
5. 修正时要清理无用旧代码,避免留下史山。
|
||||
6. 如果改了 SQL,按仓库规则同步本地 MySQL `xzs`。
|
||||
4. 如果 PR 本身逻辑有 bug,而用户又明确要求继续合并,AI 需要直接修改 PR 分支代码并修正。
|
||||
5. 修正完成后,必须把修改提交回 PR 分支并推送到远端;如果没有权限推送到 PR 来源分支,必须立刻停止并明确告知用户,不能假装 PR 已修好。
|
||||
6. 推送完成后,必须重新拉取一次实时 PR 元数据与最新 diff,确认线上 PR 已包含本次修复,再决定是否继续合并。
|
||||
7. 修正时要清理无用旧代码,避免留下史山。
|
||||
8. 如果改了 SQL,按仓库规则同步本地 MySQL `xzs`。
|
||||
|
||||
### 阶段 4:本地修复后的自检清单
|
||||
### 阶段 4:PR 修复推送后的自检清单
|
||||
|
||||
完成临时合并与本地修复后,必须自检下面这些项目:
|
||||
完成 PR 分支修复并推送后,必须自检下面这些项目:
|
||||
|
||||
1. `git status` 中不能再有未处理冲突。
|
||||
2. 相关文件中不能残留冲突标记。
|
||||
3. 新旧逻辑之间不能出现字段名、消息名、步骤编号、状态名不一致。
|
||||
4. 需要检查与本次功能直接相关的上下游代码,不能只改当前冲突文件。
|
||||
5. 要重新查看最终 diff,确认本地修复没有引入明显回归。
|
||||
5. 要重新查看最终线上 diff,确认修复后的 PR 没有引入明显回归。
|
||||
6. 要重新拉取一次实时 PR 元数据,确认该 PR 的 `baseRefName` 仍然是 `dev`;如果不是,停止后续合并并先反馈用户。
|
||||
7. 默认不编译测试,最后提醒用户自行测试。
|
||||
7. 要确认 PR 当前已经回到可继续处理的状态,例如不再是 `draft`,且 `mergeable` / `mergeStateStatus` 没有出现新的阻塞。
|
||||
8. 默认不编译测试,最后提醒用户自行测试。
|
||||
|
||||
## 合并提交信息规则
|
||||
|
||||
@@ -212,6 +230,8 @@ git merge --no-ff --no-commit origin/pr-<PR_NUMBER>
|
||||
|
||||
必须重新分析“最终合并结果到底做了什么”,然后重写提交信息。
|
||||
|
||||
这些规则同样适用于最终执行 `gh pr merge` 时使用的标题与正文。
|
||||
|
||||
### 提交信息要求
|
||||
|
||||
1. 标题必须描述最终落地功能,不是描述 Git 动作。
|
||||
@@ -243,9 +263,23 @@ feat: support SUB2API mode for OAuth generation and callback handling
|
||||
|
||||
## 最终合并与收尾
|
||||
|
||||
如果本地已经解决该 PR 的内容,并且最终代码已经进入 `dev` 分支,则继续执行下面动作。
|
||||
如果 PR 分支上的冲突 / 问题已经修好,并且复检确认该 PR 可以继续合并到 `dev`,则继续执行下面动作。
|
||||
|
||||
### 1. 感谢作者
|
||||
### 1. 合并 PR
|
||||
|
||||
优先直接合并这个 PR,而不是只在本地偷偷吸收代码后手工关闭:
|
||||
|
||||
```powershell
|
||||
gh pr merge <PR_NUMBER> --merge --subject "<TITLE>" --body "<BODY>" --repo <OWNER/REPO>
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
1. `--subject` 与 `--body` 必须遵守上面的“合并提交信息规则”。
|
||||
2. 合并前必须再次确认 PR 仍然是打开状态、目标分支仍然是 `dev`、线上 diff 已包含你刚刚推送的修复。
|
||||
3. 如果合并时发现新的冲突、状态检查阻塞或权限问题,必须停止并把真实阻塞原因反馈给用户。
|
||||
|
||||
### 2. 感谢作者
|
||||
|
||||
此时需要自动给 PR 留一条感谢评论。
|
||||
|
||||
@@ -263,7 +297,7 @@ feat: support SUB2API mode for OAuth generation and callback handling
|
||||
感谢贡献这次改动,核心思路和主体实现已经吸收进 dev 分支了。我这边补了一下合并过程里的冲突和相关修正,后续如果你还有类似改进也欢迎继续提交。
|
||||
```
|
||||
|
||||
### 2. 自动关闭 PR
|
||||
### 3. 自动关闭 PR
|
||||
|
||||
如果 PR 还没有因为合并而自动关闭,则感谢评论发完后,自动关闭该 PR:
|
||||
|
||||
@@ -273,7 +307,7 @@ gh pr close <PR_NUMBER> --repo <OWNER/REPO>
|
||||
|
||||
如果需要,先评论再关闭,不要把感谢遗漏掉。
|
||||
|
||||
### 3. 评论编码复检
|
||||
### 4. 评论编码复检
|
||||
|
||||
无论是问题评论、感谢评论,还是其他直接发到 GitHub 的回复,只要消息已经发出,AI 必须执行一次“发送后复检”:
|
||||
|
||||
@@ -291,11 +325,14 @@ AI 在对当前用户做最终反馈时,至少要说明:
|
||||
3. 是否已经执行 `master -> dev` 自动转向
|
||||
4. 是否发现重复的 `dev` PR
|
||||
5. 是否发现问题
|
||||
6. 是否已经发了 PR 评论
|
||||
7. 是否已经本地合并并修复
|
||||
8. 是否已经关闭 PR
|
||||
9. 如果改了代码但没跑测试,要明确提醒用户测试
|
||||
6. 是否已经给 PR 添加 `审查中` 标签
|
||||
7. 是否已经发了 PR 评论 / 行级代码评论
|
||||
8. 是否已经要求 PR 修改后再继续
|
||||
9. 是否已经修改 PR 分支并推回远端
|
||||
10. 是否已经完成 PR 合并
|
||||
11. 是否已经关闭 PR
|
||||
12. 如果改了代码但没跑测试,要明确提醒用户测试
|
||||
|
||||
## 给 AI 的一句话执行要求
|
||||
|
||||
拿到本文件后,AI 必须按“先真实读取 PR 元数据,先校正目标分支,再拉取最新 diff 做分析,再按用户要求决定是否进入临时合并修复,最后重写提交信息并在完成后感谢作者、关闭 PR”的顺序执行,不能跳步,不能猜,不能偷懒。
|
||||
拿到本文件后,AI 必须按“先真实读取 PR 元数据,先校正目标分支,再拉取最新 diff,在正式审查开始时先给 PR 打上 `审查中` 标签并确认已生效;如果审核不通过,就在对应代码附件上评论并明确要求更改;如果用户要求继续处理冲突和问题,就先修好并推回 PR 分支,再按规则合并 PR,最后感谢作者、关闭 PR”的顺序执行,不能跳步,不能猜,不能偷懒。
|
||||
|
||||
Reference in New Issue
Block a user