mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
feat(account): 支持按 5h/7d 用量阈值自动暂停账号调度
This commit is contained in:
@@ -1787,6 +1787,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="account?.platform === 'openai'"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4"
|
||||
>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.autoPause5hThreshold') }}</label>
|
||||
<input
|
||||
v-model.number="autoPause5hThreshold"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
class="input"
|
||||
data-testid="auto-pause-5h-threshold"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.autoPauseThresholdHint') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.autoPause7dThreshold') }}</label>
|
||||
<input
|
||||
v-model.number="autoPause7dThreshold"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
class="input"
|
||||
data-testid="auto-pause-7d-threshold"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.autoPauseThresholdHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配额控制 (Anthropic OAuth/SetupToken: 亲和 + 窗口费用 + 会话 + RPM 等) -->
|
||||
<div
|
||||
v-if="account?.platform === 'anthropic' && (account?.type === 'oauth' || account?.type === 'setup-token')"
|
||||
@@ -2447,6 +2479,8 @@ const selectedErrorCodes = ref<number[]>([])
|
||||
const customErrorCodeInput = ref<number | null>(null)
|
||||
const interceptWarmupRequests = ref(false)
|
||||
const autoPauseOnExpired = ref(false)
|
||||
const autoPause5hThreshold = ref<number | null>(null)
|
||||
const autoPause7dThreshold = ref<number | null>(null)
|
||||
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
||||
const allowOverages = ref(false) // For antigravity accounts: enable AI Credits overages
|
||||
const antigravityModelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
||||
@@ -2862,9 +2896,11 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
// Load mixed scheduling setting (only for antigravity accounts)
|
||||
mixedScheduling.value = false
|
||||
allowOverages.value = false
|
||||
const extra = newAccount.extra as Record<string, unknown> | undefined
|
||||
mixedScheduling.value = extra?.mixed_scheduling === true
|
||||
allowOverages.value = extra?.allow_overages === true
|
||||
const extra = newAccount.extra as Record<string, unknown> | undefined
|
||||
mixedScheduling.value = extra?.mixed_scheduling === true
|
||||
allowOverages.value = extra?.allow_overages === true
|
||||
autoPause5hThreshold.value = typeof extra?.auto_pause_5h_threshold === 'number' ? extra.auto_pause_5h_threshold * 100 : null
|
||||
autoPause7dThreshold.value = typeof extra?.auto_pause_7d_threshold === 'number' ? extra.auto_pause_7d_threshold * 100 : null
|
||||
|
||||
// Load OpenAI passthrough toggle (OpenAI OAuth/API Key)
|
||||
openaiPassthroughEnabled.value = false
|
||||
@@ -3987,9 +4023,9 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
// For OpenAI OAuth/API Key accounts, handle passthrough mode in extra
|
||||
if (props.account.platform === 'openai' && (props.account.type === 'oauth' || props.account.type === 'apikey')) {
|
||||
const currentExtra = (props.account.extra as Record<string, unknown>) || {}
|
||||
const newExtra: Record<string, unknown> = { ...currentExtra }
|
||||
if (props.account.platform === 'openai' && (props.account.type === 'oauth' || props.account.type === 'apikey')) {
|
||||
const currentExtra = (props.account.extra as Record<string, unknown>) || {}
|
||||
const newExtra: Record<string, unknown> = { ...currentExtra }
|
||||
const hadCodexCLIOnlyEnabled = currentExtra.codex_cli_only === true
|
||||
if (props.account.type === 'oauth') {
|
||||
newExtra.openai_oauth_responses_websockets_v2_mode = openaiOAuthResponsesWebSocketV2Mode.value
|
||||
@@ -4011,15 +4047,25 @@ const handleSubmit = async () => {
|
||||
} else {
|
||||
newExtra.openai_compact_mode = openAICompactMode.value
|
||||
}
|
||||
if (props.account.type === 'apikey') {
|
||||
if (props.account.type === 'apikey') {
|
||||
if (!openAITextGenerationCapabilityEnabled.value || openAIResponsesMode.value === 'auto') {
|
||||
delete newExtra.openai_responses_mode
|
||||
} else {
|
||||
newExtra.openai_responses_mode = openAIResponsesMode.value
|
||||
}
|
||||
}
|
||||
}
|
||||
if (autoPause5hThreshold.value != null && autoPause5hThreshold.value > 0) {
|
||||
newExtra.auto_pause_5h_threshold = autoPause5hThreshold.value / 100
|
||||
} else {
|
||||
delete newExtra.auto_pause_5h_threshold
|
||||
}
|
||||
if (autoPause7dThreshold.value != null && autoPause7dThreshold.value > 0) {
|
||||
newExtra.auto_pause_7d_threshold = autoPause7dThreshold.value / 100
|
||||
} else {
|
||||
delete newExtra.auto_pause_7d_threshold
|
||||
}
|
||||
|
||||
delete newExtra.codex_image_generation_bridge_enabled
|
||||
delete newExtra.codex_image_generation_bridge_enabled
|
||||
if (codexImageGenerationBridgeMode.value === 'inherit') {
|
||||
delete newExtra.codex_image_generation_bridge
|
||||
} else {
|
||||
|
||||
@@ -330,6 +330,28 @@ describe('EditAccountModal', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('submits OpenAI quota auto-pause thresholds in extra', async () => {
|
||||
const account = buildAccount()
|
||||
account.extra = {
|
||||
auto_pause_5h_threshold: 0.9,
|
||||
auto_pause_7d_threshold: 0.8
|
||||
}
|
||||
updateAccountMock.mockReset()
|
||||
checkMixedChannelRiskMock.mockReset()
|
||||
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
|
||||
updateAccountMock.mockResolvedValue(account)
|
||||
|
||||
const wrapper = mountModal(account)
|
||||
|
||||
await wrapper.get('[data-testid="auto-pause-5h-threshold"]').setValue('95')
|
||||
await wrapper.get('[data-testid="auto-pause-7d-threshold"]').setValue('96')
|
||||
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||
|
||||
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.auto_pause_5h_threshold).toBe(0.95)
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.auto_pause_7d_threshold).toBe(0.96)
|
||||
})
|
||||
|
||||
it('keeps at least one OpenAI APIKey endpoint capability selected', async () => {
|
||||
const account = buildAccount()
|
||||
updateAccountMock.mockReset()
|
||||
|
||||
@@ -3475,6 +3475,9 @@ export default {
|
||||
'When enabled, warmup requests like title generation will return mock responses without consuming upstream tokens',
|
||||
autoPauseOnExpired: 'Auto Pause On Expired',
|
||||
autoPauseOnExpiredDesc: 'When enabled, the account will auto pause scheduling after it expires',
|
||||
autoPause5hThreshold: '5h Usage Threshold (%)',
|
||||
autoPause7dThreshold: '7d Usage Threshold (%)',
|
||||
autoPauseThresholdHint: 'Leave empty or set 0 to disable. Reaching the threshold only skips the account during scheduling and does not modify schedulable.',
|
||||
// Quota control (Anthropic OAuth/SetupToken only)
|
||||
quotaControl: {
|
||||
title: 'Quota Control',
|
||||
|
||||
@@ -3613,6 +3613,9 @@ export default {
|
||||
interceptWarmupRequestsDesc: '启用后,标题生成等预热请求将返回 mock 响应,不消耗上游 token',
|
||||
autoPauseOnExpired: '过期自动暂停调度',
|
||||
autoPauseOnExpiredDesc: '启用后,账号过期将自动暂停调度',
|
||||
autoPause5hThreshold: '5h 用量阈值(%)',
|
||||
autoPause7dThreshold: '7d 用量阈值(%)',
|
||||
autoPauseThresholdHint: '填 0 或留空表示不启用;达到阈值后仅在调度时跳过账号,不修改 schedulable。',
|
||||
// Quota control (Anthropic OAuth/SetupToken only)
|
||||
quotaControl: {
|
||||
title: '配额控制',
|
||||
|
||||
Reference in New Issue
Block a user