diff --git a/background.js b/background.js
index 74003d3..9a053e1 100644
--- a/background.js
+++ b/background.js
@@ -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':
diff --git a/background/panel-bridge.js b/background/panel-bridge.js
index 2b0f077..a4dff4c 100644
--- a/background/panel-bridge.js
+++ b/background/panel-bridge.js
@@ -293,6 +293,7 @@
sub2apiPassword: state.sub2apiPassword,
sub2apiGroupName: groupName,
sub2apiDefaultProxyName: state.sub2apiDefaultProxyName,
+ sub2apiAccountPriority: state.sub2apiAccountPriority,
logStep: 7,
},
}, {
diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js
index 8237339..e9fb06c 100644
--- a/background/steps/platform-verify.js
+++ b/background/steps/platform-verify.js
@@ -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,
diff --git a/content/sub2api-panel.js b/content/sub2api-panel.js
index 1bf82b7..8f5c39c 100644
--- a/content/sub2api-panel.js
+++ b/content/sub2api-panel.js
@@ -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,
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 6c96092..43ffcbe 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -210,6 +210,10 @@
+
+ 优先级
+
+
默认代理
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index dbbc2b9..b5e89aa 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -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');
diff --git a/tests/background-panel-bridge-module.test.js b/tests/background-panel-bridge-module.test.js
index 3e655af..957a0c2 100644
--- a/tests/background-panel-bridge-module.test.js
+++ b/tests/background-panel-bridge-module.test.js
@@ -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);
+});
diff --git a/tests/background-platform-verify-cpa-api.test.js b/tests/background-platform-verify-cpa-api.test.js
index d815a7b..082745d 100644
--- a/tests/background-platform-verify-cpa-api.test.js
+++ b/tests/background-platform-verify-cpa-api.test.js
@@ -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);
+});
diff --git a/tests/sidepanel-sub2api-priority-settings.test.js b/tests/sidepanel-sub2api-priority-settings.test.js
new file mode 100644
index 0000000..d1cfbb1
--- /dev/null
+++ b/tests/sidepanel-sub2api-priority-settings.test.js
@@ -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>/);
+ const inputTag = html.match(/]*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 \}\)/
+ );
+});
diff --git a/tests/sub2api-panel-proxy.test.js b/tests/sub2api-panel-proxy.test.js
index 64ca9cc..4e8664e 100644
--- a/tests/sub2api-panel-proxy.test.js
+++ b/tests/sub2api-panel-proxy.test.js
@@ -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);
+});