feat: add configurable operation delay

This commit is contained in:
root
2026-05-08 12:50:20 -04:00
parent 872f382815
commit c23461261b
44 changed files with 4770 additions and 193 deletions
+344 -96
View File
@@ -6,6 +6,14 @@ console.log('[MultiPage:signup-page] Content script loaded on', location.href);
const SIGNUP_PAGE_LISTENER_SENTINEL = 'data-multipage-signup-page-listener';
function getOperationDelayRunner() {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function'
? gate
: async (_metadata, operation) => operation();
}
if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1') {
document.documentElement.setAttribute(SIGNUP_PAGE_LISTENER_SENTINEL, '1');
@@ -251,6 +259,13 @@ function isEmailVerificationPage() {
}
async function resendVerificationCode(step, timeout = 45000) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
if (step === 8) {
await waitForLoginVerificationPageReady(10000, step);
}
@@ -275,7 +290,9 @@ async function resendVerificationCode(step, timeout = 45000) {
if (action && isActionEnabled(action)) {
log(`步骤 ${step}:重新发送验证码按钮已可用。`);
await humanPause(350, 900);
simulateClick(action);
await performOperationWithDelay({ stepKey: step === 8 ? 'oauth-login' : 'fetch-signup-code', kind: 'click', label: 'resend-verification-code' }, async () => {
simulateClick(action);
});
await sleep(1200);
// After clicking resend, check if 405 error appeared
@@ -955,6 +972,13 @@ function logSignupPasswordDiagnostics(context, level = 'warn') {
}
async function waitForSignupEntryState(options = {}) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const {
timeout = 15000,
autoOpenEntry = false,
@@ -995,7 +1019,9 @@ async function waitForSignupEntryState(options = {}) {
}
log('步骤 2:检测到手机号输入模式,正在切换到邮箱输入模式...');
await humanPause(350, 900);
simulateClick(snapshot.switchToEmailTrigger);
await performOperationWithDelay({ stepKey: 'signup-entry', kind: 'click', label: 'switch-to-signup-email' }, async () => {
simulateClick(snapshot.switchToEmailTrigger);
});
} else if (!snapshot.switchToEmailTrigger && !loggedMissingSwitchToEmail) {
loggedMissingSwitchToEmail = true;
log('步骤 2:检测到手机号输入模式,但暂未识别到“改用邮箱/继续使用电子邮件地址登录”按钮,继续等待界面稳定...', 'warn');
@@ -1023,7 +1049,9 @@ async function waitForSignupEntryState(options = {}) {
}
log('步骤 2:正在点击官网注册入口...');
await humanPause(350, 900);
simulateClick(snapshot.signupTrigger);
await performOperationWithDelay({ stepKey: 'signup-entry', kind: 'click', label: 'open-signup-entry' }, async () => {
simulateClick(snapshot.signupTrigger);
});
}
}
@@ -1093,6 +1121,13 @@ async function ensureSignupPasswordPageReady(timeout = 20000) {
}
async function fillSignupEmailAndContinue(email, step) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
if (!email) throw new Error(`未提供邮箱地址,步骤 ${step} 无法继续。`);
const normalizedEmail = String(email || '').trim().toLowerCase();
@@ -1123,7 +1158,9 @@ async function fillSignupEmailAndContinue(email, step) {
log(`步骤 ${step}:正在填写邮箱:${email}`);
await humanPause(500, 1400);
fillInput(snapshot.emailInput, email);
await performOperationWithDelay({ stepKey: step === 2 ? 'signup-entry' : 'fill-password', kind: 'fill', label: 'signup-email' }, async () => {
fillInput(snapshot.emailInput, email);
});
log(`步骤 ${step}:邮箱已填写`);
const continueButton = snapshot.continueButton || getSignupEmailContinueButton({ allowDisabled: true });
@@ -1132,16 +1169,18 @@ async function fillSignupEmailAndContinue(email, step) {
}
log(`步骤 ${step}:邮箱已准备提交,正在前往密码页...`);
window.setTimeout(() => {
try {
throwIfStopped();
try {
await sleep(120);
throwIfStopped();
await performOperationWithDelay({ stepKey: step === 2 ? 'signup-entry' : 'fill-password', kind: 'submit', label: 'submit-signup-email' }, async () => {
simulateClick(continueButton);
} catch (error) {
if (!isStopError(error)) {
console.error('[MultiPage:signup-page] deferred signup email submit failed:', error?.message || error);
}
});
} catch (error) {
if (!isStopError(error)) {
console.error('[MultiPage:signup-page] signup email submit failed:', error?.message || error);
}
}, 120);
throw error;
}
return {
submitted: true,
@@ -1655,6 +1694,13 @@ function findSignupPhoneCountryOptionByPhoneNumber(phoneInput, phoneNumber) {
}
async function trySelectSignupPhoneCountryOption(select, targetOption, phoneInput = getSignupPhoneInput(), options = {}) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
if (!select || !targetOption) {
return false;
}
@@ -1662,12 +1708,16 @@ async function trySelectSignupPhoneCountryOption(select, targetOption, phoneInpu
? (select.options?.[select.selectedIndex] || null)
: null;
if (selectedOption && isSameSignupCountryOption(selectedOption, targetOption)) {
dispatchSignupPhoneFieldEvents(select);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'select', label: 'signup-phone-country-select' }, async () => {
dispatchSignupPhoneFieldEvents(select);
});
await sleep(120);
return isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options);
}
select.value = String(targetOption.value || '');
dispatchSignupPhoneFieldEvents(select);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'select', label: 'signup-phone-country-select' }, async () => {
select.value = String(targetOption.value || '');
dispatchSignupPhoneFieldEvents(select);
});
await sleep(250);
return isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options);
}
@@ -1722,6 +1772,13 @@ function findSignupPhoneCountryListboxOption(targetOption, options = {}) {
}
async function trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption, options = {}) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const button = getSignupPhoneCountryButton(phoneInput);
if (!button) {
return false;
@@ -1802,7 +1859,9 @@ async function trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption
return scrolled;
};
simulateClick(button);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'click', label: 'open-signup-phone-country-listbox' }, async () => {
simulateClick(button);
});
await sleep(200);
resetListboxScroll();
@@ -1812,7 +1871,9 @@ async function trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption
throwIfStopped();
const option = findSignupPhoneCountryListboxOption(targetOption, options);
if (option) {
simulateClick(option);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'select', label: 'signup-phone-country-listbox-option' }, async () => {
simulateClick(option);
});
await sleep(450);
if (isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options)) {
return true;
@@ -2108,6 +2169,13 @@ function getLoginPhoneInputCandidateDiagnostics(limit = 12) {
}
async function fillLoginPhoneInputAndConfirm(phoneInput, options = {}) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const {
phoneNumber = '',
dialCode = '',
@@ -2132,7 +2200,9 @@ async function fillLoginPhoneInputAndConfirm(phoneInput, options = {}) {
const fillCandidates = getLoginPhoneFillCandidates(phoneNumber, dialCode, currentInput);
for (const attemptedValue of fillCandidates) {
currentInput.focus?.();
fillInput(currentInput, attemptedValue);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'fill', label: 'login-phone-number' }, async () => {
fillInput(currentInput, attemptedValue);
});
lastVerification = await waitForPhoneInputValue(currentInput, inputValue, {
resolvePhoneInput,
phoneNumber,
@@ -2142,7 +2212,9 @@ async function fillLoginPhoneInputAndConfirm(phoneInput, options = {}) {
});
if (lastVerification.ok) {
const verifiedInput = lastVerification.input || currentInput;
const hiddenSync = syncPhoneHiddenFormValue(verifiedInput, { phoneNumber, dialCode, inputValue });
const hiddenSync = await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'hidden-sync', label: 'login-phone-hidden-sync' }, async () => (
syncPhoneHiddenFormValue(verifiedInput, { phoneNumber, dialCode, inputValue })
));
const expectedHiddenDigits = normalizePhoneDigits(phoneNumber) || `${normalizePhoneDigits(dialCode)}${normalizePhoneDigits(inputValue)}`;
if (hiddenSync && expectedHiddenDigits && normalizePhoneDigits(hiddenSync.value) !== expectedHiddenDigits) {
throw new Error(`\u6b65\u9aa4 ${visibleStep}\uff1a\u624b\u673a\u53f7\u9690\u85cf\u63d0\u4ea4\u5b57\u6bb5\u540c\u6b65\u5931\u8d25\uff0c\u671f\u671b ${expectedHiddenDigits}\uff0c\u5b9e\u9645 ${normalizePhoneDigits(hiddenSync.value) || '\u7a7a'}\u3002`);
@@ -2198,6 +2270,13 @@ function resolveSignupPhoneDialCode(phoneInput, options = {}) {
}
async function waitForSignupPhoneEntryState(options = {}) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const {
timeout = 20000,
step = 2,
@@ -2226,14 +2305,18 @@ async function waitForSignupPhoneEntryState(options = {}) {
lastSwitchToPhoneAt = Date.now();
log(`步骤 ${step}:检测到邮箱输入模式,正在切换到手机号注册入口...`);
await humanPause(350, 900);
simulateClick(switchToPhone);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'click', label: 'switch-to-signup-phone' }, async () => {
simulateClick(switchToPhone);
});
} else {
const moreOptionsTrigger = findSignupMoreOptionsTrigger();
if (moreOptionsTrigger && Date.now() - lastMoreOptionsClickAt >= 1500) {
lastMoreOptionsClickAt = Date.now();
log(`步骤 ${step}:手机号入口可能隐藏在更多选项中,正在展开更多选项...`);
await humanPause(350, 900);
simulateClick(moreOptionsTrigger);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'click', label: 'signup-phone-more-options' }, async () => {
simulateClick(moreOptionsTrigger);
});
} else if (!switchToPhone && !slowSnapshotLogged && Date.now() - start >= 5000) {
slowSnapshotLogged = true;
log(`步骤 ${step}:尚未找到手机号入口,页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn');
@@ -2248,7 +2331,9 @@ async function waitForSignupPhoneEntryState(options = {}) {
lastTriggerClickAt = Date.now();
log(`步骤 ${step}:正在点击官网注册入口...`);
await humanPause(350, 900);
simulateClick(snapshot.signupTrigger);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'click', label: 'open-signup-entry' }, async () => {
simulateClick(snapshot.signupTrigger);
});
}
await sleep(250);
continue;
@@ -2268,6 +2353,13 @@ async function waitForSignupPhoneEntryState(options = {}) {
}
async function submitSignupPhoneNumberAndContinue(payload = {}) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const phoneNumber = String(payload.phoneNumber || '').trim();
const countryLabel = String(payload.countryLabel || '').trim();
if (!phoneNumber) {
@@ -2312,11 +2404,15 @@ async function submitSignupPhoneNumberAndContinue(payload = {}) {
log(`步骤 2:正在填写手机号:${phoneNumber}`);
await humanPause(500, 1400);
fillInput(snapshot.phoneInput, inputValue);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'fill', label: 'signup-phone-number' }, async () => {
fillInput(snapshot.phoneInput, inputValue);
});
const hiddenPhoneNumberInput = getSignupPhoneHiddenNumberInput(snapshot.phoneInput);
const e164PhoneNumber = toE164PhoneNumber(phoneNumber, dialCode);
if (hiddenPhoneNumberInput && e164PhoneNumber) {
fillInput(hiddenPhoneNumberInput, e164PhoneNumber);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'hidden-sync', label: 'signup-phone-hidden-sync' }, async () => {
fillInput(hiddenPhoneNumberInput, e164PhoneNumber);
});
}
log(`步骤 2:手机号已填写:${phoneNumber}${dialCode ? `(区号 +${dialCode},本地号 ${inputValue}` : ''}`);
@@ -2326,16 +2422,18 @@ async function submitSignupPhoneNumberAndContinue(payload = {}) {
}
log('步骤 2:手机号已准备提交,正在前往下一页...');
window.setTimeout(() => {
try {
throwIfStopped();
try {
await sleep(120);
throwIfStopped();
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'submit', label: 'submit-signup-phone' }, async () => {
simulateClick(continueButton);
} catch (error) {
if (!isStopError(error)) {
console.error('[MultiPage:signup-page] deferred signup phone submit failed:', error?.message || error);
}
});
} catch (error) {
if (!isStopError(error)) {
console.error('[MultiPage:signup-page] signup phone submit failed:', error?.message || error);
}
}, 120);
throw error;
}
return {
submitted: true,
@@ -2363,6 +2461,13 @@ async function step2_clickRegister(payload = {}) {
// ============================================================
async function step3_fillEmailPassword(payload) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const { email, password } = payload;
if (!password) throw new Error('未提供密码,步骤 3 需要可用密码。');
const normalizedEmail = String(email || '').trim().toLowerCase();
@@ -2426,7 +2531,9 @@ async function step3_fillEmailPassword(payload) {
}
await humanPause(600, 1500);
fillInput(snapshot.passwordInput, password);
await performOperationWithDelay({ stepKey: 'fill-password', kind: 'fill', label: 'signup-password' }, async () => {
fillInput(snapshot.passwordInput, password);
});
log('步骤 3:密码已填写');
const submitBtn = snapshot.submitButton
@@ -2439,8 +2546,6 @@ async function step3_fillEmailPassword(payload) {
logSignupPasswordDiagnostics('步骤 3:当前密码页同时存在一次性验证码入口', 'info');
}
// Report complete BEFORE submit, because submit causes page navigation
// which kills the content script connection
const signupVerificationRequestedAt = submitBtn ? Date.now() : null;
const completionPayload = {
email,
@@ -2450,6 +2555,7 @@ async function step3_fillEmailPassword(payload) {
signupVerificationRequestedAt,
deferredSubmit: Boolean(submitBtn),
};
reportComplete(3, completionPayload);
if (submitBtn) {
@@ -2458,7 +2564,9 @@ async function step3_fillEmailPassword(payload) {
throwIfStopped();
await sleep(500);
await humanPause(500, 1300);
simulateClick(submitBtn);
await performOperationWithDelay({ stepKey: 'fill-password', kind: 'submit', label: 'submit-signup-password' }, async () => {
simulateClick(submitBtn);
});
log('步骤 3:表单已提交');
} catch (error) {
if (!isStopError(error)) {
@@ -3282,6 +3390,13 @@ function getCurrentAuthRetryPageState(flow = 'auth') {
}
async function recoverCurrentAuthRetryPage(payload = {}) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const {
flow = 'auth',
logLabel = '',
@@ -3300,6 +3415,7 @@ async function recoverCurrentAuthRetryPage(payload = {}) {
maxClickAttempts,
pathPatterns: resolvedPathPatterns,
step,
stepKey: step === 8 || flow === 'login' ? 'oauth-login' : 'fetch-signup-code',
timeoutMs,
waitAfterClickMs,
});
@@ -3332,7 +3448,9 @@ async function recoverCurrentAuthRetryPage(payload = {}) {
clickCount += 1;
log(`${logLabel || `步骤 ${step || '?'}:检测到重试页,正在点击“重试”恢复`}(第 ${clickCount} 次)...`, 'warn');
await humanPause(300, 800);
simulateClick(retryState.retryButton);
await performOperationWithDelay({ stepKey: step === 8 || flow === 'login' ? 'oauth-login' : 'fetch-signup-code', kind: 'click', label: 'auth-retry-click' }, async () => {
simulateClick(retryState.retryButton);
});
const settleStart = Date.now();
while (Date.now() - settleStart < waitAfterClickMs) {
throwIfStopped();
@@ -4369,28 +4487,37 @@ function throwForStep6FatalState(snapshot, visibleStep = 7) {
async function triggerLoginSubmitAction(button, fallbackField) {
const form = button?.form || fallbackField?.form || button?.closest?.('form') || fallbackField?.closest?.('form') || null;
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
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();
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'submit', label: 'login-submit' }, async () => {
if (button && isActionEnabled(button)) {
simulateClick(button);
return;
}
return;
}
if (button && typeof button.click === 'function') {
button.click();
return;
}
if (form && typeof form.requestSubmit === 'function') {
if (button && button.form === form) {
form.requestSubmit(button);
} else {
form.requestSubmit();
}
return;
}
throw new Error('未找到可用的登录提交按钮。URL: ' + location.href);
if (button && typeof button.click === 'function') {
button.click();
return;
}
throw new Error('未找到可用的登录提交按钮。URL: ' + location.href);
});
}
function isSignupPasswordErrorPage() {
@@ -4480,6 +4607,13 @@ async function waitForSignupVerificationTransition(timeout = 5000) {
}
async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const { password } = payload;
const prepareSource = String(payload?.prepareSource || '').trim() || 'step4_execute';
const prepareLogLabel = String(payload?.prepareLogLabel || '').trim()
@@ -4554,13 +4688,17 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
if ((snapshot.passwordInput.value || '') !== password) {
log(`${prepareLogLabel}:页面仍停留在密码页,正在重新填写密码...`, 'warn');
await humanPause(450, 1100);
fillInput(snapshot.passwordInput, password);
await performOperationWithDelay({ stepKey: 'fill-password', kind: 'fill', label: 'retry-signup-password' }, async () => {
fillInput(snapshot.passwordInput, password);
});
}
if (snapshot.submitButton && isActionEnabled(snapshot.submitButton)) {
log(`${prepareLogLabel}:页面仍停留在密码页,正在重新点击“继续”(第 ${recoveryRound}/${maxRecoveryRounds} 次)...`, 'warn');
await humanPause(350, 900);
simulateClick(snapshot.submitButton);
await performOperationWithDelay({ stepKey: 'fill-password', kind: 'submit', label: 'retry-submit-signup-password' }, async () => {
simulateClick(snapshot.submitButton);
});
await sleep(1200);
continue;
}
@@ -4763,6 +4901,13 @@ async function waitForSplitVerificationInputsFilled(inputs, code, timeout = 2500
async function fillVerificationCode(step, payload) {
const { code, signupProfile } = payload;
if (!code) throw new Error('未提供验证码。');
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
if (step === 4) {
const postVerificationState = getStep4PostVerificationState();
@@ -4854,17 +4999,18 @@ async function fillVerificationCode(step, payload) {
if (splitInputs?.length >= 6) {
log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`);
for (let i = 0; i < 6 && i < splitInputs.length; i++) {
const targetInput = splitInputs[i];
try {
targetInput.focus?.();
} catch {}
fillInput(splitInputs[i], code[i]);
try {
targetInput.dispatchEvent(new KeyboardEvent('keyup', { key: code[i], bubbles: true }));
} catch {}
await sleep(100);
}
await performOperationWithDelay({ stepKey: 'fetch-signup-code', kind: 'grouped-code', label: 'split-code' }, async () => {
for (let i = 0; i < 6 && i < splitInputs.length; i++) {
const targetInput = splitInputs[i];
try {
targetInput.focus?.();
} catch {}
fillInput(splitInputs[i], code[i]);
try {
targetInput.dispatchEvent(new KeyboardEvent('keyup', { key: code[i], bubbles: true }));
} catch {}
}
});
const filled = await waitForSplitVerificationInputsFilled(splitInputs, code, 2500);
if (!filled) {
const current = Array.from(splitInputs)
@@ -4880,7 +5026,9 @@ async function fillVerificationCode(step, payload) {
const splitSubmitBtn = await waitForVerificationSubmitButton(splitInputs[0], 2000).catch(() => null);
if (splitSubmitBtn) {
await humanPause(450, 1200);
simulateClick(splitSubmitBtn);
await performOperationWithDelay({ stepKey: 'fetch-signup-code', kind: 'submit', label: 'submit-code' }, async () => {
simulateClick(splitSubmitBtn);
});
log(`步骤 ${step}:分格验证码已提交`);
} else {
log(`步骤 ${step}:分格验证码页面未找到可点击提交按钮,继续等待页面自动推进。`, 'info');
@@ -4906,7 +5054,9 @@ async function fillVerificationCode(step, payload) {
throw new Error('未找到验证码输入框。URL: ' + location.href);
}
fillInput(codeInput, code);
await performOperationWithDelay({ stepKey: step === 8 ? 'oauth-login' : 'fetch-signup-code', kind: 'fill', label: 'verification-code' }, async () => {
fillInput(codeInput, code);
});
log(`步骤 ${step}:验证码已填写`);
// Submit
@@ -4915,7 +5065,9 @@ async function fillVerificationCode(step, payload) {
if (submitBtn) {
await humanPause(450, 1200);
simulateClick(submitBtn);
await performOperationWithDelay({ stepKey: step === 8 ? 'oauth-login' : 'fetch-signup-code', kind: 'submit', label: 'submit-code' }, async () => {
simulateClick(submitBtn);
});
log(`步骤 ${step}:验证码已提交`);
} else {
log(`步骤 ${step}:未找到可提交的验证码按钮,先等待页面自动推进或反馈结果。`, 'warn');
@@ -5199,6 +5351,13 @@ async function waitForPhoneLoginEntrySwitchTransition(timeout = 10000) {
}
async function step6OpenLoginEntry(payload, snapshot) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7;
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
const preferPhoneLogin = String(payload?.loginIdentifierType || '').trim() === 'phone' || (!payload?.email && payload?.phoneNumber);
@@ -5215,7 +5374,9 @@ async function step6OpenLoginEntry(payload, snapshot) {
log(`检测到登录入口页,正在点击 "${getActionText(trigger).slice(0, 80)}"...`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
await humanPause(350, 900);
simulateClick(trigger);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'click', label: 'open-login-entry' }, async () => {
simulateClick(trigger);
});
const nextSnapshot = await waitForLoginEntryOpenTransition();
if (nextSnapshot.state === 'email_page') {
@@ -5267,6 +5428,13 @@ async function step6OpenLoginEntry(payload, snapshot) {
}
async function step6SwitchToOneTimeCodeLogin(payload, snapshot) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7;
const switchTrigger = snapshot?.switchTrigger || findOneTimeCodeLoginTrigger();
if (!switchTrigger || !isActionEnabled(switchTrigger)) {
@@ -5278,7 +5446,9 @@ async function step6SwitchToOneTimeCodeLogin(payload, snapshot) {
log('已检测到一次性验证码登录入口,准备切换...', 'info', { step: visibleStep, stepKey: 'oauth-login' });
const loginVerificationRequestedAt = Date.now();
await humanPause(350, 900);
simulateClick(switchTrigger);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'click', label: 'switch-one-time-code-login' }, async () => {
simulateClick(switchTrigger);
});
log('已点击一次性验证码登录', 'info', { step: visibleStep, stepKey: 'oauth-login' });
await sleep(1200);
const result = await waitForStep6SwitchTransition(loginVerificationRequestedAt, 10000, { visibleStep });
@@ -5305,6 +5475,13 @@ async function step6SwitchToOneTimeCodeLogin(payload, snapshot) {
}
async function step6LoginFromPhonePage(payload, snapshot) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7;
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
const phoneInput = currentSnapshot.phoneInput || getLoginPhoneInput();
@@ -5358,7 +5535,9 @@ async function step6LoginFromPhonePage(payload, snapshot) {
await sleep(500);
const verifiedPhoneInput = fillResult.input || phoneInput;
const hiddenSync = syncPhoneHiddenFormValue(verifiedPhoneInput, { phoneNumber, dialCode, inputValue });
const hiddenSync = await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'hidden-sync', label: 'login-phone-pre-submit-hidden-sync' }, async () => (
syncPhoneHiddenFormValue(verifiedPhoneInput, { phoneNumber, dialCode, inputValue })
));
const submitButton = getLoginSubmitButton({ allowDisabled: true }) || currentSnapshot.submitButton;
const preSubmitRenderedValue = getPhoneInputRenderedValue(verifiedPhoneInput);
const preSubmitHiddenInput = hiddenSync?.input || getPhoneHiddenValueInput(verifiedPhoneInput);
@@ -5416,6 +5595,13 @@ async function step6LoginFromPhonePage(payload, snapshot) {
}
async function switchFromEmailPageToPhoneLogin(payload, snapshot) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7;
let currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
let phoneEntryTrigger = currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger();
@@ -5427,7 +5613,9 @@ async function switchFromEmailPageToPhoneLogin(payload, snapshot) {
stepKey: 'oauth-login',
});
await humanPause(350, 900);
simulateClick(moreOptionsTrigger);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'click', label: 'login-more-options' }, async () => {
simulateClick(moreOptionsTrigger);
});
await sleep(800);
currentSnapshot = normalizeStep6Snapshot(inspectLoginAuthState());
phoneEntryTrigger = currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger();
@@ -5442,7 +5630,9 @@ async function switchFromEmailPageToPhoneLogin(payload, snapshot) {
log(`步骤 ${visibleStep}:当前在邮箱入口,正在切换到手机号登录...`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
await humanPause(350, 900);
simulateClick(phoneEntryTrigger);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'click', label: 'switch-phone-login' }, async () => {
simulateClick(phoneEntryTrigger);
});
const nextSnapshot = normalizeStep6Snapshot(await waitForPhoneLoginEntrySwitchTransition(20000));
if (nextSnapshot.state === 'phone_entry_page') {
return step6LoginFromPhonePage(payload, nextSnapshot);
@@ -5491,6 +5681,13 @@ async function switchFromEmailPageToPhoneLogin(payload, snapshot) {
}
async function step6LoginFromPasswordPage(payload, snapshot) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7;
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
const hasPassword = Boolean(String(payload?.password || '').trim());
@@ -5509,7 +5706,9 @@ async function step6LoginFromPasswordPage(payload, snapshot) {
log('已进入密码页,准备填写密码...', 'info', { step: visibleStep, stepKey: 'oauth-login' });
await humanPause(550, 1450);
fillInput(currentSnapshot.passwordInput, payload.password);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'fill', label: 'login-password' }, async () => {
fillInput(currentSnapshot.passwordInput, payload.password);
});
log('已填写密码', 'info', { step: visibleStep, stepKey: 'oauth-login' });
await sleep(500);
@@ -5560,6 +5759,13 @@ async function step6LoginFromPasswordPage(payload, snapshot) {
}
async function step6LoginFromEmailPage(payload, snapshot) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7;
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
if (String(payload?.loginIdentifierType || '').trim() === 'phone' && payload?.phoneNumber) {
@@ -5572,7 +5778,9 @@ async function step6LoginFromEmailPage(payload, snapshot) {
if ((emailInput.value || '').trim() !== payload.email) {
await humanPause(500, 1400);
fillInput(emailInput, payload.email);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'fill', label: 'login-email' }, async () => {
fillInput(emailInput, payload.email);
});
log('已填写邮箱', 'info', { step: visibleStep, stepKey: 'oauth-login' });
} else {
log('邮箱已在输入框中,准备提交...', 'info', { step: visibleStep, stepKey: 'oauth-login' });
@@ -5783,6 +5991,13 @@ async function waitForAddEmailSubmitOutcome(timeout = 45000) {
}
async function submitAddEmailAndContinue(payload = {}) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const email = String(payload.email || '').trim().toLowerCase();
if (!email) {
throw new Error('未提供邮箱地址,无法添加邮箱。');
@@ -5795,7 +6010,9 @@ async function submitAddEmailAndContinue(payload = {}) {
}
await humanPause(500, 1400);
fillInput(emailInput, email);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'fill', label: 'add-email' }, async () => {
fillInput(emailInput, email);
});
log(`步骤 8:已填写邮箱:${email}`);
await sleep(500);
@@ -6087,6 +6304,13 @@ async function waitForCombinedSignupVerificationProfilePage(timeout = 2500) {
async function step5_fillNameBirthday(payload) {
const { firstName, lastName, age, year, month, day, prefillOnly = false } = payload;
if (!firstName || !lastName) throw new Error('未提供姓名数据。');
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null);
const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value)));
@@ -6113,7 +6337,9 @@ async function step5_fillNameBirthday(payload) {
throw new Error('未找到姓名输入框。URL: ' + location.href);
}
await humanPause(500, 1300);
fillInput(nameInput, fullName);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'fill', label: 'fill-name' }, async () => {
fillInput(nameInput, fullName);
});
log(`步骤 5:姓名已填写:${fullName}`);
let birthdayMode = false;
@@ -6183,11 +6409,17 @@ async function step5_fillNameBirthday(payload) {
log('步骤 5:检测到 React Aria 下拉生日字段,正在填写生日...');
await humanPause(450, 1100);
await setReactAriaBirthdaySelect(yearReactSelect, year);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'select', label: 'select-birthday-year' }, async () => {
await setReactAriaBirthdaySelect(yearReactSelect, year);
});
await humanPause(250, 650);
await setReactAriaBirthdaySelect(monthReactSelect, month);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'select', label: 'select-birthday-month' }, async () => {
await setReactAriaBirthdaySelect(monthReactSelect, month);
});
await humanPause(250, 650);
await setReactAriaBirthdaySelect(dayReactSelect, day);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'select', label: 'select-birthday-day' }, async () => {
await setReactAriaBirthdaySelect(dayReactSelect, day);
});
if (hiddenBirthday) {
const start = Date.now();
@@ -6228,20 +6460,28 @@ async function step5_fillNameBirthday(payload) {
}
await humanPause(450, 1100);
await setSpinButton(yearSpinner, year);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'fill', label: 'fill-birthday-year' }, async () => {
await setSpinButton(yearSpinner, year);
});
await humanPause(250, 650);
await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'fill', label: 'fill-birthday-month' }, async () => {
await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
});
await humanPause(250, 650);
await setSpinButton(daySpinner, String(day).padStart(2, '0'));
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'fill', label: 'fill-birthday-day' }, async () => {
await setSpinButton(daySpinner, String(day).padStart(2, '0'));
});
log(`步骤 5:生日已填写:${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
}
const hiddenBirthday = document.querySelector('input[name="birthday"]');
if (hiddenBirthday) {
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
hiddenBirthday.value = dateStr;
hiddenBirthday.dispatchEvent(new Event('input', { bubbles: true }));
hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'hidden-sync', label: 'profile-dom-sync' }, async () => {
hiddenBirthday.value = dateStr;
hiddenBirthday.dispatchEvent(new Event('input', { bubbles: true }));
hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
});
log(`步骤 5:已设置隐藏生日输入框:${dateStr}`);
}
} else if (ageInput) {
@@ -6249,7 +6489,9 @@ async function step5_fillNameBirthday(payload) {
throw new Error('检测到年龄字段,但未提供年龄数据。');
}
await humanPause(500, 1300);
fillInput(ageInput, String(resolvedAge));
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'fill', label: 'fill-birthday' }, async () => {
fillInput(ageInput, String(resolvedAge));
});
log(`步骤 5:年龄已填写:${resolvedAge}`);
} else {
throw new Error('未找到生日或年龄输入项。URL: ' + location.href);
@@ -6261,15 +6503,19 @@ async function step5_fillNameBirthday(payload) {
if (!isStep5CheckboxChecked(allConsentCheckbox)) {
const checkboxLabel = allConsentCheckbox.closest('label');
await humanPause(500, 1500);
if (checkboxLabel && isVisibleElement(checkboxLabel)) {
simulateClick(checkboxLabel);
} else {
simulateClick(allConsentCheckbox);
}
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'click', label: 'accept-profile-consent' }, async () => {
if (checkboxLabel && isVisibleElement(checkboxLabel)) {
simulateClick(checkboxLabel);
} else {
simulateClick(allConsentCheckbox);
}
});
await sleep(250);
if (!isStep5CheckboxChecked(allConsentCheckbox)) {
allConsentCheckbox.click();
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'click', label: 'accept-profile-consent-fallback' }, async () => {
allConsentCheckbox.click();
});
await sleep(250);
}
@@ -6303,7 +6549,9 @@ async function step5_fillNameBirthday(payload) {
}
await humanPause(500, 1300);
simulateClick(completeBtn);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'submit', label: 'submit-profile' }, async () => {
simulateClick(completeBtn);
});
const completionPayload = getStep5DirectCompletionPayload({ isAgeMode });
reportComplete(5, completionPayload);