Add configurable SUB2API account priority
This commit is contained in:
@@ -242,6 +242,7 @@ const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro';
|
||||
const DEFAULT_SUB2API_GROUP_NAME = 'codex';
|
||||
const DEFAULT_SUB2API_PROXY_NAME = '';
|
||||
const DEFAULT_SUB2API_ACCOUNT_PRIORITY = 1;
|
||||
const CONTRIBUTION_SOURCE_CPA = 'cpa';
|
||||
const CONTRIBUTION_SOURCE_SUB2API = 'sub2api';
|
||||
const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池';
|
||||
@@ -574,6 +575,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
sub2apiPassword: '',
|
||||
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME,
|
||||
sub2apiGroupNames: DEFAULT_SUB2API_GROUP_NAMES,
|
||||
sub2apiAccountPriority: DEFAULT_SUB2API_ACCOUNT_PRIORITY,
|
||||
sub2apiDefaultProxyName: DEFAULT_SUB2API_PROXY_NAME,
|
||||
ipProxyEnabled: false,
|
||||
ipProxyService: DEFAULT_IP_PROXY_SERVICE,
|
||||
@@ -2105,6 +2107,18 @@ function normalizeSub2ApiGroupNames(value = '') {
|
||||
return names;
|
||||
}
|
||||
|
||||
function normalizeSub2ApiAccountPriority(value, fallback = DEFAULT_SUB2API_ACCOUNT_PRIORITY) {
|
||||
const rawValue = String(value ?? '').trim();
|
||||
const numeric = Number(rawValue);
|
||||
if (!rawValue || !Number.isSafeInteger(numeric) || numeric < 1) {
|
||||
const fallbackNumber = Number(fallback);
|
||||
return Number.isSafeInteger(fallbackNumber) && fallbackNumber >= 1
|
||||
? fallbackNumber
|
||||
: DEFAULT_SUB2API_ACCOUNT_PRIORITY;
|
||||
}
|
||||
return numeric;
|
||||
}
|
||||
|
||||
function normalizePersistentSettingValue(key, value) {
|
||||
switch (key) {
|
||||
case 'panelMode':
|
||||
@@ -2125,6 +2139,8 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return String(value || '').trim();
|
||||
case 'sub2apiGroupNames':
|
||||
return normalizeSub2ApiGroupNames(value);
|
||||
case 'sub2apiAccountPriority':
|
||||
return normalizeSub2ApiAccountPriority(value);
|
||||
case 'sub2apiDefaultProxyName':
|
||||
return String(value || '').trim();
|
||||
case 'ipProxyEnabled':
|
||||
|
||||
@@ -293,6 +293,7 @@
|
||||
sub2apiPassword: state.sub2apiPassword,
|
||||
sub2apiGroupName: groupName,
|
||||
sub2apiDefaultProxyName: state.sub2apiDefaultProxyName,
|
||||
sub2apiAccountPriority: state.sub2apiAccountPriority,
|
||||
logStep: 7,
|
||||
},
|
||||
}, {
|
||||
|
||||
@@ -380,6 +380,7 @@
|
||||
sub2apiPassword: state.sub2apiPassword,
|
||||
sub2apiGroupName: state.sub2apiGroupName,
|
||||
sub2apiDefaultProxyName: state.sub2apiDefaultProxyName,
|
||||
sub2apiAccountPriority: state.sub2apiAccountPriority,
|
||||
sub2apiProxyId: state.sub2apiProxyId,
|
||||
sub2apiSessionId: state.sub2apiSessionId,
|
||||
sub2apiOAuthState: state.sub2apiOAuthState,
|
||||
|
||||
@@ -252,6 +252,21 @@ function resolveSub2ApiProxyPreference(payload = {}, backgroundState = {}) {
|
||||
return SUB2API_DEFAULT_PROXY_NAME;
|
||||
}
|
||||
|
||||
function resolveSub2ApiAccountPriority(payload = {}, backgroundState = {}) {
|
||||
const candidate = payload.sub2apiAccountPriority !== undefined
|
||||
? payload.sub2apiAccountPriority
|
||||
: backgroundState.sub2apiAccountPriority;
|
||||
const rawValue = String(candidate ?? '').trim();
|
||||
if (!rawValue) {
|
||||
return SUB2API_DEFAULT_PRIORITY;
|
||||
}
|
||||
const numeric = Number(rawValue);
|
||||
if (!Number.isSafeInteger(numeric) || numeric < 1) {
|
||||
throw new Error('SUB2API 账号优先级必须是大于等于 1 的整数。');
|
||||
}
|
||||
return numeric;
|
||||
}
|
||||
|
||||
function normalizeProxyId(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return null;
|
||||
@@ -568,6 +583,7 @@ async function step9_submitOpenAiCallback(payload = {}) {
|
||||
const proxySelector = preferredProxyId || proxyPreference;
|
||||
const proxy = proxySelector ? await resolveSub2ApiProxy(origin, token, proxySelector) : null;
|
||||
const proxyId = normalizeProxyId(proxy?.id);
|
||||
const accountPriority = resolveSub2ApiAccountPriority(payload, backgroundState);
|
||||
const storedGroupIds = Array.isArray(payload.sub2apiGroupIds)
|
||||
? payload.sub2apiGroupIds
|
||||
: (Array.isArray(backgroundState.sub2apiGroupIds) ? backgroundState.sub2apiGroupIds : []);
|
||||
@@ -627,7 +643,7 @@ async function step9_submitOpenAiCallback(payload = {}) {
|
||||
type: 'oauth',
|
||||
credentials,
|
||||
concurrency: SUB2API_DEFAULT_CONCURRENCY,
|
||||
priority: SUB2API_DEFAULT_PRIORITY,
|
||||
priority: accountPriority,
|
||||
rate_multiplier: SUB2API_DEFAULT_RATE_MULTIPLIER,
|
||||
group_ids: groupIds,
|
||||
auto_pause_on_expired: true,
|
||||
|
||||
@@ -210,6 +210,10 @@
|
||||
<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-account-priority" style="display:none;">
|
||||
<span class="data-label">优先级</span>
|
||||
<input type="number" id="input-sub2api-account-priority" class="data-input mono" value="1" min="1" step="1" />
|
||||
</div>
|
||||
<div class="data-row" id="row-sub2api-default-proxy" style="display:none;">
|
||||
<span class="data-label">默认代理</span>
|
||||
<input type="text" id="input-sub2api-default-proxy" class="data-input" placeholder="留空则不使用代理;或填写代理名称 / ID" />
|
||||
|
||||
@@ -100,6 +100,8 @@ 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 rowSub2ApiAccountPriority = document.getElementById('row-sub2api-account-priority');
|
||||
const inputSub2ApiAccountPriority = document.getElementById('input-sub2api-account-priority');
|
||||
const rowSub2ApiDefaultProxy = document.getElementById('row-sub2api-default-proxy');
|
||||
const inputSub2ApiDefaultProxy = document.getElementById('input-sub2api-default-proxy');
|
||||
const rowIpProxyEnabled = document.getElementById('row-ip-proxy-enabled');
|
||||
@@ -1150,6 +1152,15 @@ function normalizeSub2ApiGroupOptions(...sources) {
|
||||
return normalizeEditableListValues(...sources);
|
||||
}
|
||||
|
||||
function normalizeSub2ApiAccountPriorityValue(value) {
|
||||
const rawValue = String(value ?? '').trim();
|
||||
const numeric = Number(rawValue);
|
||||
if (!rawValue || !Number.isSafeInteger(numeric) || numeric < 1) {
|
||||
return 1;
|
||||
}
|
||||
return numeric;
|
||||
}
|
||||
|
||||
function getSelectedSub2ApiGroupName() {
|
||||
return String(inputSub2ApiGroup?.value || '').trim()
|
||||
|| DEFAULT_SUB2API_GROUP_OPTIONS[0];
|
||||
@@ -3181,6 +3192,12 @@ function collectSettingsPayload() {
|
||||
if (sub2apiGroupNames.length === 0) {
|
||||
appendSub2ApiGroupNames(['codex', 'openai-plus']);
|
||||
}
|
||||
const sub2apiAccountPriorityNormalizer = typeof normalizeSub2ApiAccountPriorityValue === 'function'
|
||||
? normalizeSub2ApiAccountPriorityValue
|
||||
: ((value) => {
|
||||
const numeric = Number(String(value ?? '').trim());
|
||||
return Number.isSafeInteger(numeric) && numeric >= 1 ? numeric : 1;
|
||||
});
|
||||
return {
|
||||
...(contributionModeEnabled ? {} : {
|
||||
panelMode: selectPanelMode.value,
|
||||
@@ -3193,6 +3210,11 @@ function collectSettingsPayload() {
|
||||
sub2apiPassword: inputSub2ApiPassword.value,
|
||||
sub2apiGroupName: selectedSub2ApiGroupName,
|
||||
sub2apiGroupNames,
|
||||
sub2apiAccountPriority: sub2apiAccountPriorityNormalizer(
|
||||
typeof inputSub2ApiAccountPriority !== 'undefined' && inputSub2ApiAccountPriority
|
||||
? inputSub2ApiAccountPriority.value
|
||||
: latestState?.sub2apiAccountPriority
|
||||
),
|
||||
sub2apiDefaultProxyName: inputSub2ApiDefaultProxy.value.trim(),
|
||||
ipProxyEnabled: getSelectedIpProxyEnabledSafe(),
|
||||
ipProxyService: selectedIpProxyService,
|
||||
@@ -7550,6 +7572,9 @@ function applyAutoRunStatus(payload = currentAutoRun) {
|
||||
if (typeof inputSignupPhone !== 'undefined' && inputSignupPhone) {
|
||||
inputSignupPhone.disabled = locked;
|
||||
}
|
||||
if (typeof inputSub2ApiAccountPriority !== 'undefined' && inputSub2ApiAccountPriority) {
|
||||
inputSub2ApiAccountPriority.disabled = locked;
|
||||
}
|
||||
inputAutoSkipFailures.disabled = scheduled;
|
||||
|
||||
const lockedRunCount = typeof getLockedRunCountFromEmailPool === 'function'
|
||||
@@ -7847,6 +7872,9 @@ function applySettingsState(state) {
|
||||
inputSub2ApiEmail.value = state?.sub2apiEmail || '';
|
||||
inputSub2ApiPassword.value = state?.sub2apiPassword || '';
|
||||
renderSub2ApiGroupOptions(state, state?.sub2apiGroupName || '');
|
||||
if (typeof inputSub2ApiAccountPriority !== 'undefined' && inputSub2ApiAccountPriority) {
|
||||
inputSub2ApiAccountPriority.value = String(normalizeSub2ApiAccountPriorityValue(state?.sub2apiAccountPriority));
|
||||
}
|
||||
inputSub2ApiDefaultProxy.value = state?.sub2apiDefaultProxyName || '';
|
||||
const normalizedIpProxyService = resolveIpProxyService(state?.ipProxyService);
|
||||
const normalizedIpProxyServiceProfiles = typeof normalizeIpProxyServiceProfiles === 'function'
|
||||
@@ -9613,6 +9641,7 @@ function updatePanelModeUI() {
|
||||
rowSub2ApiEmail.style.display = useSub2Api ? '' : 'none';
|
||||
rowSub2ApiPassword.style.display = useSub2Api ? '' : 'none';
|
||||
rowSub2ApiGroup.style.display = useSub2Api ? '' : 'none';
|
||||
rowSub2ApiAccountPriority.style.display = useSub2Api ? '' : 'none';
|
||||
rowSub2ApiDefaultProxy.style.display = useSub2Api ? '' : 'none';
|
||||
rowCodex2ApiUrl.style.display = useCodex2Api ? '' : 'none';
|
||||
rowCodex2ApiAdminKey.style.display = useCodex2Api ? '' : 'none';
|
||||
@@ -10354,6 +10383,7 @@ const contributionModeManager = window.SidepanelContributionMode?.createContribu
|
||||
rowPhoneVerificationEnabled,
|
||||
rowCustomPassword,
|
||||
rowLocalCpaStep9Mode,
|
||||
rowSub2ApiAccountPriority,
|
||||
rowSub2ApiDefaultProxy,
|
||||
rowSub2ApiEmail,
|
||||
rowSub2ApiGroup,
|
||||
@@ -11700,6 +11730,16 @@ inputSub2ApiGroup.addEventListener('change', () => {
|
||||
markSettingsDirty(true);
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
inputSub2ApiAccountPriority.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
scheduleSettingsAutoSave();
|
||||
});
|
||||
inputSub2ApiAccountPriority.addEventListener('blur', () => {
|
||||
inputSub2ApiAccountPriority.value = String(normalizeSub2ApiAccountPriorityValue(inputSub2ApiAccountPriority.value));
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
btnAddSub2ApiGroup?.addEventListener('click', () => {
|
||||
handleAddSub2ApiGroup().catch((error) => {
|
||||
showToast(error?.message || '添加 SUB2API 分组失败。', 'error');
|
||||
|
||||
@@ -116,3 +116,49 @@ test('panel bridge can request cpa oauth url via management api', async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('panel bridge forwards SUB2API account priority when requesting oauth url', async () => {
|
||||
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
|
||||
const sentMessages = [];
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({});
|
||||
const bridge = api.createPanelBridge({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
create: async () => ({ id: 72 }),
|
||||
},
|
||||
},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getPanelMode: () => 'sub2api',
|
||||
normalizeCodex2ApiUrl: (value) => value,
|
||||
normalizeSub2ApiUrl: (value) => value,
|
||||
rememberSourceLastUrl: async () => {},
|
||||
sendToContentScript: async (sourceName, message, options) => {
|
||||
sentMessages.push({ sourceName, message, options });
|
||||
return {
|
||||
oauthUrl: 'https://auth.openai.com/authorize?state=oauth-state',
|
||||
sub2apiSessionId: 'session-123',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
};
|
||||
},
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
waitForTabUrlFamily: async () => ({ id: 72 }),
|
||||
DEFAULT_SUB2API_GROUP_NAME: 'codex',
|
||||
SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000,
|
||||
});
|
||||
|
||||
await bridge.requestOAuthUrlFromPanel({
|
||||
panelMode: 'sub2api',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiGroupName: 'codex',
|
||||
sub2apiAccountPriority: 3,
|
||||
}, { logLabel: '步骤 7' });
|
||||
|
||||
assert.equal(sentMessages.length, 1);
|
||||
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
|
||||
assert.equal(sentMessages[0].message.payload.sub2apiAccountPriority, 3);
|
||||
});
|
||||
|
||||
@@ -272,3 +272,34 @@ test('platform verify module sends Plus visible step 13 to SUB2API panel', async
|
||||
assert.equal(sentMessages[0].message.step, 13);
|
||||
});
|
||||
|
||||
test('platform verify module forwards SUB2API account priority to panel', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const sentMessages = [];
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps } = createDeps({
|
||||
getPanelMode: () => 'sub2api',
|
||||
getTabId: async () => 91,
|
||||
isTabAlive: async () => true,
|
||||
sendToContentScript: async (sourceName, message, options) => {
|
||||
sentMessages.push({ sourceName, message, options });
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await executor.executeStep10({
|
||||
panelMode: 'sub2api',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiSessionId: 'session-1',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
sub2apiAccountPriority: 2,
|
||||
});
|
||||
|
||||
assert.equal(sentMessages.length, 1);
|
||||
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
|
||||
assert.equal(sentMessages[0].message.payload.sub2apiAccountPriority, 2);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||
|
||||
test('sidepanel exposes SUB2API account priority below group setting', () => {
|
||||
assert.match(html, /id="row-sub2api-account-priority"/);
|
||||
assert.match(html, /id="input-sub2api-account-priority"/);
|
||||
assert.match(html, /<span class="data-label">优先级<\/span>/);
|
||||
const inputTag = html.match(/<input[^>]*id="input-sub2api-account-priority"[^>]*>/)?.[0] || '';
|
||||
assert.match(inputTag, /type="number"/);
|
||||
assert.match(inputTag, /min="1"/);
|
||||
assert.match(inputTag, /step="1"/);
|
||||
assert.ok(
|
||||
html.indexOf('id="row-sub2api-account-priority"') > html.indexOf('id="row-sub2api-group"'),
|
||||
'priority row should be placed below the SUB2API group row'
|
||||
);
|
||||
assert.ok(
|
||||
html.indexOf('id="row-sub2api-account-priority"') < html.indexOf('id="row-sub2api-default-proxy"'),
|
||||
'priority row should remain above the SUB2API default proxy row'
|
||||
);
|
||||
});
|
||||
|
||||
test('sidepanel persists and locks SUB2API account priority setting', () => {
|
||||
assert.match(
|
||||
source,
|
||||
/const rowSub2ApiAccountPriority = document\.getElementById\('row-sub2api-account-priority'\);/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/const inputSub2ApiAccountPriority = document\.getElementById\('input-sub2api-account-priority'\);/
|
||||
);
|
||||
assert.match(source, /function normalizeSub2ApiAccountPriorityValue\(/);
|
||||
assert.match(source, /const sub2apiAccountPriorityNormalizer = typeof normalizeSub2ApiAccountPriorityValue === 'function'/);
|
||||
assert.match(source, /sub2apiAccountPriority: sub2apiAccountPriorityNormalizer\(/);
|
||||
assert.match(
|
||||
source,
|
||||
/inputSub2ApiAccountPriority\.value = String\(normalizeSub2ApiAccountPriorityValue\(state\?\.sub2apiAccountPriority\)\);/
|
||||
);
|
||||
assert.match(source, /rowSub2ApiAccountPriority\.style\.display = useSub2Api \? '' : 'none';/);
|
||||
assert.match(source, /inputSub2ApiAccountPriority\.disabled = locked;/);
|
||||
assert.match(
|
||||
source,
|
||||
/inputSub2ApiAccountPriority\.addEventListener\('input', \(\) => \{[\s\S]*scheduleSettingsAutoSave\(\);[\s\S]*\}\);/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/inputSub2ApiAccountPriority\.addEventListener\('blur', \(\) => \{[\s\S]*saveSettings\(\{ silent: true \}\)/
|
||||
);
|
||||
});
|
||||
@@ -277,3 +277,49 @@ test('SUB2API step 10 omits proxy_id when no proxy is configured', async () => {
|
||||
assert.equal(Object.hasOwn(exchangeCall.body, 'proxy_id'), false);
|
||||
assert.equal(Object.hasOwn(createCall.body, 'proxy_id'), false);
|
||||
});
|
||||
|
||||
test('SUB2API step 10 creates account with configured account priority', async () => {
|
||||
const fetchCalls = [];
|
||||
const context = createSub2ApiPanelContext(fetchCalls);
|
||||
|
||||
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',
|
||||
sub2apiSessionId: 'session-1',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
sub2apiGroupId: 5,
|
||||
sub2apiAccountPriority: 3
|
||||
})
|
||||
`, context);
|
||||
|
||||
const createCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts');
|
||||
assert.equal(createCall.body.priority, 3);
|
||||
});
|
||||
|
||||
test('SUB2API account priority must be an integer greater than or equal to 1', async () => {
|
||||
const fetchCalls = [];
|
||||
const context = createSub2ApiPanelContext(fetchCalls);
|
||||
|
||||
await assert.rejects(
|
||||
() => 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',
|
||||
sub2apiSessionId: 'session-1',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
sub2apiGroupId: 5,
|
||||
sub2apiAccountPriority: 0
|
||||
})
|
||||
`, context),
|
||||
/SUB2API 账号优先级必须是大于等于 1 的整数/
|
||||
);
|
||||
|
||||
assert.equal(fetchCalls.some((call) => call.path === '/api/v1/admin/accounts'), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user