diff --git a/background.js b/background.js
index 20b2788..678fdd7 100644
--- a/background.js
+++ b/background.js
@@ -10097,6 +10097,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
setEmailState,
SIGNUP_ENTRY_URL,
SIGNUP_PAGE_INJECT_FILES,
+ waitForTabStableComplete,
waitForTabUrlMatch,
});
const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.createVerificationFlowHelpers({
@@ -10232,6 +10233,7 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({
shouldUseCustomRegistrationEmail,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
throwIfStopped,
+ waitForTabStableComplete,
phoneVerificationHelpers,
resolveSignupMethod,
});
diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js
index f46ecae..c087af1 100644
--- a/background/phone-verification-flow.js
+++ b/background/phone-verification-flow.js
@@ -4079,15 +4079,21 @@
if (!normalizedActivation) {
throw new Error('步骤 2:接码平台返回的手机号订单无效。');
}
+ const countryConfig = resolveCountryConfigFromActivation(normalizedActivation, state);
+ const signupActivation = normalizeActivation({
+ ...normalizedActivation,
+ countryId: countryConfig?.id ?? normalizedActivation.countryId,
+ countryLabel: normalizedActivation.countryLabel || countryConfig?.label || '',
+ }) || normalizedActivation;
await persistSignupPhoneRuntimeState({
- signupPhoneNumber: normalizedActivation.phoneNumber,
- signupPhoneActivation: normalizedActivation,
+ signupPhoneNumber: signupActivation.phoneNumber,
+ signupPhoneActivation: signupActivation,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: 'signup',
accountIdentifierType: 'phone',
- accountIdentifier: normalizedActivation.phoneNumber,
+ accountIdentifier: signupActivation.phoneNumber,
});
- return normalizedActivation;
+ return signupActivation;
});
}
diff --git a/background/signup-flow-helpers.js b/background/signup-flow-helpers.js
index c8172a1..36671c2 100644
--- a/background/signup-flow-helpers.js
+++ b/background/signup-flow-helpers.js
@@ -24,6 +24,7 @@
setEmailState,
SIGNUP_ENTRY_URL,
SIGNUP_PAGE_INJECT_FILES,
+ waitForTabStableComplete = null,
waitForTabUrlMatch,
} = deps;
@@ -139,6 +140,22 @@
throw new Error(`注册身份提交后未能识别当前页面,既不是密码页、验证码页,也不是资料页。URL: ${landingUrl || 'unknown'}`);
}
+ if (landingState !== 'password_page' && typeof waitForTabStableComplete === 'function') {
+ const stableTab = await waitForTabStableComplete(tabId, {
+ timeoutMs: 45000,
+ retryDelayMs: 300,
+ stableMs: 800,
+ initialDelayMs: 300,
+ });
+ if (stableTab?.url) {
+ const stableState = resolveSignupPostIdentityState(stableTab.url);
+ if (stableState) {
+ landingUrl = stableTab.url;
+ landingState = stableState;
+ }
+ }
+ }
+
await ensureContentScriptReadyOnTab('signup-page', tabId, {
inject: SIGNUP_PAGE_INJECT_FILES,
injectSource: 'signup-page',
diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js
index 4c986c7..0d2051f 100644
--- a/background/steps/fetch-signup-code.js
+++ b/background/steps/fetch-signup-code.js
@@ -27,6 +27,7 @@
shouldUseCustomRegistrationEmail,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
throwIfStopped,
+ waitForTabStableComplete = null,
phoneVerificationHelpers = null,
resolveSignupMethod = () => 'email',
} = deps;
@@ -119,6 +120,16 @@
await chrome.tabs.update(signupTabId, { active: true });
throwIfStopped();
+ if (typeof waitForTabStableComplete === 'function') {
+ await addLog('步骤 4:等待注册验证码页面完成加载后再继续...', 'info');
+ await waitForTabStableComplete(signupTabId, {
+ timeoutMs: 45000,
+ retryDelayMs: 300,
+ stableMs: 800,
+ initialDelayMs: 300,
+ });
+ }
+ throwIfStopped();
await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
const prepareRequest = {
diff --git a/content/signup-page.js b/content/signup-page.js
index 235e8c2..862e01e 100644
--- a/content/signup-page.js
+++ b/content/signup-page.js
@@ -1147,8 +1147,8 @@ function normalizePhoneDigits(value) {
}
function extractDialCodeFromText(value) {
- const match = String(value || '').match(/\(\+\s*(\d{1,4})\s*\)|\+\s*(\d{1,4})\b/);
- return String(match?.[1] || match?.[2] || '').trim();
+ const match = String(value || '').match(/\(\+\s*(\d{1,4})\s*\)|\+\s*\(\s*(\d{1,4})\s*\)|\+\s*(\d{1,4})\b/);
+ return String(match?.[1] || match?.[2] || match?.[3] || '').trim();
}
function dispatchSignupPhoneFieldEvents(element) {
@@ -1162,12 +1162,48 @@ function normalizeSignupCountryLabel(value) {
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/&/g, ' and ')
- .replace(/[^\w\s]/g, ' ')
+ .replace(/[^\p{L}\p{N}\s]/gu, ' ')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
+function getSignupCountryLabelAliases(value) {
+ const aliases = new Set();
+ const addAlias = (alias) => {
+ const normalized = normalizeSignupCountryLabel(alias);
+ if (normalized) {
+ aliases.add(normalized);
+ }
+ };
+
+ const raw = String(value || '').trim();
+ addAlias(raw);
+
+ const normalized = normalizeSignupCountryLabel(raw);
+ const compact = normalized.replace(/\s+/g, '');
+ if (
+ /(?:^|\s)(?:gb|uk)(?:\s|$)/i.test(raw)
+ || /england|united\s*kingdom|great\s*britain|\bbritain\b/i.test(raw)
+ || /英国|英格兰|大不列颠/.test(raw)
+ || ['gb', 'uk', 'england', 'unitedkingdom', 'greatbritain', 'britain'].includes(compact)
+ ) {
+ [
+ 'GB',
+ 'UK',
+ 'United Kingdom',
+ 'Great Britain',
+ 'Britain',
+ 'England',
+ '英国',
+ '英格兰',
+ '大不列颠',
+ ].forEach(addAlias);
+ }
+
+ return Array.from(aliases);
+}
+
function getSignupPhoneOptionLabel(option) {
return String(option?.textContent || option?.label || '')
.replace(/\s+/g, ' ')
@@ -1242,12 +1278,72 @@ function getSignupPhoneForm(phoneInput = getSignupPhoneInput()) {
return phoneInput?.closest?.('form') || null;
}
-function getSignupPhoneCountrySelect(phoneInput = getSignupPhoneInput()) {
- const form = getSignupPhoneForm(phoneInput);
- if (!form || typeof form.querySelector !== 'function') {
- return null;
+function getSignupPhoneControlRoots(phoneInput = getSignupPhoneInput()) {
+ const roots = [];
+ const addRoot = (root) => {
+ if (root && !roots.includes(root)) {
+ roots.push(root);
+ }
+ };
+
+ addRoot(phoneInput?.closest?.('form'));
+ addRoot(phoneInput?.closest?.('fieldset'));
+ addRoot(phoneInput?.closest?.('[data-rac]'));
+ addRoot(phoneInput?.closest?.('[role="group"]'));
+ addRoot(phoneInput?.parentElement);
+ addRoot(phoneInput?.parentElement?.parentElement);
+ addRoot(document);
+
+ return roots;
+}
+
+function querySignupPhoneCountryElements(root, selector) {
+ if (!root || !selector) {
+ return [];
}
- return form.querySelector('select');
+ if (typeof root.querySelectorAll === 'function') {
+ const directMatches = Array.from(root.querySelectorAll(selector));
+ if (directMatches.length > 0) {
+ return directMatches;
+ }
+ }
+ if (typeof root.querySelector === 'function') {
+ const selectors = String(selector || '')
+ .split(',')
+ .map((part) => part.trim())
+ .filter(Boolean);
+ const matches = [];
+ for (const part of selectors) {
+ const element = root.querySelector(part);
+ if (element && !matches.includes(element)) {
+ matches.push(element);
+ }
+ }
+ return matches;
+ }
+ return [];
+}
+
+function isSignupPhoneCountrySelect(select) {
+ if (!select) {
+ return false;
+ }
+ return Array.from(select.options || []).some((option) => (
+ extractDialCodeFromText(getSignupPhoneOptionLabel(option))
+ || /^[A-Z]{2}$/.test(normalizeSignupCountryOptionValue(option?.value))
+ ));
+}
+
+function getSignupPhoneCountrySelect(phoneInput = getSignupPhoneInput()) {
+ const selects = [];
+ for (const root of getSignupPhoneControlRoots(phoneInput)) {
+ for (const select of querySignupPhoneCountryElements(root, 'select')) {
+ if (!selects.includes(select)) {
+ selects.push(select);
+ }
+ }
+ }
+ return selects.find(isSignupPhoneCountrySelect) || selects[0] || null;
}
function getSignupPhoneSelectedCountryOption(phoneInput = getSignupPhoneInput()) {
@@ -1259,9 +1355,7 @@ function getSignupPhoneSelectedCountryOption(phoneInput = getSignupPhoneInput())
}
function getSignupPhoneCountryButtonText(phoneInput = getSignupPhoneInput()) {
- const form = getSignupPhoneForm(phoneInput);
- if (!form || typeof form.querySelector !== 'function') return '';
- const button = form.querySelector('button[aria-haspopup="listbox"]');
+ const button = getSignupPhoneCountryButton(phoneInput);
if (!button) return '';
const valueNode = button.querySelector('.react-aria-SelectValue');
return String(valueNode?.textContent || button.textContent || '')
@@ -1269,6 +1363,24 @@ function getSignupPhoneCountryButtonText(phoneInput = getSignupPhoneInput()) {
.trim();
}
+function getSignupPhoneCountryButton(phoneInput = getSignupPhoneInput()) {
+ const candidates = [];
+ for (const root of getSignupPhoneControlRoots(phoneInput)) {
+ const buttons = querySignupPhoneCountryElements(
+ root,
+ 'button[aria-haspopup="listbox"], [role="button"][aria-haspopup="listbox"], [role="combobox"][aria-haspopup="listbox"], button[aria-expanded]'
+ );
+ for (const button of buttons) {
+ if (!candidates.includes(button)) {
+ candidates.push(button);
+ }
+ }
+ }
+ return candidates.find((button) => isVisibleElement(button) && extractDialCodeFromText(getActionText(button)))
+ || candidates.find(isVisibleElement)
+ || null;
+}
+
function getSignupPhoneDisplayedDialCode(phoneInput = getSignupPhoneInput()) {
const buttonDialCode = extractDialCodeFromText(getSignupPhoneCountryButtonText(phoneInput));
if (buttonDialCode) {
@@ -1295,29 +1407,134 @@ function getSignupPhoneHiddenNumberInput(phoneInput = getSignupPhoneInput()) {
return form.querySelector('input[name="phoneNumber"]');
}
+function resolveSignupPhoneDialCodeFromNumber(phoneNumber = '', texts = []) {
+ const digits = normalizePhoneDigits(phoneNumber);
+ if (!digits) {
+ return '';
+ }
+
+ const textDialCodes = texts
+ .map((text) => normalizePhoneDigits(extractDialCodeFromText(text)))
+ .filter((dialCode) => dialCode && digits.startsWith(dialCode) && digits.length > dialCode.length)
+ .sort((left, right) => right.length - left.length);
+ if (textDialCodes[0]) {
+ return textDialCodes[0];
+ }
+
+ const knownDialCodes = [
+ '1246', '1264', '1268', '1284', '1340', '1345', '1441', '1473', '1649', '1664', '1670', '1671', '1684',
+ '1721', '1758', '1767', '1784', '1809', '1829', '1849', '1868', '1869', '1876',
+ '971', '962', '886', '880', '856', '855', '852', '853', '673', '672', '670', '599', '598', '597', '596',
+ '595', '594', '593', '592', '591', '590', '509', '508', '507', '506', '505', '504', '503', '502', '501',
+ '423', '421', '420', '389', '387', '386', '385', '383', '382', '381', '380', '379', '378', '377', '376',
+ '375', '374', '373', '372', '371', '370', '359', '358', '357', '356', '355', '354', '353', '352', '351',
+ '350', '299', '298', '297', '291', '290', '269', '268', '267', '266', '265', '264', '263', '262', '261',
+ '260', '258', '257', '256', '255', '254', '253', '252', '251', '250', '249', '248', '247', '246', '245',
+ '244', '243', '242', '241', '240', '239', '238', '237', '236', '235', '234', '233', '232', '231', '230',
+ '229', '228', '227', '226', '225', '224', '223', '222', '221', '220', '218', '216', '213', '212', '211',
+ '98', '95', '94', '93', '92', '91', '90', '89', '88', '86', '84', '82', '81', '66', '65', '64', '63',
+ '62', '61', '60', '58', '57', '56', '55', '54', '53', '52', '51', '49', '48', '47', '46', '45', '44',
+ '43', '41', '40', '39', '36', '34', '33', '32', '31', '30', '27', '20', '7', '1',
+ ];
+ return knownDialCodes.find((code) => digits.startsWith(code) && digits.length > code.length) || '';
+}
+
+function resolveSignupPhoneTargetDialCode(options = {}, targetOption = null) {
+ const optionDialCode = extractDialCodeFromText(getSignupPhoneOptionLabel(targetOption));
+ if (optionDialCode) {
+ return optionDialCode;
+ }
+
+ const countryText = String(options.countryLabel || '').trim();
+ if (/australia|澳大利亚/i.test(countryText)) return '61';
+ if (/thailand|泰国/i.test(countryText)) return '66';
+ if (/vietnam|越南/i.test(countryText)) return '84';
+ if (/england|united\s*kingdom|great\s*britain|\bbritain\b|英国|英格兰|uk|gb/i.test(countryText)) return '44';
+
+ return resolveSignupPhoneDialCodeFromNumber(options.phoneNumber);
+}
+
+function getSignupPhoneCountryTargetLabels(targetOption, options = {}) {
+ const labels = new Set();
+ const addLabel = (value) => {
+ getSignupCountryLabelAliases(value).forEach((alias) => labels.add(alias));
+ };
+
+ addLabel(options.countryLabel);
+ if (targetOption) {
+ getSignupPhoneCountryMatchLabels(targetOption).forEach(addLabel);
+ }
+
+ return Array.from(labels);
+}
+
+function doesSignupPhoneCountryTextMatchTarget(text, targetOption, options = {}) {
+ const normalizedText = normalizeSignupCountryLabel(text);
+ if (!normalizedText) {
+ return false;
+ }
+
+ const labels = getSignupPhoneCountryTargetLabels(targetOption, options);
+ if (labels.some((label) => (
+ label
+ && (
+ normalizedText === label
+ || (label.length > 1 && normalizedText.includes(label))
+ || (normalizedText.length > 2 && label.includes(normalizedText))
+ )
+ ))) {
+ return true;
+ }
+
+ const targetDialCode = resolveSignupPhoneTargetDialCode(options, targetOption);
+ return Boolean(targetDialCode && extractDialCodeFromText(text) === targetDialCode);
+}
+
+function isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options = {}) {
+ const targetDialCode = resolveSignupPhoneTargetDialCode(options, targetOption);
+ const displayedText = getSignupPhoneCountryButtonText(phoneInput);
+ const displayedDialCode = extractDialCodeFromText(displayedText);
+
+ if (targetDialCode && displayedDialCode) {
+ return displayedDialCode === targetDialCode
+ && (!displayedText || doesSignupPhoneCountryTextMatchTarget(displayedText, targetOption, options));
+ }
+
+ if (displayedText && doesSignupPhoneCountryTextMatchTarget(displayedText, targetOption, options)) {
+ return true;
+ }
+
+ const selectedOption = getSignupPhoneSelectedCountryOption(phoneInput);
+ if (selectedOption && targetOption && isSameSignupCountryOption(selectedOption, targetOption)) {
+ return !displayedDialCode || !targetDialCode || displayedDialCode === targetDialCode;
+ }
+
+ return Boolean(selectedOption && !targetOption && targetDialCode && displayedDialCode === targetDialCode);
+}
+
function findSignupPhoneCountryOptionByLabel(phoneInput, countryLabel) {
const select = getSignupPhoneCountrySelect(phoneInput);
if (!select) {
return null;
}
- const normalizedTarget = normalizeSignupCountryLabel(countryLabel);
- if (!normalizedTarget) {
+ const normalizedTargets = getSignupCountryLabelAliases(countryLabel);
+ if (normalizedTargets.length === 0) {
return null;
}
const options = Array.from(select.options || []);
return options.find((option) => (
- getSignupPhoneCountryMatchLabels(option).some((label) => normalizeSignupCountryLabel(label) === normalizedTarget)
+ getSignupPhoneCountryMatchLabels(option).some((label) => normalizedTargets.includes(normalizeSignupCountryLabel(label)))
))
|| options.find((option) => {
const normalizedLabels = getSignupPhoneCountryMatchLabels(option)
.map((label) => normalizeSignupCountryLabel(label))
.filter(Boolean);
- return normalizedLabels.some((optionLabel) => (
- optionLabel.length > 2
- && normalizedTarget.length > 2
- && (optionLabel.includes(normalizedTarget) || normalizedTarget.includes(optionLabel))
- ));
+ return normalizedLabels.some((optionLabel) => normalizedTargets.some((normalizedTarget) => (
+ optionLabel.length > 2
+ && normalizedTarget.length > 2
+ && (optionLabel.includes(normalizedTarget) || normalizedTarget.includes(optionLabel))
+ )));
})
|| null;
}
@@ -1347,7 +1564,7 @@ function findSignupPhoneCountryOptionByPhoneNumber(phoneInput, phoneNumber) {
return bestMatch;
}
-async function trySelectSignupPhoneCountryOption(select, targetOption) {
+async function trySelectSignupPhoneCountryOption(select, targetOption, phoneInput = getSignupPhoneInput(), options = {}) {
if (!select || !targetOption) {
return false;
}
@@ -1355,47 +1572,123 @@ async function trySelectSignupPhoneCountryOption(select, targetOption) {
? (select.options?.[select.selectedIndex] || null)
: null;
if (selectedOption && isSameSignupCountryOption(selectedOption, targetOption)) {
- return true;
+ dispatchSignupPhoneFieldEvents(select);
+ await sleep(120);
+ return isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options);
}
select.value = String(targetOption.value || '');
dispatchSignupPhoneFieldEvents(select);
await sleep(250);
- const nextSelectedOption = select.selectedIndex >= 0
- ? (select.options?.[select.selectedIndex] || null)
- : null;
- return Boolean(nextSelectedOption && isSameSignupCountryOption(nextSelectedOption, targetOption));
+ return isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options);
+}
+
+function getVisibleSignupPhoneCountryListboxOptions() {
+ const seen = new Set();
+ return Array.from(document.querySelectorAll('[role="listbox"] [role="option"], [role="option"]'))
+ .filter((option) => {
+ if (!option || seen.has(option)) {
+ return false;
+ }
+ seen.add(option);
+ return isVisibleElement(option);
+ });
+}
+
+function findSignupPhoneCountryListboxOption(targetOption, options = {}) {
+ const candidates = getVisibleSignupPhoneCountryListboxOptions();
+ const byLabel = candidates.find((option) => doesSignupPhoneCountryTextMatchTarget(getActionText(option), targetOption, options));
+ if (byLabel) {
+ return byLabel;
+ }
+
+ const targetDialCode = resolveSignupPhoneTargetDialCode(options, targetOption);
+ if (!targetDialCode) {
+ const digits = normalizePhoneDigits(options.phoneNumber);
+ let bestMatch = null;
+ let bestDialCodeLength = 0;
+ for (const option of candidates) {
+ const dialCode = normalizePhoneDigits(extractDialCodeFromText(getActionText(option)));
+ if (!dialCode || !digits.startsWith(dialCode) || dialCode.length <= bestDialCodeLength) {
+ continue;
+ }
+ bestMatch = option;
+ bestDialCodeLength = dialCode.length;
+ }
+ return bestMatch;
+ }
+ return candidates.find((option) => extractDialCodeFromText(getActionText(option)) === targetDialCode) || null;
+}
+
+async function trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption, options = {}) {
+ const button = getSignupPhoneCountryButton(phoneInput);
+ if (!button) {
+ return false;
+ }
+
+ simulateClick(button);
+ await sleep(200);
+
+ const start = Date.now();
+ while (Date.now() - start < 3000) {
+ throwIfStopped();
+ const option = findSignupPhoneCountryListboxOption(targetOption, options);
+ if (option) {
+ simulateClick(option);
+ await sleep(450);
+ if (isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options)) {
+ return true;
+ }
+ }
+ await sleep(150);
+ }
+
+ return false;
}
async function ensureSignupPhoneCountrySelected(phoneInput, options = {}) {
const select = getSignupPhoneCountrySelect(phoneInput);
- if (!select) {
+ const hasCountryControl = Boolean(select || getSignupPhoneCountryButton(phoneInput));
+ if (!hasCountryControl) {
return {
hasSelect: false,
+ hasCountryControl: false,
matched: false,
selectedOption: null,
};
}
const byLabel = findSignupPhoneCountryOptionByLabel(phoneInput, options.countryLabel);
- if (await trySelectSignupPhoneCountryOption(select, byLabel)) {
- return {
- hasSelect: true,
- matched: true,
- selectedOption: getSignupPhoneSelectedCountryOption(phoneInput),
- };
- }
-
const byPhoneNumber = findSignupPhoneCountryOptionByPhoneNumber(phoneInput, options.phoneNumber);
- if (await trySelectSignupPhoneCountryOption(select, byPhoneNumber)) {
- return {
- hasSelect: true,
- matched: true,
- selectedOption: getSignupPhoneSelectedCountryOption(phoneInput),
- };
+ const targets = [byLabel, byPhoneNumber, null].filter((target, index, list) => (
+ index === list.findIndex((item) => (
+ (!item && !target)
+ || (item && target && isSameSignupCountryOption(item, target))
+ ))
+ ));
+
+ for (const targetOption of targets) {
+ if (await trySelectSignupPhoneCountryOption(select, targetOption, phoneInput, options)) {
+ return {
+ hasSelect: Boolean(select),
+ hasCountryControl: true,
+ matched: true,
+ selectedOption: getSignupPhoneSelectedCountryOption(phoneInput),
+ };
+ }
+
+ if (await trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption, options)) {
+ return {
+ hasSelect: Boolean(select),
+ hasCountryControl: true,
+ matched: true,
+ selectedOption: getSignupPhoneSelectedCountryOption(phoneInput),
+ };
+ }
}
return {
- hasSelect: true,
+ hasSelect: Boolean(select),
+ hasCountryControl: true,
matched: false,
selectedOption: getSignupPhoneSelectedCountryOption(phoneInput),
};
@@ -1549,8 +1842,9 @@ async function submitSignupPhoneNumberAndContinue(payload = {}) {
countryLabel,
phoneNumber,
});
- if (countrySelection.hasSelect && !countrySelection.matched) {
- log(`步骤 2:手机号国家下拉框未能自动切换到 ${countryLabel || phoneNumber},将继续使用当前国家。`, 'warn');
+ if (countrySelection.hasCountryControl && !countrySelection.matched) {
+ const currentCountryText = getSignupPhoneCountryButtonText(snapshot.phoneInput) || '未知';
+ throw new Error(`步骤 2:手机号国家下拉框未能自动切换到 ${countryLabel || phoneNumber},当前显示为 ${currentCountryText},已停止提交以避免区号不匹配。`);
}
const dialCode = resolveSignupPhoneDialCode(snapshot.phoneInput, {
@@ -2046,6 +2340,44 @@ function isPhoneVerificationPageReady() {
&& /check\s+your\s+phone|phone\s+verification|verify\s+your\s+phone|sms|text\s+message|code\s+to\s+\+/.test(pageText);
}
+function getDocumentReadyStateSnapshot() {
+ const readyState = typeof document !== 'undefined' && document
+ ? String(document.readyState || '').trim().toLowerCase()
+ : '';
+ return readyState || 'complete';
+}
+
+function isDocumentLoadComplete() {
+ return getDocumentReadyStateSnapshot() === 'complete';
+}
+
+async function waitForDocumentLoadComplete(timeout = 15000, label = '页面') {
+ const start = Date.now();
+
+ while (Date.now() - start < timeout) {
+ throwIfStopped();
+ if (isDocumentLoadComplete()) {
+ return true;
+ }
+ await sleep(150);
+ }
+
+ throw new Error(`${label}长时间未完成加载,当前 readyState=${getDocumentReadyStateSnapshot()}。URL: ${location.href}`);
+}
+
+function isSignupVerificationPageInteractiveReady(snapshot = null) {
+ if (!isDocumentLoadComplete()) {
+ return false;
+ }
+
+ const resolvedSnapshot = snapshot || inspectSignupVerificationState();
+ if (resolvedSnapshot?.state !== 'verification') {
+ return false;
+ }
+
+ return Boolean(getVerificationCodeTarget());
+}
+
function isStep8Ready() {
const continueBtn = getPrimaryContinueButton();
if (!continueBtn) return false;
@@ -3191,6 +3523,10 @@ async function waitForSignupVerificationTransition(timeout = 5000) {
throwIfStopped();
const snapshot = inspectSignupVerificationState();
+ if (snapshot.state === 'verification' && !isSignupVerificationPageInteractiveReady(snapshot)) {
+ await sleep(200);
+ continue;
+ }
if (
snapshot.state === 'step5'
|| snapshot.state === 'logged_in_home'
@@ -3241,7 +3577,9 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
}
if (snapshot.state === 'verification') {
- log(`${prepareLogLabel}:验证码页面已就绪${recoveryRound ? `(期间自动恢复 ${recoveryRound} 次)` : ''}。`, 'ok');
+ await waitForDocumentLoadComplete(15000, `${prepareLogLabel}:注册验证码页面`);
+ await waitForVerificationCodeTarget(15000);
+ log(`${prepareLogLabel}:验证码页面已完成加载并就绪${recoveryRound ? `(期间自动恢复 ${recoveryRound} 次)` : ''}。`, 'ok');
return { ready: true, retried: recoveryRound, prepareSource };
}
@@ -3516,6 +3854,9 @@ async function fillVerificationCode(step, payload) {
allowPhoneVerificationPage: payload?.purpose === 'login' || payload?.loginIdentifierType === 'phone',
});
}
+ if (step === 4) {
+ await waitForDocumentLoadComplete(15000, `步骤 ${step}:注册验证码页面`);
+ }
const combinedSignupProfilePage = step === 4
&& await waitForCombinedSignupVerificationProfilePage();
diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css
index f3cf62f..8f23474 100644
--- a/sidepanel/sidepanel.css
+++ b/sidepanel/sidepanel.css
@@ -2271,6 +2271,11 @@ header {
min-height: 33px;
}
+.signup-phone-runtime-inline,
+.signup-phone-runtime-input {
+ width: 100%;
+}
+
.hero-sms-price-preview-stack {
width: 100%;
display: flex;
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 0792333..e1f387f 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -492,12 +492,6 @@
-
延迟
@@ -1424,32 +1418,38 @@
-
-
运行状态
-
-
- 当前分配
- 未分配
-
-
- 倒计时
- 未启动
-
-
- 验证码
- 未获取
-
-
- 优先号码
-
-
-
-
+
+
运行状态
+
+
+ 当前分配
+ 未分配
+
+
+ 倒计时
+ 未启动
+
+
+ 验证码
+ 未获取
+
+
+ 优先号码
+
+
+
+
+
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index fb53c33..161aa15 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -6851,11 +6851,6 @@ function updatePhoneVerificationSettingsUI() {
typeof rowNexSmsServiceCode !== 'undefined' ? rowNexSmsServiceCode : null,
typeof rowHeroSmsMaxPrice !== 'undefined' ? rowHeroSmsMaxPrice : null,
typeof rowFiveSimOperator !== 'undefined' ? rowFiveSimOperator : null,
- typeof rowHeroSmsRuntimePair !== 'undefined' ? rowHeroSmsRuntimePair : null,
- typeof rowHeroSmsCurrentNumber !== 'undefined' ? rowHeroSmsCurrentNumber : null,
- typeof rowHeroSmsCurrentCountdown !== 'undefined' ? rowHeroSmsCurrentCountdown : null,
- typeof rowHeroSmsCurrentCode !== 'undefined' ? rowHeroSmsCurrentCode : null,
- typeof rowHeroSmsPreferredActivation !== 'undefined' ? rowHeroSmsPreferredActivation : null,
typeof rowPhoneCodeSettingsGroup !== 'undefined' ? rowPhoneCodeSettingsGroup : null,
typeof rowPhoneVerificationResendCount !== 'undefined' ? rowPhoneVerificationResendCount : null,
typeof rowPhoneReplacementLimit !== 'undefined' ? rowPhoneReplacementLimit : null,
@@ -6885,6 +6880,21 @@ function updatePhoneVerificationSettingsUI() {
if (rowFiveSimOperator) {
rowFiveSimOperator.style.display = showSettings && fiveSimProvider ? '' : 'none';
}
+ const runtimeVisible = enabled;
+ [
+ typeof rowHeroSmsRuntimePair !== 'undefined' ? rowHeroSmsRuntimePair : null,
+ typeof rowHeroSmsCurrentNumber !== 'undefined' ? rowHeroSmsCurrentNumber : null,
+ typeof rowHeroSmsCurrentCountdown !== 'undefined' ? rowHeroSmsCurrentCountdown : null,
+ typeof rowHeroSmsCurrentCode !== 'undefined' ? rowHeroSmsCurrentCode : null,
+ typeof rowHeroSmsPreferredActivation !== 'undefined' ? rowHeroSmsPreferredActivation : null,
+ ].forEach((row) => {
+ if (row) {
+ row.style.display = runtimeVisible ? '' : 'none';
+ }
+ });
+ if (typeof syncSignupPhoneInputFromState === 'function') {
+ syncSignupPhoneInputFromState(latestState);
+ }
if (!showSettings && typeof rowHeroSmsPriceTiers !== 'undefined' && rowHeroSmsPriceTiers) {
rowHeroSmsPriceTiers.style.display = 'none';
}
@@ -7008,6 +7018,9 @@ function syncSignupPhoneInputFromState(state = latestState) {
inputSignupPhone.value = signupPhone;
}
if (typeof rowSignupPhone !== 'undefined' && rowSignupPhone) {
+ const phoneVerificationEnabled = typeof inputPhoneVerificationEnabled !== 'undefined' && inputPhoneVerificationEnabled
+ ? Boolean(inputPhoneVerificationEnabled.checked)
+ : Boolean(state?.phoneVerificationEnabled || latestState?.phoneVerificationEnabled);
const rawSignupMethod = state?.signupMethod || (
typeof getSelectedSignupMethod === 'function'
? getSelectedSignupMethod()
@@ -7016,7 +7029,7 @@ function syncSignupPhoneInputFromState(state = latestState) {
const selectedMethod = typeof normalizeSignupMethod === 'function'
? normalizeSignupMethod(rawSignupMethod)
: (String(rawSignupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email');
- rowSignupPhone.style.display = (selectedMethod === 'phone' || Boolean(signupPhone)) ? '' : 'none';
+ rowSignupPhone.style.display = phoneVerificationEnabled && (selectedMethod === 'phone' || Boolean(signupPhone)) ? '' : 'none';
}
}
@@ -7044,7 +7057,7 @@ async function openPlusManualConfirmationDialog(options = {}) {
}
const result = await sharedFormDialog.open({
title: String(options.title || '').trim() || 'GPC OTP 验证',
- message: String(options.message || '').trim() || '请输入收到的 OTP 验证码。',
+ message: String(options.message || '').trim() || '请在WhatsApp里面获取验证码(耐心等待三十秒左右)',
fields: [
{
key: 'otp',
diff --git a/tests/sidepanel-phone-verification-settings.test.js b/tests/sidepanel-phone-verification-settings.test.js
index 609973b..9586a9f 100644
--- a/tests/sidepanel-phone-verification-settings.test.js
+++ b/tests/sidepanel-phone-verification-settings.test.js
@@ -62,6 +62,18 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
assert.match(html, /id="row-signup-method"/);
assert.match(html, /id="row-signup-phone"/);
assert.match(html, /id="input-signup-phone"/);
+ assert.ok(
+ html.indexOf('id="row-signup-phone"') > html.indexOf('id="phone-verification-section"'),
+ 'signup phone runtime row should live inside the phone verification card'
+ );
+ assert.ok(
+ html.indexOf('id="row-signup-phone"') > html.indexOf('id="row-hero-sms-runtime-pair"'),
+ 'signup phone runtime row should sit below the SMS order runtime row'
+ );
+ assert.ok(
+ html.indexOf('id="row-signup-phone"') > html.indexOf('hero-sms-runtime-grid'),
+ 'signup phone runtime row should not be embedded in the SMS order runtime grid'
+ );
assert.match(html, /data-signup-method="email"/);
assert.match(html, /data-signup-method="phone"/);
assert.match(html, /id="row-phone-sms-provider"/);
@@ -229,12 +241,13 @@ return {
test('updatePhoneVerificationSettingsUI toggles SMS rows from the sms switch and provider selection', () => {
const api = new Function(`
-const phoneVerificationSectionExpanded = true;
+let phoneVerificationSectionExpanded = false;
let latestState = {};
const inputPhoneVerificationEnabled = { checked: false };
const rowPhoneVerificationEnabled = { style: { display: 'none' } };
const rowPhoneVerificationFold = { style: { display: 'none' } };
const rowSignupMethod = { style: { display: 'none' } };
+const rowSignupPhone = { style: { display: 'none' } };
const rowPhoneSmsProvider = { style: { display: 'none' } };
const rowPhoneSmsProviderOrder = { style: { display: 'none' } };
const rowPhoneSmsProviderOrderActions = { style: { display: 'none' } };
@@ -289,6 +302,7 @@ const rowNexSmsApiKey = { style: { display: 'none' } };
const rowNexSmsCountry = { style: { display: 'none' } };
const rowNexSmsCountryFallback = { style: { display: 'none' } };
const rowNexSmsServiceCode = { style: { display: 'none' } };
+const rowHeroSmsRuntimePair = { style: { display: 'none' } };
const rowHeroSmsCurrentNumber = { style: { display: 'none' } };
const rowHeroSmsCurrentCountdown = { style: { display: 'none' } };
const rowHeroSmsPriceTiers = { style: { display: 'none' } };
@@ -309,14 +323,19 @@ function updateHeroSmsPlatformDisplay() {}
function updateSignupMethodUI() {
rowSignupMethod.style.display = inputPhoneVerificationEnabled.checked ? '' : 'none';
}
+function syncSignupPhoneInputFromState() {
+ rowSignupPhone.style.display = inputPhoneVerificationEnabled.checked && latestState.signupPhoneNumber ? '' : 'none';
+}
${extractFunction('updatePhoneVerificationSettingsUI')}
return {
+ setExpanded(value) { phoneVerificationSectionExpanded = Boolean(value); },
setLatestState: (state) => { latestState = state || {}; },
rowPhoneVerificationEnabled,
rowPhoneVerificationFold,
rowSignupMethod,
+ rowSignupPhone,
rowPhoneSmsProvider,
rowPhoneSmsProviderOrder,
rowPhoneSmsProviderOrderActions,
@@ -338,6 +357,7 @@ return {
rowNexSmsCountry,
rowNexSmsCountryFallback,
rowNexSmsServiceCode,
+ rowHeroSmsRuntimePair,
rowHeroSmsCurrentNumber,
rowHeroSmsCurrentCountdown,
rowHeroSmsPriceTiers,
@@ -358,12 +378,14 @@ return {
assert.equal(api.rowPhoneVerificationEnabled.style.display, '');
assert.equal(api.rowPhoneVerificationFold.style.display, 'none');
assert.equal(api.rowSignupMethod.style.display, 'none');
+ assert.equal(api.rowSignupPhone.style.display, 'none');
assert.equal(api.rowPhoneSmsProvider.style.display, 'none');
assert.equal(api.rowPhoneSmsProviderOrder.style.display, 'none');
assert.equal(api.rowPhoneSmsProviderOrderActions.style.display, 'none');
assert.equal(api.btnTogglePhoneVerificationSection.disabled, true);
assert.equal(api.btnTogglePhoneVerificationSection.textContent, '展开设置');
assert.equal(api.rowHeroSmsPlatform.style.display, '');
+ assert.equal(api.rowHeroSmsRuntimePair.style.display, 'none');
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
assert.equal(api.rowHeroSmsCountryFallback.style.display, 'none');
assert.equal(api.rowHeroSmsAcquirePriority.style.display, 'none');
@@ -392,6 +414,19 @@ return {
assert.equal(api.rowNexSmsServiceCode.style.display, 'none');
api.inputPhoneVerificationEnabled.checked = true;
+ api.setLatestState({ signupPhoneNumber: '66959916439' });
+ api.updatePhoneVerificationSettingsUI();
+ assert.equal(api.rowPhoneVerificationFold.style.display, 'none');
+ assert.equal(api.rowSignupMethod.style.display, '');
+ assert.equal(api.rowSignupPhone.style.display, '');
+ assert.equal(api.rowPhoneSmsProvider.style.display, 'none');
+ assert.equal(api.rowHeroSmsRuntimePair.style.display, '');
+ assert.equal(api.rowHeroSmsCurrentNumber.style.display, '');
+ assert.equal(api.rowHeroSmsCurrentCountdown.style.display, '');
+ assert.equal(api.rowHeroSmsCurrentCode.style.display, '');
+ assert.equal(api.rowHeroSmsPreferredActivation.style.display, '');
+
+ api.setExpanded(true);
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowPhoneVerificationFold.style.display, '');
assert.equal(api.rowSignupMethod.style.display, '');
diff --git a/tests/sidepanel-plus-payment-method.test.js b/tests/sidepanel-plus-payment-method.test.js
index e9822c2..c81e807 100644
--- a/tests/sidepanel-plus-payment-method.test.js
+++ b/tests/sidepanel-plus-payment-method.test.js
@@ -331,7 +331,7 @@ let latestState = {
plusManualConfirmationStep: 7,
plusManualConfirmationMethod: 'gopay-otp',
plusManualConfirmationTitle: 'GPC OTP 验证',
- plusManualConfirmationMessage: '请输入 OTP。',
+ plusManualConfirmationMessage: '',
};
let activePlusManualConfirmationRequestId = '';
let plusManualConfirmationDialogInFlight = false;
@@ -364,6 +364,7 @@ return { events, syncPlusManualConfirmationDialog };
await api.syncPlusManualConfirmationDialog();
assert.equal(api.events[0].type, 'form');
+ assert.equal(api.events[0].options.message, '请在WhatsApp里面获取验证码(耐心等待三十秒左右)');
assert.equal(api.events[0].options.confirmLabel, '提交 OTP');
const sendEvent = api.events.find((event) => event.type === 'send');
assert.deepEqual(sendEvent.message.payload, {
diff --git a/tests/signup-step2-email-switch.test.js b/tests/signup-step2-email-switch.test.js
index 744afd5..197aaf9 100644
--- a/tests/signup-step2-email-switch.test.js
+++ b/tests/signup-step2-email-switch.test.js
@@ -709,6 +709,9 @@ const countryButton = {
get textContent() {
return selectValueNode.textContent;
},
+ getBoundingClientRect() {
+ return { width: 240, height: 48 };
+ },
};
const form = {
@@ -862,20 +865,33 @@ ${extractFunction('normalizePhoneDigits')}
${extractFunction('extractDialCodeFromText')}
${extractFunction('dispatchSignupPhoneFieldEvents')}
${extractFunction('normalizeSignupCountryLabel')}
+${extractFunction('getSignupCountryLabelAliases')}
${extractFunction('getSignupPhoneOptionLabel')}
${extractFunction('normalizeSignupCountryOptionValue')}
${extractFunction('getSignupRegionDisplayName')}
${extractFunction('getSignupPhoneCountryMatchLabels')}
${extractFunction('isSameSignupCountryOption')}
${extractFunction('getSignupPhoneForm')}
+${extractFunction('getSignupPhoneControlRoots')}
+${extractFunction('querySignupPhoneCountryElements')}
+${extractFunction('isSignupPhoneCountrySelect')}
${extractFunction('getSignupPhoneCountrySelect')}
${extractFunction('getSignupPhoneSelectedCountryOption')}
${extractFunction('getSignupPhoneCountryButtonText')}
+${extractFunction('getSignupPhoneCountryButton')}
${extractFunction('getSignupPhoneDisplayedDialCode')}
${extractFunction('getSignupPhoneHiddenNumberInput')}
+${extractFunction('resolveSignupPhoneDialCodeFromNumber')}
+${extractFunction('resolveSignupPhoneTargetDialCode')}
+${extractFunction('getSignupPhoneCountryTargetLabels')}
+${extractFunction('doesSignupPhoneCountryTextMatchTarget')}
+${extractFunction('isSignupPhoneCountrySelectionSynced')}
${extractFunction('findSignupPhoneCountryOptionByLabel')}
${extractFunction('findSignupPhoneCountryOptionByPhoneNumber')}
${extractFunction('trySelectSignupPhoneCountryOption')}
+${extractFunction('getVisibleSignupPhoneCountryListboxOptions')}
+${extractFunction('findSignupPhoneCountryListboxOption')}
+${extractFunction('trySelectSignupPhoneCountryListboxOption')}
${extractFunction('ensureSignupPhoneCountrySelected')}
${extractFunction('toNationalPhoneNumber')}
${extractFunction('toE164PhoneNumber')}
@@ -918,6 +934,584 @@ return {
]);
});
+test('submitSignupPhoneNumberAndContinue clicks the visible country listbox when the hidden select does not update the button', async () => {
+ const api = new Function(`
+const logs = [];
+const clicks = [];
+const filled = [];
+const selectEvents = [];
+let now = 0;
+let listboxOpen = false;
+let visibleCountryValue = 'AU';
+
+const continueButton = {
+ textContent: 'Continue',
+ value: '',
+ disabled: false,
+ getAttribute(name) {
+ if (name === 'type') return 'submit';
+ if (name === 'aria-disabled') return 'false';
+ return '';
+ },
+ getBoundingClientRect() {
+ return { width: 200, height: 48 };
+ },
+};
+
+const countryOptions = [
+ { value: 'AU', textContent: 'Australia (+61)', buttonText: '澳大利亚 (+61)' },
+ { value: 'GB', textContent: 'United Kingdom (+44)', buttonText: '英国 (+44)' },
+];
+
+const countrySelect = {
+ options: countryOptions,
+ selectedIndex: 0,
+ dispatchEvent(event) {
+ selectEvents.push(event?.type || '');
+ return true;
+ },
+};
+
+Object.defineProperty(countrySelect, 'value', {
+ get() {
+ return countryOptions[countrySelect.selectedIndex]?.value || '';
+ },
+ set(nextValue) {
+ const nextIndex = countryOptions.findIndex((option) => option.value === String(nextValue || ''));
+ if (nextIndex >= 0) {
+ countrySelect.selectedIndex = nextIndex;
+ }
+ },
+});
+
+const hiddenPhoneInput = {
+ kind: 'hidden-phone',
+ value: '',
+ getAttribute() {
+ return '';
+ },
+};
+
+const phoneInput = {
+ kind: 'phone',
+ value: '',
+ getAttribute(name) {
+ if (name === 'type') return 'tel';
+ return '';
+ },
+ closest(selector) {
+ if (selector === 'form') {
+ return form;
+ }
+ return form;
+ },
+};
+
+const selectValueNode = {
+ get textContent() {
+ return countryOptions.find((option) => option.value === visibleCountryValue)?.buttonText || '';
+ },
+};
+
+const countryButton = {
+ querySelector(selector) {
+ return selector === '.react-aria-SelectValue' ? selectValueNode : null;
+ },
+ get textContent() {
+ return selectValueNode.textContent;
+ },
+};
+
+const gbOption = {
+ textContent: '英国 (+44)',
+ value: '',
+ getAttribute() {
+ return '';
+ },
+};
+
+const form = {
+ querySelector(selector) {
+ if (selector === 'select') return countrySelect;
+ if (selector === 'input[name="phoneNumber"]') return hiddenPhoneInput;
+ if (selector === 'button[aria-haspopup="listbox"]') return countryButton;
+ return null;
+ },
+};
+
+const document = {
+ documentElement: {
+ lang: 'zh-CN',
+ getAttribute(name) {
+ return name === 'lang' ? 'zh-CN' : '';
+ },
+ },
+ querySelector(selector) {
+ if (selector === SIGNUP_PHONE_INPUT_SELECTOR) {
+ return phoneInput;
+ }
+ if (selector === 'button[type="submit"], input[type="submit"]') {
+ return continueButton;
+ }
+ return null;
+ },
+ querySelectorAll(selector) {
+ if (selector === 'button, a, [role="button"], [role="link"]') {
+ return [];
+ }
+ if (selector === 'a, button, [role="button"], [role="link"]') {
+ return [];
+ }
+ if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
+ return [continueButton];
+ }
+ if (selector === 'input') {
+ return [phoneInput];
+ }
+ if (selector.includes('[role="option"]')) {
+ return listboxOpen ? [gbOption] : [];
+ }
+ return [];
+ },
+};
+
+const location = {
+ href: 'https://chatgpt.com/',
+};
+
+const window = {
+ setTimeout(fn) {
+ fn();
+ },
+};
+
+const Date = {
+ now() {
+ return now;
+ },
+};
+
+class Event {
+ constructor(type) {
+ this.type = type;
+ }
+}
+
+function isVisibleElement(el) {
+ return Boolean(el);
+}
+
+function isActionEnabled(el) {
+ return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
+}
+
+function getActionText(el) {
+ return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
+ .filter(Boolean)
+ .join(' ')
+ .replace(/\\s+/g, ' ')
+ .trim();
+}
+
+function getSignupPasswordInput() {
+ return null;
+}
+
+function isSignupPasswordPage() {
+ return false;
+}
+
+function getSignupPasswordSubmitButton() {
+ return null;
+}
+
+function findSignupEntryTrigger() {
+ return null;
+}
+
+function getSignupPasswordDisplayedEmail() {
+ return '';
+}
+
+function getPageTextSnapshot() {
+ return countryButton.textContent;
+}
+
+function throwIfStopped() {}
+function isStopError() { return false; }
+
+function log(message, level = 'info') {
+ logs.push({ message, level });
+}
+
+async function humanPause() {}
+
+function simulateClick(target) {
+ clicks.push(getActionText(target));
+ if (target === countryButton) {
+ listboxOpen = true;
+ }
+ if (target === gbOption) {
+ visibleCountryValue = 'GB';
+ countrySelect.value = 'GB';
+ listboxOpen = false;
+ }
+}
+
+function fillInput(target, value) {
+ target.value = value;
+ filled.push({ target: target.kind, value });
+}
+
+async function sleep(ms) {
+ now += ms;
+}
+
+${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')}
+${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')}
+${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
+${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')}
+${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')}
+${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')}
+${extractConst('SIGNUP_WORK_EMAIL_PATTERN')}
+${extractConst('SIGNUP_PHONE_ACTION_PATTERN')}
+${extractConst('SIGNUP_SWITCH_TO_PHONE_PATTERN')}
+${extractConst('SIGNUP_MORE_OPTIONS_PATTERN')}
+
+${extractFunction('getSignupEmailInput')}
+${extractFunction('getSignupPhoneInput')}
+${extractFunction('findSignupUseEmailTrigger')}
+${extractFunction('findSignupUsePhoneTrigger')}
+${extractFunction('findSignupMoreOptionsTrigger')}
+${extractFunction('getSignupEmailContinueButton')}
+${extractFunction('inspectSignupEntryState')}
+${extractFunction('getSignupEntryStateSummary')}
+function getSignupEntryDiagnostics() { return {}; }
+${extractFunction('normalizePhoneDigits')}
+${extractFunction('extractDialCodeFromText')}
+${extractFunction('dispatchSignupPhoneFieldEvents')}
+${extractFunction('normalizeSignupCountryLabel')}
+${extractFunction('getSignupCountryLabelAliases')}
+${extractFunction('getSignupPhoneOptionLabel')}
+${extractFunction('normalizeSignupCountryOptionValue')}
+${extractFunction('getSignupRegionDisplayName')}
+${extractFunction('getSignupPhoneCountryMatchLabels')}
+${extractFunction('isSameSignupCountryOption')}
+${extractFunction('getSignupPhoneForm')}
+${extractFunction('getSignupPhoneControlRoots')}
+${extractFunction('querySignupPhoneCountryElements')}
+${extractFunction('isSignupPhoneCountrySelect')}
+${extractFunction('getSignupPhoneCountrySelect')}
+${extractFunction('getSignupPhoneSelectedCountryOption')}
+${extractFunction('getSignupPhoneCountryButtonText')}
+${extractFunction('getSignupPhoneCountryButton')}
+${extractFunction('getSignupPhoneDisplayedDialCode')}
+${extractFunction('getSignupPhoneHiddenNumberInput')}
+${extractFunction('resolveSignupPhoneDialCodeFromNumber')}
+${extractFunction('resolveSignupPhoneTargetDialCode')}
+${extractFunction('getSignupPhoneCountryTargetLabels')}
+${extractFunction('doesSignupPhoneCountryTextMatchTarget')}
+${extractFunction('isSignupPhoneCountrySelectionSynced')}
+${extractFunction('findSignupPhoneCountryOptionByLabel')}
+${extractFunction('findSignupPhoneCountryOptionByPhoneNumber')}
+${extractFunction('trySelectSignupPhoneCountryOption')}
+${extractFunction('getVisibleSignupPhoneCountryListboxOptions')}
+${extractFunction('findSignupPhoneCountryListboxOption')}
+${extractFunction('trySelectSignupPhoneCountryListboxOption')}
+${extractFunction('ensureSignupPhoneCountrySelected')}
+${extractFunction('toNationalPhoneNumber')}
+${extractFunction('toE164PhoneNumber')}
+${extractFunction('resolveSignupPhoneDialCode')}
+${extractFunction('waitForSignupPhoneEntryState')}
+${extractFunction('submitSignupPhoneNumberAndContinue')}
+
+return {
+ async run() {
+ return submitSignupPhoneNumberAndContinue({
+ phoneNumber: '+447859232013',
+ countryLabel: 'United Kingdom',
+ });
+ },
+ getClicks() {
+ return clicks.slice();
+ },
+ getFilled() {
+ return filled.slice();
+ },
+ getSelectValue() {
+ return countrySelect.value;
+ },
+ getVisibleCountryText() {
+ return countryButton.textContent;
+ },
+ getSelectEvents() {
+ return selectEvents.slice();
+ },
+};
+`)();
+
+ const result = await api.run();
+
+ assert.equal(result.submitted, true);
+ assert.equal(result.phoneInputValue, '7859232013');
+ assert.equal(api.getSelectValue(), 'GB');
+ assert.equal(api.getVisibleCountryText(), '英国 (+44)');
+ assert.deepEqual(api.getSelectEvents(), ['input', 'change']);
+ assert.deepEqual(api.getClicks(), ['澳大利亚 (+61)', '英国 (+44)', 'Continue']);
+ assert.deepEqual(api.getFilled(), [
+ { target: 'phone', value: '7859232013' },
+ { target: 'hidden-phone', value: '+447859232013' },
+ ]);
+});
+
+test('submitSignupPhoneNumberAndContinue can select country by phone dial code without country label or hidden select', async () => {
+ const api = new Function(`
+const clicks = [];
+const filled = [];
+let now = 0;
+let listboxOpen = false;
+let visibleCountryText = '印度尼西亚 +(62)';
+
+const continueButton = {
+ textContent: 'Continue',
+ value: '',
+ disabled: false,
+ getAttribute(name) {
+ if (name === 'type') return 'submit';
+ if (name === 'aria-disabled') return 'false';
+ return '';
+ },
+ getBoundingClientRect() {
+ return { width: 200, height: 48 };
+ },
+};
+
+const phoneInput = {
+ kind: 'phone',
+ value: '',
+ parentElement: null,
+ getAttribute(name) {
+ if (name === 'type') return 'tel';
+ return '';
+ },
+ closest() {
+ return null;
+ },
+};
+
+const countryButton = {
+ textContent: '',
+ querySelector(selector) {
+ return selector === '.react-aria-SelectValue' ? { textContent: visibleCountryText } : null;
+ },
+ getAttribute(name) {
+ if (name === 'aria-haspopup') return 'listbox';
+ return '';
+ },
+ getBoundingClientRect() {
+ return { width: 240, height: 48 };
+ },
+};
+
+const gbOption = {
+ textContent: '英国 +(44)',
+ getAttribute() {
+ return '';
+ },
+ getBoundingClientRect() {
+ return { width: 200, height: 36 };
+ },
+};
+
+const idOption = {
+ textContent: '印度尼西亚 +(62)',
+ getAttribute() {
+ return '';
+ },
+ getBoundingClientRect() {
+ return { width: 200, height: 36 };
+ },
+};
+
+const document = {
+ documentElement: {
+ lang: 'zh-CN',
+ getAttribute(name) {
+ return name === 'lang' ? 'zh-CN' : '';
+ },
+ },
+ querySelector(selector) {
+ if (selector === SIGNUP_PHONE_INPUT_SELECTOR) return phoneInput;
+ if (selector === 'button[type="submit"], input[type="submit"]') return continueButton;
+ return null;
+ },
+ querySelectorAll(selector) {
+ if (selector === 'button, a, [role="button"], [role="link"]') return [];
+ if (selector === 'a, button, [role="button"], [role="link"]') return [];
+ if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') return [continueButton];
+ if (selector === 'input') return [phoneInput];
+ if (selector === 'select') return [];
+ if (selector.includes('aria-haspopup="listbox"') || selector.includes('aria-expanded')) return [countryButton];
+ if (selector.includes('[role="option"]')) return listboxOpen ? [idOption, gbOption] : [];
+ return [];
+ },
+};
+
+const location = {
+ href: 'https://chatgpt.com/',
+};
+
+const window = {
+ setTimeout(fn) {
+ fn();
+ },
+};
+
+const Date = {
+ now() {
+ return now;
+ },
+};
+
+class Event {
+ constructor(type) {
+ this.type = type;
+ }
+}
+
+function isVisibleElement(el) {
+ return Boolean(el) && (!el.getBoundingClientRect || el.getBoundingClientRect().width > 0);
+}
+
+function isActionEnabled(el) {
+ return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
+}
+
+function getActionText(el) {
+ return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
+ .filter(Boolean)
+ .join(' ')
+ .replace(/\\s+/g, ' ')
+ .trim();
+}
+
+function getSignupPasswordInput() { return null; }
+function isSignupPasswordPage() { return false; }
+function getSignupPasswordSubmitButton() { return null; }
+function findSignupEntryTrigger() { return null; }
+function getSignupPasswordDisplayedEmail() { return ''; }
+function getPageTextSnapshot() { return visibleCountryText; }
+function throwIfStopped() {}
+function isStopError() { return false; }
+function log() {}
+async function humanPause() {}
+
+function simulateClick(target) {
+ clicks.push(getActionText(target) || visibleCountryText);
+ if (target === countryButton) {
+ listboxOpen = true;
+ }
+ if (target === gbOption) {
+ visibleCountryText = '英国 +(44)';
+ listboxOpen = false;
+ }
+}
+
+function fillInput(target, value) {
+ target.value = value;
+ filled.push({ target: target.kind, value });
+}
+
+async function sleep(ms) {
+ now += ms;
+}
+
+${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')}
+${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')}
+${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
+${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')}
+${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')}
+${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')}
+${extractConst('SIGNUP_WORK_EMAIL_PATTERN')}
+${extractConst('SIGNUP_PHONE_ACTION_PATTERN')}
+${extractConst('SIGNUP_SWITCH_TO_PHONE_PATTERN')}
+${extractConst('SIGNUP_MORE_OPTIONS_PATTERN')}
+
+${extractFunction('getSignupEmailInput')}
+${extractFunction('getSignupPhoneInput')}
+${extractFunction('findSignupUseEmailTrigger')}
+${extractFunction('findSignupUsePhoneTrigger')}
+${extractFunction('findSignupMoreOptionsTrigger')}
+${extractFunction('getSignupEmailContinueButton')}
+${extractFunction('inspectSignupEntryState')}
+${extractFunction('getSignupEntryStateSummary')}
+function getSignupEntryDiagnostics() { return {}; }
+${extractFunction('normalizePhoneDigits')}
+${extractFunction('extractDialCodeFromText')}
+${extractFunction('dispatchSignupPhoneFieldEvents')}
+${extractFunction('normalizeSignupCountryLabel')}
+${extractFunction('getSignupCountryLabelAliases')}
+${extractFunction('getSignupPhoneOptionLabel')}
+${extractFunction('normalizeSignupCountryOptionValue')}
+${extractFunction('getSignupRegionDisplayName')}
+${extractFunction('getSignupPhoneCountryMatchLabels')}
+${extractFunction('isSameSignupCountryOption')}
+${extractFunction('getSignupPhoneForm')}
+${extractFunction('getSignupPhoneControlRoots')}
+${extractFunction('querySignupPhoneCountryElements')}
+${extractFunction('isSignupPhoneCountrySelect')}
+${extractFunction('getSignupPhoneCountrySelect')}
+${extractFunction('getSignupPhoneSelectedCountryOption')}
+${extractFunction('getSignupPhoneCountryButtonText')}
+${extractFunction('getSignupPhoneCountryButton')}
+${extractFunction('getSignupPhoneDisplayedDialCode')}
+${extractFunction('getSignupPhoneHiddenNumberInput')}
+${extractFunction('resolveSignupPhoneDialCodeFromNumber')}
+${extractFunction('resolveSignupPhoneTargetDialCode')}
+${extractFunction('getSignupPhoneCountryTargetLabels')}
+${extractFunction('doesSignupPhoneCountryTextMatchTarget')}
+${extractFunction('isSignupPhoneCountrySelectionSynced')}
+${extractFunction('findSignupPhoneCountryOptionByLabel')}
+${extractFunction('findSignupPhoneCountryOptionByPhoneNumber')}
+${extractFunction('trySelectSignupPhoneCountryOption')}
+${extractFunction('getVisibleSignupPhoneCountryListboxOptions')}
+${extractFunction('findSignupPhoneCountryListboxOption')}
+${extractFunction('trySelectSignupPhoneCountryListboxOption')}
+${extractFunction('ensureSignupPhoneCountrySelected')}
+${extractFunction('toNationalPhoneNumber')}
+${extractFunction('toE164PhoneNumber')}
+${extractFunction('resolveSignupPhoneDialCode')}
+${extractFunction('waitForSignupPhoneEntryState')}
+${extractFunction('submitSignupPhoneNumberAndContinue')}
+
+return {
+ async run() {
+ return submitSignupPhoneNumberAndContinue({
+ phoneNumber: '447423278610',
+ countryLabel: '',
+ });
+ },
+ getClicks() {
+ return clicks.slice();
+ },
+ getFilled() {
+ return filled.slice();
+ },
+ getVisibleCountryText() {
+ return visibleCountryText;
+ },
+};
+`)();
+
+ const result = await api.run();
+
+ assert.equal(result.submitted, true);
+ assert.equal(result.phoneInputValue, '7423278610');
+ assert.equal(api.getVisibleCountryText(), '英国 +(44)');
+ assert.deepEqual(api.getClicks(), ['印度尼西亚 +(62)', '英国 +(44)', 'Continue']);
+ assert.deepEqual(api.getFilled(), [{ target: 'phone', value: '7423278610' }]);
+});
+
test('submitSignupPhoneNumberAndContinue switches from email mode to phone mode and submits local number', async () => {
const api = new Function(`
const logs = [];
@@ -1110,20 +1704,33 @@ ${extractFunction('normalizePhoneDigits')}
${extractFunction('extractDialCodeFromText')}
${extractFunction('dispatchSignupPhoneFieldEvents')}
${extractFunction('normalizeSignupCountryLabel')}
+${extractFunction('getSignupCountryLabelAliases')}
${extractFunction('getSignupPhoneOptionLabel')}
${extractFunction('normalizeSignupCountryOptionValue')}
${extractFunction('getSignupRegionDisplayName')}
${extractFunction('getSignupPhoneCountryMatchLabels')}
${extractFunction('isSameSignupCountryOption')}
${extractFunction('getSignupPhoneForm')}
+${extractFunction('getSignupPhoneControlRoots')}
+${extractFunction('querySignupPhoneCountryElements')}
+${extractFunction('isSignupPhoneCountrySelect')}
${extractFunction('getSignupPhoneCountrySelect')}
${extractFunction('getSignupPhoneSelectedCountryOption')}
${extractFunction('getSignupPhoneCountryButtonText')}
+${extractFunction('getSignupPhoneCountryButton')}
${extractFunction('getSignupPhoneDisplayedDialCode')}
${extractFunction('getSignupPhoneHiddenNumberInput')}
+${extractFunction('resolveSignupPhoneDialCodeFromNumber')}
+${extractFunction('resolveSignupPhoneTargetDialCode')}
+${extractFunction('getSignupPhoneCountryTargetLabels')}
+${extractFunction('doesSignupPhoneCountryTextMatchTarget')}
+${extractFunction('isSignupPhoneCountrySelectionSynced')}
${extractFunction('findSignupPhoneCountryOptionByLabel')}
${extractFunction('findSignupPhoneCountryOptionByPhoneNumber')}
${extractFunction('trySelectSignupPhoneCountryOption')}
+${extractFunction('getVisibleSignupPhoneCountryListboxOptions')}
+${extractFunction('findSignupPhoneCountryListboxOption')}
+${extractFunction('trySelectSignupPhoneCountryListboxOption')}
${extractFunction('ensureSignupPhoneCountrySelected')}
${extractFunction('toNationalPhoneNumber')}
${extractFunction('toE164PhoneNumber')}
diff --git a/tests/step4-split-code-submit.test.js b/tests/step4-split-code-submit.test.js
index 73bc6d8..52e022f 100644
--- a/tests/step4-split-code-submit.test.js
+++ b/tests/step4-split-code-submit.test.js
@@ -115,6 +115,7 @@ function fillInput(el, value) {
filledValues.push(value);
}
async function sleep() {}
+async function waitForDocumentLoadComplete() {}
function isStep5Ready() { return false; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
@@ -177,6 +178,7 @@ function is405MethodNotAllowedPage() { return false; }
async function handle405ResendError() {}
function fillInput() {}
async function sleep() {}
+async function waitForDocumentLoadComplete() {}
function isStep5Ready() { return true; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
@@ -336,6 +338,7 @@ function fillInput(el, value) {
filledValues.push({ target: el === nameInput ? 'name' : (el === ageInput ? 'age' : 'code'), value });
}
async function sleep() {}
+async function waitForDocumentLoadComplete() {}
function isStep5Ready() { return true; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
@@ -531,6 +534,7 @@ function fillInput(el, value) {
el.value = value;
}
async function sleep() {}
+async function waitForDocumentLoadComplete() {}
function isStep5Ready() { return false; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
@@ -614,3 +618,102 @@ return {
assert.equal(snapshot.submitClicked, true);
assert.equal(snapshot.nameQueryCount >= 3, true);
});
+
+test('prepareSignupVerificationFlow waits for complete verification page before reporting ready', async () => {
+ const api = new Function(`
+const logs = [];
+let now = 0;
+let sleepCalls = 0;
+let targetChecks = 0;
+const location = {
+ href: 'https://auth.openai.com/email-verification',
+ pathname: '/email-verification',
+};
+const document = {
+ readyState: 'loading',
+ title: '',
+ body: {
+ textContent: 'Enter the verification code we just sent',
+ innerText: 'Enter the verification code we just sent',
+ },
+ querySelector(selector) {
+ if (selector === 'form[action*="email-verification" i]') {
+ return {};
+ }
+ return null;
+ },
+ querySelectorAll(selector) {
+ if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
+ return [];
+ }
+ return [];
+ },
+};
+
+Date.now = () => now;
+function throwIfStopped() {}
+function log(message, level = 'info') { logs.push({ message, level }); }
+async function sleep(ms = 0) {
+ sleepCalls += 1;
+ now += ms || 200;
+ if (sleepCalls >= 3) {
+ document.readyState = 'complete';
+ }
+}
+function isVisibleElement() { return true; }
+function isActionEnabled() { return true; }
+function getActionText(el) { return el?.textContent || ''; }
+function getCurrentAuthRetryPageState() { return null; }
+function isPhoneVerificationPageReady() { return false; }
+function findResendVerificationCodeTrigger() { return null; }
+function isEmailVerificationPage() { return true; }
+function getPageTextSnapshot() { return document.body.textContent; }
+function getVerificationCodeTarget() {
+ targetChecks += 1;
+ return document.readyState === 'complete'
+ ? { type: 'single', element: { value: '' } }
+ : null;
+}
+function is405MethodNotAllowedPage() { return false; }
+async function recoverCurrentAuthRetryPage() {}
+function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
+function getSignupPasswordInput() { return null; }
+function getSignupPasswordSubmitButton() { return null; }
+function isSignupEmailAlreadyExistsPage() { return false; }
+function isSignupPasswordErrorPage() { return false; }
+function getSignupPasswordTimeoutErrorPageState() { return null; }
+function isStep5Ready() { return false; }
+
+const VERIFICATION_PAGE_PATTERN = /verification code/i;
+
+${extractFunction('getDocumentReadyStateSnapshot')}
+${extractFunction('isDocumentLoadComplete')}
+${extractFunction('waitForDocumentLoadComplete')}
+${extractFunction('isSignupVerificationPageInteractiveReady')}
+${extractFunction('isVerificationPageStillVisible')}
+${extractFunction('isSignupProfilePageUrl')}
+${extractFunction('isLikelyLoggedInChatgptHomeUrl')}
+${extractFunction('getStep4PostVerificationState')}
+${extractFunction('inspectSignupVerificationState')}
+${extractFunction('waitForVerificationCodeTarget')}
+${extractFunction('waitForSignupVerificationTransition')}
+${extractFunction('prepareSignupVerificationFlow')}
+
+return {
+ run() {
+ return prepareSignupVerificationFlow({ prepareLogLabel: '步骤 4 执行' }, 10000);
+ },
+ snapshot() {
+ return { logs, sleepCalls, targetChecks, readyState: document.readyState };
+ },
+};
+`)();
+
+ const result = await api.run();
+ const snapshot = api.snapshot();
+
+ assert.equal(result.ready, true);
+ assert.equal(snapshot.readyState, 'complete');
+ assert.equal(snapshot.sleepCalls >= 3, true);
+ assert.equal(snapshot.targetChecks >= 1, true);
+});
diff --git a/项目完整链路说明.md b/项目完整链路说明.md
index 563227c..604b24b 100644
--- a/项目完整链路说明.md
+++ b/项目完整链路说明.md
@@ -326,7 +326,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
2. 打开或复用注册页
3. 邮箱注册时,如果注册弹窗默认停留在手机号输入模式,会先尝试点击 `继续使用电子邮件地址登录 / Continue using email address` 一类按钮切回邮箱输入模式
4. 邮箱注册分支:提交邮箱;邮箱输入框识别同时兼容本地化占位与 `aria-label`
-5. 手机号注册分支:先点击官网“免费注册”并切换到手机号注册入口,必要时展开更多选项;确认手机号输入页就绪后,再从接码平台申请号码,填写手机号并提交
+5. 手机号注册分支:先点击官网“免费注册”并切换到手机号注册入口,必要时展开更多选项;确认手机号输入页就绪后,再从接码平台申请号码;内容脚本会按号码区号和平台国家名切换手机号国家下拉框,必须确认可视下拉按钮的区号已同步后才填写并提交,若仍显示旧国家则停止提交
6. 以当前统一账号标识先写入一条“停止(流程尚未完成)”的记录占位;邮箱账号占位邮箱,phone-only 账号占位手机号
7. 等待身份提交后的真实落地页
8. 如果进入密码页,则继续执行 Step 3
diff --git a/项目文件结构说明.md b/项目文件结构说明.md
index 36ef5f2..f1c1885 100644
--- a/项目文件结构说明.md
+++ b/项目文件结构说明.md
@@ -92,7 +92,7 @@
- `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。
- `content/plus-checkout.js`:ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
-- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、手机号填写与提交;Step 7 登录链路会额外识别手机号输入页、手机号登录入口和 `phone-verification` 页面,避免把手机验证码页误判成邮箱验证码页或普通未知页。
+- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、手机号国家下拉框同步校验、手机号填写与提交;Step 7 登录链路会额外识别手机号输入页、手机号登录入口和 `phone-verification` 页面,避免把手机验证码页误判成邮箱验证码页或普通未知页。
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;平台验证请求会读取 `visibleStep`,普通模式承接步骤 10,Plus 模式承接步骤 13。
- `content/utils.js`:内容脚本公共工具层,负责结构化日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击;内容脚本日志通过 `{ step, stepKey }` 上报,正文不再承担步骤号解析职责。
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做平台验证判定;当前支持普通步骤 10 与 Plus 可见步骤 13。
@@ -238,7 +238,7 @@
- `tests/sidepanel-mail2925-mode.test.js`:测试侧边栏保留 `2925 provide / receive` 模式行与独立的号池配置行,并验证 sidepanel 只在 provide 模式下把 2925 视为别名邮箱 provider。
- `tests/sidepanel-paypal-manager.test.js`:测试侧边栏公共表单弹窗脚本与 PayPal manager 的加载顺序,以及 PayPal 账号保存后会立即选中当前账号。
- `tests/signup-entry-diagnostics.test.js`:测试注册入口诊断快照输出。
-- `tests/signup-step2-email-switch.test.js`:测试 Step 2 在手机号输入模式下切回邮箱输入模式,以及本地化邮箱输入框识别。
+- `tests/signup-step2-email-switch.test.js`:测试 Step 2 在手机号输入模式下切回邮箱输入模式、本地化邮箱输入框识别,以及手机号注册时国家下拉框可视区号同步。
- `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
- `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。
- `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报,并保留延迟提交回调的可执行性验证。