feat: 增强步骤 3 的手机号注册逻辑,确保手机号身份优先处理并添加相关测试用例
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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。');
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user