feat: add configurable operation delay
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
isActionEnabled,
|
||||
isVisibleElement,
|
||||
log,
|
||||
performOperationWithDelay: injectedPerformOperationWithDelay,
|
||||
routeErrorPattern = null,
|
||||
simulateClick,
|
||||
sleep,
|
||||
@@ -122,6 +123,10 @@
|
||||
}
|
||||
|
||||
async function recoverAuthRetryPage(options = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
const performOperationWithDelay = injectedPerformOperationWithDelay
|
||||
|| rootScope.CodexOperationDelay?.performOperationWithDelay
|
||||
|| (async (_metadata, operation) => operation());
|
||||
const {
|
||||
logLabel = '',
|
||||
maxClickAttempts = 5,
|
||||
@@ -173,7 +178,9 @@
|
||||
if (typeof humanPause === 'function') {
|
||||
await humanPause(300, 800);
|
||||
}
|
||||
simulateClick(retryState.retryButton);
|
||||
await performOperationWithDelay({ stepKey: options.stepKey || '', kind: 'click', label: 'auth-retry-click' }, async () => {
|
||||
simulateClick(retryState.retryButton);
|
||||
});
|
||||
const recoveryResult = await waitForRetryPageRecoveryAfterClick({
|
||||
pathPatterns,
|
||||
pollIntervalMs,
|
||||
|
||||
@@ -95,11 +95,13 @@ async function fetchDuckEmail(payload = {}) {
|
||||
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
await humanPause(500, 1300);
|
||||
if (typeof simulateClick === 'function') {
|
||||
simulateClick(generatorButton);
|
||||
} else {
|
||||
generatorButton.click();
|
||||
}
|
||||
await window.CodexOperationDelay.performOperationWithDelay({ stepKey: 'fetch-signup-code', kind: 'click', label: 'duck-generate-address' }, async () => {
|
||||
if (typeof simulateClick === 'function') {
|
||||
simulateClick(generatorButton);
|
||||
} else {
|
||||
generatorButton.click();
|
||||
}
|
||||
});
|
||||
log(`Duck 邮箱:已点击“生成 Duck 私有地址”按钮(${attempt}/2)`);
|
||||
|
||||
try {
|
||||
|
||||
+77
-15
@@ -35,6 +35,12 @@ if (document.documentElement.getAttribute(GOPAY_FLOW_LISTENER_SENTINEL) !== '1')
|
||||
console.log('[MultiPage:gopay-flow] 消息监听已存在,跳过重复注册');
|
||||
}
|
||||
|
||||
async function performGoPayOperationWithDelay(metadata, operation) {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
}
|
||||
|
||||
async function handleGoPayCommand(message) {
|
||||
switch (message.type) {
|
||||
case 'GOPAY_GET_STATE':
|
||||
@@ -622,20 +628,33 @@ function fillVisibleOtpInputs(code = '') {
|
||||
}
|
||||
|
||||
async function submitGoPayPhone(payload = {}) {
|
||||
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
|
||||
? performGoPayOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
const countryCode = normalizeGoPayCountryCode(payload.countryCode || payload.gopayCountryCode || '+86');
|
||||
const phone = normalizeGoPayNationalPhone(payload.phone || payload.gopayPhone || '', countryCode);
|
||||
if (!phone) {
|
||||
throw new Error('GoPay 手机号为空,请先在侧边栏配置。');
|
||||
}
|
||||
const countryResult = await ensureGoPayCountryCode(countryCode);
|
||||
const input = await waitUntil(() => findPhoneInput(), {
|
||||
label: 'GoPay 手机号输入框',
|
||||
intervalMs: 250,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
fillInput(input, phone);
|
||||
const clickResult = await clickContinueIfPresent();
|
||||
const { countryResult, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-phone' }, async () => {
|
||||
const nextCountryResult = await ensureGoPayCountryCode(countryCode);
|
||||
fillInput(input, phone);
|
||||
const nextClickResult = await clickContinueIfPresent();
|
||||
return {
|
||||
countryResult: nextCountryResult,
|
||||
clickResult: nextClickResult,
|
||||
};
|
||||
});
|
||||
return {
|
||||
phoneSubmitted: true,
|
||||
countryCode,
|
||||
@@ -647,17 +666,27 @@ async function submitGoPayPhone(payload = {}) {
|
||||
}
|
||||
|
||||
async function submitGoPayOtp(payload = {}) {
|
||||
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
|
||||
? performGoPayOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
const code = normalizeOtp(payload.code || payload.otp || '');
|
||||
if (!code) {
|
||||
throw new Error('GoPay WhatsApp 验证码为空。');
|
||||
}
|
||||
const filled = await waitUntil(() => fillVisibleOtpInputs(code), {
|
||||
label: 'GoPay 验证码输入框',
|
||||
intervalMs: 250,
|
||||
timeoutMs: 15000,
|
||||
const { filled, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-otp' }, async () => {
|
||||
const filledOtp = await waitUntil(() => fillVisibleOtpInputs(code), {
|
||||
label: 'GoPay 验证码输入框',
|
||||
intervalMs: 250,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
const continueResult = await clickContinueIfPresent();
|
||||
return { filled: filledOtp, clickResult: continueResult };
|
||||
});
|
||||
const clickResult = await clickContinueIfPresent();
|
||||
return {
|
||||
otpSubmitted: Boolean(filled),
|
||||
clicked: Boolean(clickResult.clicked),
|
||||
@@ -667,17 +696,27 @@ async function submitGoPayOtp(payload = {}) {
|
||||
}
|
||||
|
||||
async function submitGoPayPin(payload = {}) {
|
||||
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
|
||||
? performGoPayOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
const pin = normalizeOtp(payload.pin || payload.gopayPin || '');
|
||||
if (!pin) {
|
||||
throw new Error('GoPay PIN 为空,请先在侧边栏配置。');
|
||||
}
|
||||
const filled = await waitUntil(() => fillVisiblePinInputs(pin), {
|
||||
label: 'GoPay PIN 输入框',
|
||||
intervalMs: 250,
|
||||
timeoutMs: 15000,
|
||||
const { filled, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-pin' }, async () => {
|
||||
const filledPin = await waitUntil(() => fillVisiblePinInputs(pin), {
|
||||
label: 'GoPay PIN 输入框',
|
||||
intervalMs: 250,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
const continueResult = await clickContinueIfPresent();
|
||||
return { filled: filledPin, clickResult: continueResult };
|
||||
});
|
||||
const clickResult = await clickContinueIfPresent();
|
||||
return {
|
||||
pinSubmitted: Boolean(filled),
|
||||
clicked: Boolean(clickResult.clicked),
|
||||
@@ -722,18 +761,41 @@ function getGoPayPayNowTarget() {
|
||||
}
|
||||
|
||||
async function clickGoPayContinue() {
|
||||
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
|
||||
? performGoPayOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
const clickResult = await clickContinueIfPresent({ afterMs: 1200 });
|
||||
const button = findContinueButton();
|
||||
if (!button) {
|
||||
return { clicked: false, clickTarget: '' };
|
||||
}
|
||||
const clickResult = await delayOperation({ stepKey: 'gopay-approve', kind: 'click', label: 'click-continue' }, async () => {
|
||||
await humanClickElement(button, { afterMs: 1200 });
|
||||
return { clicked: true, target: describeElement(button) };
|
||||
});
|
||||
return { clicked: Boolean(clickResult.clicked), clickTarget: clickResult.target || '' };
|
||||
}
|
||||
|
||||
async function clickGoPayPayNow() {
|
||||
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
|
||||
? performGoPayOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
const button = findPayNowButton();
|
||||
if (!button) {
|
||||
return { clicked: false, clickTarget: '' };
|
||||
}
|
||||
await humanClickElement(button, { afterMs: 1500 });
|
||||
await delayOperation({ stepKey: 'gopay-approve', kind: 'click', label: 'click-pay-now' }, async () => {
|
||||
await humanClickElement(button, { afterMs: 1500 });
|
||||
});
|
||||
return { clicked: true, clickTarget: describeElement(button) };
|
||||
}
|
||||
|
||||
|
||||
+17
-4
@@ -501,7 +501,9 @@ async function ensureAgreementChecked() {
|
||||
if (isCheckboxChecked(checkbox)) {
|
||||
continue;
|
||||
}
|
||||
simulateClick(checkbox);
|
||||
await performOperationWithDelay({ stepKey: 'fetch-signup-code', kind: 'click', label: 'mail2925-agreement-checkbox' }, async () => {
|
||||
simulateClick(checkbox);
|
||||
});
|
||||
changed = true;
|
||||
await sleep(120);
|
||||
}
|
||||
@@ -1052,6 +1054,11 @@ async function waitForMail2925View(targetView, timeoutMs = 45000) {
|
||||
return detectMail2925ViewState();
|
||||
}
|
||||
|
||||
async function performOperationWithDelay(metadata, operation) {
|
||||
const gate = window.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
}
|
||||
|
||||
async function ensureMail2925Session(payload = {}) {
|
||||
const email = String(payload?.email || '').trim();
|
||||
const password = String(payload?.password || '');
|
||||
@@ -1135,13 +1142,19 @@ async function ensureMail2925Session(payload = {}) {
|
||||
}
|
||||
|
||||
await ensureAgreementChecked();
|
||||
fillInput(emailInput, email);
|
||||
await performOperationWithDelay({ stepKey: 'fetch-signup-code', kind: 'fill', label: 'mail2925-login-email' }, async () => {
|
||||
fillInput(emailInput, email);
|
||||
});
|
||||
await sleep(150);
|
||||
fillInput(passwordInput, password);
|
||||
await performOperationWithDelay({ stepKey: 'fetch-signup-code', kind: 'fill', label: 'mail2925-login-password' }, async () => {
|
||||
fillInput(passwordInput, password);
|
||||
});
|
||||
await sleep(200);
|
||||
await sleep(1000);
|
||||
log(`步骤 0:2925 已定位到登录表单,准备点击“登录”,当前地址 ${location.href}`, 'info');
|
||||
simulateClick(loginButton);
|
||||
await performOperationWithDelay({ stepKey: 'fetch-signup-code', kind: 'submit', label: 'mail2925-login-submit' }, async () => {
|
||||
simulateClick(loginButton);
|
||||
});
|
||||
log(`步骤 0:2925 已点击“登录”,点击后地址 ${location.href}`, 'info');
|
||||
|
||||
const finalState = await waitForMail2925View('mailbox', 40000);
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
(function attachOperationDelay(root) {
|
||||
const OPERATION_DELAY_MS = 2000;
|
||||
const SETTING_RESTORE_FALLBACK_MS = 50;
|
||||
const EXCLUDED_STEP_KEYS = new Set(['confirm-oauth', 'platform-verify']);
|
||||
let operationDelayEnabled = true;
|
||||
let operationDelaySettingReady = null;
|
||||
let operationDelaySettingRevision = 0;
|
||||
|
||||
function normalizeOperationDelayEnabled(value) {
|
||||
return typeof value === 'boolean' ? value : true;
|
||||
}
|
||||
|
||||
function getOperationDelayEnabled() {
|
||||
return operationDelayEnabled;
|
||||
}
|
||||
|
||||
async function refreshOperationDelaySetting() {
|
||||
const restoreRevision = ++operationDelaySettingRevision;
|
||||
const ready = (async () => {
|
||||
let nextEnabled = true;
|
||||
try {
|
||||
const data = await root.chrome?.storage?.local?.get?.(['operationDelayEnabled']);
|
||||
nextEnabled = normalizeOperationDelayEnabled(data?.operationDelayEnabled);
|
||||
} catch {
|
||||
nextEnabled = true;
|
||||
}
|
||||
if (operationDelaySettingRevision === restoreRevision) {
|
||||
operationDelayEnabled = nextEnabled;
|
||||
}
|
||||
return operationDelayEnabled;
|
||||
})();
|
||||
operationDelaySettingReady = ready;
|
||||
try {
|
||||
return await ready;
|
||||
} finally {
|
||||
if (operationDelaySettingReady === ready) {
|
||||
operationDelaySettingReady = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shouldDelayOperation(metadata = {}) {
|
||||
if (metadata.skipOperationDelay === true) return false;
|
||||
if (EXCLUDED_STEP_KEYS.has(String(metadata.stepKey || '').trim())) return false;
|
||||
const enabled = Object.prototype.hasOwnProperty.call(metadata, 'enabled')
|
||||
? normalizeOperationDelayEnabled(metadata.enabled)
|
||||
: getOperationDelayEnabled();
|
||||
if (enabled === false) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function waitForOperationDelaySettingFallback() {
|
||||
return new Promise((resolve) => {
|
||||
const schedule = root.setTimeout || (typeof setTimeout === 'function' ? setTimeout : null);
|
||||
if (typeof schedule === 'function') {
|
||||
schedule(() => resolve(getOperationDelayEnabled()), SETTING_RESTORE_FALLBACK_MS);
|
||||
return;
|
||||
}
|
||||
resolve(getOperationDelayEnabled());
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveOperationDelayEnabled(metadata = {}, options = {}) {
|
||||
if (typeof options.getEnabled === 'function') {
|
||||
return normalizeOperationDelayEnabled(options.getEnabled());
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(metadata, 'enabled')) {
|
||||
return normalizeOperationDelayEnabled(metadata.enabled);
|
||||
}
|
||||
if (operationDelaySettingReady) {
|
||||
return normalizeOperationDelayEnabled(await Promise.race([
|
||||
operationDelaySettingReady,
|
||||
waitForOperationDelaySettingFallback(),
|
||||
]));
|
||||
}
|
||||
return getOperationDelayEnabled();
|
||||
}
|
||||
|
||||
async function performOperationWithDelay(metadata = {}, operation, options = {}) {
|
||||
const result = await operation();
|
||||
const enabled = await resolveOperationDelayEnabled(metadata, options);
|
||||
if (shouldDelayOperation({ ...metadata, enabled })) {
|
||||
const wait = options.sleep || root.sleep;
|
||||
await wait(OPERATION_DELAY_MS);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
root.chrome?.storage?.onChanged?.addListener?.((changes, areaName) => {
|
||||
if (areaName === 'local' && Object.prototype.hasOwnProperty.call(changes, 'operationDelayEnabled')) {
|
||||
operationDelaySettingRevision += 1;
|
||||
operationDelayEnabled = normalizeOperationDelayEnabled(changes.operationDelayEnabled?.newValue);
|
||||
}
|
||||
});
|
||||
|
||||
refreshOperationDelaySetting().catch(() => { operationDelayEnabled = true; });
|
||||
root.CodexOperationDelay = { OPERATION_DELAY_MS, normalizeOperationDelayEnabled, refreshOperationDelaySetting, getOperationDelayEnabled, shouldDelayOperation, performOperationWithDelay };
|
||||
})(typeof self !== 'undefined' ? self : globalThis);
|
||||
+107
-33
@@ -31,6 +31,12 @@ if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1'
|
||||
console.log('[MultiPage:paypal-flow] 消息监听已存在,跳过重复注册');
|
||||
}
|
||||
|
||||
async function performPayPalOperationWithDelay(metadata, operation) {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
}
|
||||
|
||||
async function handlePayPalCommand(message) {
|
||||
switch (message.type) {
|
||||
case 'PAYPAL_GET_STATE':
|
||||
@@ -142,15 +148,50 @@ function findInputByPatterns(patterns) {
|
||||
}
|
||||
|
||||
function findEmailInput() {
|
||||
return findInputByPatterns([
|
||||
const isPasswordCandidate = (input) => {
|
||||
const type = String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase();
|
||||
const metadataText = normalizeText([
|
||||
input?.textContent,
|
||||
input?.getAttribute?.('aria-label'),
|
||||
input?.getAttribute?.('title'),
|
||||
input?.getAttribute?.('placeholder'),
|
||||
input?.getAttribute?.('name'),
|
||||
input?.id,
|
||||
].filter(Boolean).join(' '));
|
||||
return type === 'password' || /password|pass|密码/i.test(metadataText);
|
||||
};
|
||||
const inputs = getVisibleControls('input')
|
||||
.filter((input) => {
|
||||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||||
return isEnabledControl(input)
|
||||
&& !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type)
|
||||
&& !isPasswordCandidate(input);
|
||||
});
|
||||
return inputs.find((input) => [
|
||||
/email|login|user|账号|邮箱/i,
|
||||
]) || getVisibleControls('input[type="email"]').find(isVisibleElement) || null;
|
||||
].some((pattern) => pattern.test(getActionText(input))))
|
||||
|| getVisibleControls('input[type="email"]').find((input) => isVisibleElement(input) && !isPasswordCandidate(input))
|
||||
|| null;
|
||||
}
|
||||
|
||||
function findPasswordInput() {
|
||||
return findInputByPatterns([
|
||||
/password|pass|密码/i,
|
||||
]) || getVisibleControls('input[type="password"]').find(isVisibleElement) || null;
|
||||
const inputs = getVisibleControls('input')
|
||||
.filter((input) => {
|
||||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||||
return isEnabledControl(input) && !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
|
||||
});
|
||||
return inputs.find((input) => {
|
||||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||||
const metadataText = normalizeText([
|
||||
input?.textContent,
|
||||
input?.getAttribute?.('aria-label'),
|
||||
input?.getAttribute?.('title'),
|
||||
input?.getAttribute?.('placeholder'),
|
||||
input?.getAttribute?.('name'),
|
||||
input?.id,
|
||||
].filter(Boolean).join(' '));
|
||||
return type === 'password' || /password|pass|密码/i.test(metadataText);
|
||||
}) || getVisibleControls('input[type="password"]').find(isVisibleElement) || null;
|
||||
}
|
||||
|
||||
function findLoginNextButton() {
|
||||
@@ -240,6 +281,13 @@ function refillPayPalEmailInput(emailInput, email) {
|
||||
}
|
||||
|
||||
async function submitPayPalLogin(payload = {}) {
|
||||
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
|
||||
? performPayPalOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
|
||||
const email = normalizeText(payload.email || '');
|
||||
@@ -253,8 +301,10 @@ async function submitPayPalLogin(payload = {}) {
|
||||
const emailNextButton = findEmailNextButton();
|
||||
|
||||
if (emailInput && emailNextButton && isEnabledControl(emailNextButton) && (!passwordInput || !findPasswordLoginButton())) {
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
simulateClick(emailNextButton);
|
||||
await delayOperation({ stepKey: 'paypal-approve', kind: 'submit', label: 'paypal-email' }, async () => {
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
simulateClick(emailNextButton);
|
||||
});
|
||||
return {
|
||||
submitted: false,
|
||||
phase: 'email_submitted',
|
||||
@@ -263,16 +313,18 @@ async function submitPayPalLogin(payload = {}) {
|
||||
}
|
||||
|
||||
if (!passwordInput && emailInput && email) {
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
const nextButton = await waitUntil(() => {
|
||||
const button = findEmailNextButton() || findLoginNextButton();
|
||||
return button && isEnabledControl(button) ? button : null;
|
||||
}, {
|
||||
intervalMs: 250,
|
||||
timeoutMs: 8000,
|
||||
timeoutMessage: 'PayPal email page did not expose a clickable next/continue button.',
|
||||
await delayOperation({ stepKey: 'paypal-approve', kind: 'submit', label: 'paypal-email' }, async () => {
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
const nextButton = await waitUntil(() => {
|
||||
const button = findEmailNextButton() || findLoginNextButton();
|
||||
return button && isEnabledControl(button) ? button : null;
|
||||
}, {
|
||||
intervalMs: 250,
|
||||
timeoutMs: 8000,
|
||||
timeoutMessage: 'PayPal email page did not expose a clickable next/continue button.',
|
||||
});
|
||||
simulateClick(nextButton);
|
||||
});
|
||||
simulateClick(nextButton);
|
||||
return {
|
||||
submitted: false,
|
||||
phase: 'email_submitted',
|
||||
@@ -281,7 +333,9 @@ async function submitPayPalLogin(payload = {}) {
|
||||
} else if (!passwordInput && emailInput && !email) {
|
||||
throw new Error('PayPal 账号为空,请先在侧边栏配置。');
|
||||
} else if (emailInput && email) {
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
await delayOperation({ stepKey: 'paypal-approve', kind: 'fill', label: 'paypal-email' }, async () => {
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
});
|
||||
}
|
||||
|
||||
passwordInput = passwordInput || await waitUntil(() => findPasswordInput(), {
|
||||
@@ -289,22 +343,24 @@ async function submitPayPalLogin(payload = {}) {
|
||||
timeoutMs: 8000,
|
||||
timeoutMessage: 'PayPal password page did not expose a password input.',
|
||||
});
|
||||
fillInput(passwordInput, password);
|
||||
await sleep(1000);
|
||||
await delayOperation({ stepKey: 'paypal-approve', kind: 'submit', label: 'paypal-password' }, async () => {
|
||||
fillInput(passwordInput, password);
|
||||
await sleep(1000);
|
||||
|
||||
const loginButton = await waitUntil(() => {
|
||||
const button = findClickableByText([
|
||||
/login|log\s*in|sign\s*in|continue/i,
|
||||
/登录|登入|继续/i,
|
||||
]);
|
||||
return button && isEnabledControl(button) ? button : null;
|
||||
}, {
|
||||
intervalMs: 250,
|
||||
timeoutMs: 8000,
|
||||
timeoutMessage: 'PayPal password page did not expose a clickable login/continue button.',
|
||||
const loginButton = await waitUntil(() => {
|
||||
const button = findClickableByText([
|
||||
/login|log\s*in|sign\s*in|continue/i,
|
||||
/登录|登入|继续/i,
|
||||
]);
|
||||
return button && isEnabledControl(button) ? button : null;
|
||||
}, {
|
||||
intervalMs: 250,
|
||||
timeoutMs: 8000,
|
||||
timeoutMessage: 'PayPal password page did not expose a clickable login/continue button.',
|
||||
});
|
||||
|
||||
simulateClick(loginButton);
|
||||
});
|
||||
|
||||
simulateClick(loginButton);
|
||||
return {
|
||||
submitted: true,
|
||||
phase: 'password_submitted',
|
||||
@@ -313,6 +369,13 @@ async function submitPayPalLogin(payload = {}) {
|
||||
}
|
||||
|
||||
async function dismissPayPalPrompts() {
|
||||
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
|
||||
? performPayPalOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
const buttons = findPasskeyPromptButtons();
|
||||
let clicked = 0;
|
||||
@@ -320,7 +383,9 @@ async function dismissPayPalPrompts() {
|
||||
if (!isVisibleElement(button) || !isEnabledControl(button)) {
|
||||
continue;
|
||||
}
|
||||
simulateClick(button);
|
||||
await delayOperation({ stepKey: 'paypal-approve', kind: 'click', label: 'paypal-dismiss-prompt' }, async () => {
|
||||
simulateClick(button);
|
||||
});
|
||||
clicked += 1;
|
||||
await sleep(500);
|
||||
}
|
||||
@@ -331,6 +396,13 @@ async function dismissPayPalPrompts() {
|
||||
}
|
||||
|
||||
async function clickPayPalApprove() {
|
||||
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
|
||||
? performPayPalOperationWithDelay
|
||||
: async (metadata, operation) => {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
};
|
||||
await waitForDocumentComplete();
|
||||
await dismissPayPalPrompts().catch(() => ({ clicked: 0 }));
|
||||
|
||||
@@ -342,7 +414,9 @@ async function clickPayPalApprove() {
|
||||
};
|
||||
}
|
||||
|
||||
simulateClick(button);
|
||||
await delayOperation({ stepKey: 'paypal-approve', kind: 'click', label: 'paypal-approve' }, async () => {
|
||||
simulateClick(button);
|
||||
});
|
||||
return {
|
||||
clicked: true,
|
||||
buttonText: getActionText(button),
|
||||
|
||||
+38
-11
@@ -13,6 +13,7 @@
|
||||
isConsentReady,
|
||||
isPhoneVerificationPageReady,
|
||||
isVisibleElement,
|
||||
performOperationWithDelay: injectedPerformOperationWithDelay,
|
||||
simulateClick,
|
||||
sleep,
|
||||
throwIfStopped,
|
||||
@@ -34,6 +35,11 @@
|
||||
let lastPhoneRoute405RecoveryFailedAt = 0;
|
||||
let activePhoneResendPromise = null;
|
||||
|
||||
async function performOperationWithDelay(metadata, operation) {
|
||||
const gate = injectedPerformOperationWithDelay || rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
}
|
||||
|
||||
function dispatchInputEvents(element) {
|
||||
if (!element) return;
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
@@ -334,10 +340,15 @@
|
||||
}
|
||||
const selectedOption = getSelectedCountryOption();
|
||||
if (selectedOption && isSameCountryOption(selectedOption, targetOption)) {
|
||||
await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'select', label: 'phone-country-select' }, async () => {
|
||||
dispatchInputEvents(select);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
select.value = String(targetOption.value || '');
|
||||
dispatchInputEvents(select);
|
||||
await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'select', label: 'phone-country-select' }, async () => {
|
||||
select.value = String(targetOption.value || '');
|
||||
dispatchInputEvents(select);
|
||||
});
|
||||
await sleep(250);
|
||||
const nextSelectedOption = getSelectedCountryOption();
|
||||
return Boolean(nextSelectedOption && isSameCountryOption(nextSelectedOption, targetOption));
|
||||
@@ -616,7 +627,9 @@
|
||||
}
|
||||
clicked += 1;
|
||||
await humanPause(200, 500);
|
||||
simulateClick(retryButton);
|
||||
await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'click', label: 'phone-route-retry' }, async () => {
|
||||
simulateClick(retryButton);
|
||||
});
|
||||
await sleep(1000);
|
||||
continue;
|
||||
}
|
||||
@@ -715,13 +728,19 @@
|
||||
}
|
||||
|
||||
await humanPause(250, 700);
|
||||
fillInput(phoneInput, nationalPhoneNumber);
|
||||
await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'fill', label: 'phone-number' }, async () => {
|
||||
fillInput(phoneInput, nationalPhoneNumber);
|
||||
});
|
||||
if (hiddenPhoneNumberInput) {
|
||||
hiddenPhoneNumberInput.value = phoneNumber;
|
||||
dispatchInputEvents(hiddenPhoneNumberInput);
|
||||
await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'hidden-sync', label: 'phone-number-hidden-sync' }, async () => {
|
||||
hiddenPhoneNumberInput.value = phoneNumber;
|
||||
dispatchInputEvents(hiddenPhoneNumberInput);
|
||||
});
|
||||
}
|
||||
await sleep(250);
|
||||
simulateClick(submitButton);
|
||||
await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'submit', label: 'phone-number-submit' }, async () => {
|
||||
simulateClick(submitButton);
|
||||
});
|
||||
return waitForPhoneVerificationReady();
|
||||
}
|
||||
|
||||
@@ -797,9 +816,13 @@
|
||||
}
|
||||
|
||||
await humanPause(250, 700);
|
||||
fillInput(codeInput, code);
|
||||
await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'fill', label: 'phone-verification-code' }, async () => {
|
||||
fillInput(codeInput, code);
|
||||
});
|
||||
await sleep(250);
|
||||
simulateClick(submitButton);
|
||||
await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'submit', label: 'phone-verification-submit' }, async () => {
|
||||
simulateClick(submitButton);
|
||||
});
|
||||
if (is405MethodNotAllowedPage()) {
|
||||
await recoverPhoneRoute405(12000);
|
||||
}
|
||||
@@ -850,7 +873,9 @@
|
||||
const resendButton = getPhoneVerificationResendButton({ allowDisabled: true });
|
||||
if (resendButton && isActionEnabled(resendButton)) {
|
||||
await humanPause(250, 700);
|
||||
simulateClick(resendButton);
|
||||
await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'click', label: 'phone-verification-resend' }, async () => {
|
||||
simulateClick(resendButton);
|
||||
});
|
||||
await sleep(1000);
|
||||
if (is405MethodNotAllowedPage()) {
|
||||
await recoverRoute405WithinResend();
|
||||
@@ -902,7 +927,9 @@
|
||||
throw new Error('The auth page is not currently on phone verification or add-phone page.');
|
||||
}
|
||||
|
||||
location.assign('/add-phone');
|
||||
await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'navigation', label: 'phone-return-add-phone' }, async () => {
|
||||
location.assign('/add-phone');
|
||||
});
|
||||
await waitForAddPhoneReady(timeout);
|
||||
return {
|
||||
addPhonePage: true,
|
||||
|
||||
+38
-13
@@ -60,6 +60,12 @@ const PAYMENT_METHOD_CONFIGS = {
|
||||
},
|
||||
};
|
||||
|
||||
async function performOperationWithDelay(metadata, operation) {
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
|
||||
return typeof gate === 'function' ? gate(metadata, operation) : operation();
|
||||
}
|
||||
|
||||
if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== '1') {
|
||||
document.documentElement.setAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL, '1');
|
||||
|
||||
@@ -688,7 +694,9 @@ async function selectPaymentMethod(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
intervalMs: 250,
|
||||
});
|
||||
console.info(`[MultiPage:plus-checkout] ${config.label} target selected`, summarizeElementForDebug(target));
|
||||
simulateClick(target);
|
||||
await performOperationWithDelay({ stepKey: 'plus-checkout-billing', kind: 'select', label: 'select-payment-method' }, async () => {
|
||||
simulateClick(target);
|
||||
});
|
||||
log(`Plus Checkout:已点击 ${config.label} 付款方式,正在确认选中状态。`);
|
||||
|
||||
if (!await waitForPaymentMethodActive(config.id)) {
|
||||
@@ -1417,8 +1425,6 @@ async function fillPlusBillingAndSubmit(payload = {}) {
|
||||
|
||||
async function fillPlusBillingAddress(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
await fillFullName(payload.fullName || '');
|
||||
|
||||
const countryText = readCountryText();
|
||||
const seed = payload.addressSeed || {
|
||||
query: 'Berlin Mitte',
|
||||
@@ -1434,10 +1440,21 @@ async function fillPlusBillingAddress(payload = {}) {
|
||||
const fields = getStructuredAddressFields();
|
||||
const useDirectStructuredBranch = Boolean(seed.skipAutocomplete || isDropdownStructuredAddressForm(fields));
|
||||
if (!useDirectStructuredBranch) {
|
||||
selected = await selectAddressSuggestion(seed);
|
||||
await performOperationWithDelay({ stepKey: 'plus-checkout-billing', kind: 'fill', label: 'fill-address-query' }, async () => {
|
||||
await fillFullName(payload.fullName || '');
|
||||
await fillAddressQuery(seed);
|
||||
});
|
||||
selected = await performOperationWithDelay({ stepKey: 'plus-checkout-billing', kind: 'select', label: 'select-address-suggestion' }, async () => (
|
||||
clickAddressSuggestion(seed)
|
||||
));
|
||||
}
|
||||
const structuredAddress = await ensureStructuredAddress(seed, {
|
||||
overwrite: useDirectStructuredBranch,
|
||||
const structuredAddress = await performOperationWithDelay({ stepKey: 'plus-checkout-billing', kind: 'fill', label: 'fill-billing-address' }, async () => {
|
||||
if (useDirectStructuredBranch) {
|
||||
await fillFullName(payload.fullName || '');
|
||||
}
|
||||
return ensureStructuredAddress(seed, {
|
||||
overwrite: useDirectStructuredBranch,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -1449,9 +1466,11 @@ async function fillPlusBillingAddress(payload = {}) {
|
||||
|
||||
async function fillPlusAddressQuery(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
await fillFullName(payload.fullName || '');
|
||||
const seed = payload.addressSeed || {};
|
||||
await fillAddressQuery(seed);
|
||||
await performOperationWithDelay({ stepKey: 'plus-checkout-billing', kind: 'fill', label: 'fill-address-query' }, async () => {
|
||||
await fillFullName(payload.fullName || '');
|
||||
await fillAddressQuery(seed);
|
||||
});
|
||||
return {
|
||||
countryText: readCountryText(),
|
||||
queryFilled: true,
|
||||
@@ -1460,7 +1479,9 @@ async function fillPlusAddressQuery(payload = {}) {
|
||||
|
||||
async function selectPlusAddressSuggestion(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const selected = await clickAddressSuggestion(payload.addressSeed || {});
|
||||
const selected = await performOperationWithDelay({ stepKey: 'plus-checkout-billing', kind: 'select', label: 'select-address-suggestion' }, async () => (
|
||||
clickAddressSuggestion(payload.addressSeed || {})
|
||||
));
|
||||
return {
|
||||
selectedAddressText: selected.selectedText,
|
||||
suggestionIndex: selected.suggestionIndex,
|
||||
@@ -1469,9 +1490,11 @@ async function selectPlusAddressSuggestion(payload = {}) {
|
||||
|
||||
async function ensurePlusStructuredBillingAddress(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const structuredAddress = await ensureStructuredAddress(payload.addressSeed || {}, {
|
||||
overwrite: Boolean(payload.overwriteStructuredAddress),
|
||||
});
|
||||
const structuredAddress = await performOperationWithDelay({ stepKey: 'plus-checkout-billing', kind: 'fill', label: 'fill-billing-address' }, async () => (
|
||||
ensureStructuredAddress(payload.addressSeed || {}, {
|
||||
overwrite: Boolean(payload.overwriteStructuredAddress),
|
||||
})
|
||||
));
|
||||
return {
|
||||
countryText: readCountryText(),
|
||||
structuredAddress,
|
||||
@@ -1494,7 +1517,9 @@ async function clickPlusSubscribe(payload = {}) {
|
||||
});
|
||||
|
||||
await sleep(Math.max(0, Math.floor(Number(payload.beforeClickDelayMs) || 0)));
|
||||
await humanLikeClick(subscribeButton);
|
||||
await performOperationWithDelay({ stepKey: 'plus-checkout-billing', kind: 'submit', label: 'click-subscribe' }, async () => {
|
||||
await humanLikeClick(subscribeButton);
|
||||
});
|
||||
return {
|
||||
clicked: true,
|
||||
};
|
||||
|
||||
+344
-96
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user