diff --git a/background.js b/background.js index 12df754..d665f84 100644 --- a/background.js +++ b/background.js @@ -245,6 +245,10 @@ const CONTRIBUTION_SOURCE_CPA = 'cpa'; const CONTRIBUTION_SOURCE_SUB2API = 'sub2api'; const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池'; const CONTRIBUTION_SUB2API_PLUS_GROUP_NAME = 'openai-plus'; +const DEFAULT_SUB2API_GROUP_NAMES = [ + DEFAULT_SUB2API_GROUP_NAME, + CONTRIBUTION_SUB2API_PLUS_GROUP_NAME, +]; const DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback'; const DEFAULT_IP_PROXY_SERVICE = '711proxy'; const IP_PROXY_SERVICE_VALUES = ['711proxy', 'lumiproxy', 'iproyal', 'omegaproxy']; @@ -568,6 +572,7 @@ const PERSISTED_SETTING_DEFAULTS = { sub2apiEmail: '', sub2apiPassword: '', sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME, + sub2apiGroupNames: DEFAULT_SUB2API_GROUP_NAMES, sub2apiDefaultProxyName: DEFAULT_SUB2API_PROXY_NAME, ipProxyEnabled: false, ipProxyService: DEFAULT_IP_PROXY_SERVICE, @@ -2058,6 +2063,24 @@ function resolveCloudflareTempEmailPollTargetEmail(state = {}, pollPayload = {}, return normalizeCloudflareTempEmailReceiveMailbox(state.email); } +function normalizeSub2ApiGroupNames(value = '') { + const source = Array.isArray(value) + ? value + : String(value || '').split(/[\r\n,,、]+/); + const names = []; + const seen = new Set(); + for (const item of source) { + const name = String(item || '').trim(); + const key = name.toLowerCase(); + if (!key || seen.has(key)) { + continue; + } + seen.add(key); + names.push(name); + } + return names; +} + function normalizePersistentSettingValue(key, value) { switch (key) { case 'panelMode': @@ -2076,6 +2099,8 @@ function normalizePersistentSettingValue(key, value) { return String(value || ''); case 'sub2apiGroupName': return String(value || '').trim(); + case 'sub2apiGroupNames': + return normalizeSub2ApiGroupNames(value); case 'sub2apiDefaultProxyName': return String(value || '').trim(); case 'ipProxyEnabled': @@ -2413,6 +2438,18 @@ function buildPersistentSettingsPayload(input = {}, options = {}) { } payload.cloudflareTempEmailDomains = domains; } + if ( + Object.prototype.hasOwnProperty.call(payload, 'sub2apiGroupName') + || Object.prototype.hasOwnProperty.call(payload, 'sub2apiGroupNames') + ) { + const groupNames = normalizeSub2ApiGroupNames([ + ...(Array.isArray(payload.sub2apiGroupNames) ? payload.sub2apiGroupNames : []), + payload.sub2apiGroupName, + ]); + payload.sub2apiGroupNames = groupNames.length + ? groupNames + : [...DEFAULT_SUB2API_GROUP_NAMES]; + } const nextSignupConstraintState = { ...PERSISTED_SETTING_DEFAULTS, ...payload, @@ -10137,6 +10174,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe ensureHotmailAccountForFlow, ensureMail2925AccountForFlow, ensureLuckmailPurchaseForFlow, + fetchGeneratedEmail, getTabId, isGeneratedAliasProvider, isReusableGeneratedAliasEmail, @@ -10157,6 +10195,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe reuseOrCreateTab, sendToContentScriptResilient, setEmailState, + setState, SIGNUP_ENTRY_URL, SIGNUP_PAGE_INJECT_FILES, waitForTabStableComplete, @@ -10256,6 +10295,7 @@ const step3Executor = self.MultiPageBackgroundStep3?.createStep3Executor({ generatePassword, getTabId, isTabAlive, + resolveSignupMethod, sendToContentScript, setPasswordState, setState, diff --git a/background/signup-flow-helpers.js b/background/signup-flow-helpers.js index 36671c2..b85736e 100644 --- a/background/signup-flow-helpers.js +++ b/background/signup-flow-helpers.js @@ -10,6 +10,7 @@ ensureHotmailAccountForFlow, ensureMail2925AccountForFlow, ensureLuckmailPurchaseForFlow, + fetchGeneratedEmail, isGeneratedAliasProvider, isReusableGeneratedAliasEmail, isHotmailProvider, @@ -22,6 +23,7 @@ reuseOrCreateTab, sendToContentScriptResilient, setEmailState, + setState, SIGNUP_ENTRY_URL, SIGNUP_PAGE_INJECT_FILES, waitForTabStableComplete = null, @@ -256,8 +258,52 @@ return result || {}; } - async function resolveSignupEmailForFlow(state) { + function getPreservedPhoneIdentityForEmailResolution(state = {}, options = {}) { + if (!Boolean(options?.preserveAccountIdentity)) { + return null; + } + const accountIdentifierType = String(state?.accountIdentifierType || '').trim().toLowerCase(); + const signupPhoneNumber = String( + state?.signupPhoneNumber + || (accountIdentifierType === 'phone' ? state?.accountIdentifier : '') + || state?.signupPhoneCompletedActivation?.phoneNumber + || state?.signupPhoneActivation?.phoneNumber + || '' + ).trim(); + if (accountIdentifierType !== 'phone' && !signupPhoneNumber) { + return null; + } + return { + accountIdentifierType: 'phone', + accountIdentifier: signupPhoneNumber || String(state?.accountIdentifier || '').trim(), + signupPhoneNumber, + signupPhoneActivation: state?.signupPhoneActivation || null, + signupPhoneCompletedActivation: state?.signupPhoneCompletedActivation || null, + signupPhoneVerificationRequestedAt: state?.signupPhoneVerificationRequestedAt ?? null, + signupPhoneVerificationPurpose: state?.signupPhoneVerificationPurpose || '', + }; + } + + async function persistResolvedSignupEmail(resolvedEmail, state = {}, options = {}) { + if (resolvedEmail === state.email && !options?.preserveAccountIdentity) { + return; + } + const preservedPhoneIdentity = getPreservedPhoneIdentityForEmailResolution(state, options); + if (preservedPhoneIdentity && typeof setState === 'function') { + await setState({ + email: resolvedEmail, + ...preservedPhoneIdentity, + }); + return; + } + if (resolvedEmail !== state.email) { + await setEmailState(resolvedEmail); + } + } + + async function resolveSignupEmailForFlow(state, options = {}) { let resolvedEmail = state.email; + let generatedEmailAlreadyPersisted = false; if (isHotmailProvider(state)) { const account = await ensureHotmailAccountForFlow({ allowAllocate: true, @@ -281,14 +327,17 @@ if (!isReusableGeneratedAliasEmail?.(state, resolvedEmail)) { resolvedEmail = buildGeneratedAliasEmail(state); } + } else if (!resolvedEmail && typeof fetchGeneratedEmail === 'function') { + resolvedEmail = await fetchGeneratedEmail(state, options); + generatedEmailAlreadyPersisted = true; } if (!resolvedEmail) { throw new Error('缺少邮箱地址,请先在侧边栏粘贴邮箱。'); } - if (resolvedEmail !== state.email) { - await setEmailState(resolvedEmail); + if (!generatedEmailAlreadyPersisted || options?.preserveAccountIdentity) { + await persistResolvedSignupEmail(resolvedEmail, state, options); } return resolvedEmail; diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 41851d2..b0c7fcc 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -145,7 +145,9 @@ } const latestState = typeof getState === 'function' ? await getState() : state; - const resolvedEmail = await resolveSignupEmailForFlow(latestState); + const resolvedEmail = await resolveSignupEmailForFlow(latestState, { + preserveAccountIdentity: true, + }); await addLog(`步骤 ${visibleStep}:检测到添加邮箱页,正在添加邮箱 ${resolvedEmail} 并进入邮箱验证码页...`); const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' diff --git a/background/steps/fill-password.js b/background/steps/fill-password.js index 1f1dc55..9d92d24 100644 --- a/background/steps/fill-password.js +++ b/background/steps/fill-password.js @@ -9,22 +9,47 @@ generatePassword, getTabId, isTabAlive, + resolveSignupMethod, sendToContentScript, setPasswordState, setState, SIGNUP_PAGE_INJECT_FILES, } = deps; + function normalizeSignupMethod(value = '') { + return String(value || '').trim().toLowerCase() === 'phone' + ? 'phone' + : 'email'; + } + + function getResolvedSignupMethodForStep3(state = {}) { + if (typeof resolveSignupMethod === 'function') { + return normalizeSignupMethod(resolveSignupMethod(state)); + } + const frozenMethod = String(state?.resolvedSignupMethod || '').trim().toLowerCase(); + if (frozenMethod === 'phone' || frozenMethod === 'email') { + return normalizeSignupMethod(frozenMethod); + } + return normalizeSignupMethod(state?.signupMethod); + } + function resolveStep3AccountIdentity(state = {}) { const resolvedEmail = String(state?.email || '').trim(); + const rawAccountIdentifierType = String(state?.accountIdentifierType || '').trim().toLowerCase(); const signupPhoneNumber = String( state?.signupPhoneNumber - || (state?.accountIdentifierType === 'phone' ? state?.accountIdentifier : '') + || (rawAccountIdentifierType === 'phone' ? state?.accountIdentifier : '') || '' ).trim(); - const accountIdentifierType = signupPhoneNumber && state?.accountIdentifierType === 'phone' + const explicitEmailIdentity = rawAccountIdentifierType === 'email' && resolvedEmail; + const shouldUsePhoneIdentity = !explicitEmailIdentity && ( + rawAccountIdentifierType === 'phone' + || Boolean(signupPhoneNumber) + || getResolvedSignupMethodForStep3(state) === 'phone' + ); + const accountIdentifierType = shouldUsePhoneIdentity ? 'phone' - : (resolvedEmail ? 'email' : (signupPhoneNumber ? 'phone' : 'email')); + : (resolvedEmail ? 'email' : 'email'); const accountIdentifier = accountIdentifierType === 'phone' ? signupPhoneNumber : resolvedEmail; @@ -40,6 +65,9 @@ async function executeStep3(state) { const identity = resolveStep3AccountIdentity(state); if (!identity.accountIdentifier) { + if (identity.accountIdentifierType === 'phone') { + throw new Error('缺少注册手机号,请先完成步骤 2 或在侧栏填写注册手机号后再执行步骤 3。'); + } throw new Error('缺少注册账号,请先完成步骤 2。'); } diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 8f23474..45cb3c8 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -1971,6 +1971,102 @@ header { .data-select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); } [data-theme="dark"] .data-select { color-scheme: dark; } +.sub2api-group-picker { + position: relative; + flex: 1; + min-width: 0; +} + +.sub2api-group-trigger { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + font-family: 'JetBrains Mono', monospace; + text-align: left; +} + +.sub2api-group-caret { + width: 8px; + height: 8px; + border-right: 1.5px solid currentColor; + border-bottom: 1.5px solid currentColor; + transform: translateY(-2px) rotate(45deg); + flex: 0 0 auto; + opacity: 0.7; +} + +.sub2api-group-trigger.is-open .sub2api-group-caret { + transform: translateY(2px) rotate(225deg); +} + +.sub2api-group-menu { + position: absolute; + z-index: 70; + top: calc(100% + 4px); + left: 0; + right: 0; + max-height: 220px; + overflow: auto; + padding: 4px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-base); + box-shadow: var(--shadow-md); +} + +.sub2api-group-option-row { + display: flex; + align-items: center; + gap: 4px; + min-height: 30px; +} + +.sub2api-group-option, +.sub2api-group-delete { + border: none; + border-radius: var(--radius-sm); + background: transparent; + color: var(--text-secondary); + cursor: pointer; + transition: background var(--transition), color var(--transition); +} + +.sub2api-group-option { + flex: 1; + min-width: 0; + padding: 6px 8px; + text-align: left; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sub2api-group-option[aria-selected="true"] { + color: var(--blue); + background: var(--blue-soft); +} + +.sub2api-group-option:hover, +.sub2api-group-delete:hover:not(:disabled) { + color: var(--text-primary); + background: var(--bg-hover); +} + +.sub2api-group-delete { + flex: 0 0 auto; + padding: 5px 7px; + font-size: 11px; +} + +.sub2api-group-delete:disabled { + cursor: not-allowed; + opacity: 0.42; +} + .data-check { display: flex; align-items: flex-start; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index e1f387f..b509b79 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -197,7 +197,18 @@