feat(sub2api): 支持多分组创建账号

(cherry picked from commit cc7671e6d2863b68527134008301e14fb86a8666)
This commit is contained in:
朴圣佑
2026-04-26 22:23:25 +08:00
committed by QLHazyCoder
parent aa4e52b9a7
commit 63c53accfe
6 changed files with 130 additions and 11 deletions
+3
View File
@@ -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);
}
+21
View File
@@ -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);
+1
View File
@@ -364,6 +364,7 @@
sub2apiSessionId: state.sub2apiSessionId,
sub2apiOAuthState: state.sub2apiOAuthState,
sub2apiGroupId: state.sub2apiGroupId,
sub2apiGroupIds: state.sub2apiGroupIds,
sub2apiDraftName: state.sub2apiDraftName,
},
};
+70 -9
View File
@@ -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) {
+1 -1
View File
@@ -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>
+34 -1
View File
@@ -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);