feat: 增强步骤 3 的手机号注册逻辑,确保手机号身份优先处理并添加相关测试用例

This commit is contained in:
QLHazyCoder
2026-05-05 02:47:57 +08:00
parent 16baad20a3
commit cb41f5c243
15 changed files with 735 additions and 19 deletions
+40
View File
@@ -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,
+52 -3
View File
@@ -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;
+3 -1
View File
@@ -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'
+31 -3
View File
@@ -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。');
}
+96
View File
@@ -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;
+12 -1
View File
@@ -197,7 +197,18 @@
</div>
<div class="data-row" id="row-sub2api-group" style="display:none;">
<span class="data-label">分组</span>
<input type="text" id="input-sub2api-group" class="data-input" placeholder="默认 codex;多个用逗号或换行分隔" />
<div class="data-inline">
<input type="hidden" id="input-sub2api-group" value="codex" />
<div id="sub2api-group-picker" class="sub2api-group-picker">
<button id="btn-sub2api-group-menu" class="data-select sub2api-group-trigger" type="button"
aria-haspopup="listbox" aria-expanded="false">
<span id="sub2api-group-current" class="truncate">codex</span>
<span class="sub2api-group-caret" aria-hidden="true"></span>
</button>
<div id="sub2api-group-menu" class="sub2api-group-menu" role="listbox" hidden></div>
</div>
<button id="btn-add-sub2api-group" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
</div>
</div>
<div class="data-row" id="row-sub2api-default-proxy" style="display:none;">
<span class="data-label">默认代理</span>
+309 -8
View File
@@ -95,6 +95,11 @@ const rowSub2ApiPassword = document.getElementById('row-sub2api-password');
const inputSub2ApiPassword = document.getElementById('input-sub2api-password');
const rowSub2ApiGroup = document.getElementById('row-sub2api-group');
const inputSub2ApiGroup = document.getElementById('input-sub2api-group');
const sub2ApiGroupPicker = document.getElementById('sub2api-group-picker');
const btnSub2ApiGroupMenu = document.getElementById('btn-sub2api-group-menu');
const sub2ApiGroupCurrent = document.getElementById('sub2api-group-current');
const sub2ApiGroupMenu = document.getElementById('sub2api-group-menu');
const btnAddSub2ApiGroup = document.getElementById('btn-add-sub2api-group');
const rowSub2ApiDefaultProxy = document.getElementById('row-sub2api-default-proxy');
const inputSub2ApiDefaultProxy = document.getElementById('input-sub2api-default-proxy');
const rowIpProxyEnabled = document.getElementById('row-ip-proxy-enabled');
@@ -1092,6 +1097,138 @@ let configActionInFlight = false;
let currentReleaseSnapshot = null;
let currentContributionContentSnapshot = null;
let contributionContentSnapshotRequestInFlight = null;
const DEFAULT_SUB2API_GROUP_OPTIONS = ['codex', 'openai-plus'];
let sub2ApiGroupMenuOpen = false;
function splitSub2ApiGroupNames(value = '') {
return String(value || '')
.split(/[\r\n,,、]+/)
.map((name) => name.trim())
.filter(Boolean);
}
function normalizeSub2ApiGroupOptions(...sources) {
const groups = [];
const seen = new Set();
const append = (value) => {
if (Array.isArray(value)) {
value.forEach(append);
return;
}
for (const name of splitSub2ApiGroupNames(value)) {
const key = name.toLowerCase();
if (!key || seen.has(key)) {
continue;
}
seen.add(key);
groups.push(name);
}
};
sources.forEach(append);
return groups;
}
function getSelectedSub2ApiGroupName() {
return String(inputSub2ApiGroup?.value || '').trim()
|| DEFAULT_SUB2API_GROUP_OPTIONS[0];
}
function getSub2ApiGroupOptionsState(state = latestState) {
const options = normalizeSub2ApiGroupOptions(
state?.sub2apiGroupNames,
state?.sub2apiGroupName
);
return options.length ? options : [...DEFAULT_SUB2API_GROUP_OPTIONS];
}
function setSub2ApiGroupMenuOpen(open) {
sub2ApiGroupMenuOpen = Boolean(open);
if (sub2ApiGroupMenu) {
sub2ApiGroupMenu.hidden = !sub2ApiGroupMenuOpen;
}
if (btnSub2ApiGroupMenu) {
btnSub2ApiGroupMenu.setAttribute('aria-expanded', sub2ApiGroupMenuOpen ? 'true' : 'false');
btnSub2ApiGroupMenu.classList?.toggle('is-open', sub2ApiGroupMenuOpen);
}
}
function setSub2ApiGroupSelection(groupName, options = {}) {
const selected = String(groupName || '').trim() || DEFAULT_SUB2API_GROUP_OPTIONS[0];
if (inputSub2ApiGroup) {
inputSub2ApiGroup.value = selected;
if (options.emit && typeof inputSub2ApiGroup.dispatchEvent === 'function') {
inputSub2ApiGroup.dispatchEvent(new Event('change', { bubbles: true }));
}
}
if (sub2ApiGroupCurrent) {
sub2ApiGroupCurrent.textContent = selected;
}
}
function renderSub2ApiGroupOptions(state = latestState, selectedValue = '') {
if (!inputSub2ApiGroup) {
return;
}
const selected = String(selectedValue || state?.sub2apiGroupName || '').trim();
const options = getSub2ApiGroupOptionsState({
...(state || {}),
sub2apiGroupName: selected || state?.sub2apiGroupName,
});
if (selected && !options.some((name) => name.toLowerCase() === selected.toLowerCase())) {
options.unshift(selected);
}
if (
!sub2ApiGroupMenu
|| typeof sub2ApiGroupMenu.appendChild !== 'function'
|| typeof document === 'undefined'
|| typeof document.createElement !== 'function'
) {
setSub2ApiGroupSelection(selected || options[0] || DEFAULT_SUB2API_GROUP_OPTIONS[0]);
return;
}
sub2ApiGroupMenu.innerHTML = '';
options.forEach((name) => {
const row = document.createElement('div');
row.className = 'sub2api-group-option-row';
const option = document.createElement('button');
option.type = 'button';
option.className = 'sub2api-group-option';
option.setAttribute('role', 'option');
option.setAttribute('aria-selected', name === selected ? 'true' : 'false');
option.textContent = name;
option.addEventListener('click', () => {
setSub2ApiGroupSelection(name, { emit: true });
setSub2ApiGroupMenuOpen(false);
});
const deleteButton = document.createElement('button');
deleteButton.type = 'button';
deleteButton.className = 'sub2api-group-delete';
deleteButton.textContent = '删除';
deleteButton.title = `删除分组 ${name}`;
deleteButton.setAttribute('aria-label', `删除分组 ${name}`);
deleteButton.disabled = options.length <= 1;
deleteButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
handleDeleteSub2ApiGroup(name).catch((error) => {
showToast(error?.message || '删除 SUB2API 分组失败。', 'error');
});
});
row.appendChild(option);
row.appendChild(deleteButton);
sub2ApiGroupMenu.appendChild(row);
});
setSub2ApiGroupSelection(selected || options[0] || DEFAULT_SUB2API_GROUP_OPTIONS[0]);
}
let customEmailPoolEntriesState = [];
const EYE_OPEN_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
@@ -3118,6 +3255,35 @@ function collectSettingsPayload() {
: (String(latestState?.signupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'))
);
const plusPaymentMethod = getSelectedPlusPaymentMethod();
const selectedSub2ApiGroupName = String(inputSub2ApiGroup.value || '').trim();
const sub2apiGroupNames = [];
const seenSub2ApiGroupNames = new Set();
const appendSub2ApiGroupNames = (value) => {
if (Array.isArray(value)) {
value.forEach(appendSub2ApiGroupNames);
return;
}
String(value || '')
.split(/[\r\n,,、]+/)
.map((name) => name.trim())
.filter(Boolean)
.forEach((name) => {
const key = name.toLowerCase();
if (!key || seenSub2ApiGroupNames.has(key)) {
return;
}
seenSub2ApiGroupNames.add(key);
sub2apiGroupNames.push(name);
});
};
[
latestState?.sub2apiGroupNames,
latestState?.sub2apiGroupName,
selectedSub2ApiGroupName,
].forEach(appendSub2ApiGroupNames);
if (sub2apiGroupNames.length === 0) {
appendSub2ApiGroupNames(['codex', 'openai-plus']);
}
return {
...(contributionModeEnabled ? {} : {
panelMode: selectPanelMode.value,
@@ -3128,7 +3294,8 @@ function collectSettingsPayload() {
sub2apiUrl: inputSub2ApiUrl.value.trim(),
sub2apiEmail: inputSub2ApiEmail.value.trim(),
sub2apiPassword: inputSub2ApiPassword.value,
sub2apiGroupName: inputSub2ApiGroup.value.trim(),
sub2apiGroupName: selectedSub2ApiGroupName,
sub2apiGroupNames,
sub2apiDefaultProxyName: inputSub2ApiDefaultProxy.value.trim(),
ipProxyEnabled: getSelectedIpProxyEnabledSafe(),
ipProxyService: selectedIpProxyService,
@@ -7015,6 +7182,18 @@ function getRuntimeSignupPhoneValue(state = latestState) {
).trim();
}
function shouldExecuteStep3WithSignupPhoneIdentity(state = latestState) {
const identifierType = String(state?.accountIdentifierType || '').trim().toLowerCase();
const resolvedMethod = normalizeSignupMethod(
state?.resolvedSignupMethod
|| state?.signupMethod
|| (typeof getSelectedSignupMethod === 'function' ? getSelectedSignupMethod() : DEFAULT_SIGNUP_METHOD)
);
return identifierType === 'phone'
|| Boolean(getRuntimeSignupPhoneValue(state))
|| resolvedMethod === SIGNUP_METHOD_PHONE;
}
function getSignupPhoneInputValue() {
return typeof inputSignupPhone !== 'undefined' && inputSignupPhone
? String(inputSignupPhone.value || '').trim()
@@ -7715,7 +7894,7 @@ function applySettingsState(state) {
inputSub2ApiUrl.value = state?.sub2apiUrl || '';
inputSub2ApiEmail.value = state?.sub2apiEmail || '';
inputSub2ApiPassword.value = state?.sub2apiPassword || '';
inputSub2ApiGroup.value = state?.sub2apiGroupName || '';
renderSub2ApiGroupOptions(state, state?.sub2apiGroupName || '');
inputSub2ApiDefaultProxy.value = state?.sub2apiDefaultProxyName || '';
const normalizedIpProxyService = resolveIpProxyService(state?.ipProxyService);
const normalizedIpProxyServiceProfiles = typeof normalizeIpProxyServiceProfiles === 'function'
@@ -9336,6 +9515,90 @@ async function saveCloudflareTempEmailDomainSettings(domains, activeDomain, opti
}
}
async function handleAddSub2ApiGroup() {
if (!sharedFormDialog?.open) {
showToast('表单弹窗未加载,请刷新扩展后重试。', 'error');
return;
}
const result = await sharedFormDialog.open({
title: '添加 SUB2API 分组',
confirmLabel: '添加',
confirmVariant: 'btn-primary',
fields: [
{
key: 'groupName',
label: '分组',
type: 'text',
placeholder: '例如 openai-plus',
autocomplete: 'off',
required: true,
requiredMessage: '请先填写 SUB2API 分组名称。',
validate: (value) => {
const names = normalizeSub2ApiGroupOptions(value);
return names.length ? '' : '请先填写 SUB2API 分组名称。';
},
},
],
});
if (!result) {
return;
}
const newGroups = normalizeSub2ApiGroupOptions(result.groupName);
if (!newGroups.length) {
return;
}
const selectedGroup = newGroups[0];
const nextGroups = normalizeSub2ApiGroupOptions(
getSub2ApiGroupOptionsState(latestState),
newGroups
);
syncLatestState({
sub2apiGroupName: selectedGroup,
sub2apiGroupNames: nextGroups,
});
renderSub2ApiGroupOptions(latestState, selectedGroup);
markSettingsDirty(true);
await saveSettings({ silent: true }).catch(() => { });
showToast(`已添加并切换到 SUB2API 分组:${selectedGroup}`, 'success', 1800);
}
async function handleDeleteSub2ApiGroup(groupName) {
const targetName = String(groupName || '').trim();
if (!targetName) {
return;
}
const currentGroups = getSub2ApiGroupOptionsState(latestState);
if (currentGroups.length <= 1) {
showToast('至少保留一个 SUB2API 分组。', 'warn', 1800);
return;
}
const targetKey = targetName.toLowerCase();
const nextGroups = currentGroups.filter((name) => name.toLowerCase() !== targetKey);
if (nextGroups.length === currentGroups.length) {
return;
}
const currentGroup = getSelectedSub2ApiGroupName();
const nextSelectedGroup = currentGroup.toLowerCase() === targetKey
? nextGroups[0]
: (nextGroups.find((name) => name.toLowerCase() === currentGroup.toLowerCase()) || nextGroups[0]);
syncLatestState({
sub2apiGroupName: nextSelectedGroup,
sub2apiGroupNames: nextGroups,
});
renderSub2ApiGroupOptions(latestState, nextSelectedGroup);
setSub2ApiGroupMenuOpen(true);
markSettingsDirty(true);
await saveSettings({ silent: true }).catch(() => { });
showToast(`已删除 SUB2API 分组:${targetName}`, 'success', 1600);
}
function updatePanelModeUI() {
const useSub2Api = selectPanelMode.value === 'sub2api';
const useCodex2Api = selectPanelMode.value === 'codex2api';
@@ -10338,8 +10601,12 @@ stepsList?.addEventListener('click', async (event) => {
});
syncLatestState({ customPassword: inputPassword.value });
}
let email = inputEmail.value.trim();
if (selectMailProvider.value === 'hotmail-api' || isLuckmailProvider()) {
if (shouldExecuteStep3WithSignupPhoneIdentity(latestState)) {
const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
if (response?.error) {
throw new Error(response.error);
}
} else if (selectMailProvider.value === 'hotmail-api' || isLuckmailProvider()) {
const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
if (response?.error) {
throw new Error(response.error);
@@ -11412,13 +11679,34 @@ inputSub2ApiPassword.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
});
inputSub2ApiGroup.addEventListener('input', () => {
inputSub2ApiGroup.addEventListener('change', () => {
syncLatestState({
sub2apiGroupName: getSelectedSub2ApiGroupName(),
sub2apiGroupNames: normalizeSub2ApiGroupOptions(
getSub2ApiGroupOptionsState(latestState),
getSelectedSub2ApiGroupName()
),
});
markSettingsDirty(true);
scheduleSettingsAutoSave();
});
inputSub2ApiGroup.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
});
btnSub2ApiGroupMenu?.addEventListener('click', (event) => {
event.stopPropagation();
setSub2ApiGroupMenuOpen(!sub2ApiGroupMenuOpen);
});
btnSub2ApiGroupMenu?.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
setSub2ApiGroupMenuOpen(false);
}
});
sub2ApiGroupMenu?.addEventListener('click', (event) => {
event.stopPropagation();
});
btnAddSub2ApiGroup?.addEventListener('click', () => {
handleAddSub2ApiGroup().catch((error) => {
showToast(error?.message || '添加 SUB2API 分组失败。', 'error');
});
});
inputSub2ApiDefaultProxy.addEventListener('input', () => {
markSettingsDirty(true);
@@ -12570,6 +12858,12 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
selectPanelMode.value = message.payload.panelMode || 'cpa';
updatePanelModeUI();
}
if (
message.payload.sub2apiGroupName !== undefined
|| message.payload.sub2apiGroupNames !== undefined
) {
renderSub2ApiGroupOptions(latestState, latestState?.sub2apiGroupName || '');
}
if (
message.payload.ipProxyEnabled !== undefined
|| message.payload.ipProxyService !== undefined
@@ -13171,6 +13465,7 @@ document.addEventListener('click', (event) => {
const clickedInsideFiveSimCountryMenu = Boolean(fiveSimCountryMenuShell?.contains(event.target));
const clickedInsideNexSmsCountryMenu = Boolean(nexSmsCountryMenuShell?.contains(event.target));
const clickedInsideProviderOrderMenu = Boolean(phoneSmsProviderOrderMenuShell?.contains(event.target));
const clickedInsideSub2ApiGroupPicker = Boolean(sub2ApiGroupPicker?.contains(event.target));
if (configMenuOpen && !clickedInsideConfigMenu) {
closeConfigMenu();
@@ -13192,6 +13487,9 @@ document.addEventListener('click', (event) => {
if (providerOrderMenuOpen && !clickedInsideProviderOrderMenu) {
setPhoneSmsProviderOrderMenuOpen(false);
}
if (sub2ApiGroupMenuOpen && !clickedInsideSub2ApiGroupPicker) {
setSub2ApiGroupMenuOpen(false);
}
});
document.addEventListener('keydown', (event) => {
@@ -13213,6 +13511,9 @@ document.addEventListener('keydown', (event) => {
if (btnPhoneSmsProviderOrderMenu?.getAttribute('aria-expanded') === 'true') {
setPhoneSmsProviderOrderMenuOpen(false);
}
if (sub2ApiGroupMenuOpen) {
setSub2ApiGroupMenuOpen(false);
}
});
window.addEventListener('resize', () => {
@@ -77,6 +77,7 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeFiveSimOperator'),
extractFunction('normalizeFiveSimMaxPrice'),
extractFunction('normalizeFiveSimCountryFallback'),
extractFunction('normalizeSub2ApiGroupNames'),
extractFunction('normalizePersistentSettingValue'),
].join('\n');
@@ -247,6 +248,10 @@ return {
api.normalizePersistentSettingValue('sub2apiDefaultProxyName', ' proxy-a '),
'proxy-a'
);
assert.deepStrictEqual(
api.normalizePersistentSettingValue('sub2apiGroupNames', [' codex ', 'openai-plus', 'CODEX']),
['codex', 'openai-plus']
);
assert.equal(
api.normalizePersistentSettingValue('codex2apiUrl', 'localhost:8080/admin'),
'http://localhost:8080/admin/accounts'
@@ -512,6 +512,73 @@ test('signup flow helper reuses existing managed alias email when it is still co
assert.equal(setEmailCalls, 0);
});
test('signup flow helper can generate an email on demand when add-email starts from phone signup', async () => {
const fetchedStates = [];
const setStateCalls = [];
const helpers = signupFlowApi.createSignupFlowHelpers({
buildGeneratedAliasEmail: () => '',
chrome: { tabs: { get: async () => ({ id: 21, url: 'https://auth.openai.com/create-account/password' }) } },
ensureContentScriptReadyOnTab: async () => {},
ensureHotmailAccountForFlow: async () => ({}),
ensureLuckmailPurchaseForFlow: async () => ({}),
fetchGeneratedEmail: async (state, options) => {
fetchedStates.push({ state, options });
return 'duck.generated@example.com';
},
isGeneratedAliasProvider: () => false,
isReusableGeneratedAliasEmail: () => false,
isHotmailProvider: () => false,
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => true,
reuseOrCreateTab: async () => 21,
sendToContentScriptResilient: async () => ({}),
setEmailState: async () => {
throw new Error('fetchGeneratedEmail already persists the generated email');
},
setState: async (updates) => {
setStateCalls.push(updates);
},
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
SIGNUP_PAGE_INJECT_FILES: [],
waitForTabUrlMatch: async () => null,
});
const email = await helpers.resolveSignupEmailForFlow({
email: '',
emailGenerator: 'duck',
accountIdentifierType: 'phone',
accountIdentifier: '+447780579093',
signupPhoneNumber: '+447780579093',
signupPhoneCompletedActivation: {
activationId: 'signup-completed',
phoneNumber: '+447780579093',
},
}, {
preserveAccountIdentity: true,
});
assert.equal(email, 'duck.generated@example.com');
assert.equal(fetchedStates.length, 1);
assert.equal(fetchedStates[0].options.preserveAccountIdentity, true);
assert.deepStrictEqual(setStateCalls, [
{
email: 'duck.generated@example.com',
accountIdentifierType: 'phone',
accountIdentifier: '+447780579093',
signupPhoneNumber: '+447780579093',
signupPhoneActivation: null,
signupPhoneCompletedActivation: {
activationId: 'signup-completed',
phoneNumber: '+447780579093',
},
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
},
]);
});
test('signup flow helper finalizes step 3 submit by reusing signup verification preparation', async () => {
let ensureCalls = 0;
const messages = [];
@@ -122,3 +122,81 @@ test('step 3 supports phone-only signup identity when password page is present',
},
]);
});
test('step 3 phone signup intent does not fall back to a stale email identity', async () => {
const events = {
passwordStates: [],
messages: [],
stateUpdates: [],
};
const executor = api.createStep3Executor({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
ensureContentScriptReadyOnTab: async () => {},
generatePassword: () => 'Generated123!',
getTabId: async () => 88,
isTabAlive: async () => true,
resolveSignupMethod: () => 'phone',
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 assert.rejects(
() => executor.executeStep3({
email: 'stale@example.com',
signupMethod: 'phone',
resolvedSignupMethod: 'phone',
accountIdentifierType: null,
accountIdentifier: '',
customPassword: 'PhoneSecret123!',
accounts: [],
}),
/缺少注册手机号,请先完成步骤 2 或在侧栏填写注册手机号后再执行步骤 3。/
);
assert.deepStrictEqual(events.passwordStates, []);
assert.deepStrictEqual(events.stateUpdates, []);
assert.deepStrictEqual(events.messages, []);
});
test('step 3 respects resolved email fallback when phone signup is unavailable', async () => {
const events = {
messages: [],
};
const executor = api.createStep3Executor({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
ensureContentScriptReadyOnTab: async () => {},
generatePassword: () => 'Generated123!',
getTabId: async () => 88,
isTabAlive: async () => true,
resolveSignupMethod: () => 'email',
sendToContentScript: async (_source, message) => {
events.messages.push(message);
},
setPasswordState: async () => {},
setState: async () => {},
SIGNUP_PAGE_INJECT_FILES: [],
});
await executor.executeStep3({
email: 'fallback@example.com',
signupMethod: 'phone',
resolvedSignupMethod: 'email',
customPassword: 'EmailSecret123!',
accounts: [],
});
assert.equal(events.messages[0].payload.accountIdentifierType, 'email');
assert.equal(events.messages[0].payload.accountIdentifier, 'fallback@example.com');
});
+3 -1
View File
@@ -300,8 +300,9 @@ test('step 8 submits add-email before polling the email verification code', asyn
isTabAlive: async () => true,
isVerificationMailPollingError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
resolveSignupEmailForFlow: async (state) => {
resolveSignupEmailForFlow: async (state, options = {}) => {
calls.resolvedStates.push(state);
calls.resolveOptions = options;
return 'new.user@example.com';
},
resolveVerificationStep: async (_step, state, _mail, options) => {
@@ -336,6 +337,7 @@ test('step 8 submits add-email before polling the email verification code', asyn
assert.equal(calls.contentMessages.length, 1);
assert.equal(calls.resolvedStates.length, 1);
assert.equal(calls.resolveOptions.preserveAccountIdentity, true);
assert.equal(calls.mailStates[0].email, 'new.user@example.com');
assert.equal(calls.resolvedVerification.state.email, 'new.user@example.com');
assert.equal(calls.resolvedVerification.options.targetEmail, 'new.user@example.com');
@@ -141,6 +141,9 @@ test('sidepanel html contains account records overlay and manager script', () =>
assert.match(html, /id="btn-toggle-account-records-selection"/);
assert.match(html, /id="btn-delete-selected-account-records"/);
assert.match(html, /id="input-sub2api-default-proxy"/);
assert.match(html, /id="sub2api-group-picker"/);
assert.match(html, /id="input-sub2api-group" value="codex"/);
assert.match(html, /id="btn-add-sub2api-group"/);
assert.notEqual(managerIndex, -1);
assert.notEqual(sidepanelIndex, -1);
assert.ok(managerIndex < sidepanelIndex);
+1
View File
@@ -512,6 +512,7 @@ function updateAccountRunHistorySettingsUI() {}
function updatePhoneVerificationSettingsUI() {}
function updatePanelModeUI() {}
function updateMailProviderUI() { calls.push({ target: selectIcloudTargetMailboxType.value, provider: selectIcloudForwardMailProvider.value }); }
function renderSub2ApiGroupOptions() {}
function isLuckmailProvider() { return false; }
function updateButtonStates() {}
${bundle}
@@ -136,6 +136,7 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
test('sidepanel source wires runtime signup phone field to background sync messages', () => {
assert.match(sidepanelSource, /function getRuntimeSignupPhoneValue\(state = latestState\)/);
assert.match(sidepanelSource, /function shouldExecuteStep3WithSignupPhoneIdentity\(state = latestState\)/);
assert.match(sidepanelSource, /function shouldPreserveSignupPhoneInputValue\(stateSignupPhone = ''\)/);
assert.match(sidepanelSource, /function syncSignupPhoneInputFromState\(state = latestState\)/);
assert.match(sidepanelSource, /async function persistSignupPhoneInputForAction\(\)/);
@@ -143,10 +144,40 @@ test('sidepanel source wires runtime signup phone field to background sync messa
assert.match(sidepanelSource, /final \? 'SAVE_SIGNUP_PHONE' : 'SET_SIGNUP_PHONE_STATE'/);
assert.match(sidepanelSource, /message\.payload\.signupPhoneNumber !== undefined/);
assert.match(sidepanelSource, /await persistSignupPhoneInputForAction\(\);\s*await saveSettings/);
assert.match(sidepanelSource, /if \(shouldExecuteStep3WithSignupPhoneIdentity\(latestState\)\)[\s\S]*payload: \{ step \}/);
assert.match(sidepanelSource, /async function handleSkipStep\(step\)[\s\S]*await persistCurrentSettingsForAction\(\);/);
assert.match(sidepanelSource, /inputSignupPhone\.addEventListener\('input'[\s\S]*signupPhoneInputDirty = true/);
});
test('manual step 3 uses phone identity without requiring registration email', () => {
const api = new Function(`
let latestState = { signupMethod: 'phone', phoneVerificationEnabled: true, signupPhoneNumber: '+441111111111', accountIdentifierType: 'phone', accountIdentifier: '+441111111111' };
const DEFAULT_SIGNUP_METHOD = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
function getSelectedSignupMethod() { return 'phone'; }
${extractFunction('normalizeSignupMethod')}
${extractFunction('getRuntimeSignupPhoneValue')}
${extractFunction('shouldExecuteStep3WithSignupPhoneIdentity')}
return { shouldExecuteStep3WithSignupPhoneIdentity };
`)();
assert.equal(api.shouldExecuteStep3WithSignupPhoneIdentity({
signupMethod: 'phone',
phoneVerificationEnabled: true,
accountIdentifierType: 'phone',
accountIdentifier: '+441111111111',
signupPhoneNumber: '+441111111111',
email: '',
}), true);
assert.equal(api.shouldExecuteStep3WithSignupPhoneIdentity({
signupMethod: 'email',
accountIdentifierType: 'email',
accountIdentifier: 'user@example.com',
signupPhoneNumber: '',
email: 'user@example.com',
}), false);
});
test('runtime signup phone sync preserves active manual input until it is saved', () => {
const api = new Function(`
let latestState = { signupMethod: 'phone', phoneVerificationEnabled: true, signupPhoneNumber: '+441111111111' };
+4 -2
View File
@@ -355,6 +355,8 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
补充:
- 手机号注册如果在 Step 2 后直接落到短信验证码页、资料页或已登录页,Step 3 会按页面状态自动跳过,不再因为缺少邮箱而失败。
- 手动点击 Step 3 时,侧栏会先判断当前统一账号身份:如果本轮是手机号注册,或运行态已经写入 `accountIdentifierType = phone / signupPhoneNumber`,则直接执行 Step 3,不再尝试生成或校验注册邮箱,避免把手机号注册链路误切回邮箱注册链路。
- 后台 Step 3 会再次按 `resolveSignupMethod` 与统一账号身份兜底校验;手机号注册意图下如果缺少手机号,会明确提示先完成 Step 2 或填写注册手机号,不会回退使用旧邮箱。
### Step 4 / Step 8
@@ -459,7 +461,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
1. 先确认当前认证页真实状态,而不是只看 `accountIdentifierType`
2. 当前是 `add-email`
- 调用 `resolveSignupEmailForFlow` 获取或生成本轮绑定邮箱
- 调用 `resolveSignupEmailForFlow` 按需获取或生成本轮绑定邮箱;手机号注册不会在 Step 2 提前生成邮箱,只有真实进入 `add-email` 时才占用邮箱资源
- 内容脚本发送 `SUBMIT_ADD_EMAIL` 填写邮箱并提交
- 页面进入邮箱验证码页后,用绑定邮箱作为 `step8VerificationTargetEmail`
3. 当前是邮箱验证码页:
@@ -479,7 +481,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
-`2925 receive` 而言,Step 8 会把当前目标注册邮箱一并传给 2925 内容脚本;只有当邮件里显式写出了其他邮箱时才会跳过,从而在不破坏历史兼容性的前提下,尽量降低误收验证码的概率。
-`custom` provider 而言,Step 8 仍使用手动验证码确认弹窗;弹窗当前额外提供“出现手机号验证”按钮,点击后会直接抛出与真实 `add-phone` 页面一致的 fatal 错误,供 Auto 按既有 add-phone 分支继续下一邮箱。
- 当登录验证码提交后进入 `phone-verification` 页时,内容脚本会显式把该页面识别为“手机验证码页”,避免与邮箱验证码页混淆。
- 手机号注册身份本身不会让 Step 8 自动走短信;只有真实页面状态是 `phone_verification_page` 时才调用短信 helper。手机号注册登录后出现 `add-email` 时,Step 8 走“绑定邮箱 -> 邮箱验证码”链路。
- 手机号注册身份本身不会让 Step 8 自动走短信;只有真实页面状态是 `phone_verification_page` 时才调用短信 helper。手机号注册登录后出现 `add-email` 时,Step 8 走“按需生成邮箱 -> 绑定邮箱 -> 邮箱验证码”链路。
- `email_in_use` 会在 Step 8 当前步骤内清理当前邮箱并重新打开 `add-email` 获取新邮箱;`max_check_attempts` 不继续点击认证页 `重试`,只恢复当前 Step 8。
### Step 9