feat: add DuckDuckGo Email Protection autofill settings

- Implemented a new content script for DuckDuckGo Email autofill functionality.
- Enhanced email polling scripts (mail-163.js, qq-mail.js, signup-page.js, vps-panel.js) to handle user flow interruptions.
- Updated utility functions to manage flow stopping and error handling.
- Introduced a stop button in the side panel for user control over ongoing processes.
- Improved UI elements and styles for better user experience, including new button states and messages.
- Adjusted the side panel to fetch DuckDuckGo email automatically and display it in the input field.
This commit is contained in:
Jimmy
2026-04-05 18:30:16 +08:00
parent 387e177e00
commit 59c9a13edb
13 changed files with 715 additions and 152 deletions
+74
View File
@@ -0,0 +1,74 @@
// content/duck-mail.js — Content script for DuckDuckGo Email Protection autofill settings
console.log('[MultiPage:duck-mail] Content script loaded on', location.href);
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type !== 'FETCH_DUCK_EMAIL') return;
resetStopState();
fetchDuckEmail(message.payload).then(result => {
sendResponse(result);
}).catch(err => {
if (isStopError(err)) {
log('Duck Mail: Stopped by user.', 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
sendResponse({ error: err.message });
});
return true;
});
async function fetchDuckEmail(payload = {}) {
const { generateNew = true } = payload;
log(`Duck Mail: ${generateNew ? 'Generating' : 'Reading'} private address...`);
await waitForElement(
'input.AutofillSettingsPanel__PrivateDuckAddressValue, button.AutofillSettingsPanel__GeneratorButton',
15000
);
const getAddressInput = () => document.querySelector('input.AutofillSettingsPanel__PrivateDuckAddressValue');
const getGeneratorButton = () => document.querySelector('button.AutofillSettingsPanel__GeneratorButton')
|| Array.from(document.querySelectorAll('button')).find(btn => /generate private duck address/i.test(btn.textContent || ''));
const readEmail = () => {
const value = getAddressInput()?.value?.trim() || '';
return value.includes('@duck.com') ? value : '';
};
const waitForEmailValue = async (previousValue = '') => {
for (let i = 0; i < 100; i++) {
const nextValue = readEmail();
if (nextValue && nextValue !== previousValue) {
return nextValue;
}
await sleep(150);
}
throw new Error('Timed out waiting for Duck address to appear.');
};
const currentEmail = readEmail();
if (currentEmail && !generateNew) {
log(`Duck Mail: Found existing address ${currentEmail}`);
return { email: currentEmail, generated: false };
}
await humanPause(500, 1300);
const generatorButton = getGeneratorButton();
if (!generatorButton) {
if (currentEmail) {
log(`Duck Mail: Reusing existing address ${currentEmail}`, 'warn');
return { email: currentEmail, generated: false };
}
throw new Error('Could not find "Generate Private Duck Address" button.');
}
generatorButton.click();
log('Duck Mail: Clicked "Generate Private Duck Address"');
const nextEmail = await waitForEmailValue(currentEmail);
log(`Duck Mail: Ready address ${nextEmail}`, 'ok');
return { email: nextEmail, generated: true };
}
+6
View File
@@ -41,9 +41,15 @@ async function persistSeenCodes() {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'POLL_EMAIL') {
resetStopState();
handlePollEmail(message.step, message.payload).then(result => {
sendResponse(result);
}).catch(err => {
if (isStopError(err)) {
log(`Step ${message.step}: Stopped by user.`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
reportError(message.step, err.message);
sendResponse({ error: err.message });
});
+6
View File
@@ -22,9 +22,15 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse({ ok: false, reason: 'wrong-frame' });
return;
}
resetStopState();
handlePollEmail(message.step, message.payload).then(result => {
sendResponse(result);
}).catch(err => {
if (isStopError(err)) {
log(`Step ${message.step}: Stopped by user.`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
reportError(message.step, err.message);
sendResponse({ error: err.message });
});
+96 -75
View File
@@ -6,9 +6,15 @@ console.log('[MultiPage:signup-page] Content script loaded on', location.href);
// Listen for commands from Background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE') {
resetStopState();
handleCommand(message).then(() => {
sendResponse({ ok: true });
}).catch(err => {
if (isStopError(err)) {
log(`Step ${message.step}: Stopped by user.`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
reportError(message.step, err.message);
sendResponse({ error: err.message });
});
@@ -59,6 +65,7 @@ async function step2_clickRegister() {
}
}
await humanPause(450, 1200);
reportComplete(2);
simulateClick(registerBtn);
log('Step 2: Clicked Register button');
@@ -85,6 +92,7 @@ async function step3_fillEmailPassword(payload) {
throw new Error('Could not find email input field on signup page. URL: ' + location.href);
}
await humanPause(500, 1400);
fillInput(emailInput, email);
log('Step 3: Email filled');
@@ -98,6 +106,7 @@ async function step3_fillEmailPassword(payload) {
|| await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
if (submitBtn) {
await humanPause(400, 1100);
simulateClick(submitBtn);
log('Step 3: Submitted email, waiting for password field...');
await sleep(2000);
@@ -111,6 +120,7 @@ async function step3_fillEmailPassword(payload) {
}
if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
await humanPause(600, 1500);
fillInput(passwordInput, payload.password);
log('Step 3: Password filled');
@@ -124,6 +134,7 @@ async function step3_fillEmailPassword(payload) {
|| await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
if (submitBtn) {
await humanPause(500, 1300);
simulateClick(submitBtn);
log('Step 3: Form submitted');
}
@@ -174,6 +185,7 @@ async function fillVerificationCode(step, payload) {
|| await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证/i, 5000).catch(() => null);
if (submitBtn) {
await humanPause(450, 1200);
simulateClick(submitBtn);
log(`Step ${step}: Verification submitted`);
}
@@ -200,6 +212,7 @@ async function step6_login(payload) {
throw new Error('Could not find email input on login page. URL: ' + location.href);
}
await humanPause(500, 1400);
fillInput(emailInput, email);
log('Step 6: Email filled');
@@ -208,6 +221,7 @@ async function step6_login(payload) {
const submitBtn1 = document.querySelector('button[type="submit"]')
|| await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
if (submitBtn1) {
await humanPause(400, 1100);
simulateClick(submitBtn1);
log('Step 6: Submitted email');
}
@@ -218,6 +232,7 @@ async function step6_login(payload) {
const passwordInput = document.querySelector('input[type="password"]');
if (passwordInput) {
log('Step 6: Password field found, filling password...');
await humanPause(550, 1450);
fillInput(passwordInput, password);
await sleep(500);
@@ -227,6 +242,7 @@ async function step6_login(payload) {
reportComplete(6, { needsOTP: true });
if (submitBtn2) {
await humanPause(450, 1200);
simulateClick(submitBtn2);
log('Step 6: Submitted password, may need verification code (step 7)');
}
@@ -239,14 +255,14 @@ async function step6_login(payload) {
}
// ============================================================
// Step 8: Click "继续" on OAuth consent page
// Step 8: Focus "继续" on OAuth consent page for manual click
// ============================================================
// After login + verification, page shows:
// "使用 ChatGPT 登录到 Codex" with a "继续" submit button.
// Clicking it triggers redirect to localhost URL.
// We only locate and focus it so the user can click manually.
async function step8_clickContinue() {
log('Step 8: Looking for OAuth consent "继续" button...');
log('Step 8: Looking for OAuth consent "继续" button for manual click...');
// Wait for the consent page to be ready
// Look for the submit button with text "继续" or data-dd-action-name="Continue"
@@ -264,40 +280,33 @@ async function step8_clickContinue() {
}
}
log('Step 8: Found "继续" button, clicking...');
// Use native .click() — simulateClick (dispatchEvent) may not trigger form submit
continueBtn.click();
log('Step 8: Clicked via .click()');
// Also try submitting the form directly as a fallback
await sleep(500);
const form = continueBtn.closest('form');
if (form) {
form.requestSubmit(continueBtn);
log('Step 8: Also triggered form.requestSubmit()');
}
log('Step 8: Redirecting to localhost... (background will capture URL)');
// Don't reportComplete — background handles it via webNavigation listener
await humanPause(350, 900);
continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
continueBtn.focus();
log('Step 8: Found "继续" button and focused it. Please click it manually.');
}
// ============================================================
// Step 5: Fill Name & Birthday
// Step 5: Fill Name & Birthday / Age
// ============================================================
async function step5_fillNameBirthday(payload) {
const { firstName, lastName, year, month, day } = payload;
const { firstName, lastName, age, year, month, day } = payload;
if (!firstName || !lastName) throw new Error('No name data provided.');
const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null);
const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value)));
if (!hasBirthdayData && (resolvedAge == null || Number.isNaN(Number(resolvedAge)))) {
throw new Error('No birthday or age data provided.');
}
const fullName = `${firstName} ${lastName}`;
log(`Step 5: Filling name: ${fullName}, Birthday: ${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}`);
log(`Step 5: Filling name: ${fullName}`);
// Actual DOM structure:
// - Full name: <input name="name" placeholder="全名" type="text">
// - Birthday: React Aria DateField with 3 spinbutton divs (year/month/day)
// + <input type="hidden" name="birthday" value="2026-04-05">
// - Birthday: React Aria DateField or hidden input[name="birthday"]
// - Age: <input name="age" type="text|number">
// --- Full Name (single field, not first+last) ---
let nameInput = null;
@@ -309,74 +318,85 @@ async function step5_fillNameBirthday(payload) {
} catch {
throw new Error('Could not find name input. URL: ' + location.href);
}
await humanPause(500, 1300);
fillInput(nameInput, fullName);
log(`Step 5: Name filled: ${fullName}`);
// --- Birthday (React Aria DateField with spinbutton segments) ---
// The date field has three contenteditable divs with role="spinbutton"
// and data-type="year", data-type="month", data-type="day"
// There's also a hidden input[name="birthday"] that stores the actual value
let birthdayMode = false;
let ageInput = null;
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
for (let i = 0; i < 100; i++) {
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
const hiddenBirthday = document.querySelector('input[name="birthday"]');
ageInput = document.querySelector('input[name="age"]');
if (yearSpinner && monthSpinner && daySpinner) {
log('Step 5: Found React Aria DateField spinbuttons');
if ((yearSpinner && monthSpinner && daySpinner) || hiddenBirthday) {
birthdayMode = true;
break;
}
if (ageInput) break;
await sleep(100);
}
// Helper to set a spinbutton value via focus + keyboard input
async function setSpinButton(el, value) {
el.focus();
await sleep(100);
// Select all existing text
document.execCommand('selectAll', false, null);
await sleep(50);
// Type the new value digit by digit
const valueStr = String(value);
for (const char of valueStr) {
el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: `Digit${char}`, bubbles: true }));
// Also use InputEvent for React Aria
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: char, bubbles: true }));
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true }));
await sleep(50);
}
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', code: 'Tab', bubbles: true }));
el.blur();
await sleep(100);
if (birthdayMode) {
if (!hasBirthdayData) {
throw new Error('Birthday field detected, but no birthday data provided.');
}
await setSpinButton(yearSpinner, year);
log(`Step 5: Year set: ${year}`);
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
log(`Step 5: Month set: ${month}`);
if (yearSpinner && monthSpinner && daySpinner) {
log('Step 5: Birthday fields detected, filling birthday...');
await setSpinButton(daySpinner, String(day).padStart(2, '0'));
log(`Step 5: Day set: ${day}`);
async function setSpinButton(el, value) {
el.focus();
await sleep(100);
document.execCommand('selectAll', false, null);
await sleep(50);
// Also update the hidden input directly as a safety measure
const hiddenBirthday = document.querySelector('input[type="hidden"][name="birthday"]');
const valueStr = String(value);
for (const char of valueStr) {
el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: `Digit${char}`, bubbles: true }));
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: char, bubbles: true }));
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true }));
await sleep(50);
}
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', code: 'Tab', bubbles: true }));
el.blur();
await sleep(100);
}
await humanPause(450, 1100);
await setSpinButton(yearSpinner, year);
await humanPause(250, 650);
await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
await humanPause(250, 650);
await setSpinButton(daySpinner, String(day).padStart(2, '0'));
log(`Step 5: Birthday filled: ${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('change', { bubbles: true }));
log(`Step 5: Hidden birthday input set: ${dateStr}`);
}
} else {
// Fallback: try setting hidden input directly
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('change', { bubbles: true }));
log(`Step 5: Birthday set via hidden input: ${dateStr}`);
} else {
log('Step 5: WARNING - Could not find birthday fields. May need to adjust selectors.', 'warn');
} else if (ageInput) {
if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) {
throw new Error('Age field detected, but no age data provided.');
}
await humanPause(500, 1300);
fillInput(ageInput, String(resolvedAge));
log(`Step 5: Age filled: ${resolvedAge}`);
} else {
throw new Error('Could not find birthday or age input. URL: ' + location.href);
}
// Click "完成帐户创建" button
@@ -388,6 +408,7 @@ async function step5_fillNameBirthday(payload) {
reportComplete(5);
if (completeBtn) {
await humanPause(500, 1300);
simulateClick(completeBtn);
log('Step 5: Clicked "完成帐户创建"');
}
+110 -7
View File
@@ -5,12 +5,37 @@ const SCRIPT_SOURCE = (() => {
if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
if (url.includes('mail.qq.com')) return 'qq-mail';
if (url.includes('mail.163.com')) return 'mail-163';
if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
if (url.includes('chatgpt.com')) return 'chatgpt';
// VPS panel — detected dynamically since URL is configurable
return 'vps-panel';
})();
const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
let flowStopped = false;
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'STOP_FLOW') {
flowStopped = true;
console.warn(LOG_PREFIX, STOP_ERROR_MESSAGE);
}
});
function resetStopState() {
flowStopped = false;
}
function isStopError(error) {
const message = typeof error === 'string' ? error : error?.message;
return message === STOP_ERROR_MESSAGE;
}
function throwIfStopped() {
if (flowStopped) {
throw new Error(STOP_ERROR_MESSAGE);
}
}
/**
* Wait for a DOM element to appear.
@@ -20,6 +45,8 @@ const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
*/
function waitForElement(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
throwIfStopped();
const existing = document.querySelector(selector);
if (existing) {
console.log(LOG_PREFIX, `Found immediately: ${selector}`);
@@ -31,11 +58,25 @@ function waitForElement(selector, timeout = 10000) {
console.log(LOG_PREFIX, `Waiting for: ${selector} (timeout: ${timeout}ms)`);
log(`Waiting for selector: ${selector}...`);
let settled = false;
let stopTimer = null;
const cleanup = () => {
if (settled) return;
settled = true;
observer.disconnect();
clearTimeout(timer);
clearTimeout(stopTimer);
};
const observer = new MutationObserver(() => {
if (flowStopped) {
cleanup();
reject(new Error(STOP_ERROR_MESSAGE));
return;
}
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
clearTimeout(timer);
cleanup();
console.log(LOG_PREFIX, `Found after wait: ${selector}`);
log(`Found element: ${selector}`);
resolve(el);
@@ -48,11 +89,22 @@ function waitForElement(selector, timeout = 10000) {
});
const timer = setTimeout(() => {
observer.disconnect();
cleanup();
const msg = `Timeout waiting for ${selector} after ${timeout}ms on ${location.href}`;
console.error(LOG_PREFIX, msg);
reject(new Error(msg));
}, timeout);
const pollStop = () => {
if (settled) return;
if (flowStopped) {
cleanup();
reject(new Error(STOP_ERROR_MESSAGE));
return;
}
stopTimer = setTimeout(pollStop, 100);
};
pollStop();
});
}
@@ -65,6 +117,8 @@ function waitForElement(selector, timeout = 10000) {
*/
function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
return new Promise((resolve, reject) => {
throwIfStopped();
function search() {
const candidates = document.querySelectorAll(containerSelector);
for (const el of candidates) {
@@ -86,11 +140,25 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
console.log(LOG_PREFIX, `Waiting for text match: ${containerSelector} / ${textPattern}`);
log(`Waiting for element with text: ${textPattern}...`);
let settled = false;
let stopTimer = null;
const cleanup = () => {
if (settled) return;
settled = true;
observer.disconnect();
clearTimeout(timer);
clearTimeout(stopTimer);
};
const observer = new MutationObserver(() => {
if (flowStopped) {
cleanup();
reject(new Error(STOP_ERROR_MESSAGE));
return;
}
const el = search();
if (el) {
observer.disconnect();
clearTimeout(timer);
cleanup();
console.log(LOG_PREFIX, `Found by text after wait: ${textPattern}`);
log(`Found element by text: ${textPattern}`);
resolve(el);
@@ -103,11 +171,22 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
});
const timer = setTimeout(() => {
observer.disconnect();
cleanup();
const msg = `Timeout waiting for text "${textPattern}" in "${containerSelector}" after ${timeout}ms on ${location.href}`;
console.error(LOG_PREFIX, msg);
reject(new Error(msg));
}, timeout);
const pollStop = () => {
if (settled) return;
if (flowStopped) {
cleanup();
reject(new Error(STOP_ERROR_MESSAGE));
return;
}
stopTimer = setTimeout(pollStop, 100);
};
pollStop();
});
}
@@ -118,6 +197,7 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
* @param {string} value
*/
function fillInput(el, value) {
throwIfStopped();
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
@@ -135,6 +215,7 @@ function fillInput(el, value) {
* @param {string} value
*/
function fillSelect(el, value) {
throwIfStopped();
el.value = value;
el.dispatchEvent(new Event('change', { bubbles: true }));
console.log(LOG_PREFIX, `Selected value ${value} in ${el.name || el.id}`);
@@ -209,6 +290,7 @@ function reportError(step, errorMessage) {
* @param {Element} el
*/
function simulateClick(el) {
throwIfStopped();
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
console.log(LOG_PREFIX, `Clicked: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`);
log(`Clicked [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`);
@@ -220,7 +302,28 @@ function simulateClick(el) {
* @returns {Promise<void>}
*/
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
return new Promise((resolve, reject) => {
const start = Date.now();
function tick() {
if (flowStopped) {
reject(new Error(STOP_ERROR_MESSAGE));
return;
}
if (Date.now() - start >= ms) {
resolve();
return;
}
setTimeout(tick, Math.min(100, Math.max(25, ms - (Date.now() - start))));
}
tick();
});
}
async function humanPause(min = 250, max = 850) {
const duration = Math.floor(Math.random() * (max - min + 1)) + min;
await sleep(duration);
}
// Auto-report ready on load
+9
View File
@@ -28,9 +28,15 @@ console.log('[MultiPage:vps-panel] Content script loaded on', location.href);
// Listen for commands from Background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'EXECUTE_STEP') {
resetStopState();
handleStep(message.step, message.payload).then(() => {
sendResponse({ ok: true });
}).catch(err => {
if (isStopError(err)) {
log(`Step ${message.step}: Stopped by user.`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
reportError(message.step, err.message);
sendResponse({ error: err.message });
});
@@ -77,6 +83,7 @@ async function step1_getOAuthLink() {
if (loginBtn.disabled) {
log('Step 1: Login button is disabled (already loading), waiting for auth URL...');
} else {
await humanPause(500, 1400);
simulateClick(loginBtn);
log('Step 1: Clicked login button, waiting for auth URL...');
}
@@ -133,6 +140,7 @@ async function step9_vpsVerify(payload) {
}
}
await humanPause(600, 1500);
fillInput(urlInput, localhostUrl);
log(`Step 9: Filled callback URL: ${localhostUrl.slice(0, 80)}...`);
@@ -152,6 +160,7 @@ async function step9_vpsVerify(payload) {
}
}
await humanPause(450, 1200);
simulateClick(submitBtn);
log('Step 9: Clicked "提交回调 URL", waiting for authentication result...');