feat(sub2api): 支持多分组创建账号
(cherry picked from commit cc7671e6d2863b68527134008301e14fb86a8666)
This commit is contained in:
@@ -7776,6 +7776,9 @@ async function handleStepData(step, payload) {
|
||||
if (payload.cpaManagementOrigin !== undefined) updates.cpaManagementOrigin = payload.cpaManagementOrigin || null;
|
||||
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
|
||||
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
|
||||
if (payload.sub2apiGroupIds !== undefined) updates.sub2apiGroupIds = Array.isArray(payload.sub2apiGroupIds)
|
||||
? payload.sub2apiGroupIds
|
||||
: [];
|
||||
if (Object.keys(updates).length) {
|
||||
await setState(updates);
|
||||
}
|
||||
|
||||
@@ -292,6 +292,27 @@
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case 1: {
|
||||
const updates = {};
|
||||
if (payload.oauthUrl) {
|
||||
updates.oauthUrl = payload.oauthUrl;
|
||||
broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
|
||||
}
|
||||
if (payload.sub2apiSessionId !== undefined) updates.sub2apiSessionId = payload.sub2apiSessionId || null;
|
||||
if (payload.sub2apiOAuthState !== undefined) updates.sub2apiOAuthState = payload.sub2apiOAuthState || null;
|
||||
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
|
||||
if (payload.sub2apiGroupIds !== undefined) updates.sub2apiGroupIds = Array.isArray(payload.sub2apiGroupIds)
|
||||
? payload.sub2apiGroupIds
|
||||
: [];
|
||||
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||||
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
|
||||
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
|
||||
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
|
||||
if (Object.keys(updates).length) {
|
||||
await setState(updates);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
if (payload.email) {
|
||||
await setEmailState(payload.email);
|
||||
|
||||
@@ -364,6 +364,7 @@
|
||||
sub2apiSessionId: state.sub2apiSessionId,
|
||||
sub2apiOAuthState: state.sub2apiOAuthState,
|
||||
sub2apiGroupId: state.sub2apiGroupId,
|
||||
sub2apiGroupIds: state.sub2apiGroupIds,
|
||||
sub2apiDraftName: state.sub2apiDraftName,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -191,6 +191,53 @@ async function getGroupByName(origin, token, groupName) {
|
||||
return group;
|
||||
}
|
||||
|
||||
function normalizeSub2ApiGroupNames(value) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '').split(/[\r\n,,;;]+/);
|
||||
const seen = new Set();
|
||||
const names = [];
|
||||
for (const item of source) {
|
||||
const name = String(item || '').trim();
|
||||
const key = name.toLowerCase();
|
||||
if (!name || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
names.push(name);
|
||||
}
|
||||
return names.length ? names : [SUB2API_DEFAULT_GROUP_NAME];
|
||||
}
|
||||
|
||||
async function getGroupsByNames(origin, token, groupNames) {
|
||||
const targetNames = normalizeSub2ApiGroupNames(groupNames);
|
||||
const groups = await requestJson(origin, '/api/v1/admin/groups/all', {
|
||||
method: 'GET',
|
||||
token,
|
||||
});
|
||||
const matched = [];
|
||||
const missing = [];
|
||||
|
||||
for (const targetName of targetNames) {
|
||||
const normalized = targetName.toLowerCase();
|
||||
const group = (groups || []).find((item) => {
|
||||
const itemName = String(item?.name || '').trim().toLowerCase();
|
||||
if (!itemName) return false;
|
||||
if (itemName !== normalized) return false;
|
||||
return !item.platform || item.platform === 'openai';
|
||||
});
|
||||
if (group) {
|
||||
matched.push(group);
|
||||
} else {
|
||||
missing.push(targetName);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length) {
|
||||
throw new Error(`SUB2API 中未找到以下 openai 分组:${missing.join('、')}。`);
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
function normalizeSub2ApiProxyPreference(value) {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
@@ -448,16 +495,19 @@ async function step1_generateOpenAiAuthUrl(payload = {}, options = {}) {
|
||||
const { report = true } = options;
|
||||
const logStep = Number.isInteger(payload?.logStep) ? payload.logStep : 1;
|
||||
const redirectUri = normalizeRedirectUri();
|
||||
const groupName = (payload.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME).trim() || SUB2API_DEFAULT_GROUP_NAME;
|
||||
const groupNames = normalizeSub2ApiGroupNames(payload.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME);
|
||||
const groupName = groupNames[0] || SUB2API_DEFAULT_GROUP_NAME;
|
||||
|
||||
const { origin, token } = await loginSub2Api(payload);
|
||||
const group = await getGroupByName(origin, token, groupName);
|
||||
const groups = await getGroupsByNames(origin, token, groupNames);
|
||||
const group = groups[0];
|
||||
const proxyPreference = resolveSub2ApiProxyPreference(payload);
|
||||
const proxy = proxyPreference ? await resolveSub2ApiProxy(origin, token, proxyPreference) : null;
|
||||
const proxyId = normalizeProxyId(proxy?.id);
|
||||
const draftName = buildDraftAccountName(group.name || groupName);
|
||||
const groupLabel = groups.map((item) => `${item.name}(#${item.id})`).join('、');
|
||||
|
||||
log(`步骤 ${logStep}:已登录 SUB2API,使用分组 ${group.name}(#${group.id})。`);
|
||||
log(`步骤 ${logStep}:已登录 SUB2API,使用分组 ${groupLabel}。`);
|
||||
if (proxy) {
|
||||
log(`步骤 ${logStep}:已选择 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`);
|
||||
} else {
|
||||
@@ -492,6 +542,7 @@ async function step1_generateOpenAiAuthUrl(payload = {}, options = {}) {
|
||||
sub2apiSessionId: sessionId,
|
||||
sub2apiOAuthState: oauthState,
|
||||
sub2apiGroupId: group.id,
|
||||
sub2apiGroupIds: groups.map((item) => item.id),
|
||||
sub2apiDraftName: draftName,
|
||||
sub2apiProxyId: proxyId,
|
||||
};
|
||||
@@ -517,9 +568,17 @@ async function step9_submitOpenAiCallback(payload = {}) {
|
||||
const proxySelector = preferredProxyId || proxyPreference;
|
||||
const proxy = proxySelector ? await resolveSub2ApiProxy(origin, token, proxySelector) : null;
|
||||
const proxyId = normalizeProxyId(proxy?.id);
|
||||
const group = payload.sub2apiGroupId
|
||||
? { id: payload.sub2apiGroupId, name: payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME }
|
||||
: await getGroupByName(origin, token, payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME);
|
||||
const storedGroupIds = Array.isArray(payload.sub2apiGroupIds)
|
||||
? payload.sub2apiGroupIds
|
||||
: (Array.isArray(backgroundState.sub2apiGroupIds) ? backgroundState.sub2apiGroupIds : []);
|
||||
const groupIdsFromState = storedGroupIds
|
||||
.map((id) => Number(id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
const groups = groupIdsFromState.length
|
||||
? groupIdsFromState.map((id) => ({ id }))
|
||||
: (payload.sub2apiGroupId
|
||||
? [{ id: payload.sub2apiGroupId, name: payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME }]
|
||||
: await getGroupsByNames(origin, token, payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME));
|
||||
|
||||
if (!sessionId) {
|
||||
throw new Error('缺少 SUB2API session_id,请重新执行步骤 1。');
|
||||
@@ -551,8 +610,10 @@ async function step9_submitOpenAiCallback(payload = {}) {
|
||||
const credentials = buildOpenAiCredentials(exchangeData);
|
||||
const extra = buildOpenAiExtra(exchangeData);
|
||||
const resolvedEmail = String(exchangeData?.email || credentials?.email || '').trim();
|
||||
const groupId = Number(group.id);
|
||||
if (!Number.isFinite(groupId) || groupId <= 0) {
|
||||
const groupIds = groups
|
||||
.map((group) => Number(group.id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
if (!groupIds.length) {
|
||||
throw new Error('SUB2API 返回的目标分组 ID 无效。');
|
||||
}
|
||||
const accountName = resolvedEmail
|
||||
@@ -568,7 +629,7 @@ async function step9_submitOpenAiCallback(payload = {}) {
|
||||
concurrency: SUB2API_DEFAULT_CONCURRENCY,
|
||||
priority: SUB2API_DEFAULT_PRIORITY,
|
||||
rate_multiplier: SUB2API_DEFAULT_RATE_MULTIPLIER,
|
||||
group_ids: [groupId],
|
||||
group_ids: groupIds,
|
||||
auto_pause_on_expired: true,
|
||||
};
|
||||
if (proxyId) {
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
</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" />
|
||||
<input type="text" id="input-sub2api-group" class="data-input" placeholder="默认 codex;多个用逗号或换行分隔" />
|
||||
</div>
|
||||
<div class="data-row" id="row-sub2api-default-proxy" style="display:none;">
|
||||
<span class="data-label">默认代理</span>
|
||||
|
||||
@@ -79,7 +79,10 @@ function createSub2ApiPanelContext(fetchCalls = []) {
|
||||
if (parsed.pathname === '/api/v1/admin/groups/all') {
|
||||
return createJsonResponse({
|
||||
code: 0,
|
||||
data: [{ id: 5, name: 'codex', platform: 'openai' }],
|
||||
data: [
|
||||
{ id: 5, name: 'codex', platform: 'openai' },
|
||||
{ id: 9, name: 'codex-plus', platform: 'openai' },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (parsed.pathname === '/api/v1/admin/proxies/all') {
|
||||
@@ -220,6 +223,36 @@ test('SUB2API step 1 omits proxy_id when default proxy is empty', async () => {
|
||||
assert.equal(Object.hasOwn(generateCall.body, 'proxy_id'), false);
|
||||
});
|
||||
|
||||
test('SUB2API step 10 creates accounts in multiple configured groups', async () => {
|
||||
const fetchCalls = [];
|
||||
const context = createSub2ApiPanelContext(fetchCalls);
|
||||
|
||||
const step1Result = await vm.runInContext(`
|
||||
step1_generateOpenAiAuthUrl({
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiGroupName: 'codex, codex-plus'
|
||||
}, { report: false })
|
||||
`, context);
|
||||
|
||||
await vm.runInContext(`
|
||||
step9_submitOpenAiCallback({
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiGroupName: 'codex, codex-plus',
|
||||
sub2apiSessionId: 'session-1',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
sub2apiGroupIds: ${JSON.stringify(step1Result.sub2apiGroupIds)}
|
||||
})
|
||||
`, context);
|
||||
|
||||
const createCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts');
|
||||
assert.deepEqual(Array.from(step1Result.sub2apiGroupIds), [5, 9]);
|
||||
assert.deepEqual(createCall.body.group_ids, [5, 9]);
|
||||
});
|
||||
|
||||
test('SUB2API step 10 omits proxy_id when no proxy is configured', async () => {
|
||||
const fetchCalls = [];
|
||||
const context = createSub2ApiPanelContext(fetchCalls);
|
||||
|
||||
Reference in New Issue
Block a user