手机号注册模式

This commit is contained in:
QLHazyCoder
2026-05-04 12:56:35 +08:00
parent ff8b86a03b
commit c04b64c966
38 changed files with 4509 additions and 540 deletions
+309 -16
View File
@@ -377,6 +377,9 @@ const PERSISTENT_ALIAS_STATE_KEYS = [
'icloudAliasCacheAt',
];
const ACCOUNT_RUN_HISTORY_STORAGE_KEY = 'accountRunHistory';
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
const CONTRIBUTION_RUNTIME_DEFAULTS = self.MultiPageBackgroundContributionOAuth?.RUNTIME_DEFAULTS || {
contributionMode: false,
contributionModeExpected: false,
@@ -564,6 +567,7 @@ const PERSISTED_SETTING_DEFAULTS = {
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
phoneVerificationEnabled: false,
signupMethod: DEFAULT_SIGNUP_METHOD,
phoneSmsProvider: DEFAULT_PHONE_SMS_PROVIDER,
phoneSmsProviderOrder: [],
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
@@ -624,12 +628,17 @@ const PERSISTED_SETTING_DEFAULTS = {
heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL,
heroSmsCountryFallback: [],
fiveSimApiKey: '',
fiveSimProduct: DEFAULT_FIVE_SIM_PRODUCT,
fiveSimCountryId: FIVE_SIM_COUNTRY_ID,
fiveSimCountryLabel: FIVE_SIM_COUNTRY_LABEL,
fiveSimCountryFallback: [],
fiveSimCountryOrder: [FIVE_SIM_COUNTRY_ID],
fiveSimCountryOrder: [...DEFAULT_FIVE_SIM_COUNTRY_ORDER],
fiveSimMaxPrice: '',
fiveSimOperator: FIVE_SIM_OPERATOR,
nexSmsApiKey: '',
nexSmsCountryOrder: [...DEFAULT_NEX_SMS_COUNTRY_ORDER],
nexSmsServiceCode: DEFAULT_NEX_SMS_SERVICE_CODE,
phonePreferredActivation: null,
};
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
@@ -658,6 +667,9 @@ const DEFAULT_STATE = {
stepStatuses: Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending'])),
...CONTRIBUTION_RUNTIME_DEFAULTS,
oauthUrl: null, // 运行时抓取到的 OAuth 地址,不要手动预填。
resolvedSignupMethod: null, // 当前自动轮次冻结后的实际注册方式。
accountIdentifierType: null,
accountIdentifier: '',
email: null, // 运行时邮箱,由程序自动获取并写入,不能手动预填。
password: null, // 运行时实际密码,由 customPassword 或程序自动生成后写入。
accounts: [], // 已生成账号记录:{ email, password, createdAt }。
@@ -724,6 +736,11 @@ const DEFAULT_STATE = {
currentPhoneVerificationCountdownWindowTotal: 0,
reusablePhoneActivation: null,
phoneReusableActivationPool: [],
signupPhoneNumber: '',
signupPhoneActivation: null,
signupPhoneCompletedActivation: null,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
heroSmsLastPriceTiers: [],
heroSmsLastPriceCountryId: 0,
heroSmsLastPriceCountryLabel: '',
@@ -999,9 +1016,100 @@ function normalizePhoneSmsProvider(value = '') {
return rootScope.PhoneSmsProviderRegistry.normalizeProviderId(value);
}
const normalized = String(value || '').trim().toLowerCase();
return normalized === PHONE_SMS_PROVIDER_FIVE_SIM
? PHONE_SMS_PROVIDER_FIVE_SIM
: PHONE_SMS_PROVIDER_HERO_SMS;
if (normalized === PHONE_SMS_PROVIDER_FIVE_SIM) {
return PHONE_SMS_PROVIDER_FIVE_SIM;
}
if (normalized === PHONE_SMS_PROVIDER_NEXSMS) {
return PHONE_SMS_PROVIDER_NEXSMS;
}
return PHONE_SMS_PROVIDER_HERO_SMS;
}
function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) {
const source = Array.isArray(value)
? value
: String(value || '')
.split(/[\r\n,;]+/)
.map((entry) => String(entry || '').trim())
.filter(Boolean);
const normalized = [];
const seen = new Set();
source.forEach((entry) => {
const provider = normalizePhoneSmsProvider(
entry && typeof entry === 'object' && !Array.isArray(entry)
? (entry.provider || entry.id || entry.value || '')
: entry
);
if (!provider || seen.has(provider)) {
return;
}
seen.add(provider);
normalized.push(provider);
});
if (normalized.length) {
return normalized.slice(0, DEFAULT_PHONE_SMS_PROVIDER_ORDER.length);
}
const fallback = Array.isArray(fallbackOrder) ? fallbackOrder : [];
fallback.forEach((entry) => {
const provider = normalizePhoneSmsProvider(
entry && typeof entry === 'object' && !Array.isArray(entry)
? (entry.provider || entry.id || entry.value || '')
: entry
);
if (!provider || seen.has(provider)) {
return;
}
seen.add(provider);
normalized.push(provider);
});
return normalized.slice(0, DEFAULT_PHONE_SMS_PROVIDER_ORDER.length);
}
function normalizeSignupMethod(value = '') {
return String(value || '').trim().toLowerCase() === SIGNUP_METHOD_PHONE
? SIGNUP_METHOD_PHONE
: SIGNUP_METHOD_EMAIL;
}
function canUsePhoneSignup(state = {}) {
return Boolean(state?.phoneVerificationEnabled)
&& !Boolean(state?.plusModeEnabled)
&& !Boolean(state?.contributionMode);
}
function resolveSignupMethod(state = {}) {
const frozenMethod = String(state?.resolvedSignupMethod || '').trim().toLowerCase();
if (frozenMethod === SIGNUP_METHOD_EMAIL || frozenMethod === SIGNUP_METHOD_PHONE) {
return normalizeSignupMethod(frozenMethod);
}
const method = normalizeSignupMethod(state?.signupMethod);
return method === SIGNUP_METHOD_PHONE && canUsePhoneSignup(state)
? SIGNUP_METHOD_PHONE
: SIGNUP_METHOD_EMAIL;
}
async function ensureResolvedSignupMethodForRun(options = {}) {
const state = await getState();
const force = Boolean(options.force);
const existing = String(state?.resolvedSignupMethod || '').trim().toLowerCase();
if (!force && (existing === SIGNUP_METHOD_EMAIL || existing === SIGNUP_METHOD_PHONE)) {
return normalizeSignupMethod(existing);
}
const configuredMethod = normalizeSignupMethod(state?.signupMethod);
const resolvedMethod = resolveSignupMethod({
...state,
resolvedSignupMethod: null,
});
await setState({ resolvedSignupMethod: resolvedMethod });
if (configuredMethod === SIGNUP_METHOD_PHONE && resolvedMethod !== SIGNUP_METHOD_PHONE) {
await addLog('当前模式暂不支持手机号注册,本轮已固定为邮箱注册。', 'warn');
}
return resolvedMethod;
}
function normalizePlusPaymentMethod(value = '') {
@@ -1031,6 +1139,115 @@ function normalizeFiveSimCountryId(value, fallback = FIVE_SIM_COUNTRY_ID) {
return FIVE_SIM_SUPPORTED_COUNTRY_ID_SET.has(normalizedFallback) ? normalizedFallback : FIVE_SIM_COUNTRY_ID;
}
function normalizeFiveSimCountryCode(value = '', fallback = 'thailand') {
const normalized = String(value || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9_-]+/g, '');
return normalized || fallback;
}
function normalizeFiveSimCountryOrder(value = []) {
const source = Array.isArray(value)
? value
: String(value || '')
.split(/[\r\n,;]+/)
.map((entry) => String(entry || '').trim())
.filter(Boolean);
const normalized = [];
const seen = new Set();
source.forEach((entry) => {
const code = normalizeFiveSimCountryCode(
entry && typeof entry === 'object' && !Array.isArray(entry)
? (entry.code || entry.country || entry.id || '')
: entry,
''
);
if (!code || seen.has(code)) {
return;
}
seen.add(code);
normalized.push(code);
});
return normalized.slice(0, 10);
}
function normalizeNexSmsCountryId(value, fallback = 0) {
const parsed = Math.floor(Number(value));
if (Number.isFinite(parsed) && parsed >= 0) {
return parsed;
}
const fallbackParsed = Math.floor(Number(fallback));
if (Number.isFinite(fallbackParsed) && fallbackParsed >= 0) {
return fallbackParsed;
}
return 0;
}
function normalizeNexSmsCountryOrder(value = []) {
const source = Array.isArray(value)
? value
: String(value || '')
.split(/[\r\n,;]+/)
.map((entry) => String(entry || '').trim())
.filter(Boolean);
const normalized = [];
const seen = new Set();
source.forEach((entry) => {
const id = normalizeNexSmsCountryId(
entry && typeof entry === 'object' && !Array.isArray(entry)
? (entry.id || entry.countryId || entry.country || '')
: entry,
-1
);
if (id < 0 || seen.has(id)) {
return;
}
seen.add(id);
normalized.push(id);
});
return normalized.slice(0, 10);
}
function normalizeNexSmsServiceCode(value = '', fallback = DEFAULT_NEX_SMS_SERVICE_CODE) {
const normalized = String(value || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9_-]/g, '');
if (normalized) {
return normalized;
}
const fallbackNormalized = String(fallback || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9_-]/g, '');
return fallbackNormalized || DEFAULT_NEX_SMS_SERVICE_CODE;
}
function normalizePhonePreferredActivation(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
const activationId = String(value.activationId ?? value.id ?? value.activation ?? '').trim();
const phoneNumber = String(value.phoneNumber ?? value.number ?? value.phone ?? '').trim();
if (!activationId || !phoneNumber) {
return null;
}
const provider = normalizePhoneSmsProvider(value.provider || value.smsProvider || DEFAULT_PHONE_SMS_PROVIDER);
return {
...value,
provider,
activationId,
phoneNumber,
countryId: value.countryId ?? value.country ?? value.countryCode ?? null,
countryLabel: String(value.countryLabel || value.label || '').trim(),
successfulUses: Math.max(0, Math.floor(Number(value.successfulUses) || 0)),
maxUses: Math.max(1, Math.floor(Number(value.maxUses) || 1)),
};
}
function normalizeFiveSimCountryLabel(value = '', fallback = FIVE_SIM_COUNTRY_LABEL) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryLabel) {
@@ -1863,6 +2080,8 @@ function normalizePersistentSettingValue(key, value) {
return String(value || '').trim();
case 'customPassword':
return String(value || '');
case 'signupMethod':
return normalizeSignupMethod(value);
case 'plusPaymentMethod':
return normalizePlusPaymentMethod(value);
case 'paypalEmail':
@@ -2002,6 +2221,8 @@ function normalizePersistentSettingValue(key, value) {
return normalizeHeroSmsAcquirePriority(value);
case 'heroSmsMaxPrice':
return normalizeHeroSmsMaxPrice(value);
case 'heroSmsPreferredPrice':
return normalizeHeroSmsMaxPrice(value);
case 'heroSmsCountryId': {
const parsed = Math.floor(Number(value));
if (Number.isFinite(parsed) && HERO_SMS_SUPPORTED_COUNTRY_ID_SET.has(String(parsed))) {
@@ -2015,6 +2236,8 @@ function normalizePersistentSettingValue(key, value) {
return normalizeHeroSmsCountryFallback(value);
case 'fiveSimApiKey':
return String(value || '');
case 'fiveSimProduct':
return normalizeFiveSimCountryCode(value, DEFAULT_FIVE_SIM_PRODUCT);
case 'fiveSimCountryId':
return normalizeFiveSimCountryId(value);
case 'fiveSimCountryLabel':
@@ -2022,13 +2245,19 @@ function normalizePersistentSettingValue(key, value) {
case 'fiveSimCountryFallback':
return normalizeFiveSimCountryFallback(value);
case 'fiveSimCountryOrder':
return normalizeFiveSimCountryFallback(value)
.map((entry) => entry.id)
.filter(Boolean);
return normalizeFiveSimCountryOrder(value);
case 'fiveSimMaxPrice':
return normalizeFiveSimMaxPrice(value);
case 'fiveSimOperator':
return normalizeFiveSimOperator(value);
case 'nexSmsApiKey':
return String(value || '');
case 'nexSmsCountryOrder':
return normalizeNexSmsCountryOrder(value);
case 'nexSmsServiceCode':
return normalizeNexSmsServiceCode(value);
case 'phonePreferredActivation':
return normalizePhonePreferredActivation(value);
default:
return value;
}
@@ -2085,6 +2314,16 @@ function buildPersistentSettingsPayload(input = {}, options = {}) {
}
payload.cloudflareTempEmailDomains = domains;
}
const nextSignupConstraintState = {
...PERSISTED_SETTING_DEFAULTS,
...payload,
resolvedSignupMethod: null,
};
if (Object.prototype.hasOwnProperty.call(payload, 'phoneVerificationEnabled')
|| Object.prototype.hasOwnProperty.call(payload, 'plusModeEnabled')
|| Object.prototype.hasOwnProperty.call(payload, 'signupMethod')) {
payload.signupMethod = resolveSignupMethod(nextSignupConstraintState);
}
if (payload.ipProxyServiceProfiles) {
const selectedService = normalizeIpProxyProviderValue(
payload.ipProxyService || PERSISTED_SETTING_DEFAULTS.ipProxyService
@@ -2133,11 +2372,11 @@ async function getPersistedAliasState() {
const preservedAliases = normalizeBooleanMap(stored.preservedAliases);
return {
manualAliasUsage,
preservedAliases,
icloudAliasCache: normalizeIcloudAliasCacheList(stored.icloudAliasCache, {
usedEmails: toNormalizedEmailSet(manualAliasUsage),
preservedEmails: toNormalizedEmailSet(preservedAliases),
}),
preservedAliases,
icloudAliasCache: normalizeIcloudAliasCacheList(stored.icloudAliasCache, {
usedEmails: toNormalizedEmailSet(manualAliasUsage),
preservedEmails: toNormalizedEmailSet(preservedAliases),
}),
icloudAliasCacheAt: Math.max(0, Number(stored.icloudAliasCacheAt) || 0),
};
} catch (err) {
@@ -6766,8 +7005,10 @@ function getLoginAuthStateLabel(state) {
}
switch (state) {
case 'verification_page': return '登录验证码页';
case 'phone_verification_page': return '手机验证码页';
case 'password_page': return '密码页';
case 'email_page': return '邮箱输入页';
case 'phone_entry_page': return '手机号输入页';
case 'login_timeout_error_page': return '登录超时报错页';
case 'oauth_consent_page': return 'OAuth 授权页';
case 'add_phone_page': return '手机号页';
@@ -7768,12 +8009,21 @@ async function handleStepData(step, payload) {
}
case 2:
if (payload.email) await setEmailState(payload.email);
if (payload.accountIdentifierType || payload.accountIdentifier || payload.signupPhoneNumber || payload.signupPhoneActivation) {
await setState({
accountIdentifierType: payload.accountIdentifierType || null,
accountIdentifier: String(payload.accountIdentifier || '').trim(),
signupPhoneNumber: String(payload.signupPhoneNumber || '').trim(),
signupPhoneActivation: payload.signupPhoneActivation || null,
});
}
if (payload.skippedPasswordStep) {
const latestState = await getState();
const step3Status = latestState.stepStatuses?.[3];
if (step3Status !== 'running' && step3Status !== 'completed' && step3Status !== 'manual_completed') {
await setStepStatus(3, 'skipped');
await addLog('步骤 2:提交邮箱后页面直接进入邮箱验证码页,已自动跳过步骤 3。', 'warn');
const identityLabel = payload.accountIdentifierType === 'phone' ? '手机号' : '邮箱';
await addLog(`步骤 2:提交${identityLabel}后页面直接进入验证码页,已自动跳过步骤 3。`, 'warn');
}
}
break;
@@ -7782,6 +8032,14 @@ async function handleStepData(step, payload) {
if (payload.signupVerificationRequestedAt) {
await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt });
}
if (payload.skipProfileStep) {
const latestState = await getState();
const step5Status = latestState.stepStatuses?.[5];
if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') {
await setStepStatus(5, 'skipped');
await addLog('步骤 3:页面已直接进入已登录态,已自动跳过步骤 5。', 'warn');
}
}
if (payload.loginVerificationRequestedAt) {
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
}
@@ -7793,13 +8051,25 @@ async function handleStepData(step, payload) {
break;
case 4:
await setState({
lastEmailTimestamp: payload.emailTimestamp || null,
...(payload.phoneVerification ? {
currentPhoneVerificationCode: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
} : {
lastEmailTimestamp: payload.emailTimestamp || null,
}),
signupVerificationRequestedAt: null,
});
break;
case 8:
await setState({
lastEmailTimestamp: payload.emailTimestamp || null,
...(payload.phoneVerification || payload.loginPhoneVerification ? {
currentPhoneVerificationCode: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
} : {
lastEmailTimestamp: payload.emailTimestamp || null,
}),
loginVerificationRequestedAt: null,
});
break;
@@ -9276,6 +9546,7 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
let step4RestartCount = 0;
let currentStartStep = startStep;
let continueCurrentAttempt = continued;
const resolvedSignupMethod = await ensureResolvedSignupMethodForRun();
while (true) {
@@ -9290,7 +9561,11 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
}
if (currentStartStep <= 2) {
await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns);
if (resolvedSignupMethod === SIGNUP_METHOD_PHONE) {
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:本轮注册方式为手机号注册,将跳过邮箱预获取 ===`, 'info');
} else {
await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns);
}
await executeStepAndWait(2, AUTO_STEP_DELAYS[2]);
}
@@ -9570,6 +9845,14 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
isGeneratedAliasProvider,
isReusableGeneratedAliasEmail,
isSignupEmailVerificationPageUrl,
isSignupPhoneVerificationPageUrl: (rawUrl) => {
const parsed = parseUrlSafely(rawUrl);
return Boolean(parsed && isSignupPageHost(parsed.hostname) && /\/phone-verification(?:[/?#]|$)/i.test(parsed.pathname || ''));
},
isSignupProfilePageUrl: (rawUrl) => {
const parsed = parseUrlSafely(rawUrl);
return Boolean(parsed && isSignupPageHost(parsed.hostname) && /\/create-account\/profile(?:[/?#]|$)/i.test(parsed.pathname || ''));
},
isRetryableContentScriptTransportError,
isHotmailProvider,
isLuckmailProvider,
@@ -9660,8 +9943,11 @@ const step2Executor = self.MultiPageBackgroundStep2?.createStep2Executor({
ensureSignupAuthEntryPageReady,
ensureSignupEntryPageReady,
ensureSignupPostEmailPageReadyInTab,
ensureSignupPostIdentityPageReadyInTab: signupFlowHelpers.ensureSignupPostIdentityPageReadyInTab,
getTabId,
isTabAlive,
phoneVerificationHelpers,
resolveSignupMethod,
resolveSignupEmailForFlow,
sendToContentScriptResilient,
SIGNUP_PAGE_INJECT_FILES,
@@ -9712,6 +9998,8 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({
shouldUseCustomRegistrationEmail,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
throwIfStopped,
phoneVerificationHelpers,
resolveSignupMethod,
});
const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({
addLog,
@@ -9760,7 +10048,9 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
isVerificationMailPollingError,
LUCKMAIL_PROVIDER,
resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep,
phoneVerificationHelpers,
rerunStep7ForStep8Recovery: (...args) => rerunStep7ForStep8Recovery(...args),
resolveSignupMethod,
reuseOrCreateTab,
setState,
shouldUseCustomRegistrationEmail,
@@ -9938,6 +10228,9 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
getStepDefinitionForState,
getStepIdsForState,
getLastStepIdForState,
normalizeSignupMethod,
canUsePhoneSignup,
resolveSignupMethod,
getTabId,
getStopRequested: () => stopRequested,
handleCloudflareSecurityBlocked,
+102 -12
View File
@@ -90,8 +90,61 @@
return '流程失败';
}
function buildRecordId(email = '') {
return String(email || '').trim().toLowerCase();
function normalizeAccountIdentifierType(value = '') {
return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
}
function normalizeAccountIdentifierValue(value = '', identifierType = 'email') {
const normalizedValue = String(value || '').trim();
if (!normalizedValue) {
return '';
}
return normalizeAccountIdentifierType(identifierType) === 'phone'
? normalizedValue
: normalizedValue.toLowerCase();
}
function resolveRecordIdentity(record = {}) {
const email = String(record.email || '').trim().toLowerCase();
const phoneNumber = String(record.phoneNumber ?? record.phone ?? record.number ?? '').trim();
const rawIdentifierType = String(record.accountIdentifierType || '').trim().toLowerCase();
const inferredIdentifierType = rawIdentifierType === 'phone'
|| (!email && phoneNumber)
? 'phone'
: 'email';
const rawAccountIdentifier = String(
record.accountIdentifier
|| (inferredIdentifierType === 'phone' ? phoneNumber : email)
|| ''
).trim();
const accountIdentifierType = rawAccountIdentifier
? normalizeAccountIdentifierType(inferredIdentifierType)
: (email ? 'email' : (phoneNumber ? 'phone' : ''));
const accountIdentifier = normalizeAccountIdentifierValue(
rawAccountIdentifier || (accountIdentifierType === 'phone' ? phoneNumber : email),
accountIdentifierType || inferredIdentifierType
);
return {
email,
phoneNumber,
accountIdentifierType,
accountIdentifier,
};
}
function buildRecordId(identifier = '', identifierType = 'email') {
const normalizedIdentifierType = normalizeAccountIdentifierType(identifierType);
const normalizedIdentifier = normalizeAccountIdentifierValue(identifier, normalizedIdentifierType);
if (!normalizedIdentifier) {
return '';
}
if (normalizedIdentifierType === 'phone' && /^phone:/i.test(normalizedIdentifier)) {
return normalizedIdentifier.toLowerCase();
}
return normalizedIdentifierType === 'phone'
? `phone:${normalizedIdentifier.toLowerCase()}`
: normalizedIdentifier;
}
function normalizeSource(value = '') {
@@ -140,11 +193,15 @@
return null;
}
const email = String(record.email || '').trim();
const password = String(record.password || '').trim();
const identity = resolveRecordIdentity(record);
const email = identity.email;
const phoneNumber = identity.phoneNumber;
const accountIdentifierType = identity.accountIdentifierType;
const accountIdentifier = identity.accountIdentifier;
const password = String(record.password ?? '').trim();
const finalStatus = normalizeFinalStatus(record.finalStatus || record.status || '');
if (!email || !password || !finalStatus) {
if (!accountIdentifier || !finalStatus) {
return null;
}
@@ -167,8 +224,11 @@
const rawFailureLabel = String(record.failureLabel || '').trim();
return {
recordId: String(record.recordId || '').trim() || buildRecordId(email),
recordId: String(record.recordId || '').trim() || buildRecordId(accountIdentifier, accountIdentifierType),
accountIdentifierType,
accountIdentifier,
email,
phoneNumber,
password,
finalStatus,
finishedAt,
@@ -215,11 +275,20 @@
}
function buildAccountRunHistoryRecord(state = {}, status = '', reason = '') {
const email = String(state.email || '').trim();
const password = String(state.password || state.customPassword || '').trim() || '无';
const identity = resolveRecordIdentity({
accountIdentifierType: state.accountIdentifierType,
accountIdentifier: state.accountIdentifier,
email: state.email,
phoneNumber: state.phoneNumber || state.signupPhoneNumber,
});
const email = identity.email;
const phoneNumber = identity.phoneNumber;
const accountIdentifierType = identity.accountIdentifierType;
const accountIdentifier = identity.accountIdentifier;
const password = String(state.password || state.customPassword || '').trim();
const finalStatus = normalizeFinalStatus(status);
if (!email || !finalStatus) {
if (!accountIdentifier || !finalStatus) {
return null;
}
@@ -233,8 +302,11 @@
const finishedAt = new Date().toISOString();
return {
recordId: buildRecordId(email),
recordId: buildRecordId(accountIdentifier, accountIdentifierType),
accountIdentifierType,
accountIdentifier,
email,
phoneNumber,
password,
finalStatus,
finishedAt,
@@ -257,10 +329,23 @@
const recordId = String(record.recordId || '').trim();
const emailKey = String(record.email || '').trim().toLowerCase();
const phoneKey = String(record.phoneNumber || '').trim().toLowerCase();
const identifierKey = buildRecordId(
record.accountIdentifier || record.email || record.phoneNumber,
record.accountIdentifierType || (phoneKey && !emailKey ? 'phone' : 'email')
);
const nextHistory = normalizedHistory.filter((item) => {
const itemRecordId = String(item.recordId || '').trim();
const itemEmailKey = String(item.email || '').trim().toLowerCase();
return itemRecordId !== recordId && itemEmailKey !== emailKey;
const itemPhoneKey = String(item.phoneNumber || '').trim().toLowerCase();
const itemIdentifierKey = buildRecordId(
item.accountIdentifier || item.email || item.phoneNumber,
item.accountIdentifierType || (itemPhoneKey && !itemEmailKey ? 'phone' : 'email')
);
return itemRecordId !== recordId
&& itemIdentifierKey !== identifierKey
&& (!emailKey || itemEmailKey !== emailKey)
&& (!phoneKey || itemPhoneKey !== phoneKey);
});
nextHistory.unshift(record);
@@ -283,7 +368,12 @@
}
const selectedIds = new Set(normalizedIds);
const nextHistory = normalizedHistory.filter((record) => !selectedIds.has(buildRecordId(record.recordId || record.email)));
const nextHistory = normalizedHistory.filter((record) => !selectedIds.has(buildRecordId(
record.recordId || record.accountIdentifier || record.email || record.phoneNumber,
String(record.recordId || '').startsWith('phone:') || String(record.accountIdentifierType || '').trim().toLowerCase() === 'phone'
? 'phone'
: 'email'
)));
return {
deletedCount: normalizedHistory.length - nextHistory.length,
+1
View File
@@ -417,6 +417,7 @@
autoRunDelayEnabled: prevState.autoRunDelayEnabled,
autoRunDelayMinutes: prevState.autoRunDelayMinutes,
autoStepDelaySeconds: prevState.autoStepDelaySeconds,
signupMethod: prevState.signupMethod,
mailProvider: prevState.mailProvider,
emailGenerator: prevState.emailGenerator,
gmailBaseEmail: prevState.gmailBaseEmail,
+51 -4
View File
@@ -48,6 +48,14 @@
getStepDefinitionForState,
getStepIdsForState,
getLastStepIdForState,
normalizeSignupMethod = (value = '') => String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email',
canUsePhoneSignup = (state = {}) => Boolean(state?.phoneVerificationEnabled)
&& !Boolean(state?.plusModeEnabled)
&& !Boolean(state?.contributionMode),
resolveSignupMethod = (state = {}) => {
const method = normalizeSignupMethod(state?.signupMethod);
return method === 'phone' && canUsePhoneSignup(state) ? 'phone' : 'email';
},
getTabId,
getStopRequested,
handleAutoRunLoopUnhandledError,
@@ -275,7 +283,13 @@
if (stepKey === 'fetch-login-code') {
await setState({
lastEmailTimestamp: payload.emailTimestamp || null,
...(payload.phoneVerification || payload.loginPhoneVerification ? {
currentPhoneVerificationCode: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
} : {
lastEmailTimestamp: payload.emailTimestamp || null,
}),
loginVerificationRequestedAt: null,
});
return;
@@ -340,7 +354,8 @@
const step3Status = latestState.stepStatuses?.[3];
if (step3Status !== 'running' && step3Status !== 'completed' && step3Status !== 'manual_completed') {
await setStepStatus(3, 'skipped');
await addLog('步骤 2:提交邮箱后页面直接进入邮箱验证码页,已自动跳过步骤 3。', 'warn');
const identityLabel = payload.accountIdentifierType === 'phone' ? '手机号' : '邮箱';
await addLog(`步骤 2:提交${identityLabel}后页面直接进入验证码页,已自动跳过步骤 3。`, 'warn');
}
}
break;
@@ -349,13 +364,27 @@
if (payload.signupVerificationRequestedAt) {
await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt });
}
if (payload.skipProfileStep) {
const latestState = await getState();
const step5Status = latestState.stepStatuses?.[5];
if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') {
await setStepStatus(5, 'skipped');
await addLog('步骤 3:页面已直接进入已登录态,已自动跳过步骤 5。', 'warn');
}
}
if (payload.loginVerificationRequestedAt) {
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
}
break;
case 4:
await setState({
lastEmailTimestamp: payload.emailTimestamp || null,
...(payload.phoneVerification ? {
currentPhoneVerificationCode: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
} : {
lastEmailTimestamp: payload.emailTimestamp || null,
}),
signupVerificationRequestedAt: null,
});
if (payload.skipProfileStep) {
@@ -373,7 +402,13 @@
break;
case 8:
await setState({
lastEmailTimestamp: payload.emailTimestamp || null,
...(payload.phoneVerification || payload.loginPhoneVerification ? {
currentPhoneVerificationCode: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
} : {
lastEmailTimestamp: payload.emailTimestamp || null,
}),
loginVerificationRequestedAt: null,
});
break;
@@ -758,6 +793,18 @@
const currentState = await getState();
const updates = buildPersistentSettingsPayload(message.payload || {});
const sessionUpdates = buildLuckmailSessionSettingsPayload(message.payload || {});
const nextSignupState = {
...currentState,
...updates,
resolvedSignupMethod: null,
};
if (
Object.prototype.hasOwnProperty.call(updates, 'phoneVerificationEnabled')
|| Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
|| Object.prototype.hasOwnProperty.call(updates, 'signupMethod')
) {
updates.signupMethod = resolveSignupMethod(nextSignupState);
}
const modeChanged = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
&& Boolean(currentState?.plusModeEnabled) !== Boolean(updates.plusModeEnabled);
const plusPaymentChanged = Object.prototype.hasOwnProperty.call(updates, 'plusPaymentMethod')
+619 -1
View File
@@ -92,6 +92,7 @@
const MAX_ACTIVATION_PRICE_HINTS = 256;
const activationPriceHintsByKey = new Map();
let activePhoneVerificationLogStep = null;
let activePhoneVerificationLogStepKey = null;
function normalizeLogStep(value) {
const step = Math.floor(Number(value) || 0);
@@ -114,13 +115,26 @@
if (step) {
normalizedOptions.step = step;
if (!normalizedOptions.stepKey) {
normalizedOptions.stepKey = 'phone-verification';
normalizedOptions.stepKey = activePhoneVerificationLogStepKey || 'phone-verification';
}
}
delete normalizedOptions.visibleStep;
return rawAddLog(normalizePhoneVerificationLogMessage(message), level, normalizedOptions);
}
async function withPhoneVerificationLogContext(options = {}, action) {
const previousStep = activePhoneVerificationLogStep;
const previousStepKey = activePhoneVerificationLogStepKey;
activePhoneVerificationLogStep = normalizeLogStep(options.step || options.visibleStep) || previousStep;
activePhoneVerificationLogStepKey = String(options.stepKey || '').trim() || previousStepKey;
try {
return await action();
} finally {
activePhoneVerificationLogStep = previousStep;
activePhoneVerificationLogStepKey = previousStepKey;
}
}
function normalizeUrl(value, fallback = DEFAULT_HERO_SMS_BASE_URL) {
const trimmed = String(value || '').trim();
if (!trimmed) {
@@ -3661,6 +3675,60 @@
return result || {};
}
async function submitSignupPhoneVerificationCode(tabId, code, options = {}) {
const visibleStep = 4;
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(45000, { step: visibleStep, actionLabel: '提交注册手机验证码' })
: 45000;
const result = await sendToContentScriptResilient('signup-page', {
type: 'SUBMIT_PHONE_VERIFICATION_CODE',
step: visibleStep,
source: 'background',
payload: {
code,
purpose: 'signup',
signupProfile: options.signupProfile || null,
},
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: '步骤 4:等待注册手机验证码页面就绪后填写短信验证码...',
logStep: visibleStep,
logStepKey: 'fetch-signup-code',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function resendSignupPhoneVerificationCode(tabId) {
const visibleStep = 4;
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(65000, { step: visibleStep, actionLabel: '重新发送注册手机验证码' })
: 65000;
const result = await sendToContentScriptResilient('signup-page', {
type: 'RESEND_VERIFICATION_CODE',
step: visibleStep,
source: 'background',
payload: {},
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: '步骤 4:等待注册手机验证码重发按钮出现...',
logStep: visibleStep,
logStepKey: 'fetch-signup-code',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function returnToAddPhone(tabId) {
const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9;
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
@@ -3773,6 +3841,25 @@
});
}
async function persistSignupPhoneRuntimeState(updates = {}) {
await setPhoneRuntimeState({
signupPhoneNumber: '',
signupPhoneActivation: null,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
accountIdentifierType: null,
accountIdentifier: '',
...updates,
});
}
async function clearSignupPhoneRuntimeState(extraUpdates = {}) {
await persistSignupPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
...extraUpdates,
});
}
async function acquirePhoneActivation(state = {}, options = {}) {
const provider = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER);
const providerOrder = resolvePhoneProviderOrder(state, provider);
@@ -3982,6 +4069,28 @@
throw lastProviderError || new Error('Step 9: failed to acquire phone activation.');
}
async function prepareSignupPhoneActivation(state = {}, options = {}) {
return withPhoneVerificationLogContext({ step: 2, stepKey: 'submit-signup-email' }, async () => {
const activation = await acquirePhoneActivation(state, {
...options,
logLabel: options?.logLabel || '步骤 2',
});
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
throw new Error('步骤 2:接码平台返回的手机号订单无效。');
}
await persistSignupPhoneRuntimeState({
signupPhoneNumber: normalizedActivation.phoneNumber,
signupPhoneActivation: normalizedActivation,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: 'signup',
accountIdentifierType: 'phone',
accountIdentifier: normalizedActivation.phoneNumber,
});
return normalizedActivation;
});
}
async function markActivationReusableAfterSuccess(state, activation) {
const normalizedActivation = normalizeActivation(activation);
if (!isPhoneSmsReuseEnabled(state)) {
@@ -4151,8 +4260,505 @@
throw new Error('手机号验证未完成。');
}
function buildCompletedActivationSnapshot(activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
return null;
}
return {
...normalizedActivation,
successfulUses: normalizedActivation.successfulUses + 1,
};
}
async function waitForScopedPhoneCode(state = {}, activation, options = {}) {
const normalizedActivation = normalizeActivation(activation);
const visibleStep = normalizeLogStep(options?.step) || 4;
const stepKey = String(options?.stepKey || 'fetch-signup-code').trim() || 'fetch-signup-code';
const purpose = String(options?.purpose || 'signup').trim() || 'signup';
const actionLabelPrefix = String(options?.actionLabelPrefix || 'signup phone verification').trim() || 'phone verification';
if (!normalizedActivation) {
throw new Error(options?.missingActivationMessage || `步骤 ${visibleStep}:手机号激活记录缺失,请重新执行前置步骤。`);
}
return withPhoneVerificationLogContext({ step: visibleStep, stepKey }, async () => {
const providerLabel = getPhoneSmsProviderLabel(normalizedActivation.provider);
const waitSeconds = normalizePhoneCodeWaitSeconds(state?.phoneCodeWaitSeconds);
const timeoutWindows = normalizePhoneCodeTimeoutWindows(state?.phoneCodeTimeoutWindows);
const pollIntervalSeconds = normalizePhoneCodePollIntervalSeconds(state?.phoneCodePollIntervalSeconds);
const pollMaxRounds = normalizePhoneCodePollMaxRounds(state?.phoneCodePollMaxRounds);
let lastLoggedStatus = '';
let lastLoggedPollCount = 0;
for (let windowIndex = 1; windowIndex <= timeoutWindows; windowIndex += 1) {
await setPhoneRuntimeState({
signupPhoneActivation: normalizedActivation,
signupPhoneNumber: normalizedActivation.phoneNumber,
signupPhoneVerificationPurpose: purpose,
signupPhoneVerificationRequestedAt: Date.now(),
[PHONE_RUNTIME_COUNTDOWN_ENDS_AT_KEY]: Date.now() + waitSeconds * 1000,
[PHONE_RUNTIME_COUNTDOWN_WINDOW_INDEX_KEY]: windowIndex,
[PHONE_RUNTIME_COUNTDOWN_WINDOW_TOTAL_KEY]: timeoutWindows,
});
await addLog(
`步骤 ${visibleStep}:正在等待 ${normalizedActivation.phoneNumber} 的短信验证码(${windowIndex}/${timeoutWindows},最长 ${waitSeconds} 秒)。`,
'info',
{ step: visibleStep, stepKey }
);
try {
const code = await pollPhoneActivationCode(state, normalizedActivation, {
actionLabel: windowIndex === 1
? `poll ${actionLabelPrefix} code from ${providerLabel}`
: `poll resent ${actionLabelPrefix} code from ${providerLabel}`,
timeoutMs: waitSeconds * 1000,
intervalMs: pollIntervalSeconds * 1000,
maxRounds: pollMaxRounds,
onStatus: async ({ elapsedMs, pollCount, statusText }) => {
const shouldLog = (
pollCount === 1
|| statusText !== lastLoggedStatus
|| pollCount - lastLoggedPollCount >= 3
);
if (!shouldLog) {
return;
}
lastLoggedStatus = statusText;
lastLoggedPollCount = pollCount;
await addLog(
`步骤 ${visibleStep}${providerLabel} 状态 ${normalizedActivation.phoneNumber}: ${statusText}(已等待 ${Math.ceil(elapsedMs / 1000)} 秒,第 ${pollCount}/${pollMaxRounds} 轮)。`,
'info',
{ step: visibleStep, stepKey }
);
},
});
await clearPhoneRuntimeCountdown();
await setPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: String(code || '').trim(),
signupPhoneVerificationRequestedAt: Date.now(),
});
return code;
} catch (error) {
if (!isPhoneCodeTimeoutError(error)) {
if (isPhoneActivationOrderMissingError(error, normalizedActivation.provider)) {
throw new Error(`步骤 ${visibleStep}:当前手机号激活已失效,请重新执行前置步骤获取新短信。${error.message || error}`);
}
throw error;
}
if (windowIndex < timeoutWindows) {
await addLog(
`步骤 ${visibleStep}${normalizedActivation.phoneNumber}${waitSeconds} 秒内未收到短信,准备请求重发。`,
'warn',
{ step: visibleStep, stepKey }
);
await requestAdditionalPhoneSms(state, normalizedActivation);
if (typeof options.onTimeoutWindow === 'function') {
await options.onTimeoutWindow({
activation: normalizedActivation,
windowIndex,
timeoutWindows,
});
}
continue;
}
await clearPhoneRuntimeCountdown();
throw error;
}
}
throw new Error(`步骤 ${visibleStep}:手机验证码未能成功获取。`);
});
}
async function waitForSignupPhoneCode(state = {}, activation, options = {}) {
return waitForScopedPhoneCode(state, activation, {
...options,
step: 4,
stepKey: 'fetch-signup-code',
purpose: 'signup',
actionLabelPrefix: 'signup phone verification',
missingActivationMessage: '步骤 4:注册手机号激活记录缺失,请重新执行步骤 2。',
});
}
async function waitForLoginPhoneCode(state = {}, activation, options = {}) {
const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8;
return waitForScopedPhoneCode(state, activation, {
...options,
step: visibleStep,
stepKey: 'fetch-login-code',
purpose: 'login',
actionLabelPrefix: 'login phone verification',
missingActivationMessage: `步骤 ${visibleStep}:登录手机号激活记录缺失,请重新执行步骤 ${visibleStep >= 11 ? 10 : 7}`,
});
}
async function finalizeSignupPhoneActivationAfterSuccess(state = {}, activation = null) {
const normalizedActivation = normalizeActivation(activation || state?.signupPhoneActivation);
if (!normalizedActivation) {
await clearSignupPhoneRuntimeState();
return null;
}
await completePhoneActivation(state, normalizedActivation);
await markActivationReusableAfterSuccess(state, normalizedActivation);
await clearSignupPhoneRuntimeState({
signupPhoneCompletedActivation: buildCompletedActivationSnapshot(normalizedActivation),
signupPhoneNumber: normalizedActivation.phoneNumber,
accountIdentifierType: 'phone',
accountIdentifier: normalizedActivation.phoneNumber,
});
return normalizedActivation;
}
async function cancelSignupPhoneActivation(state = {}, activation = null) {
const normalizedActivation = normalizeActivation(activation || state?.signupPhoneActivation);
if (normalizedActivation) {
await cancelPhoneActivation(state, normalizedActivation);
}
await clearSignupPhoneRuntimeState();
}
async function completeSignupPhoneVerificationFlow(tabId, options = {}) {
return withPhoneVerificationLogContext({ step: 4, stepKey: 'fetch-signup-code' }, async () => {
let state = options?.state || await getState();
const activation = normalizeActivation(options?.activation || state?.signupPhoneActivation);
if (!activation) {
throw new Error('步骤 4:未找到当前注册手机号激活记录,请重新执行步骤 2。');
}
let shouldCancelActivation = true;
try {
for (let attempt = 1; attempt <= DEFAULT_PHONE_SUBMIT_ATTEMPTS; attempt += 1) {
throwIfStopped();
state = await getState();
const code = await waitForSignupPhoneCode(state, activation, {
onTimeoutWindow: async () => {
try {
await resendSignupPhoneVerificationCode(tabId);
await addLog('步骤 4:已点击注册手机验证码页面的“重新发送”。', 'info', {
step: 4,
stepKey: 'fetch-signup-code',
});
} catch (resendError) {
if (isStopRequestedError(resendError)) {
throw resendError;
}
await addLog(`步骤 4:注册手机验证码页面重发失败,将继续轮询短信。${resendError.message}`, 'warn', {
step: 4,
stepKey: 'fetch-signup-code',
});
}
},
});
await setPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: String(code || '').trim(),
signupPhoneVerificationRequestedAt: Date.now(),
signupPhoneVerificationPurpose: 'signup',
});
await addLog(`步骤 4:已获取手机验证码 ${code}`, 'info', {
step: 4,
stepKey: 'fetch-signup-code',
});
const submitResult = await submitSignupPhoneVerificationCode(tabId, code, {
signupProfile: options.signupProfile || null,
});
if (submitResult.invalidCode) {
const invalidErrorText = String(submitResult.errorText || submitResult.url || '未知错误').trim();
if (attempt >= DEFAULT_PHONE_SUBMIT_ATTEMPTS) {
throw new Error(`步骤 4:手机验证码连续 ${DEFAULT_PHONE_SUBMIT_ATTEMPTS} 次被拒绝:${invalidErrorText}`);
}
await requestAdditionalPhoneSms(state, activation);
try {
await resendSignupPhoneVerificationCode(tabId);
} catch (resendError) {
if (isStopRequestedError(resendError)) {
throw resendError;
}
await addLog(`步骤 4:验证码被拒后点击重发失败。${resendError.message}`, 'warn', {
step: 4,
stepKey: 'fetch-signup-code',
});
}
await addLog(
`步骤 4:手机验证码被拒绝,已请求新短信(${attempt + 1}/${DEFAULT_PHONE_SUBMIT_ATTEMPTS})。`,
'warn',
{ step: 4, stepKey: 'fetch-signup-code' }
);
continue;
}
await finalizeSignupPhoneActivationAfterSuccess(state, activation);
shouldCancelActivation = false;
await setPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
});
await addLog('步骤 4:手机验证码已通过,继续进入资料填写。', 'ok', {
step: 4,
stepKey: 'fetch-signup-code',
});
return submitResult || {};
}
throw new Error('步骤 4:手机验证码未能成功提交。');
} catch (error) {
if (shouldCancelActivation && activation) {
await cancelSignupPhoneActivation(state, activation).catch(() => {});
}
await setPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
});
throw sanitizePhoneCodeTimeoutError(error);
}
});
}
async function submitLoginPhoneVerificationCode(tabId, code, options = {}) {
const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8;
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(45000, { step: visibleStep, actionLabel: '提交登录手机验证码' })
: 45000;
const result = await sendToContentScriptResilient('signup-page', {
type: 'SUBMIT_PHONE_VERIFICATION_CODE',
step: visibleStep,
source: 'background',
payload: {
code,
purpose: 'login',
visibleStep,
},
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: `步骤 ${visibleStep}:等待登录手机验证码页面就绪后填写短信验证码...`,
logStep: visibleStep,
logStepKey: 'fetch-login-code',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function resendLoginPhoneVerificationCode(tabId, options = {}) {
const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8;
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(65000, { step: visibleStep, actionLabel: '重新发送登录手机验证码' })
: 65000;
const result = await sendToContentScriptResilient('signup-page', {
type: 'RESEND_VERIFICATION_CODE',
step: visibleStep,
source: 'background',
payload: {},
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: `步骤 ${visibleStep}:等待登录手机验证码重发按钮出现...`,
logStep: visibleStep,
logStepKey: 'fetch-login-code',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function prepareLoginPhoneActivation(state = {}, options = {}) {
const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8;
return withPhoneVerificationLogContext({ step: visibleStep, stepKey: 'fetch-login-code' }, async () => {
const preferredActivation = normalizeActivation(
options?.activation
|| state?.signupPhoneCompletedActivation
|| state?.signupPhoneActivation
);
if (!preferredActivation) {
throw new Error(`步骤 ${visibleStep}:缺少已注册手机号激活记录,无法继续手机号登录验证码流程。`);
}
const activeActivation = normalizeActivation(state?.signupPhoneActivation);
if (activeActivation && isSameActivation(activeActivation, preferredActivation)) {
await setPhoneRuntimeState({
signupPhoneNumber: activeActivation.phoneNumber,
signupPhoneVerificationPurpose: 'login',
});
return activeActivation;
}
const reactivated = await reactivatePhoneActivation(state, preferredActivation);
const normalizedActivation = normalizeActivation(reactivated);
if (!normalizedActivation) {
throw new Error(`步骤 ${visibleStep}:无法复用当前注册手机号,请重新执行步骤 ${visibleStep >= 11 ? 10 : 7}`);
}
await setPhoneRuntimeState({
signupPhoneActivation: normalizedActivation,
signupPhoneCompletedActivation: preferredActivation,
signupPhoneNumber: normalizedActivation.phoneNumber,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: 'login',
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
accountIdentifierType: 'phone',
accountIdentifier: normalizedActivation.phoneNumber,
});
return normalizedActivation;
});
}
async function finalizeLoginPhoneActivationAfterSuccess(state = {}, activation = null, options = {}) {
const normalizedActivation = normalizeActivation(activation || state?.signupPhoneActivation);
const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8;
if (!normalizedActivation) {
await setPhoneRuntimeState({
signupPhoneActivation: null,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
});
return null;
}
return withPhoneVerificationLogContext({ step: visibleStep, stepKey: 'fetch-login-code' }, async () => {
await completePhoneActivation(state, normalizedActivation);
await setPhoneRuntimeState({
signupPhoneActivation: null,
signupPhoneCompletedActivation: buildCompletedActivationSnapshot(normalizedActivation),
signupPhoneNumber: normalizedActivation.phoneNumber,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
accountIdentifierType: 'phone',
accountIdentifier: normalizedActivation.phoneNumber,
});
return normalizedActivation;
});
}
async function completeLoginPhoneVerificationFlow(tabId, options = {}) {
const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8;
return withPhoneVerificationLogContext({ step: visibleStep, stepKey: 'fetch-login-code' }, async () => {
let state = options?.state || await getState();
const baseActivation = normalizeActivation(
options?.activation
|| state?.signupPhoneCompletedActivation
|| state?.signupPhoneActivation
);
if (!baseActivation) {
throw new Error(`步骤 ${visibleStep}:未找到当前登录手机号激活记录,请重新执行步骤 ${visibleStep >= 11 ? 10 : 7}`);
}
let activation = await prepareLoginPhoneActivation(state, {
activation: baseActivation,
visibleStep,
});
let shouldCancelActivation = true;
try {
for (let attempt = 1; attempt <= DEFAULT_PHONE_SUBMIT_ATTEMPTS; attempt += 1) {
throwIfStopped();
state = await getState();
const code = await waitForLoginPhoneCode(state, activation, {
visibleStep,
onTimeoutWindow: async () => {
try {
await resendLoginPhoneVerificationCode(tabId, { visibleStep });
await addLog(`步骤 ${visibleStep}:已点击登录手机验证码页面的“重新发送”。`, 'info', {
step: visibleStep,
stepKey: 'fetch-login-code',
});
} catch (resendError) {
if (isStopRequestedError(resendError)) {
throw resendError;
}
await addLog(`步骤 ${visibleStep}:登录手机验证码页面重发失败,将继续轮询短信。${resendError.message}`, 'warn', {
step: visibleStep,
stepKey: 'fetch-login-code',
});
}
},
});
await setPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: String(code || '').trim(),
signupPhoneVerificationRequestedAt: Date.now(),
signupPhoneVerificationPurpose: 'login',
});
await addLog(`步骤 ${visibleStep}:已获取登录手机验证码 ${code}`, 'info', {
step: visibleStep,
stepKey: 'fetch-login-code',
});
const submitResult = await submitLoginPhoneVerificationCode(tabId, code, {
visibleStep,
});
if (submitResult.invalidCode) {
const invalidErrorText = String(submitResult.errorText || submitResult.url || '未知错误').trim();
if (attempt >= DEFAULT_PHONE_SUBMIT_ATTEMPTS) {
throw new Error(`步骤 ${visibleStep}:登录手机验证码连续 ${DEFAULT_PHONE_SUBMIT_ATTEMPTS} 次被拒绝:${invalidErrorText}`);
}
await requestAdditionalPhoneSms(state, activation);
try {
await resendLoginPhoneVerificationCode(tabId, { visibleStep });
} catch (resendError) {
if (isStopRequestedError(resendError)) {
throw resendError;
}
await addLog(`步骤 ${visibleStep}:登录手机验证码被拒后点击重发失败。${resendError.message}`, 'warn', {
step: visibleStep,
stepKey: 'fetch-login-code',
});
}
await addLog(
`步骤 ${visibleStep}:登录手机验证码被拒绝,已请求新短信(${attempt + 1}/${DEFAULT_PHONE_SUBMIT_ATTEMPTS})。`,
'warn',
{ step: visibleStep, stepKey: 'fetch-login-code' }
);
continue;
}
await finalizeLoginPhoneActivationAfterSuccess(state, activation, { visibleStep });
shouldCancelActivation = false;
await addLog(`步骤 ${visibleStep}:登录手机验证码已通过,继续进入后续授权流程。`, 'ok', {
step: visibleStep,
stepKey: 'fetch-login-code',
});
return submitResult || {};
}
throw new Error(`步骤 ${visibleStep}:登录手机验证码未能成功提交。`);
} catch (error) {
if (shouldCancelActivation && activation) {
await cancelPhoneActivation(state, activation).catch(() => {});
}
await setPhoneRuntimeState({
signupPhoneActivation: null,
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
});
throw sanitizePhoneCodeTimeoutError(error);
}
});
}
async function completePhoneVerificationFlow(tabId, initialPageState = null, options = {}) {
const previousLogStep = activePhoneVerificationLogStep;
const previousLogStepKey = activePhoneVerificationLogStepKey;
activePhoneVerificationLogStep = normalizeLogStep(options.visibleStep || options.step) || 9;
activePhoneVerificationLogStepKey = 'phone-verification';
let state = await getState();
let activation = normalizeActivation(state[PHONE_ACTIVATION_STATE_KEY]);
let pageState = initialPageState || await readPhonePageState(tabId);
@@ -4796,15 +5402,27 @@
}
await clearCurrentActivation();
throw sanitizePhoneRestartStep7Error(sanitizePhoneCodeTimeoutError(error));
} finally {
activePhoneVerificationLogStep = previousLogStep;
activePhoneVerificationLogStepKey = previousLogStepKey;
}
}
return {
cancelSignupPhoneActivation,
completeLoginPhoneVerificationFlow,
completePhoneVerificationFlow,
completeSignupPhoneVerificationFlow,
finalizeLoginPhoneActivationAfterSuccess,
finalizeSignupPhoneActivationAfterSuccess,
normalizeActivation,
pollPhoneActivationCode,
prepareLoginPhoneActivation,
prepareSignupPhoneActivation,
reactivatePhoneActivation,
requestPhoneActivation,
waitForLoginPhoneCode,
waitForSignupPhoneCode,
};
}
+51 -11
View File
@@ -17,6 +17,8 @@
isLuckmailProvider,
isSignupEmailVerificationPageUrl,
isSignupPasswordPageUrl,
isSignupPhoneVerificationPageUrl = null,
isSignupProfilePageUrl = null,
reuseOrCreateTab,
sendToContentScriptResilient,
setEmailState,
@@ -62,46 +64,79 @@
return { tabId, result: result || {} };
}
function resolveSignupPostEmailState(rawUrl) {
function parseUrlSafely(rawUrl) {
if (!rawUrl) return null;
try {
return new URL(rawUrl);
} catch {
return null;
}
}
function fallbackSignupPhoneVerificationPageUrl(rawUrl) {
const parsed = parseUrlSafely(rawUrl);
if (!parsed) return false;
return /\/phone-verification(?:[/?#]|$)/i.test(parsed.pathname || '');
}
function fallbackSignupProfilePageUrl(rawUrl) {
const parsed = parseUrlSafely(rawUrl);
if (!parsed) return false;
return /\/create-account\/profile(?:[/?#]|$)/i.test(parsed.pathname || '');
}
function resolveSignupPostIdentityState(rawUrl) {
if (isSignupPasswordPageUrl(rawUrl)) {
return 'password_page';
}
if (isSignupEmailVerificationPageUrl(rawUrl)) {
return 'verification_page';
}
const isPhoneVerificationUrl = typeof isSignupPhoneVerificationPageUrl === 'function'
? isSignupPhoneVerificationPageUrl(rawUrl)
: fallbackSignupPhoneVerificationPageUrl(rawUrl);
if (isPhoneVerificationUrl) {
return 'phone_verification_page';
}
const isProfileUrl = typeof isSignupProfilePageUrl === 'function'
? isSignupProfilePageUrl(rawUrl)
: fallbackSignupProfilePageUrl(rawUrl);
if (isProfileUrl) {
return 'profile_page';
}
return '';
}
async function ensureSignupPostEmailPageReadyInTab(tabId, step = 2, options = {}) {
async function ensureSignupPostIdentityPageReadyInTab(tabId, step = 2, options = {}) {
const { skipUrlWait = false } = options;
let landingUrl = '';
let landingState = '';
if (!skipUrlWait) {
const matchedTab = await waitForTabUrlMatch(tabId, (url) => Boolean(resolveSignupPostEmailState(url)), {
const matchedTab = await waitForTabUrlMatch(tabId, (url) => Boolean(resolveSignupPostIdentityState(url)), {
timeoutMs: 45000,
retryDelayMs: 300,
});
if (!matchedTab) {
throw new Error('等待邮箱提交后的页面跳转超时,请检查页面是否仍停留在邮箱输入页。');
throw new Error('等待注册身份提交后的页面跳转超时,请检查页面是否仍停留在输入页。');
}
landingUrl = matchedTab.url || '';
landingState = resolveSignupPostEmailState(landingUrl);
landingState = resolveSignupPostIdentityState(landingUrl);
}
if (!landingState) {
try {
const currentTab = await chrome.tabs.get(tabId);
landingUrl = landingUrl || currentTab?.url || '';
landingState = resolveSignupPostEmailState(landingUrl);
landingState = resolveSignupPostIdentityState(landingUrl);
} catch {
landingUrl = landingUrl || '';
}
}
if (!landingState) {
throw new Error(`邮箱提交后未能识别当前页面,既不是密码页也不是邮箱验证码页。URL: ${landingUrl || 'unknown'}`);
throw new Error(`注册身份提交后未能识别当前页面,既不是密码页、验证码页,也不是资料页。URL: ${landingUrl || 'unknown'}`);
}
await ensureContentScriptReadyOnTab('signup-page', tabId, {
@@ -109,12 +144,12 @@
injectSource: 'signup-page',
timeoutMs: 45000,
retryDelayMs: 900,
logMessage: landingState === 'verification_page'
? `步骤 ${step}邮箱验证码页仍在加载,正在等待页面恢复...`
: `步骤 ${step}密码页仍在加载,正在重试连接内容脚本...`,
logMessage: landingState === 'password_page'
? `步骤 ${step}码页仍在加载,正在重试连接内容脚本...`
: `步骤 ${step}注册后续页面仍在加载,正在等待页面恢复...`,
});
if (landingState === 'verification_page') {
if (landingState !== 'password_page') {
return {
ready: true,
state: landingState,
@@ -145,6 +180,10 @@
};
}
async function ensureSignupPostEmailPageReadyInTab(tabId, step = 2, options = {}) {
return ensureSignupPostIdentityPageReadyInTab(tabId, step, options);
}
async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) {
const result = await ensureSignupPostEmailPageReadyInTab(tabId, step, options);
if (result.state !== 'password_page') {
@@ -240,6 +279,7 @@
return {
ensureSignupEntryPageReady,
ensureSignupPostIdentityPageReadyInTab,
ensureSignupPostEmailPageReadyInTab,
finalizeSignupPasswordSubmitInTab,
ensureSignupPasswordPageReadyInTab,
+1
View File
@@ -52,6 +52,7 @@
async function executePlusCheckoutCreate(state = {}) {
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
const paymentMethodLabel = getPlusPaymentMethodLabel(paymentMethod);
const checkoutModeLabel = getCheckoutModeLabel(state);
await addLog('步骤 6:正在新打开 ChatGPT 会话页,准备创建 Plus Checkout...', 'info');
const tabId = await openFreshChatGptTabForCheckoutCreate();
+45 -10
View File
@@ -25,6 +25,8 @@
resolveVerificationStep,
rerunStep7ForStep8Recovery,
reuseOrCreateTab,
phoneVerificationHelpers = null,
resolveSignupMethod = () => 'email',
setState,
shouldUseCustomRegistrationEmail,
sleepWithStop,
@@ -97,6 +99,13 @@
return String(value || '').trim().toLowerCase();
}
function isPhoneLoginState(state = {}) {
return String(state?.accountIdentifierType || '').trim().toLowerCase() === 'phone'
|| resolveSignupMethod(state) === 'phone'
|| Boolean(state?.signupPhoneCompletedActivation)
|| Boolean(state?.signupPhoneActivation);
}
async function completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, options = {}) {
await setState({
step8VerificationTargetEmail: '',
@@ -206,9 +215,45 @@
return Math.max(0, Number(STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS) || 0);
}
async function executeLoginPhoneCodeStep(state, signupTabId, visibleStep) {
if (!Number.isInteger(signupTabId)) {
throw new Error(`步骤 ${visibleStep}:认证页面标签页已关闭,无法继续手机号登录验证码流程。`);
}
if (typeof phoneVerificationHelpers?.completeLoginPhoneVerificationFlow !== 'function') {
throw new Error(`步骤 ${visibleStep}:手机号登录验证码流程不可用,接码模块尚未初始化。`);
}
const result = await phoneVerificationHelpers.completeLoginPhoneVerificationFlow(signupTabId, {
state,
visibleStep,
});
await completeStepFromBackground(visibleStep, {
phoneVerification: true,
loginPhoneVerification: true,
code: result?.code || '',
});
return result || {};
}
async function runStep8Attempt(state, runtime = {}) {
const visibleStep = getVisibleStep(state, 8);
activeFetchLoginCodeStep = visibleStep;
const authTabId = await getTabId('signup-page');
if (authTabId) {
await chrome.tabs.update(authTabId, { active: true });
} else {
if (!state.oauthUrl) {
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}`);
}
await reuseOrCreateTab('signup-page', state.oauthUrl);
}
if (isPhoneLoginState(state)) {
return executeLoginPhoneCodeStep(state, authTabId, visibleStep);
}
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
@@ -222,16 +267,6 @@
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
const verificationSessionKey = `8:${stepStartedAt}`;
const authTabId = await getTabId('signup-page');
if (authTabId) {
await chrome.tabs.update(authTabId, { active: true });
} else {
if (!state.oauthUrl) {
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}`);
}
await reuseOrCreateTab('signup-page', state.oauthUrl);
}
throwIfStopped();
const pageState = await ensureStep8VerificationPageReady({
+39 -6
View File
@@ -27,6 +27,8 @@
shouldUseCustomRegistrationEmail,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
throwIfStopped,
phoneVerificationHelpers = null,
resolveSignupMethod = () => 'email',
} = deps;
function buildSignupProfileForVerificationStep() {
@@ -58,6 +60,32 @@
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
}
function isPhoneSignupState(state = {}) {
return resolveSignupMethod(state) === 'phone'
|| state?.accountIdentifierType === 'phone'
|| Boolean(state?.signupPhoneActivation);
}
async function executeSignupPhoneCodeStep(state, signupTabId) {
if (typeof phoneVerificationHelpers?.completeSignupPhoneVerificationFlow !== 'function') {
throw new Error('步骤 4:手机号注册验证码流程不可用,接码模块尚未初始化。');
}
const signupProfile = buildSignupProfileForVerificationStep();
const result = await phoneVerificationHelpers.completeSignupPhoneVerificationFlow(signupTabId, {
state,
signupProfile,
});
await completeStepFromBackground(4, {
phoneVerification: true,
code: result?.code || '',
...(result?.skipProfileStep ? { skipProfileStep: true } : {}),
...(result?.skipProfileStepReason ? { skipProfileStepReason: result.skipProfileStepReason } : {}),
});
return result || {};
}
async function focusOrOpenMailTab(mail) {
const alive = await isTabAlive(mail.source);
if (alive) {
@@ -81,13 +109,7 @@
}
async function executeStep4(state) {
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const stepStartedAt = Date.now();
const verificationFilterAfterTimestamp = mail.provider === '2925'
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
const verificationSessionKey = `4:${stepStartedAt}`;
const signupTabId = await getTabId('signup-page');
@@ -175,11 +197,22 @@
return;
}
if (isPhoneSignupState(state)) {
return executeSignupPhoneCodeStep(state, signupTabId);
}
if (shouldUseCustomRegistrationEmail(state)) {
await confirmCustomVerificationStepBypass(4);
return;
}
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const verificationFilterAfterTimestamp = mail.provider === '2925'
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info');
await ensureIcloudMailSession({
+43 -6
View File
@@ -15,10 +15,32 @@
SIGNUP_PAGE_INJECT_FILES,
} = deps;
function resolveStep3AccountIdentity(state = {}) {
const resolvedEmail = String(state?.email || '').trim();
const signupPhoneNumber = String(
state?.signupPhoneNumber
|| (state?.accountIdentifierType === 'phone' ? state?.accountIdentifier : '')
|| ''
).trim();
const accountIdentifierType = signupPhoneNumber && state?.accountIdentifierType === 'phone'
? 'phone'
: (resolvedEmail ? 'email' : (signupPhoneNumber ? 'phone' : 'email'));
const accountIdentifier = accountIdentifierType === 'phone'
? signupPhoneNumber
: resolvedEmail;
return {
accountIdentifierType,
accountIdentifier,
email: resolvedEmail,
phoneNumber: signupPhoneNumber,
};
}
async function executeStep3(state) {
const resolvedEmail = state.email;
if (!resolvedEmail) {
throw new Error('缺少邮箱地址,请先完成步骤 2。');
const identity = resolveStep3AccountIdentity(state);
if (!identity.accountIdentifier) {
throw new Error('缺少注册账号,请先完成步骤 2。');
}
const signupTabId = await getTabId('signup-page');
@@ -30,7 +52,13 @@
await setPasswordState(password);
const accounts = state.accounts || [];
accounts.push({ email: resolvedEmail, createdAt: new Date().toISOString() });
accounts.push({
email: identity.email,
phoneNumber: identity.phoneNumber,
accountIdentifierType: identity.accountIdentifierType,
accountIdentifier: identity.accountIdentifier,
createdAt: new Date().toISOString(),
});
await setState({ accounts });
await chrome.tabs.update(signupTabId, { active: true });
@@ -42,14 +70,23 @@
logMessage: '步骤 3:密码页内容脚本未就绪,正在等待页面恢复...',
});
const identityLabel = identity.accountIdentifierType === 'phone'
? `注册手机号为 ${identity.accountIdentifier}`
: `邮箱为 ${identity.accountIdentifier}`;
await addLog(
`步骤 3:正在填写密码,邮箱为 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}${password.length} 位)`
`步骤 3:正在填写密码,${identityLabel},密码为${state.customPassword ? '自定义' : '自动生成'}${password.length} 位)`
);
await sendToContentScript('signup-page', {
type: 'EXECUTE_STEP',
step: 3,
source: 'background',
payload: { email: resolvedEmail, password },
payload: {
email: identity.email,
phoneNumber: identity.phoneNumber,
accountIdentifierType: identity.accountIdentifierType,
accountIdentifier: identity.accountIdentifier,
password,
},
});
}
+56 -5
View File
@@ -41,12 +41,30 @@
}
async function executeStep7(state) {
if (!state.email) {
throw new Error('缺少邮箱地址,请先完成步骤 3。');
}
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
const completionStep = visibleStep > 0 ? visibleStep : 7;
const resolvedIdentifierType = String(
state?.accountIdentifierType
|| (state?.signupPhoneNumber ? 'phone' : '')
|| ''
).trim().toLowerCase() === 'phone'
? 'phone'
: 'email';
const phoneNumber = String(
state?.signupPhoneNumber
|| (resolvedIdentifierType === 'phone' ? state?.accountIdentifier : '')
|| state?.signupPhoneCompletedActivation?.phoneNumber
|| state?.signupPhoneActivation?.phoneNumber
|| ''
).trim();
const email = String(
state?.email
|| (resolvedIdentifierType === 'email' ? state?.accountIdentifier : '')
|| ''
).trim();
if (!email && !phoneNumber) {
throw new Error('缺少登录账号,请先完成步骤 2 和步骤 3。');
}
let attempt = 0;
let lastError = null;
@@ -57,6 +75,28 @@
try {
const currentState = attempt === 1 ? state : await getState();
const password = currentState.password || currentState.customPassword || '';
const currentIdentifierType = String(
currentState?.accountIdentifierType
|| (currentState?.signupPhoneNumber ? 'phone' : '')
|| resolvedIdentifierType
).trim().toLowerCase() === 'phone'
? 'phone'
: 'email';
const currentPhoneNumber = String(
currentState?.signupPhoneNumber
|| (currentIdentifierType === 'phone' ? currentState?.accountIdentifier : '')
|| currentState?.signupPhoneCompletedActivation?.phoneNumber
|| currentState?.signupPhoneActivation?.phoneNumber
|| phoneNumber
).trim();
const currentEmail = String(
currentState?.email
|| (currentIdentifierType === 'email' ? currentState?.accountIdentifier : '')
|| email
).trim();
const accountIdentifier = currentIdentifierType === 'phone'
? currentPhoneNumber
: currentEmail;
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
if (typeof startOAuthFlowTimeoutWindow === 'function') {
await startOAuthFlowTimeoutWindow({ step: completionStep, oauthUrl });
@@ -90,7 +130,18 @@
step: 7,
source: 'background',
payload: {
email: currentState.email,
email: currentEmail,
phoneNumber: currentPhoneNumber,
countryId: currentState?.signupPhoneCompletedActivation?.countryId
?? currentState?.signupPhoneActivation?.countryId
?? null,
countryLabel: String(
currentState?.signupPhoneCompletedActivation?.countryLabel
|| currentState?.signupPhoneActivation?.countryLabel
|| ''
).trim(),
accountIdentifier,
loginIdentifierType: currentIdentifierType,
password,
visibleStep: completionStep,
},
+108 -4
View File
@@ -10,8 +10,11 @@
ensureSignupAuthEntryPageReady,
ensureSignupEntryPageReady,
ensureSignupPostEmailPageReadyInTab,
ensureSignupPostIdentityPageReadyInTab = ensureSignupPostEmailPageReadyInTab,
getTabId,
isTabAlive,
phoneVerificationHelpers = null,
resolveSignupMethod = () => 'email',
resolveSignupEmailForFlow,
sendToContentScriptResilient,
SIGNUP_PAGE_INJECT_FILES,
@@ -94,7 +97,7 @@
throw new Error(message);
}
async function submitSignupEmail(resolvedEmail, options = {}) {
async function sendSignupIdentity(payload = {}, options = {}) {
const {
timeoutMs = 35000,
retryDelayMs = 700,
@@ -106,7 +109,7 @@
type: 'EXECUTE_STEP',
step: 2,
source: 'background',
payload: { email: resolvedEmail },
payload,
}, {
timeoutMs,
retryDelayMs,
@@ -117,9 +120,23 @@
}
}
async function executeStep2(state) {
const resolvedEmail = await resolveSignupEmailForFlow(state);
async function submitSignupEmail(resolvedEmail, options = {}) {
return sendSignupIdentity({ email: resolvedEmail }, options);
}
async function submitSignupPhone(phoneNumber, activation, options = {}) {
return sendSignupIdentity({
signupMethod: 'phone',
phoneNumber,
countryId: activation?.countryId ?? null,
countryLabel: String(activation?.countryLabel || '').trim(),
}, {
logMessage: '步骤 2:官网注册入口正在切换,等待手机号注册入口恢复...',
...options,
});
}
async function ensureSignupTabForStep2() {
let signupTabId = await getTabId('signup-page');
if (!signupTabId || !(await isTabAlive('signup-page'))) {
await addLog('步骤 2:未发现可用的注册页标签,正在重新打开 ChatGPT 官网...', 'warn');
@@ -134,6 +151,84 @@
logMessage: '步骤 2:注册入口页内容脚本未就绪,正在等待页面恢复...',
});
}
return signupTabId;
}
async function executeSignupPhoneEntry(state) {
if (typeof phoneVerificationHelpers?.prepareSignupPhoneActivation !== 'function') {
throw new Error('手机号注册流程不可用:接码模块尚未初始化。');
}
let signupTabId = await ensureSignupTabForStep2();
if (await shouldForceAuthEntryRetry(signupTabId)) {
await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交手机号。', 'warn');
try {
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
} catch (entryError) {
const entryErrorMessage = getErrorMessage(entryError);
if (await failStep2OnLoggedInSession(signupTabId, entryErrorMessage)) {
return;
}
await addLog('步骤 2:切换认证入口失败,正在重新打开官网入口并重试提交手机号...', 'warn');
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
}
}
const activation = await phoneVerificationHelpers.prepareSignupPhoneActivation(state);
let step2Result = await submitSignupPhone(activation.phoneNumber, activation, {
timeoutMs: 45000,
retryDelayMs: 700,
logMessage: '步骤 2:官网注册入口正在切换,等待手机号注册入口恢复...',
});
if (step2Result?.error) {
const errorMessage = getErrorMessage(step2Result.error);
if (isSignupEntryUnavailableErrorMessage(errorMessage) || isRetryableStep2TransportErrorMessage(errorMessage)) {
await addLog('步骤 2:手机号注册入口不可用或通信超时,正在切换认证入口页后重试一次...', 'warn');
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
step2Result = await submitSignupPhone(activation.phoneNumber, activation, {
timeoutMs: 45000,
retryDelayMs: 700,
logMessage: '步骤 2:认证入口页已打开,正在重新提交手机号...',
});
}
}
if (step2Result?.error) {
const finalErrorMessage = getErrorMessage(step2Result.error);
if (
(isSignupEntryUnavailableErrorMessage(finalErrorMessage)
|| isRetryableStep2TransportErrorMessage(finalErrorMessage))
&& await failStep2OnLoggedInSession(signupTabId, finalErrorMessage)
) {
return;
}
if (typeof phoneVerificationHelpers?.cancelSignupPhoneActivation === 'function') {
await phoneVerificationHelpers.cancelSignupPhoneActivation(state, activation).catch(() => {});
}
throw new Error(finalErrorMessage);
}
await addLog(`步骤 2:手机号 ${activation.phoneNumber} 已提交,正在等待页面加载并确认下一步入口...`);
const landingResult = await ensureSignupPostIdentityPageReadyInTab(signupTabId, 2, {
skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
});
await completeStepFromBackground(2, {
accountIdentifierType: 'phone',
accountIdentifier: activation.phoneNumber,
signupPhoneNumber: activation.phoneNumber,
signupPhoneActivation: activation,
nextSignupState: landingResult?.state || step2Result?.state || 'password_page',
nextSignupUrl: landingResult?.url || step2Result?.url || '',
skippedPasswordStep: landingResult?.state === 'phone_verification_page' || landingResult?.state === 'profile_page',
});
}
async function executeSignupEmailEntry(state) {
const resolvedEmail = await resolveSignupEmailForFlow(state);
let signupTabId = await ensureSignupTabForStep2();
if (await shouldForceAuthEntryRetry(signupTabId)) {
await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交邮箱。', 'warn');
@@ -214,12 +309,21 @@
await completeStepFromBackground(2, {
email: resolvedEmail,
accountIdentifierType: 'email',
accountIdentifier: resolvedEmail,
nextSignupState: landingResult?.state || 'password_page',
nextSignupUrl: landingResult?.url || step2Result?.url || '',
skippedPasswordStep: landingResult?.state === 'verification_page',
});
}
async function executeStep2(state) {
if (resolveSignupMethod(state) === 'phone') {
return executeSignupPhoneEntry(state);
}
return executeSignupEmailEntry(state);
}
return { executeStep2 };
}
-1
View File
@@ -631,7 +631,6 @@ function buildPlusCheckoutUrl(checkoutSessionId, paymentMethod = PLUS_PAYMENT_ME
async function createPlusCheckoutSession(options = {}) {
await waitForDocumentComplete();
const checkoutConfig = buildPlusCheckoutConfig(payload);
log('Plus:正在读取 ChatGPT 登录会话...');
const sessionResponse = await fetch('/api/auth/session', {
+731 -19
View File
File diff suppressed because it is too large Load Diff
+41 -16
View File
@@ -68,9 +68,33 @@
}
function buildRecordId(record = {}) {
return String(record.recordId || record.email || '')
.trim()
.toLowerCase();
const rawRecordId = String(record.recordId || '').trim();
if (rawRecordId) {
return rawRecordId.toLowerCase();
}
const identifierType = String(record.accountIdentifierType || '').trim().toLowerCase() === 'phone'
|| (!record.email && (record.accountIdentifier || record.phoneNumber))
? 'phone'
: 'email';
const identifier = String(
record.accountIdentifier
|| (identifierType === 'phone' ? (record.phoneNumber || record.phone || record.number || '') : (record.email || ''))
|| ''
).trim();
if (!identifier) {
return '';
}
return identifierType === 'phone'
? `phone:${identifier.toLowerCase()}`
: identifier.toLowerCase();
}
function getRecordPrimaryIdentifier(record = {}) {
const email = String(record.email || '').trim();
if (email) {
return email;
}
return String(record.accountIdentifier || record.phoneNumber || record.phone || record.number || '').trim();
}
function getAccountRunRecords(currentState = state.getLatestState()) {
@@ -262,7 +286,7 @@
}
if (!allRecords.length) {
dom.accountRecordsMeta.textContent = '暂无邮箱记录';
dom.accountRecordsMeta.textContent = '暂无账号记录';
return;
}
@@ -337,7 +361,7 @@
const message = allRecords.length
? `当前筛选“${getFilterConfig(activeFilter).metaLabel}”下暂无记录`
: '暂无邮箱记录';
: '暂无账号记录';
dom.accountRecordsList.innerHTML = `<div class="account-records-empty">${escapeHtml(message)}</div>`;
}
@@ -357,6 +381,7 @@
dom.accountRecordsList.innerHTML = visibleRecords.map((record) => {
const recordId = buildRecordId(record);
const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(空账号)';
const statusMeta = getStatusMeta(record);
const summaryText = getRecordSummaryText(record);
const retryCount = normalizeRetryCount(record.retryCount);
@@ -383,12 +408,12 @@
<div
class="${itemClassNames}"
data-account-record-id="${escapeHtml(recordId)}"
title="${escapeHtml(String(record.email || ''))}"
title="${escapeHtml(primaryIdentifier)}"
>
<div class="account-record-item-top">
<div class="account-record-item-email-row">
${selectionMarkup}
<div class="account-record-item-email mono">${escapeHtml(String(record.email || '').trim() || '(空邮箱)')}</div>
<div class="account-record-item-email mono">${escapeHtml(primaryIdentifier)}</div>
</div>
<div class="account-record-item-side">
<span class="account-record-item-status">${escapeHtml(statusMeta.label)}</span>
@@ -470,13 +495,13 @@
async function clearRecords() {
const records = getAccountRunRecords();
if (!records.length) {
helpers.showToast?.('没有可清理的邮箱记录。', 'warn', 1800);
helpers.showToast?.('没有可清理的账号记录。', 'warn', 1800);
return;
}
const confirmed = await helpers.openConfirmModal({
title: '清理邮箱记录',
message: '确认清理当前全部邮箱记录吗?该操作会同时清空面板记录与本地同步快照。',
title: '清理账号记录',
message: '确认清理当前全部账号记录吗?该操作会同时清空面板记录与本地同步快照。',
confirmLabel: '确认清理',
confirmVariant: 'btn-danger',
});
@@ -497,19 +522,19 @@
selectionMode = false;
resetSelection();
state.syncLatestState({ accountRunHistory: [] });
helpers.showToast?.(`已清理 ${Math.max(0, Number(response?.clearedCount) || 0)}邮箱记录。`, 'success', 2200);
helpers.showToast?.(`已清理 ${Math.max(0, Number(response?.clearedCount) || 0)}账号记录。`, 'success', 2200);
}
async function deleteSelectedRecords() {
const recordIds = Array.from(selectedRecordIds).filter(Boolean);
if (!recordIds.length) {
helpers.showToast?.('请先勾选要删除的邮箱记录。', 'warn', 1800);
helpers.showToast?.('请先勾选要删除的账号记录。', 'warn', 1800);
return;
}
const confirmed = await helpers.openConfirmModal({
title: '删除选中记录',
message: `确认删除选中的 ${recordIds.length}邮箱记录吗?该操作会同步更新本地 helper 快照。`,
message: `确认删除选中的 ${recordIds.length}账号记录吗?该操作会同步更新本地 helper 快照。`,
confirmLabel: '确认删除',
confirmVariant: 'btn-danger',
});
@@ -534,7 +559,7 @@
resetSelection();
state.syncLatestState({ accountRunHistory: nextRecords });
helpers.showToast?.(`已删除 ${Math.max(0, Number(response?.deletedCount) || 0)}邮箱记录。`, 'success', 2200);
helpers.showToast?.(`已删除 ${Math.max(0, Number(response?.deletedCount) || 0)}账号记录。`, 'success', 2200);
}
function handleStatsClick(event) {
@@ -619,14 +644,14 @@
try {
await deleteSelectedRecords();
} catch (error) {
helpers.showToast?.(`删除邮箱记录失败:${error.message}`, 'error');
helpers.showToast?.(`删除账号记录失败:${error.message}`, 'error');
}
});
dom.btnClearAccountRecords?.addEventListener('click', async () => {
try {
await clearRecords();
} catch (error) {
helpers.showToast?.(`清理邮箱记录失败:${error.message}`, 'error');
helpers.showToast?.(`清理账号记录失败:${error.message}`, 'error');
}
});
}
+14 -340
View File
@@ -512,343 +512,6 @@
</div>
</div>
</div>
<div id="phone-verification-section" class="data-card phone-verification-card">
<div class="section-mini-header">
<div class="section-mini-copy">
<span class="section-label">接码设置</span>
<span class="data-value">手机号验证与接码平台获取策略</span>
</div>
<div id="row-phone-verification-enabled" class="section-mini-actions phone-verification-header-actions">
<button id="btn-toggle-phone-verification-section" class="btn btn-ghost btn-xs" type="button"
aria-expanded="false" aria-controls="row-phone-verification-fold">展开设置</button>
<label class="toggle-switch" for="input-phone-verification-enabled" title="启用或禁用手机号接码流程">
<input type="checkbox" id="input-phone-verification-enabled" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
</div>
</div>
<div class="data-row" id="row-hero-sms-platform">
<span class="data-label">接码平台</span>
<div class="data-inline data-value-actions">
<select id="select-phone-sms-provider" class="data-input mono" title="选择步骤 9 使用的接码平台">
<option value="hero-sms">HeroSMS</option>
<option value="5sim">5sim</option>
</select>
<span id="display-hero-sms-platform" class="data-value mono data-value-fill">HeroSMS / OpenAI / Thailand</span>
</div>
</div>
<div class="data-row phone-verification-fold-row" id="row-phone-verification-fold" style="display:none;">
<div id="phone-verification-fold" class="phone-verification-fold">
<div class="phone-verification-fold-body">
<div class="data-row" id="row-account-run-history-helper-base-url" style="display:none;">
<span class="data-label">同步服务</span>
<input type="text" id="input-account-run-history-helper-base-url" class="data-input mono"
placeholder="http://127.0.0.1:17373" />
</div>
<div class="data-row" id="row-hero-sms-country" style="display:none;">
<span class="data-label">国家优先级</span>
<div class="data-inline hero-sms-country-stack">
<select id="select-hero-sms-country" class="data-input mono" multiple size="6" style="display:none;">
<option value="52" selected>Thailand</option>
</select>
<div class="hero-sms-country-mainline">
<div id="hero-sms-country-menu-shell" class="hero-sms-country-menu">
<button id="btn-hero-sms-country-menu" class="btn btn-outline btn-sm hero-sms-country-menu-btn" type="button" aria-haspopup="listbox" aria-expanded="false">
Thailand (1/3)
</button>
<div id="hero-sms-country-menu" class="hero-sms-country-menu-dropdown" role="listbox" aria-multiselectable="true" hidden></div>
</div>
</div>
<span class="data-value hero-sms-country-note">多选最多 3 个,按点击顺序生效。</span>
</div>
</div>
<div class="data-row" id="row-hero-sms-country-fallback" style="display:none;">
<span class="data-label">生效顺序</span>
<div class="data-inline data-value-actions">
<span id="display-hero-sms-country-fallback-order" class="data-value data-value-fill mono">Thailand(52)</span>
<button id="btn-hero-sms-country-clear" class="btn btn-ghost btn-xs data-inline-btn" type="button">清空</button>
</div>
</div>
<div class="data-row" id="row-hero-sms-acquire-priority" style="display:none;">
<span class="data-label">拿号优先级</span>
<select id="select-hero-sms-acquire-priority" class="data-input mono">
<option value="country">国家优先(默认)</option>
<option value="price">价格优先(同价按国家顺序)</option>
</select>
</div>
<div class="data-row" id="row-hero-sms-api-key" style="display:none;">
<span class="data-label">接码 API</span>
<div class="input-with-icon">
<input type="password" id="input-hero-sms-api-key" class="data-input data-input-with-icon mono"
placeholder="请输入 HeroSMS API Key" />
<button id="btn-toggle-hero-sms-api-key" class="input-icon-btn" type="button"
data-password-toggle="input-hero-sms-api-key" data-show-label="显示 HeroSMS API Key"
data-hide-label="隐藏 HeroSMS API Key" aria-label="显示 HeroSMS API Key" title="显示 HeroSMS API Key"></button>
</div>
</div>
<div class="data-row" id="row-hero-sms-max-price" style="display:none;">
<span class="data-label">价格</span>
<div class="data-inline hero-sms-price-preview-stack">
<div class="hero-sms-price-preview-head">
<button id="btn-hero-sms-price-preview" class="btn btn-outline btn-xs data-inline-btn" type="button">查询价格</button>
<button id="btn-phone-sms-balance" class="btn btn-ghost btn-xs data-inline-btn" type="button">查余额</button>
</div>
<div id="row-hero-sms-price-tiers" class="hero-sms-price-preview-result" style="display:none;">
<span id="display-hero-sms-price-tiers" class="data-value mono hero-sms-price-preview-text">未获取</span>
<span id="display-phone-sms-balance" class="data-value mono hero-sms-price-preview-text">余额未获取</span>
</div>
<div class="hero-sms-price-controls-grid">
<div class="hero-sms-price-control">
<span class="hero-sms-settings-caption">价格上限</span>
<div class="setting-controls">
<input type="number" id="input-hero-sms-max-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="0.12" min="0" step="0.0001" title="接码价格上限;可空(空=自动价格)" />
</div>
</div>
<div class="hero-sms-price-control hero-sms-price-control-reuse">
<span class="hero-sms-settings-caption">号码复用</span>
<div class="setting-controls hero-sms-toggle-controls">
<label class="toggle-switch hero-sms-price-reuse-toggle" for="input-hero-sms-reuse-enabled" title="开启后会优先复用未超次数的可用号码">
<input type="checkbox" id="input-hero-sms-reuse-enabled" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="data-row" id="row-five-sim-operator" style="display:none;">
<span class="data-label">5sim Operator</span>
<input type="text" id="input-five-sim-operator" class="data-input mono" placeholder="any" title="5sim operator,默认 any" />
</div>
<div class="data-row" id="row-phone-code-settings-group" style="display:none;">
<span class="data-label">接码参数</span>
<div class="data-inline hero-sms-settings-grid">
<div id="row-phone-verification-resend-count" class="hero-sms-settings-cell" style="display:none;">
<span class="hero-sms-settings-caption">验证码重发</span>
<div class="setting-controls">
<input type="number" id="input-verification-resend-count" class="data-input auto-delay-input" value="4"
min="0" max="20" step="1" title="自动点击重新发送验证码的次数" />
<span class="data-unit"></span>
</div>
</div>
<div id="row-phone-replacement-limit" class="hero-sms-settings-cell" style="display:none;">
<span class="hero-sms-settings-caption">换号上限</span>
<div class="setting-controls">
<input type="number" id="input-phone-replacement-limit" class="data-input auto-delay-input" value="3" min="1" max="20" step="1" title="步骤 9 内部允许更换号码的最大次数" />
<span class="data-unit"></span>
</div>
</div>
<div id="row-phone-code-wait-seconds" class="hero-sms-settings-cell" style="display:none;">
<span class="hero-sms-settings-caption">验证码限时</span>
<div class="setting-controls">
<input type="number" id="input-phone-code-wait-seconds" class="data-input auto-delay-input" value="60" min="15" max="300" step="1" title="每轮等待验证码的秒数" />
<span class="data-unit"></span>
</div>
</div>
<div id="row-phone-code-timeout-windows" class="hero-sms-settings-cell" style="display:none;">
<span class="hero-sms-settings-caption">超时次数</span>
<div class="setting-controls">
<input type="number" id="input-phone-code-timeout-windows" class="data-input auto-delay-input" value="2" min="1" max="10" step="1" title="验证码超时后,最多继续等待几轮再换号" />
<span class="data-unit"></span>
</div>
</div>
<div id="row-phone-code-poll-interval-seconds" class="hero-sms-settings-cell" style="display:none;">
<span class="hero-sms-settings-caption">轮询间隔</span>
<div class="setting-controls">
<input type="number" id="input-phone-code-poll-interval-seconds" class="data-input auto-delay-input" value="5" min="1" max="30" step="1" title="向 HeroSMS 查询验证码状态的间隔秒数" />
<span class="data-unit"></span>
</div>
</div>
<div id="row-phone-code-poll-max-rounds" class="hero-sms-settings-cell" style="display:none;">
<span class="hero-sms-settings-caption">轮询次数</span>
<div class="setting-controls">
<input type="number" id="input-phone-code-poll-max-rounds" class="data-input auto-delay-input" value="4" min="1" max="120" step="1" title="每轮验证码等待窗口最多轮询次数" />
<span class="data-unit"></span>
</div>
</div>
</div>
</div>
<div class="data-row" id="row-hero-sms-runtime-pair" style="display:none;">
<span class="data-label">运行状态</span>
<div class="data-inline hero-sms-runtime-grid">
<div id="row-hero-sms-current-number" class="hero-sms-runtime-cell" style="display:none;">
<span class="hero-sms-runtime-key">当前分配</span>
<span id="display-hero-sms-current-number" class="data-value mono hero-sms-runtime-value">未分配</span>
</div>
<div id="row-hero-sms-current-code" class="hero-sms-runtime-cell" style="display:none;">
<span class="hero-sms-runtime-key">验证码</span>
<span id="display-hero-sms-current-code" class="data-value mono hero-sms-runtime-value">未获取</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="ip-proxy-section" class="data-card ip-proxy-card">
<div class="section-mini-header">
<div class="section-mini-copy">
<span class="section-label">IP 代理</span>
<span class="data-value">用于浏览器代理接管与出口切换</span>
</div>
<div id="row-ip-proxy-enabled" class="section-mini-actions ip-proxy-header-actions">
<button id="btn-toggle-ip-proxy-section" class="btn btn-ghost btn-xs" type="button"
aria-expanded="false" aria-controls="row-ip-proxy-fold">展开设置</button>
<label class="toggle-switch" for="input-ip-proxy-enabled" title="启用或禁用 IP 代理接管">
<input type="checkbox" id="input-ip-proxy-enabled" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
</div>
</div>
<div class="data-row ip-proxy-fold-row" id="row-ip-proxy-fold" style="display:none;">
<div id="ip-proxy-fold" class="ip-proxy-fold">
<div class="ip-proxy-fold-body">
<div class="data-row" id="row-ip-proxy-service" style="display:none;">
<span class="data-label">代理服务</span>
<div class="data-inline">
<select id="select-ip-proxy-service" class="data-select" disabled>
<option value="711proxy">711Proxy(首版)</option>
</select>
<button id="btn-ip-proxy-service-login" class="btn btn-outline btn-sm data-inline-btn" type="button">
注册
</button>
</div>
</div>
<div class="data-row" id="row-ip-proxy-mode" style="display:none;">
<span class="data-label">代理模式</span>
<div id="ip-proxy-mode-group" class="choice-group" role="group" aria-label="IP代理模式">
<button type="button" class="choice-btn" data-ip-proxy-mode="account">账号密码</button>
<button type="button" class="choice-btn" data-ip-proxy-mode="api" disabled title="首版暂未开放">API 拉取(暂未开放)</button>
</div>
</div>
<div class="data-row ip-proxy-layout-row" id="row-ip-proxy-layout" style="display:none;">
<div class="ip-proxy-layout" id="ip-proxy-layout">
<div class="ip-proxy-column ip-proxy-column-account" id="ip-proxy-account-panel">
<div class="ip-proxy-column-header">账号密码模式</div>
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-account-list" style="display:none;">
<span class="data-label">账号代理列表</span>
<textarea id="input-ip-proxy-account-list" class="data-textarea mono"
placeholder="每行一条:host:port 或 host:port:username:password&#10;例如 global.rotgb.711proxy.com:10000:username:password"></textarea>
</div>
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-account-session-prefix" style="display:none;">
<span class="data-label">会话(session)</span>
<input type="text" id="input-ip-proxy-account-session-prefix" class="data-input mono"
placeholder="会话前缀,例如 ZC28qZ0KQL" />
</div>
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-account-life-minutes" style="display:none;">
<span class="data-label">时长(life)</span>
<div class="data-inline">
<input type="number" id="input-ip-proxy-account-life-minutes" class="data-input" min="1" max="1440" step="1"
placeholder="5-180(分钟)" title="711Proxy 会话时长范围:5-180 分钟" />
<span class="data-unit">分钟</span>
</div>
</div>
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-host" style="display:none;">
<span class="data-label">代理 Host</span>
<input type="text" id="input-ip-proxy-host" class="data-input" placeholder="例如 global.rotgb.711proxy.com" />
</div>
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-port" style="display:none;">
<span class="data-label">代理 Port</span>
<input type="number" id="input-ip-proxy-port" class="data-input" min="1" max="65535" step="1"
placeholder="例如 10000" />
</div>
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-protocol" style="display:none;">
<span class="data-label">代理协议</span>
<select id="select-ip-proxy-protocol" class="data-select">
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
<option value="socks5">SOCKS5</option>
<option value="socks4">SOCKS4</option>
</select>
</div>
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-username" style="display:none;">
<span class="data-label">代理账号</span>
<div class="input-with-icon">
<input type="password" id="input-ip-proxy-username" class="data-input data-input-with-icon"
placeholder="例如 USER047152-zone-custom-region-US-asn-ASN81" />
<button id="btn-toggle-ip-proxy-username" class="input-icon-btn" type="button" aria-label="显示代理账号"
title="显示代理账号"></button>
</div>
</div>
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-password" style="display:none;">
<span class="data-label">代理密码</span>
<div class="input-with-icon">
<input type="password" id="input-ip-proxy-password" class="data-input data-input-with-icon"
placeholder="账号密码代理的密码" />
<button id="btn-toggle-ip-proxy-password" class="input-icon-btn" type="button" aria-label="显示代理密码"
title="显示代理密码"></button>
</div>
</div>
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-region" style="display:none;">
<span class="data-label">地区参数</span>
<input type="text" id="input-ip-proxy-region" class="data-input" placeholder="可选,例如 US / DE / HK" />
</div>
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-pool-target-count" style="display:none;">
<span class="data-label">任务切换阈值</span>
<div class="data-inline">
<input type="number" id="input-ip-proxy-pool-target-count" class="data-input" min="1" max="500" step="1"
placeholder="20(成功轮次)" title="每成功多少轮任务后自动切换一次代理;仅 1 条节点时会跳过自动切换" />
<span class="data-unit"></span>
</div>
</div>
</div>
<div class="ip-proxy-column ip-proxy-column-api" id="ip-proxy-api-panel">
<div class="ip-proxy-column-header">API 模式</div>
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-api-url" style="display:none;">
<span class="data-label">代理API</span>
<div class="input-with-icon">
<input type="password" id="input-ip-proxy-api-url" class="data-input data-input-with-icon"
placeholder="粘贴完整 API 链接" />
<button id="btn-toggle-ip-proxy-api-url" class="input-icon-btn" type="button" aria-label="显示代理 API"
title="显示代理 API"></button>
</div>
</div>
</div>
</div>
</div>
<div class="data-row" id="row-ip-proxy-actions" style="display:none;">
<span class="data-label">切换代理</span>
<div class="data-inline ip-proxy-actions-inline">
<span id="ip-proxy-action-buttons" class="ip-proxy-action-grid">
<button id="btn-ip-proxy-refresh" class="btn btn-outline btn-sm data-inline-btn" type="button">拉取</button>
<button id="btn-ip-proxy-next" class="btn btn-outline btn-sm data-inline-btn" type="button">下一条</button>
<button id="btn-ip-proxy-change" class="btn btn-outline btn-sm data-inline-btn" type="button" title="保持当前会话并刷新出口(仅 711 + session">Change</button>
<button id="btn-ip-proxy-probe" class="btn btn-outline btn-sm data-inline-btn" type="button">检测出口</button>
</span>
<span id="ip-proxy-action-hint" class="ip-proxy-action-hint">
下一条:切到代理池下一条。Change:保持当前 session 重绑链路(仅 711 + session)。
</span>
</div>
</div>
<div class="data-row" id="row-ip-proxy-runtime-status" style="display:none;">
<span class="data-label">代理状态</span>
<div id="ip-proxy-runtime-status" class="ip-proxy-runtime-status state-idle">
<span id="ip-proxy-runtime-dot" class="ip-proxy-runtime-dot" aria-hidden="true"></span>
<div class="ip-proxy-runtime-content">
<div id="ip-proxy-runtime-text" class="ip-proxy-runtime-main">未启用,沿用浏览器默认/全局代理。</div>
<div class="ip-proxy-runtime-meta">
<span id="ip-proxy-current" class="ip-proxy-runtime-current">暂无可用代理</span>
</div>
<div class="ip-proxy-runtime-details-row">
<details id="ip-proxy-runtime-details" class="ip-proxy-runtime-details" hidden>
<summary>详情</summary>
<div id="ip-proxy-runtime-details-text" class="ip-proxy-runtime-details-text"></div>
</details>
<button id="btn-ip-proxy-check-ip" class="btn btn-outline btn-xs ip-proxy-check-ip-btn" type="button">检查IP</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="cloudflare-temp-email-section" class="data-card hotmail-card" style="display:none;">
<div class="section-mini-header">
<div class="section-mini-copy">
@@ -1430,6 +1093,13 @@
<div class="data-row phone-verification-fold-row" id="row-phone-verification-fold" style="display:none;">
<div id="phone-verification-fold" class="phone-verification-fold">
<div class="phone-verification-fold-body">
<div class="data-row" id="row-signup-method" style="display:none;">
<span class="data-label">注册方式</span>
<div id="signup-method-group" class="choice-group" role="group" aria-label="注册方式">
<button type="button" class="choice-btn" data-signup-method="email">邮箱注册</button>
<button type="button" class="choice-btn" data-signup-method="phone">手机号注册</button>
</div>
</div>
<div class="data-row" id="row-account-run-history-helper-base-url" style="display:none;">
<span class="data-label">同步服务</span>
<input type="text" id="input-account-run-history-helper-base-url" class="data-input mono"
@@ -1438,9 +1108,9 @@
<div class="data-row" id="row-phone-sms-provider" style="display:none;">
<span class="data-label">接码服务商</span>
<select id="select-phone-sms-provider" class="data-select mono">
<option value="hero-sms">HeroSMS(原有)</option>
<option value="5sim">5sim(新增)</option>
<option value="nexsms">NexSMS(新增)</option>
<option value="hero-sms">HeroSMS</option>
<option value="5sim">5sim</option>
<option value="nexsms">NexSMS</option>
</select>
</div>
<div class="data-row" id="row-phone-sms-provider-order" style="display:none;">
@@ -1603,9 +1273,11 @@
<div class="data-inline hero-sms-price-preview-stack">
<div class="hero-sms-price-preview-head">
<button id="btn-hero-sms-price-preview" class="btn btn-outline btn-xs data-inline-btn" type="button">查询价格</button>
<button id="btn-phone-sms-balance" class="btn btn-ghost btn-xs data-inline-btn" type="button">查余额</button>
</div>
<div id="row-hero-sms-price-tiers" class="hero-sms-price-preview-result" style="display:none;">
<span id="display-hero-sms-price-tiers" class="data-value mono hero-sms-price-preview-text">未获取</span>
<span id="display-phone-sms-balance" class="data-value mono hero-sms-price-preview-text">余额未获取</span>
</div>
<div class="hero-sms-price-controls-grid">
<div class="hero-sms-price-control">
@@ -1849,6 +1521,8 @@
<script src="luckmail-manager.js"></script>
<script src="custom-email-pool-manager.js"></script>
<script src="contribution-mode.js"></script>
<script src="ip-proxy-provider-711proxy.js"></script>
<script src="ip-proxy-panel.js"></script>
<script src="account-records-manager.js"></script>
<script src="sidepanel.js"></script>
</body>
+760 -33
View File
File diff suppressed because it is too large Load Diff
@@ -105,6 +105,15 @@ let currentState = {
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
signupMethod: 'phone',
resolvedSignupMethod: 'phone',
accountIdentifierType: 'phone',
accountIdentifier: '+6612345',
signupPhoneNumber: '+6612345',
signupPhoneActivation: { activationId: 'signup-activation', phoneNumber: '+6612345' },
signupPhoneCompletedActivation: { activationId: 'signup-completed', phoneNumber: '+6612345' },
signupPhoneVerificationRequestedAt: 123456,
signupPhoneVerificationPurpose: 'signup',
mailProvider: '163',
emailGenerator: 'duck',
gmailBaseEmail: 'demo@gmail.com',
@@ -162,6 +171,15 @@ async function resetState() {
autoRunDelayEnabled: prev.autoRunDelayEnabled,
autoRunDelayMinutes: prev.autoRunDelayMinutes,
autoStepDelaySeconds: prev.autoStepDelaySeconds,
signupMethod: prev.signupMethod,
resolvedSignupMethod: null,
accountIdentifierType: null,
accountIdentifier: '',
signupPhoneNumber: '',
signupPhoneActivation: null,
signupPhoneCompletedActivation: null,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
mailProvider: prev.mailProvider,
emailGenerator: prev.emailGenerator,
gmailBaseEmail: prev.gmailBaseEmail,
@@ -335,6 +353,15 @@ return {
assert.strictEqual(snapshot.currentState.autoRunSessionId, 0, 'session id should be cleared after completion');
assert.strictEqual(snapshot.currentState.gmailBaseEmail, 'demo@gmail.com', 'gmail base email should survive fresh-attempt reset');
assert.strictEqual(snapshot.currentState.mail2925BaseEmail, 'demo@2925.com', '2925 base email should survive fresh-attempt reset');
assert.strictEqual(snapshot.currentState.signupMethod, 'phone', 'signup method setting should survive fresh-attempt reset');
assert.strictEqual(snapshot.currentState.resolvedSignupMethod, null, 'resolved signup method should be cleared before the next run freezes it again');
assert.strictEqual(snapshot.currentState.accountIdentifierType, null, 'account identifier type should be runtime-only');
assert.strictEqual(snapshot.currentState.accountIdentifier, '', 'account identifier should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneNumber, '', 'signup phone number should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneActivation, null, 'signup phone activation should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneCompletedActivation, null, 'completed signup phone activation should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneVerificationRequestedAt, null, 'signup phone request time should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneVerificationPurpose, '', 'signup phone purpose should be runtime-only');
assert.deepStrictEqual(
snapshot.currentState.reusablePhoneActivation,
{
@@ -66,6 +66,7 @@ test('auto-run stops step4 restart path when mail2925 ends the current thread',
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const SIGNUP_METHOD_PHONE = 'phone';
const chrome = {
tabs: {
update: async () => {},
@@ -107,6 +108,7 @@ async function ensureAutoEmailReady() {
}
async function broadcastAutoRunStatus() {}
async function ensureResolvedSignupMethodForRun() { return 'email'; }
async function getState() {
return currentState;
+6
View File
@@ -66,6 +66,7 @@ test('auto-run restarts from step 1 with the same email after step 4 failure', a
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const SIGNUP_METHOD_PHONE = 'phone';
const chrome = {
tabs: {
update: async () => {},
@@ -111,6 +112,7 @@ async function ensureAutoEmailReady() {
}
async function broadcastAutoRunStatus() {}
async function ensureResolvedSignupMethodForRun() { return 'email'; }
async function getState() {
return currentState;
@@ -215,6 +217,7 @@ test('auto-run does not restart step 4 current attempt when user_already_exists
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const SIGNUP_METHOD_PHONE = 'phone';
const chrome = {
tabs: {
update: async () => {},
@@ -256,6 +259,7 @@ async function ensureAutoEmailReady() {
}
async function broadcastAutoRunStatus() {}
async function ensureResolvedSignupMethodForRun() { return 'email'; }
async function getState() {
return currentState;
@@ -336,6 +340,7 @@ test('auto-run skips steps 4/5 when step 2 has already marked registration chain
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const SIGNUP_METHOD_PHONE = 'phone';
const chrome = {
tabs: {
update: async () => {},
@@ -376,6 +381,7 @@ async function ensureAutoEmailReady() {
}
async function broadcastAutoRunStatus() {}
async function ensureResolvedSignupMethodForRun() { return 'email'; }
async function getState() {
return currentState;
+2
View File
@@ -74,6 +74,7 @@ function createHarness(options = {}) {
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const SIGNUP_METHOD_PHONE = 'phone';
const LOG_PREFIX = '[test]';
const chrome = {
tabs: {
@@ -93,6 +94,7 @@ async function addLog(message, level = 'info') {
}
async function ensureAutoEmailReady() {}
async function ensureResolvedSignupMethodForRun() { return 'email'; }
async function broadcastAutoRunStatus() {}
async function getState() {
return {
@@ -54,12 +54,16 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeHotmailLocalBaseUrl'),
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
extractFunction('normalizeVerificationResendCount'),
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePhoneSmsProvider'),
extractFunction('normalizePhoneSmsProviderOrder'),
extractFunction('normalizeSignupMethod'),
extractFunction('normalizeFiveSimCountryCode'),
extractFunction('normalizeFiveSimCountryOrder'),
extractFunction('normalizeNexSmsCountryId'),
extractFunction('normalizeNexSmsCountryOrder'),
extractFunction('normalizeNexSmsServiceCode'),
extractFunction('normalizePhonePreferredActivation'),
extractFunction('normalizePhoneVerificationReplacementLimit'),
extractFunction('normalizePhoneCodeWaitSeconds'),
extractFunction('normalizePhoneCodeTimeoutWindows'),
@@ -106,6 +110,14 @@ const HERO_SMS_COUNTRY_ID = 52;
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms'];
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
const FIVE_SIM_COUNTRY_ID = 'vietnam';
const FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const FIVE_SIM_OPERATOR = 'any';
@@ -155,9 +167,15 @@ return {
assert.equal(api.normalizePersistentSettingValue('phoneCodePollMaxRounds', '18'), 18);
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0.123456'), '0.1235');
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0'), '');
assert.equal(api.normalizePersistentSettingValue('heroSmsPreferredPrice', '0.051234'), '0.0512');
assert.equal(api.normalizePersistentSettingValue('signupMethod', 'phone'), 'phone');
assert.equal(api.normalizePersistentSettingValue('signupMethod', 'unknown'), 'email');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', '5SIM'), '5sim');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'NEXSMS'), 'nexsms');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms');
assert.deepStrictEqual(api.normalizePersistentSettingValue('phoneSmsProviderOrder', ['nexsms', '5sim', 'nexsms']), ['nexsms', '5sim']);
assert.equal(api.normalizePersistentSettingValue('fiveSimApiKey', ' demo-five '), ' demo-five ');
assert.equal(api.normalizePersistentSettingValue('fiveSimProduct', ' OpenAI! '), 'openai');
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ' England! '), 'vietnam');
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ''), 'vietnam');
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryLabel', ''), '越南 (Vietnam)');
@@ -212,8 +230,36 @@ return {
api.normalizePersistentSettingValue('fiveSimCountryOrder', []),
[]
);
assert.deepStrictEqual(
api.normalizePersistentSettingValue('fiveSimCountryOrder', [' Thailand ', 'vietnam', 'thailand']),
['thailand', 'vietnam']
);
assert.equal(api.normalizePersistentSettingValue('nexSmsApiKey', ' demo-nex '), ' demo-nex ');
assert.deepStrictEqual(
api.normalizePersistentSettingValue('nexSmsCountryOrder', []),
[]
);
assert.deepStrictEqual(
api.normalizePersistentSettingValue('nexSmsCountryOrder', [1, '6', 1]),
[1, 6]
);
assert.equal(api.normalizePersistentSettingValue('nexSmsServiceCode', ' OT! '), 'ot');
assert.deepStrictEqual(
api.normalizePersistentSettingValue('phonePreferredActivation', {
provider: 'nexsms',
activationId: 'abc',
phoneNumber: '+6612345',
successfulUses: 2,
maxUses: 3,
}),
{
provider: 'nexsms',
activationId: 'abc',
phoneNumber: '+6612345',
successfulUses: 2,
maxUses: 3,
countryId: null,
countryLabel: '',
}
);
});
@@ -73,7 +73,10 @@ test('account run history helper upgrades old records, keeps stopped items and s
);
assert.deepStrictEqual(record, {
recordId: 'latest@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'latest@example.com',
email: 'latest@example.com',
phoneNumber: '',
password: 'secret',
finalStatus: 'failed',
finishedAt: record.finishedAt,
@@ -140,6 +143,61 @@ test('account run history helper upgrades old records, keeps stopped items and s
assert.equal(normalizedStoppedRecord.failedStep, 7);
});
test('account run history helper accepts phone-only records without forcing email or password', () => {
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope);
const helpers = api.createAccountRunHistoryHelpers({
chrome: { storage: { local: { get: async () => ({}), set: async () => {} } } },
getState: async () => ({}),
normalizeAccountRunHistoryHelperBaseUrl: (value) => String(value || '').trim(),
});
const record = helpers.buildAccountRunHistoryRecord({
accountIdentifierType: 'phone',
accountIdentifier: '+6612345',
signupPhoneNumber: '+6612345',
password: '',
}, 'success');
assert.deepStrictEqual(record, {
recordId: 'phone:+6612345',
accountIdentifierType: 'phone',
accountIdentifier: '+6612345',
email: '',
phoneNumber: '+6612345',
password: '',
finalStatus: 'success',
finishedAt: record.finishedAt,
retryCount: 0,
failureLabel: '流程完成',
failureDetail: '',
failedStep: null,
source: 'manual',
autoRunContext: null,
plusModeEnabled: false,
contributionMode: false,
});
const normalized = helpers.normalizeAccountRunHistoryRecord({
recordId: 'phone:+6612345',
accountIdentifierType: 'phone',
accountIdentifier: '+6612345',
phoneNumber: '+6612345',
finalStatus: 'failed',
failureDetail: '步骤 8:手机号验证码超时。',
});
assert.equal(normalized.recordId, 'phone:+6612345');
assert.equal(normalized.accountIdentifierType, 'phone');
assert.equal(normalized.accountIdentifier, '+6612345');
assert.equal(normalized.email, '');
assert.equal(normalized.phoneNumber, '+6612345');
assert.equal(normalized.password, '');
assert.equal(normalized.finalStatus, 'failed');
});
test('account run history records preserve Plus and contribution mode flags', () => {
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
const globalScope = {};
@@ -143,7 +143,7 @@ test('message router skips step 3 when step 2 lands on verification page', async
assert.deepStrictEqual(events.emailStates, ['user@example.com']);
assert.deepStrictEqual(events.stepStatuses, [{ step: 3, status: 'skipped' }]);
assert.equal(events.logs[0]?.message, '步骤 2:提交邮箱后页面直接进入邮箱验证码页,已自动跳过步骤 3。');
assert.equal(events.logs[0]?.message, '步骤 2:提交邮箱后页面直接进入验证码页,已自动跳过步骤 3。');
});
test('message router does not overwrite a completed step 3 when step 2 is replayed', async () => {
@@ -0,0 +1,97 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') parenDepth += 1;
if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) signatureEnded = true;
}
if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('signup method resolution freezes per run and falls back when phone signup is unavailable', async () => {
const api = new Function(`
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
const logs = [];
let state = {
signupMethod: 'phone',
phoneVerificationEnabled: true,
plusModeEnabled: false,
contributionMode: false,
resolvedSignupMethod: null,
};
async function getState() { return { ...state }; }
async function setState(updates) { state = { ...state, ...updates }; }
async function addLog(message, level = 'info') { logs.push({ message, level }); }
${extractFunction('normalizeSignupMethod')}
${extractFunction('canUsePhoneSignup')}
${extractFunction('resolveSignupMethod')}
${extractFunction('ensureResolvedSignupMethodForRun')}
return {
logs,
get state() { return state; },
setState,
resolveSignupMethod,
ensureResolvedSignupMethodForRun,
};
`)();
assert.equal(api.resolveSignupMethod({ signupMethod: 'phone', phoneVerificationEnabled: true }), 'phone');
assert.equal(api.resolveSignupMethod({ signupMethod: 'phone', phoneVerificationEnabled: false }), 'email');
assert.equal(api.resolveSignupMethod({ signupMethod: 'phone', phoneVerificationEnabled: true, plusModeEnabled: true }), 'email');
assert.equal(api.resolveSignupMethod({ signupMethod: 'email', resolvedSignupMethod: 'phone', phoneVerificationEnabled: false }), 'phone');
assert.equal(await api.ensureResolvedSignupMethodForRun(), 'phone');
assert.equal(api.state.resolvedSignupMethod, 'phone');
await api.setState({ signupMethod: 'email', phoneVerificationEnabled: false });
assert.equal(await api.ensureResolvedSignupMethodForRun(), 'phone');
assert.equal(api.state.resolvedSignupMethod, 'phone');
await api.setState({ resolvedSignupMethod: null, signupMethod: 'phone', phoneVerificationEnabled: false });
assert.equal(await api.ensureResolvedSignupMethodForRun({ force: true }), 'email');
assert.equal(api.state.resolvedSignupMethod, 'email');
assert.equal(api.logs.some((entry) => /固定为邮箱注册/.test(entry.message)), true);
});
@@ -39,6 +39,8 @@ test('step 2 completes with password step skipped when landing on email verifica
step: 2,
payload: {
email: 'user@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'user@example.com',
nextSignupState: 'verification_page',
nextSignupUrl: 'https://auth.openai.com/email-verification',
skippedPasswordStep: true,
@@ -76,6 +78,8 @@ test('step 2 keeps password flow when landing on password page', async () => {
step: 2,
payload: {
email: 'user@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'user@example.com',
nextSignupState: 'password_page',
nextSignupUrl: 'https://auth.openai.com/create-account/password',
skippedPasswordStep: false,
@@ -84,6 +88,80 @@ test('step 2 keeps password flow when landing on password page', async () => {
]);
});
test('step 2 uses phone activation when resolved signup method is phone', async () => {
const completedPayloads = [];
const sentPayloads = [];
const activation = {
activationId: 'signup-activation',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
};
const executor = step2Api.createStep2Executor({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
completeStepFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
ensureSignupEntryPageReady: async () => ({ tabId: 14 }),
ensureSignupPostEmailPageReadyInTab: async () => {
throw new Error('email landing helper should not be used for phone signup');
},
ensureSignupPostIdentityPageReadyInTab: async () => ({
state: 'phone_verification_page',
url: 'https://auth.openai.com/phone-verification',
}),
getTabId: async () => 14,
isTabAlive: async () => true,
phoneVerificationHelpers: {
prepareSignupPhoneActivation: async () => activation,
cancelSignupPhoneActivation: async () => {
throw new Error('activation should not be cancelled on success');
},
},
resolveSignupMethod: () => 'phone',
resolveSignupEmailForFlow: async () => {
throw new Error('email resolver should not run for phone signup');
},
sendToContentScriptResilient: async (_source, message) => {
sentPayloads.push(message.payload);
return { submitted: true };
},
SIGNUP_PAGE_INJECT_FILES: [],
});
await executor.executeStep2({ signupMethod: 'phone' });
assert.deepStrictEqual(sentPayloads, [
{
signupMethod: 'phone',
phoneNumber: '66959916439',
countryId: 52,
countryLabel: 'Thailand',
},
]);
assert.deepStrictEqual(completedPayloads, [
{
step: 2,
payload: {
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
signupPhoneNumber: '66959916439',
signupPhoneActivation: activation,
nextSignupState: 'phone_verification_page',
nextSignupUrl: 'https://auth.openai.com/phone-verification',
skippedPasswordStep: true,
},
},
]);
});
test('step 2 stops with an explicit error instead of silently skipping 3/4/5 on chatgpt home', async () => {
const completedPayloads = [];
const logs = [];
@@ -44,8 +44,81 @@ test('step 3 reuses existing generated password when rerunning the same email fl
source: 'background',
payload: {
email: 'keep@example.com',
phoneNumber: '',
accountIdentifierType: 'email',
accountIdentifier: 'keep@example.com',
password: 'Secret123!',
},
},
]);
});
test('step 3 supports phone-only signup identity when password page is present', async () => {
const events = {
passwordStates: [],
messages: [],
stateUpdates: [],
logs: [],
};
const executor = api.createStep3Executor({
addLog: async (message) => {
events.logs.push(message);
},
chrome: { tabs: { update: async () => {} } },
ensureContentScriptReadyOnTab: async () => {},
generatePassword: () => 'Generated123!',
getTabId: async () => 88,
isTabAlive: async () => true,
sendToContentScript: async (_source, message) => {
events.messages.push(message);
},
setPasswordState: async (password) => {
events.passwordStates.push(password);
},
setState: async (updates) => {
events.stateUpdates.push(updates);
},
SIGNUP_PAGE_INJECT_FILES: [],
});
await executor.executeStep3({
email: '',
signupPhoneNumber: '66959916439',
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
customPassword: 'PhoneSecret123!',
accounts: [],
});
assert.deepStrictEqual(events.passwordStates, ['PhoneSecret123!']);
assert.equal(events.logs.some((message) => /注册手机号为 66959916439/.test(message)), true);
assert.equal(events.stateUpdates.length, 1);
assert.deepStrictEqual(events.stateUpdates[0].accounts.map((account) => ({
email: account.email,
phoneNumber: account.phoneNumber,
accountIdentifierType: account.accountIdentifierType,
accountIdentifier: account.accountIdentifier,
})), [
{
email: '',
phoneNumber: '66959916439',
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
},
]);
assert.deepStrictEqual(events.messages, [
{
type: 'EXECUTE_STEP',
step: 3,
source: 'background',
payload: {
email: '',
phoneNumber: '66959916439',
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
password: 'PhoneSecret123!',
},
},
]);
});
@@ -231,6 +231,82 @@ test('step 4 forwards skipProfileStep when prepare stage already reached logged-
assert.equal(resolveCalls, 0);
});
test('step 4 phone signup branch uses SMS helper and does not poll mailbox', async () => {
const completions = [];
const phoneCalls = [];
let getMailConfigCalls = 0;
let resolveCalls = 0;
const executor = api.createStep4Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
completeStepFromBackground: async (step, payload) => {
completions.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {},
getMailConfig: () => {
getMailConfigCalls += 1;
throw new Error('mail config should not be required for phone signup');
},
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
LUCKMAIL_PROVIDER: 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
phoneVerificationHelpers: {
completeSignupPhoneVerificationFlow: async (tabId, options) => {
phoneCalls.push({ tabId, options });
return {
success: true,
skipProfileStep: true,
skipProfileStepReason: 'combined_verification_profile',
};
},
},
resolveSignupMethod: () => 'phone',
resolveVerificationStep: async () => {
resolveCalls += 1;
},
reuseOrCreateTab: async () => {},
sendToContentScript: async () => ({ ready: true }),
sendToContentScriptResilient: async () => ({ ready: true }),
isRetryableContentScriptTransportError: () => false,
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
});
await executor.executeStep4({
resolvedSignupMethod: 'phone',
accountIdentifierType: 'phone',
signupPhoneNumber: '66959916439',
signupPhoneActivation: { activationId: 'signup-123', phoneNumber: '66959916439' },
});
assert.equal(getMailConfigCalls, 0);
assert.equal(resolveCalls, 0);
assert.equal(phoneCalls.length, 1);
assert.equal(phoneCalls[0].tabId, 1);
assert.equal(phoneCalls[0].options.state.accountIdentifierType, 'phone');
assert.equal(Object.prototype.hasOwnProperty.call(phoneCalls[0].options, 'signupProfile'), true);
assert.deepStrictEqual(completions, [
{
step: 4,
payload: {
phoneVerification: true,
code: '',
skipProfileStep: true,
skipProfileStepReason: 'combined_verification_profile',
},
},
]);
});
test('step 4 prepare retries transport by recovering retry page without replaying full prepare loop', async () => {
let sendToContentScriptCalls = 0;
let recoverCalls = 0;
@@ -227,6 +227,75 @@ test('step 7 forwards direct OAuth consent skip metadata when completing', async
]);
});
test('step 7 forwards phone login identity payload when account identifier is phone', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
const events = {
payloads: [],
};
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
signupPhoneNumber: '66959916439',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
countryId: 52,
countryLabel: 'Thailand',
},
password: 'secret',
}),
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async (_sourceName, message) => {
events.payloads.push(message.payload);
return {
step6Outcome: 'success',
state: 'phone_verification_page',
loginVerificationRequestedAt: 123456,
};
},
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await executor.executeStep7({
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
signupPhoneNumber: '66959916439',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
countryId: 52,
countryLabel: 'Thailand',
},
password: 'secret',
});
assert.deepStrictEqual(events.payloads, [
{
email: '',
phoneNumber: '66959916439',
countryId: 52,
countryLabel: 'Thailand',
accountIdentifier: '66959916439',
loginIdentifierType: 'phone',
password: 'secret',
visibleStep: 7,
},
]);
});
test('step 7 stops immediately when management secret is missing', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
+102
View File
@@ -90,6 +90,108 @@ test('step 8 submits login verification directly without replaying step 7', asyn
assert.equal(calls.resolveOptions.completionStep, 8);
});
test('step 8 routes phone login verification through sms helper and skips mail provider setup', async () => {
const calls = {
getMailConfigCalls: 0,
helperCalls: [],
completions: [],
};
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (step, payload) => {
calls.completions.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => {
throw new Error('phone login branch should not probe email verification page readiness');
},
getOAuthFlowRemainingMs: async () => 5000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getMailConfig: () => {
calls.getMailConfigCalls += 1;
return {
provider: 'qq',
label: 'QQ 邮箱',
};
},
getState: async () => ({
accountIdentifierType: 'phone',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
},
}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
isVerificationMailPollingError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
phoneVerificationHelpers: {
completeLoginPhoneVerificationFlow: async (tabId, options) => {
calls.helperCalls.push({ tabId, visibleStep: options.visibleStep, state: options.state });
return { code: '654321' };
},
},
resolveSignupMethod: () => 'phone',
resolveVerificationStep: async () => {
throw new Error('phone login branch should not call email verification flow');
},
rerunStep7ForStep8Recovery: async () => {
throw new Error('phone login branch should not rerun step 7 in this test');
},
reuseOrCreateTab: async () => {},
setState: async () => {},
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
});
await executor.executeStep8({
visibleStep: 8,
accountIdentifierType: 'phone',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
},
oauthUrl: 'https://oauth.example/latest',
});
assert.equal(calls.getMailConfigCalls, 0);
assert.deepStrictEqual(calls.helperCalls, [
{
tabId: 1,
visibleStep: 8,
state: {
visibleStep: 8,
accountIdentifierType: 'phone',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
},
oauthUrl: 'https://oauth.example/latest',
},
},
]);
assert.deepStrictEqual(calls.completions, [
{
step: 8,
payload: {
phoneVerification: true,
loginPhoneVerification: true,
code: '654321',
},
},
]);
});
test('Plus login-code step reuses step 8 verification logic but completes visible step 11', async () => {
let resolvedStep = null;
let resolvedOptions = null;
+408
View File
@@ -83,6 +83,414 @@ test('phone verification helper requests HeroSMS numbers with fixed OpenAI and T
assert.equal(requests[1].searchParams.get('api_key'), 'demo-key');
});
test('signup phone helper persists signup runtime state without touching add-phone activation', async () => {
const setStateCalls = [];
let currentState = {
heroSmsApiKey: 'demo-key',
currentPhoneActivation: {
activationId: 'add-phone-activation',
phoneNumber: '66880000000',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload(),
};
}
if (action === 'getNumber') {
return {
ok: true,
text: async () => 'ACCESS_NUMBER:signup-123:66959916439',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getState: async () => currentState,
sendToContentScriptResilient: async () => ({}),
setState: async (updates) => {
setStateCalls.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const activation = await helpers.prepareSignupPhoneActivation(currentState);
assert.equal(activation.activationId, 'signup-123');
assert.equal(activation.phoneNumber, '66959916439');
assert.equal(currentState.signupPhoneNumber, '66959916439');
assert.equal(currentState.signupPhoneVerificationPurpose, 'signup');
assert.deepStrictEqual(currentState.signupPhoneActivation, activation);
assert.equal(currentState.accountIdentifierType, 'phone');
assert.equal(currentState.accountIdentifier, '66959916439');
assert.equal(currentState.currentPhoneActivation.activationId, 'add-phone-activation');
assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation')));
});
test('signup phone helper polls signup SMS code and keeps activation purpose isolated', async () => {
const setStateCalls = [];
let currentState = {
heroSmsApiKey: 'demo-key',
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 2,
signupPhoneActivation: {
activationId: 'signup-123',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
currentPhoneActivation: {
activationId: 'add-phone-activation',
phoneNumber: '66880000000',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
const action = parsedUrl.searchParams.get('action');
if (action === 'getStatus') {
return {
ok: true,
text: async () => 'STATUS_OK:123456',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getOAuthFlowStepTimeoutMs: async (fallback) => fallback,
getState: async () => currentState,
sendToContentScriptResilient: async () => ({}),
setState: async (updates) => {
setStateCalls.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const code = await helpers.waitForSignupPhoneCode(currentState, currentState.signupPhoneActivation);
assert.equal(code, '123456');
assert.equal(currentState.currentPhoneVerificationCode, '123456');
assert.equal(currentState.signupPhoneNumber, '66959916439');
assert.equal(currentState.signupPhoneVerificationPurpose, 'signup');
assert.equal(currentState.currentPhoneActivation.activationId, 'add-phone-activation');
assert.ok(setStateCalls.some((updates) => updates.signupPhoneVerificationRequestedAt));
assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation')));
});
test('signup phone helper finalizes or cancels signup activation without clearing add-phone activation', async () => {
const setStateCalls = [];
const statusActions = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsReuseEnabled: false,
signupPhoneActivation: {
activationId: 'signup-123',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
currentPhoneActivation: {
activationId: 'add-phone-activation',
phoneNumber: '66880000000',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
const action = parsedUrl.searchParams.get('action');
if (action === 'setStatus') {
statusActions.push(parsedUrl.searchParams.get('status'));
return {
ok: true,
text: async () => 'ACCESS_READY',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getState: async () => currentState,
sendToContentScriptResilient: async () => ({}),
setState: async (updates) => {
setStateCalls.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await helpers.finalizeSignupPhoneActivationAfterSuccess(currentState, currentState.signupPhoneActivation);
assert.deepStrictEqual(statusActions, ['6']);
assert.equal(currentState.signupPhoneNumber, '66959916439');
assert.equal(currentState.signupPhoneActivation, null);
assert.deepStrictEqual(currentState.signupPhoneCompletedActivation, {
activationId: 'signup-123',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 1,
maxUses: 3,
});
assert.equal(currentState.signupPhoneVerificationPurpose, '');
assert.equal(currentState.accountIdentifierType, 'phone');
assert.equal(currentState.accountIdentifier, '66959916439');
assert.equal(currentState.currentPhoneActivation.activationId, 'add-phone-activation');
assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation')));
});
test('signup phone helper completes signup SMS verification without touching add-phone activation', async () => {
const setStateCalls = [];
const contentMessages = [];
const statusActions = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsReuseEnabled: false,
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 2,
signupPhoneNumber: '66959916439',
signupPhoneVerificationPurpose: 'signup',
signupPhoneActivation: {
activationId: 'signup-123',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
currentPhoneActivation: {
activationId: 'add-phone-activation',
phoneNumber: '66880000000',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
const action = parsedUrl.searchParams.get('action');
if (action === 'getStatus') {
return {
ok: true,
text: async () => 'STATUS_OK:123456',
};
}
if (action === 'setStatus') {
statusActions.push(parsedUrl.searchParams.get('status'));
return {
ok: true,
text: async () => 'ACCESS_READY',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getOAuthFlowStepTimeoutMs: async (fallback) => fallback,
getState: async () => currentState,
sendToContentScriptResilient: async (_source, message) => {
contentMessages.push(message);
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
return { success: true };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
setStateCalls.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const result = await helpers.completeSignupPhoneVerificationFlow(77, {
state: currentState,
signupProfile: {
firstName: 'Ada',
lastName: 'Lovelace',
year: 1995,
month: 1,
day: 2,
},
});
assert.deepStrictEqual(result, { success: true });
assert.deepStrictEqual(statusActions, ['6']);
assert.deepStrictEqual(contentMessages.map((message) => ({
type: message.type,
step: message.step,
code: message.payload?.code,
purpose: message.payload?.purpose,
})), [
{
type: 'SUBMIT_PHONE_VERIFICATION_CODE',
step: 4,
code: '123456',
purpose: 'signup',
},
]);
assert.equal(currentState.signupPhoneNumber, '66959916439');
assert.equal(currentState.signupPhoneActivation, null);
assert.equal(currentState.signupPhoneVerificationPurpose, '');
assert.equal(currentState.currentPhoneVerificationCode, '');
assert.equal(currentState.currentPhoneActivation.activationId, 'add-phone-activation');
assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation')));
});
test('signup phone helper completes login SMS verification by reusing the completed signup activation', async () => {
const setStateCalls = [];
const contentMessages = [];
const statusActions = [];
let currentState = {
heroSmsApiKey: 'demo-key',
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 2,
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 1,
maxUses: 3,
},
currentPhoneActivation: {
activationId: 'add-phone-activation',
phoneNumber: '66880000000',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
const action = parsedUrl.searchParams.get('action');
const id = parsedUrl.searchParams.get('id');
if (action === 'reactivate' && id === 'signup-done') {
return {
ok: true,
text: async () => 'ACCESS_READY',
};
}
if (action === 'getStatus' && id === 'signup-done') {
return {
ok: true,
text: async () => 'STATUS_OK:654321',
};
}
if (action === 'setStatus' && id === 'signup-done') {
statusActions.push(parsedUrl.searchParams.get('status'));
return {
ok: true,
text: async () => 'ACCESS_READY',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}:${id}`);
},
getOAuthFlowStepTimeoutMs: async (fallback) => fallback,
getState: async () => currentState,
sendToContentScriptResilient: async (_source, message) => {
contentMessages.push(message);
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
return { success: true };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
setStateCalls.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const result = await helpers.completeLoginPhoneVerificationFlow(77, {
state: currentState,
visibleStep: 8,
});
assert.deepStrictEqual(result, { success: true });
assert.deepStrictEqual(statusActions, ['6']);
assert.deepStrictEqual(contentMessages.map((message) => ({
type: message.type,
step: message.step,
code: message.payload?.code,
purpose: message.payload?.purpose,
})), [
{
type: 'SUBMIT_PHONE_VERIFICATION_CODE',
step: 8,
code: '654321',
purpose: 'login',
},
]);
assert.equal(currentState.signupPhoneActivation, null);
assert.equal(currentState.signupPhoneVerificationPurpose, '');
assert.equal(currentState.currentPhoneVerificationCode, '');
assert.deepStrictEqual(currentState.signupPhoneCompletedActivation, {
activationId: 'signup-done',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 2,
maxUses: 3,
});
assert.equal(currentState.currentPhoneActivation.activationId, 'add-phone-activation');
assert.ok(setStateCalls.some((updates) => updates.signupPhoneVerificationPurpose === 'login'));
assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation')));
});
test('phone verification helper ignores HeroSMS virtual-only stock when physicalCount is zero', async () => {
const requests = [];
const helpers = api.createPhoneVerificationHelpers({
@@ -336,7 +336,67 @@ test('account records manager supports filter chips and partial multi-select del
assert.match(list.innerHTML, /stopped@example\.com/);
assert.equal(btnDeleteSelectedAccountRecords.disabled, true);
assert.deepStrictEqual(toasts.at(-1), {
message: '已删除 1 条邮箱记录。',
message: '已删除 1 条账号记录。',
tone: 'success',
});
});
test('account records manager displays phone-only records with account identifier fallback', () => {
const source = fs.readFileSync('sidepanel/account-records-manager.js', 'utf8');
const windowObject = {};
const api = new Function('window', `${source}; return window.SidepanelAccountRecordsManager;`)(windowObject);
const list = createNode();
const meta = createNode();
const manager = api.createAccountRecordsManager({
state: {
getLatestState: () => ({
accountRunHistory: [
{
recordId: 'phone:+6612345',
accountIdentifierType: 'phone',
accountIdentifier: '+6612345',
phoneNumber: '+6612345',
email: '',
password: '',
finalStatus: 'success',
finishedAt: '2026-04-17T04:31:00.000Z',
retryCount: 0,
failureLabel: '流程完成',
},
],
}),
syncLatestState() {},
},
dom: {
accountRecordsList: list,
accountRecordsMeta: meta,
accountRecordsOverlay: createNode(),
accountRecordsPageLabel: createNode(),
accountRecordsStats: createNode(),
btnAccountRecordsNext: createNode(),
btnAccountRecordsPrev: createNode(),
btnClearAccountRecords: createNode(),
btnCloseAccountRecords: createNode(),
btnDeleteSelectedAccountRecords: createNode(),
btnOpenAccountRecords: createNode(),
btnToggleAccountRecordsSelection: createNode(),
},
helpers: {
escapeHtml: (value) => String(value || ''),
},
runtime: {
sendMessage: async () => ({}),
},
constants: {
displayTimeZone: 'Asia/Shanghai',
pageSize: 10,
},
});
manager.render();
assert.match(meta.textContent, /共 1 条/);
assert.match(list.innerHTML, /\+6612345/);
assert.doesNotMatch(list.innerHTML, /\(空邮箱\)/);
});
@@ -59,6 +59,9 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
assert.match(html, /id="btn-toggle-phone-verification-section"/);
assert.match(html, /id="row-phone-verification-fold"/);
assert.match(html, /id="input-phone-verification-enabled"/);
assert.match(html, /id="row-signup-method"/);
assert.match(html, /data-signup-method="email"/);
assert.match(html, /data-signup-method="phone"/);
assert.match(html, /id="row-phone-sms-provider"/);
assert.match(html, /id="select-phone-sms-provider"/);
assert.match(html, /id="row-phone-sms-provider-order"/);
@@ -124,6 +127,7 @@ let latestState = {};
const inputPhoneVerificationEnabled = { checked: false };
const rowPhoneVerificationEnabled = { style: { display: 'none' } };
const rowPhoneVerificationFold = { style: { display: 'none' } };
const rowSignupMethod = { style: { display: 'none' } };
const rowPhoneSmsProvider = { style: { display: 'none' } };
const rowPhoneSmsProviderOrder = { style: { display: 'none' } };
const rowPhoneSmsProviderOrderActions = { style: { display: 'none' } };
@@ -169,7 +173,15 @@ const rowHeroSmsCountryFallback = { style: { display: 'none' } };
const rowHeroSmsAcquirePriority = { style: { display: 'none' } };
const rowHeroSmsApiKey = { style: { display: 'none' } };
const rowHeroSmsMaxPrice = { style: { display: 'none' } };
const rowFiveSimApiKey = { style: { display: 'none' } };
const rowFiveSimCountry = { style: { display: 'none' } };
const rowFiveSimCountryFallback = { style: { display: 'none' } };
const rowFiveSimOperator = { style: { display: 'none' } };
const rowFiveSimProduct = { style: { display: 'none' } };
const rowNexSmsApiKey = { style: { display: 'none' } };
const rowNexSmsCountry = { style: { display: 'none' } };
const rowNexSmsCountryFallback = { style: { display: 'none' } };
const rowNexSmsServiceCode = { style: { display: 'none' } };
const rowHeroSmsCurrentNumber = { style: { display: 'none' } };
const rowHeroSmsCurrentCountdown = { style: { display: 'none' } };
const rowHeroSmsPriceTiers = { style: { display: 'none' } };
@@ -183,10 +195,13 @@ const rowPhoneCodePollIntervalSeconds = { style: { display: 'none' } };
const rowPhoneCodePollMaxRounds = { style: { display: 'none' } };
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
let selectedPhoneSmsProvider = PHONE_SMS_PROVIDER_HERO_SMS;
function getSelectedPhoneSmsProvider() { return selectedPhoneSmsProvider; }
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
function getSelectedPhoneSmsProvider() { return selectPhoneSmsProvider.value; }
function isFiveSimProviderSelected() { return getSelectedPhoneSmsProvider() === PHONE_SMS_PROVIDER_FIVE_SIM; }
function updateHeroSmsPlatformDisplay() {}
function updateSignupMethodUI() {
rowSignupMethod.style.display = inputPhoneVerificationEnabled.checked ? '' : 'none';
}
${extractFunction('updatePhoneVerificationSettingsUI')}
@@ -194,6 +209,7 @@ return {
setLatestState: (state) => { latestState = state || {}; },
rowPhoneVerificationEnabled,
rowPhoneVerificationFold,
rowSignupMethod,
rowPhoneSmsProvider,
rowPhoneSmsProviderOrder,
rowPhoneSmsProviderOrderActions,
@@ -206,7 +222,15 @@ return {
rowHeroSmsAcquirePriority,
rowHeroSmsApiKey,
rowHeroSmsMaxPrice,
rowFiveSimApiKey,
rowFiveSimCountry,
rowFiveSimCountryFallback,
rowFiveSimOperator,
rowFiveSimProduct,
rowNexSmsApiKey,
rowNexSmsCountry,
rowNexSmsCountryFallback,
rowNexSmsServiceCode,
rowHeroSmsCurrentNumber,
rowHeroSmsCurrentCountdown,
rowHeroSmsPriceTiers,
@@ -218,7 +242,7 @@ return {
rowPhoneCodeTimeoutWindows,
rowPhoneCodePollIntervalSeconds,
rowPhoneCodePollMaxRounds,
setSelectedPhoneSmsProvider(value) { selectedPhoneSmsProvider = value; },
setSelectedPhoneSmsProvider(value) { selectPhoneSmsProvider.value = value; },
updatePhoneVerificationSettingsUI,
};
`)();
@@ -226,6 +250,7 @@ return {
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowPhoneVerificationEnabled.style.display, '');
assert.equal(api.rowPhoneVerificationFold.style.display, 'none');
assert.equal(api.rowSignupMethod.style.display, 'none');
assert.equal(api.rowPhoneSmsProvider.style.display, 'none');
assert.equal(api.rowPhoneSmsProviderOrder.style.display, 'none');
assert.equal(api.rowPhoneSmsProviderOrderActions.style.display, 'none');
@@ -262,6 +287,7 @@ return {
api.inputPhoneVerificationEnabled.checked = true;
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowPhoneVerificationFold.style.display, '');
assert.equal(api.rowSignupMethod.style.display, '');
assert.equal(api.rowPhoneSmsProvider.style.display, '');
assert.equal(api.rowPhoneSmsProviderOrder.style.display, '');
assert.equal(api.rowPhoneSmsProviderOrderActions.style.display, '');
@@ -288,7 +314,18 @@ return {
api.setSelectedPhoneSmsProvider('5sim');
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowFiveSimApiKey.style.display, '');
assert.equal(api.rowFiveSimCountry.style.display, '');
assert.equal(api.rowFiveSimCountryFallback.style.display, '');
assert.equal(api.rowFiveSimOperator.style.display, '');
assert.equal(api.rowFiveSimProduct.style.display, '');
api.setSelectedPhoneSmsProvider('nexsms');
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowNexSmsApiKey.style.display, '');
assert.equal(api.rowNexSmsCountry.style.display, '');
assert.equal(api.rowNexSmsCountryFallback.style.display, '');
assert.equal(api.rowNexSmsServiceCode.style.display, '');
});
test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => {
@@ -345,6 +382,8 @@ const inputHeroSmsApiKey = { value: 'demo-key' };
const inputFiveSimApiKey = { value: 'five-sim-key' };
const inputFiveSimOperator = { value: 'any' };
const inputFiveSimProduct = { value: 'openai' };
const inputNexSmsApiKey = { value: 'nex-key' };
const inputNexSmsServiceCode = { value: 'ot' };
const inputHeroSmsReuseEnabled = { checked: true };
const selectHeroSmsAcquirePriority = { value: 'price' };
function getSelectedPhonePreferredActivation() {
@@ -359,7 +398,7 @@ function getSelectedPhonePreferredActivation() {
};
}
const inputHeroSmsMaxPrice = { value: '0.12' };
const inputFiveSimOperator = { value: 'any' };
const inputHeroSmsPreferredPrice = { value: '0.0512' };
const inputPhoneReplacementLimit = { value: '5' };
const inputPhoneCodeWaitSeconds = { value: '75' };
const inputPhoneCodeTimeoutWindows = { value: '3' };
@@ -390,10 +429,14 @@ const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
const DEFAULT_NEX_SMS_COUNTRY_ORDER = [1];
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const selectHeroSmsCountry = {
@@ -420,6 +463,13 @@ function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
${extractFunction('normalizePhoneSmsProvider')}
${extractFunction('normalizePhoneSmsProviderValue')}
${extractFunction('normalizeFiveSimCountryCode')}
${extractFunction('normalizeFiveSimCountryOrderValue')}
${extractFunction('normalizeFiveSimProductValue')}
${extractFunction('normalizeNexSmsCountryIdValue')}
${extractFunction('normalizeNexSmsCountryOrderValue')}
${extractFunction('normalizeNexSmsServiceCodeValue')}
function getSelectedPhoneSmsProvider() { return normalizePhoneSmsProvider(selectPhoneSmsProvider?.value || latestState?.phoneSmsProvider); }
${extractFunction('normalizeFiveSimCountryId')}
${extractFunction('normalizeFiveSimCountryLabel')}
@@ -441,6 +491,13 @@ ${extractFunction('getSelectedHeroSmsCountryOption')}
function syncHeroSmsFallbackSelectionOrderFromSelect() {
return [{ id: 52, label: 'Thailand' }, { id: 16, label: 'United Kingdom' }];
}
function getSelectedSignupMethod() { return 'phone'; }
function getSelectedFiveSimCountries() {
return [{ id: 'thailand', code: 'thailand', label: 'Thailand' }, { id: 'vietnam', code: 'vietnam', label: 'Vietnam' }];
}
function getSelectedNexSmsCountries() {
return [{ id: 1, label: 'Country #1' }];
}
${extractFunction('collectSettingsPayload')}
return { collectSettingsPayload };
`)(normalizeIcloudTargetMailboxType, normalizeIcloudForwardMailProvider);
@@ -448,17 +505,22 @@ return { collectSettingsPayload };
const payload = api.collectSettingsPayload();
assert.equal(payload.phoneVerificationEnabled, true);
assert.equal(payload.signupMethod, 'phone');
assert.equal(payload.phoneSmsProvider, 'hero-sms');
assert.equal(payload.accountRunHistoryTextEnabled, true);
assert.equal(payload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
assert.equal(payload.heroSmsApiKey, 'demo-key');
assert.equal(payload.fiveSimApiKey, 'five-sim-key');
assert.deepStrictEqual(payload.fiveSimCountryOrder, ['thailand', 'england']);
assert.deepStrictEqual(payload.fiveSimCountryOrder, ['thailand', 'vietnam']);
assert.equal(payload.fiveSimOperator, 'any');
assert.equal(payload.fiveSimProduct, 'openai');
assert.equal(payload.nexSmsApiKey, 'nex-key');
assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]);
assert.equal(payload.nexSmsServiceCode, 'ot');
assert.equal(payload.heroSmsReuseEnabled, true);
assert.equal(payload.heroSmsAcquirePriority, 'price');
assert.equal(payload.heroSmsMaxPrice, '0.12');
assert.equal(payload.heroSmsPreferredPrice, '0.0512');
assert.deepStrictEqual(payload.phonePreferredActivation, {
provider: 'hero-sms',
activationId: 'demo-activation',
@@ -476,7 +538,7 @@ return { collectSettingsPayload };
assert.equal(payload.heroSmsCountryId, 52);
assert.equal(payload.heroSmsCountryLabel, 'Thailand');
assert.deepStrictEqual(payload.heroSmsCountryFallback, [{ id: 16, label: 'United Kingdom' }]);
assert.equal(payload.fiveSimApiKey, '');
assert.equal(payload.fiveSimApiKey, 'five-sim-key');
assert.equal(payload.fiveSimCountryId, 'vietnam');
});
@@ -612,19 +674,27 @@ const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const inputHeroSmsMaxPrice = { value: '' };
const inputHeroSmsApiKey = { value: '' };
const inputFiveSimOperator = { value: 'any' };
const inputFiveSimProduct = { value: 'openai' };
const displayHeroSmsPriceTiers = { textContent: '' };
const rowHeroSmsPriceTiers = { style: { display: 'none' } };
const fetchCalls = [];
${extractFunction('normalizePhoneSmsProvider')}
${extractFunction('normalizePhoneSmsProviderValue')}
${extractFunction('normalizePhoneSmsProviderOrderValue')}
const phoneSmsProviderOrderSelection = [];
function getSelectedPhoneSmsProvider() { return '5sim'; }
function getSelectedPhoneSmsProviderOrder() { return ['5sim']; }
${extractFunction('normalizeFiveSimCountryId')}
${extractFunction('normalizeFiveSimCountryLabel')}
${extractFunction('normalizeFiveSimCountryCode')}
${extractFunction('normalizeFiveSimProductValue')}
${extractFunction('normalizeFiveSimOperator')}
${extractFunction('normalizePhoneSmsMaxPriceValue')}
${extractFunction('normalizeFiveSimMaxPriceValue')}
@@ -636,7 +706,13 @@ ${extractFunction('collectHeroSmsPriceEntriesForPreview')}
${extractFunction('formatPhoneSmsPriceEntriesSummary')}
${extractFunction('describeHeroSmsPreviewPayload')}
${extractFunction('summarizeHeroSmsPreviewError')}
${extractFunction('formatPriceTiersForPreview')}
${extractFunction('formatPriceTiersWithZeroStockForPreview')}
function normalizeHeroSmsFetchErrorMessage(error) { return error?.message || String(error); }
function getFiveSimCountryLabelByCode() { return '越南 (Vietnam)'; }
function getSelectedFiveSimCountries() {
return [{ id: 'vietnam', code: 'vietnam', label: '越南 (Vietnam)' }];
}
function syncHeroSmsFallbackSelectionOrderFromSelect() {
return [{ id: 'vietnam', label: '越南 (Vietnam)' }];
}
@@ -653,14 +729,14 @@ async function fetch(url, options = {}) {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({ openai: { Category: 'activation', Qty: 4609, Price: 0.08 } }),
json: async () => ({ openai: { Category: 'activation', Qty: 4609, Price: 0.08 } }),
};
}
if (parsed.pathname === '/v1/guest/prices') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
json: async () => ({
vietnam: {
openai: {
virtual21: { cost: 0.0769, count: 0 },
@@ -673,6 +749,7 @@ async function fetch(url, options = {}) {
throw new Error('unexpected ' + parsed.pathname);
}
${extractFunction('buildFiveSimPricePreviewLines')}
${extractFunction('previewHeroSmsPriceTiers')}
return {
@@ -685,11 +762,14 @@ return {
await api.previewHeroSmsPriceTiers();
assert.equal(api.displayHeroSmsPriceTiers.textContent, '越南 (Vietnam): 最低 0.08');
assert.equal(
api.displayHeroSmsPriceTiers.textContent,
'5sim:\n越南 (Vietnam): 最低 0.1282;档位:0.0769(x0), 0.1282(x4608)'
);
assert.equal(api.rowHeroSmsPriceTiers.style.display, '');
assert.deepStrictEqual(
api.fetchCalls.map((entry) => entry.url.pathname),
['/v1/guest/products/vietnam/any', '/v1/guest/prices']
['/v1/guest/prices']
);
});
+229
View File
@@ -136,6 +136,9 @@ ${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')}
${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')}
${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')}
${extractConst('SIGNUP_PHONE_ACTION_PATTERN')}
${extractConst('SIGNUP_SWITCH_TO_PHONE_PATTERN')}
${extractConst('SIGNUP_MORE_OPTIONS_PATTERN')}
${extractConst('SIGNUP_WORK_EMAIL_PATTERN')}
function isVisibleElement(el) {
@@ -196,6 +199,8 @@ async function sleep(ms) {
${extractFunction('getSignupEmailInput')}
${extractFunction('getSignupPhoneInput')}
${extractFunction('findSignupUseEmailTrigger')}
${extractFunction('findSignupUsePhoneTrigger')}
${extractFunction('findSignupMoreOptionsTrigger')}
${extractFunction('getSignupEmailContinueButton')}
${extractFunction('inspectSignupEntryState')}
${extractFunction('waitForSignupEntryState')}
@@ -309,6 +314,9 @@ ${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')}
${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')}
${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')}
${extractConst('SIGNUP_PHONE_ACTION_PATTERN')}
${extractConst('SIGNUP_SWITCH_TO_PHONE_PATTERN')}
${extractConst('SIGNUP_MORE_OPTIONS_PATTERN')}
${extractConst('SIGNUP_WORK_EMAIL_PATTERN')}
function isVisibleElement(el) {
@@ -369,6 +377,8 @@ async function sleep(ms) {
${extractFunction('getSignupEmailInput')}
${extractFunction('getSignupPhoneInput')}
${extractFunction('findSignupUseEmailTrigger')}
${extractFunction('findSignupUsePhoneTrigger')}
${extractFunction('findSignupMoreOptionsTrigger')}
${extractFunction('getSignupEmailContinueButton')}
${extractFunction('inspectSignupEntryState')}
${extractFunction('waitForSignupEntryState')}
@@ -429,3 +439,222 @@ return {
assert.equal(api.run()?.kind, 'localized-email');
});
test('submitSignupPhoneNumberAndContinue switches from email mode to phone mode and submits local number', async () => {
const api = new Function(`
const logs = [];
const clicks = [];
const filled = [];
let phase = 'email';
let now = 0;
const emailInput = {
kind: 'email',
value: '',
getAttribute(name) {
if (name === 'type') return 'email';
return '';
},
};
const phoneInput = {
kind: 'phone',
value: '',
textContent: 'Thailand (+66)',
getAttribute(name) {
if (name === 'type') return 'tel';
return '';
},
closest() {
return { textContent: 'Thailand (+66)' };
},
};
const switchButton = {
textContent: 'Continue with phone number',
value: '',
disabled: false,
getAttribute(name) {
if (name === 'type') return 'button';
return '';
},
getBoundingClientRect() {
return { width: 200, height: 48 };
},
};
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 document = {
querySelector(selector) {
if (selector === SIGNUP_EMAIL_INPUT_SELECTOR) {
return phase === 'email' ? emailInput : null;
}
if (selector === SIGNUP_PHONE_INPUT_SELECTOR) {
return phase === 'phone' ? phoneInput : null;
}
if (selector === 'button[type="submit"], input[type="submit"]') {
return phase === 'phone' ? continueButton : null;
}
return null;
},
querySelectorAll(selector) {
if (selector === 'button, a, [role="button"], [role="link"]') {
return phase === 'email' ? [switchButton] : [];
}
if (selector === 'a, button, [role="button"], [role="link"]') {
return [];
}
if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
return phase === 'phone' ? [continueButton] : [];
}
if (selector === 'input') {
return phase === 'phone' ? [phoneInput] : [emailInput];
}
return [];
},
};
const location = {
href: 'https://chatgpt.com/',
};
const window = {
setTimeout(fn) {
fn();
},
};
const Date = {
now() {
return now;
},
};
${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')}
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 phase === 'phone' ? 'Thailand (+66)' : '';
}
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 === switchButton) {
phase = 'phone';
}
}
function fillInput(target, value) {
target.value = value;
filled.push({ target: target.kind, value });
}
async function sleep(ms) {
now += ms;
}
${extractFunction('getSignupEmailInput')}
${extractFunction('getSignupPhoneInput')}
${extractFunction('findSignupUseEmailTrigger')}
${extractFunction('findSignupUsePhoneTrigger')}
${extractFunction('findSignupMoreOptionsTrigger')}
${extractFunction('getSignupEmailContinueButton')}
${extractFunction('inspectSignupEntryState')}
${extractFunction('getSignupEntryStateSummary')}
function getSignupEntryDiagnostics() { return {}; }
${extractFunction('normalizePhoneDigits')}
${extractFunction('extractDialCodeFromText')}
${extractFunction('toNationalPhoneNumber')}
${extractFunction('resolveSignupPhoneDialCode')}
${extractFunction('waitForSignupPhoneEntryState')}
${extractFunction('submitSignupPhoneNumberAndContinue')}
return {
async run() {
return submitSignupPhoneNumberAndContinue({
phoneNumber: '66959916439',
countryLabel: 'Thailand',
});
},
getClicks() {
return clicks.slice();
},
getFilled() {
return filled.slice();
},
};
`)();
const result = await api.run();
assert.equal(result.submitted, true);
assert.equal(result.phoneInputValue, '959916439');
assert.deepEqual(api.getClicks(), ['Continue with phone number', 'Continue']);
assert.deepEqual(api.getFilled(), [{ target: 'phone', value: '959916439' }]);
});
+20
View File
@@ -80,6 +80,26 @@ function inspectSignupEntryState() {
return snapshot;
}
function isPhoneVerificationPageReady() {
return false;
}
function getPhoneVerificationDisplayedPhone() {
return '';
}
function getVerificationCodeTarget() {
return null;
}
function getStep4PostVerificationState() {
return null;
}
function isVerificationPageStillVisible() {
return false;
}
async function ensureSignupPasswordPageReady() {
return { ready: true };
}
+27
View File
@@ -92,6 +92,10 @@ function getLoginEmailInput() {
return ${JSON.stringify(overrides.emailInput || null)};
}
function getLoginPhoneInput() {
return ${JSON.stringify(overrides.phoneInput || null)};
}
function findOneTimeCodeLoginTrigger() {
return ${JSON.stringify(overrides.switchTrigger || null)};
}
@@ -100,6 +104,14 @@ function findLoginEntryTrigger() {
return ${JSON.stringify(overrides.loginEntryTrigger || null)};
}
function findLoginPhoneEntryTrigger() {
return ${JSON.stringify(overrides.phoneEntryTrigger || null)};
}
function findLoginMoreOptionsTrigger() {
return ${JSON.stringify(overrides.moreOptionsTrigger || null)};
}
function getLoginSubmitButton() {
return ${JSON.stringify(overrides.submitButton || null)};
}
@@ -239,9 +251,24 @@ return {
assert.strictEqual(snapshot.state, 'entry_page');
}
{
const api = createApi({
phoneInput: { id: 'phone' },
submitButton: { id: 'submit' },
});
const snapshot = api.inspectLoginAuthState();
assert.strictEqual(snapshot.state, 'phone_entry_page');
}
assert.ok(
extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"),
'inspectLoginAuthState 应产出 oauth_consent_page 状态'
);
assert.ok(
extractFunction('inspectLoginAuthState').includes("state: 'phone_entry_page'"),
'inspectLoginAuthState 应产出 phone_entry_page 状态'
);
console.log('step6 login state tests passed');
+57 -35
View File
@@ -14,13 +14,13 @@
它的核心价值不是“打开一个页面点几个按钮”,而是把下面这些环节串成一条完整可恢复的自动化链路:
- 生成或选取注册邮箱
- 生成注册邮箱,或在接码开启时申请注册手机号
- 打开 ChatGPT / OpenAI 注册入口
- 提交邮箱和密码
- 轮询注册验证码
- 提交邮箱/手机号和密码
- 轮询注册验证码(邮箱或短信)
- 填写姓名和生日
- 刷新 OAuth 链接并登录
- 轮询登录验证码
- 刷新 OAuth 链接并登录(邮箱或手机号)
- 轮询登录验证码(邮箱或短信)
- 自动确认 OAuth 同意页
- 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API
@@ -41,9 +41,9 @@
- 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态
- 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `apikey.qzz.io` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现
- 在 sidepanel 初始化和点击“自动”按钮前刷新一次贡献站公开内容摘要;如果刷新失败,不阻塞主自动流程
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表
- 在日志区通过“记录”按钮打开独立的账号记录覆盖层,并展示成功/失败/停止/重试统计与分页列表phone-only 记录会回退展示手机号
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro``v` 版本误显示为比 `Ultra` 更新
- 展示一个单独的“接码”开关与“接码平台”下拉;接码平台当前支持 HeroSMS / 5sim,开启后才展示所选平台的国家、API Key、价格上限等设置,用于 OAuth 登录链路命中手机号验证页时直接续跑手机验证
- 展示一个单独的“接码”开关、注册方式 `signupMethod` 与“接码平台”下拉;接码平台当前支持 HeroSMS / 5sim / NexSMS。普通模式下开启接码后可把注册方式切到手机号注册,并在 OAuth 登录链路命中手机号登录验证页时继续复用同一手机号续跑短信验证
- 展示 `Plus 模式` 开关与 Plus 支付方式配置;支付方式支持 PayPal / GoPay,PayPal 展示账号池下拉与添加按钮,GoPay 展示手机号和 PIN;步骤列表切换为 Plus 模式 13 步定义,普通模式的 Cookie 清理步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步
- 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作
@@ -145,7 +145,10 @@
- `contributionStatusMessage`
- `contributionLastPollAt`
- OAuth 链接
- 当前轮冻结后的注册方式 `resolvedSignupMethod`
- 当前统一账号标识 `accountIdentifierType / accountIdentifier`
- 当前邮箱 / 密码
- 当前手机号注册运行态 `signupPhoneNumber / signupPhoneActivation / signupPhoneCompletedActivation`
- 第 8 步固定的验证码页显示邮箱 `step8VerificationTargetEmail`
- 当前手机号验证激活记录 `currentPhoneActivation`
- 可复用的手机号验证激活记录 `reusablePhoneActivation`
@@ -178,12 +181,13 @@
- 2925 是否启用号池模式 `mail2925UseAccountPool`
- 2925 当前选中的号池账号 ID `currentMail2925AccountId`
- Cloudflare / Temp Email 设置
- 注册方式 `signupMethod`
- 接码开关、接码平台 `phoneSmsProvider`,以及 HeroSMS / 5sim 各自的 API Key、国家/地区、价格上限和平台扩展设置
- iCloud 相关偏好
- LuckMail API 配置
- 自动运行默认配置
- OAuth 授权后链总超时开关 `oauthFlowTimeoutEnabled`:默认开启;关闭后会立即清空已存在的 OAuth 总预算 deadline,仅保留各步骤本地等待超时
- 账号运行历史 `accountRunHistory`(以邮箱为主键,保存该邮箱最近一次状态:成功/失败/停止)
- 账号运行历史 `accountRunHistory`(以统一账号标识为主键,邮箱账号保留邮箱主键;phone-only 账号改存 `accountIdentifierType / accountIdentifier / phoneNumber`,保存最近一次状态:成功/失败/停止)
- 账号运行历史本地同步 helper 地址
注意:
@@ -311,14 +315,15 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
流程:
1. 解析本轮应使用的邮箱
1. 解析本轮应使用的注册方式:默认邮箱注册,开启接码且普通模式下可切到手机号注册
2. 打开或复用注册页
3. 如果注册弹窗默认停留在手机号输入模式,会先尝试点击 `继续使用电子邮件地址登录 / Continue using email address` 一类按钮切回邮箱输入模式
4. 邮箱输入模式下提交邮箱;邮箱输入框识别同时兼容本地化占位与 `aria-label`
5. 以当前邮箱先写入一条“停止(流程尚未完成)”的记录占位
6. 等待邮箱提交后的真实落地页
7. 如果进入密码页,则继续执行 Step 3
8. 如果直接进入邮箱验证码页,则自动跳过 Step 3 并进入 Step 4
3. 邮箱注册时,如果注册弹窗默认停留在手机号输入模式,会先尝试点击 `继续使用电子邮件地址登录 / Continue using email address` 一类按钮切回邮箱输入模式
4. 邮箱注册分支:提交邮箱;邮箱输入框识别同时兼容本地化占位与 `aria-label`
5. 手机号注册分支:从接码平台申请号码,切换到手机号注册入口,必要时展开更多选项,填写手机号并提交
6. 以当前统一账号标识先写入一条“停止(流程尚未完成)”的记录占位;邮箱账号占位邮箱,phone-only 账号占位手机号
7. 等待身份提交后的真实落地页
8. 如果进入密码页,则继续执行 Step 3
9. 如果直接进入邮箱验证码页或手机验证码页,则自动跳过 Step 3 并进入 Step 4
### Step 3
@@ -331,13 +336,17 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
1. 生成或读取密码
2. 更新运行态密码
3. 记录账号快照
4. 让内容脚本填写密码
4. 让内容脚本按当前账号身份填写密码
5. 内容脚本先上报 Step 3 完成信号
6. 上报完成后再异步点击提交,避免页面跳转打断响应通道
7. 延迟提交真正触发前会再次检查 Stop 状态,避免用户已停止时页面仍继续自动提交
8. 后台在真正确认 Step 3 完成前,会额外检查提交后是否切换页面;如果出现认证页 `Try again / 重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路
9. Step 3 收尾阶段如果页面切换导致旧内容脚本失联,后台会把单次消息等待收口到当前收尾预算内,优先尽快重试重连;若最终仍未恢复,则输出中文的步骤级错误,而不是直接暴露底层英文通信超时
补充:
- 手机号注册如果在 Step 2 后直接落到短信验证码页、资料页或已登录页,Step 3 会按页面状态自动跳过,不再因为缺少邮箱而失败。
### Step 4 / Step 8
文件:
@@ -348,12 +357,15 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
这两步共享验证码主流程:
1. 确定 provider
2. 必要时重发验证码
3. 轮询邮箱或 API
4. 提取验证码
5. 回填页面
6. 若页面拒绝,则重试或回退
1. 先按账号身份分流验证码通道:
Step 4:邮箱注册走邮箱验证码,手机号注册走短信验证码
Step 8:邮箱登录走邮箱验证码,手机号登录走短信验证码
2. 确定 provider
3. 必要时重发验证码
4. 轮询邮箱、短信 API 或对应 provider 通道
5. 提取验证码
6. 回填页面
7. 若页面拒绝,则重试或回退
补充行为:
@@ -413,11 +425,15 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
- SUB2API:打开后台并生成 OAuth 地址
- Codex2API:直接调用后台协议 `/api/admin/oauth/generate-auth-url`
2. 打开最新 OAuth 链接
3. 登录;如果进入密码页且当前有密码,则填写并提交密码;如果当前没有密码但检测到一次性验证码入口,则直接切换到一次性验证码登录
4. 确保真正进入验证码页
5. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次;但一旦当前失败已经明确进入 `https://auth.openai.com/add-phone`,则立即退出步骤 7 内部重试,不再继续第 2 / 3 次尝试
6. 自动运行一旦进入步骤 7 之后的链路,若后续步骤报错且认证页未进入 `https://auth.openai.com/add-phone`,则统一回到步骤 7 重新开始授权流程
7. CPA 回调阶段固定为步骤 9,不再支持“第七步回调”跳过步骤 7/8
3. 根据统一账号标识选择登录身份:
- 邮箱账号:沿用现有邮箱登录链路
- 手机号账号:优先探测登录页是否存在手机号登录入口,必要时展开更多选项或从邮箱页切到手机号页
4. 登录;如果进入密码页且当前有密码,则填写并提交密码;如果当前没有密码但检测到一次性验证码入口,则直接切换到一次性验证码登录
5. 确保真正进入验证码页;手机号登录允许以 `phone-verification` 作为 Step 7 的成功落点
6. 如果页面没有可用的手机号登录入口,则 Step 7 直接抛出明确业务错误,不再误进入 Step 8 邮箱轮询
7. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次;但一旦当前失败已经明确进入 `https://auth.openai.com/add-phone`,则立即退出步骤 7 内部重试,不再继续第 2 / 3 次尝试
8. 自动运行一旦进入步骤 7 之后的链路,若后续步骤报错且认证页未进入 `https://auth.openai.com/add-phone`,则统一回到步骤 7 重新开始授权流程
9. CPA 回调阶段固定为步骤 9,不再支持“第七步回调”跳过步骤 7/8
贡献模式补充:
@@ -432,14 +448,19 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
流程:
1. 确认当前认证页已经进入登录验证码
2. 对非 2925 provider,固定当前验证码页显示邮箱为本次 Step 8 的目标邮箱
3. 打开邮箱页或 API 轮询入口
4. 轮询登录验证码
5. 回填登录验证码
6. 如果登录验证码提交后页面进入 `add-phone / 手机号页`,则步骤 8 会保留“登录验证码已提交成功”的结果,并把后续手机号验证需求继续交给步骤 9 处理
7. 如遇邮箱轮询类失败或显式的 `STEP8_RESTART_STEP7` 恢复错误,则按有限次数回到 Step 7 重试
8. 获取到登录验证码后不再触发“刷新 OAuth 并重走 Step 7”的前置回放,直接在当前验证码页提交并继续进入 Step 9
1. 先按账号身份判断当前登录验证码通道
2. 邮箱账号:
- 确认当前认证页已经进入邮箱验证码页
- 对非 2925 provider,固定当前验证码页显示邮箱为本次 Step 8 的目标邮箱
- 打开邮箱页或 API 轮询入口
- 轮询登录验证码并回填
3. 手机号账号:
- 直接复用注册成功后的同一手机号订单,不重新申请新号码
- 重新激活该号码,轮询短信验证码
- 在当前 `phone-verification` 页回填短信验证码
4. 如果登录验证码提交后页面进入 `add-phone / 手机号页`,则步骤 8 会保留“登录验证码已提交成功”的结果,并把后续手机号验证需求继续交给步骤 9 处理
5. 如遇邮箱轮询类失败或显式的 `STEP8_RESTART_STEP7` 恢复错误,则按有限次数回到 Step 7 重试
6. 获取到登录验证码后不再触发“刷新 OAuth 并重走 Step 7”的前置回放,直接在当前验证码页提交并继续进入 Step 9
补充:
@@ -447,6 +468,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
- 对 `2925 receive` 而言,Step 8 会把当前目标注册邮箱一并传给 2925 内容脚本;只有当邮件里显式写出了其他邮箱时才会跳过,从而在不破坏历史兼容性的前提下,尽量降低误收验证码的概率。
- 对 `custom` provider 而言,Step 8 仍使用手动验证码确认弹窗;弹窗当前额外提供“出现手机号验证”按钮,点击后会直接抛出与真实 `add-phone` 页面一致的 fatal 错误,供 Auto 按既有 add-phone 分支继续下一邮箱。
- 当登录验证码提交后进入 `phone-verification` 页时,内容脚本会显式把该页面识别为“手机验证码页”,避免与邮箱验证码页混淆。
- 手机号登录分支不会再读取邮箱 provider 配置,也不会误调用邮箱轮询;如果缺少可复用的注册手机号激活记录,会直接要求回到步骤 7 重新开始登录。
### Step 9
+9 -9
View File
@@ -43,7 +43,7 @@
## `background/`
- `background/account-run-history.js`邮箱记录模块,负责以邮箱为主键维护最新记录(同邮箱后续状态覆盖),在步骤 2 设定邮箱后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`支持清理记录,并按默认本地 helper 地址自动尝试把完整快照同步到本地 helper;若 helper 未启动,则静默跳过。
- `background/account-run-history.js`账号记录模块,负责以统一账号标识维护最新记录(优先邮箱,phone-only 时改用 `accountIdentifier / phoneNumber`),在步骤 2 设定身份后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`兼容 phone-only 记录与空密码,并按默认本地 helper 地址自动尝试把完整快照同步到本地 helper;若 helper 未启动,则静默跳过。
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 Plus 模式与 PayPal 配置、`gmailBaseEmail``mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失关键持久配置。
- `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。
@@ -54,7 +54,7 @@
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;`LOG / STEP_COMPLETE / STEP_ERROR` 会把消息里的真实 `step` 继续传给结构化日志,跳过登录验证码这类派生日志也按当前步骤 key 写入;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息;当侧栏关闭 `oauthFlowTimeoutEnabled` 时,会立即清空已存在的 OAuth 总预算 deadline。
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
- `background/paypal-account-store.js`:PayPal 账号池持久化模块,负责保存账号列表、切换当前账号,并把当前选中账号同步回兼容字段 `paypalEmail / paypalPassword`
- `background/phone-verification-flow.js`:手机号验证共享流程模块,负责 OAuth 链路命中 `add-phone / phone-verification` 页面后向 HeroSMS 申请或复用号码、轮询短信验证码、提交手机号码与短信验证码,并在号码长期收不到短信时把后续自动流拉回授权登录步骤重新拿号;该流程由 OAuth 确认步骤传入当前可见步骤号,手机号验证日志不会再固定显示普通模式 Step 9。
- `background/phone-verification-flow.js`:手机号验证共享流程模块,负责复用 HeroSMS / 5sim / NexSMS 的取号、复用、轮询、重发、换号与收尾能力;既承接 OAuth 后置 `add-phone / phone-verification` 页面,也承接手机号注册 Step 4 和手机号 OAuth 登录 Step 8,并保持 `signupPhone*` 运行态与 `currentPhoneActivation` 隔离;该流程由调用方传入当前可见步骤号,手机号日志不会再固定显示普通模式 Step 9。
- `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;内容脚本恢复等待日志支持 `logStep / logStepKey`,用于保持 Plus 复用步骤的日志标签正确;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
@@ -65,18 +65,18 @@
- `background/steps/clear-login-cookies.js`:步骤 6 实现,负责登录前 Cookie 清理。
- `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算开关,关闭后仅保留本地回调等待超时。
- `background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 Plus checkout session,打开 `chatgpt.com/checkout/openai_ie/{checkout_session_id}` 短链,并记录 checkout 运行态。
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责登录验证码阶段的邮箱轮询、验证码回填与回退控制;验证码获取后直接提交,不再在提交前回放步骤 7 刷新 OAuth;对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责按账号身份分发登录验证码流程:邮箱账号继续走邮箱轮询、验证码回填与回退控制;手机号账号则直接复用注册成功后的同一手机号订单,轮询短信验证码并在当前认证页提交;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;邮箱注册继续走邮箱验证码,手机号注册改走短信验证码;当 provider 为 2925 时,会在邮箱轮询前先确保当前 2925 账号已自动登录。
- `background/steps/fill-plus-checkout.js`:Plus 模式第 7 步实现,负责驱动 checkout 页面选择 PayPal、生成账单姓名、读取本地地址 seed、触发地址推荐、提交订阅并等待跳转到 PayPal。
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交。
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交;当前已改为身份中立,手机号注册遇到无密码页时会按页面状态自动跳过
- `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。
- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。
- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;当前会根据 `accountIdentifierType / accountIdentifier` 选择邮箱或手机号登录,手机号账号会先探测并切换手机号登录入口;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。
- `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。
- `background/steps/paypal-approve.js`:Plus 模式第 8 步实现,负责驱动 PayPal 登录、关闭可见通行密钥提示、点击“同意并继续”,并等待授权流程离开 PayPal 页面。
- `background/steps/platform-verify.js`:平台回调验证实现,普通模式为步骤 10,Plus 模式为可见步骤 13;负责 CPA / SUB2API 回调验证,以及 Codex2API 的协议式 callback code/state 交换,所有平台验证日志和完成信号都按当前可见步骤上报。
- `background/steps/plus-return-confirm.js`:Plus 模式第 9 步实现,负责等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒再完成。
- `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与提交后落地页分支判断
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责`signupMethod` 在邮箱注册与手机号注册之间分发;邮箱分支继续提交邮箱,手机号分支会申请接码平台号码、切换手机号注册入口并提交手机号,再根据落地页判断是否跳过后续密码步骤
## `content/`
@@ -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 会在注册弹窗默认处于手机号输入模式时自动切回邮箱输入模式,并兼容本地化邮箱占位与 `aria-label`;登录链路还会显式识别 `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。
@@ -132,7 +132,7 @@
- `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
- `sidepanel/mail-2925-manager.js`:侧边栏 2925 账号池管理器,负责 2925 账号的新增、导入、切换、手动登录、启停、清冷却与删除。
- `sidepanel/form-dialog.js`:侧边栏公共表单弹窗模块,负责渲染可复用的小型表单弹窗,支持动态字段、校验、确认与取消。
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。
- `sidepanel/account-records-manager.js`:侧边栏账号记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认;展示时优先显示邮箱,phone-only 记录则回退显示手机号或统一账号标识
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行(包括 Codex2API 配置)的禁用与隐藏。
- `sidepanel/ip-proxy-panel.js`:侧边栏 IP 代理面板逻辑,负责代理配置显隐、服务/模式切换、账号串参数同步、状态卡渲染,以及同步、下一条、Change、检测出口、检查 IP 等按钮行为。