Merge pull request #2120 from gaoren002/fix/rate-limit-429-cooldown-config
fix(rate-limit): make 429 fallback cooldown configurable
This commit is contained in:
@@ -805,6 +805,30 @@ export async function updateOverloadCooldownSettings(
|
||||
return data;
|
||||
}
|
||||
|
||||
// ==================== 429 Rate Limit Cooldown Settings ====================
|
||||
|
||||
export interface RateLimit429CooldownSettings {
|
||||
enabled: boolean;
|
||||
cooldown_seconds: number;
|
||||
}
|
||||
|
||||
export async function getRateLimit429CooldownSettings(): Promise<RateLimit429CooldownSettings> {
|
||||
const { data } = await apiClient.get<RateLimit429CooldownSettings>(
|
||||
"/admin/settings/rate-limit-429-cooldown",
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateRateLimit429CooldownSettings(
|
||||
settings: RateLimit429CooldownSettings,
|
||||
): Promise<RateLimit429CooldownSettings> {
|
||||
const { data } = await apiClient.put<RateLimit429CooldownSettings>(
|
||||
"/admin/settings/rate-limit-429-cooldown",
|
||||
settings,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ==================== Stream Timeout Settings ====================
|
||||
|
||||
/**
|
||||
@@ -1024,6 +1048,8 @@ export const settingsAPI = {
|
||||
deleteAdminApiKey,
|
||||
getOverloadCooldownSettings,
|
||||
updateOverloadCooldownSettings,
|
||||
getRateLimit429CooldownSettings,
|
||||
updateRateLimit429CooldownSettings,
|
||||
getStreamTimeoutSettings,
|
||||
updateStreamTimeoutSettings,
|
||||
getRectifierSettings,
|
||||
|
||||
@@ -5543,6 +5543,16 @@ export default {
|
||||
saved: 'Overload cooldown settings saved',
|
||||
saveFailed: 'Failed to save overload cooldown settings'
|
||||
},
|
||||
rateLimit429Cooldown: {
|
||||
title: '429 Default Cooldown',
|
||||
description: 'Configure the default account cooldown when upstream returns 429 without an explicit reset time',
|
||||
enabled: 'Enable 429 Default Cooldown',
|
||||
enabledHint: 'Pause account scheduling when a 429 has no reset time, then auto-recover after cooldown',
|
||||
cooldownSeconds: 'Cooldown Duration (seconds)',
|
||||
cooldownSecondsHint: 'Default cooldown duration (1-7200 seconds); explicit upstream reset times still take precedence',
|
||||
saved: '429 default cooldown settings saved',
|
||||
saveFailed: 'Failed to save 429 default cooldown settings'
|
||||
},
|
||||
streamTimeout: {
|
||||
title: 'Stream Timeout Handling',
|
||||
description: 'Configure account handling strategy when upstream response times out',
|
||||
|
||||
@@ -5703,6 +5703,16 @@ export default {
|
||||
saved: '过载冷却设置保存成功',
|
||||
saveFailed: '保存过载冷却设置失败'
|
||||
},
|
||||
rateLimit429Cooldown: {
|
||||
title: '429 默认回避',
|
||||
description: '配置上游返回 429 且没有明确重置时间时的默认账号回避策略',
|
||||
enabled: '启用 429 默认回避',
|
||||
enabledHint: '收到无重置时间的 429 时暂停该账号调度,冷却后自动恢复',
|
||||
cooldownSeconds: '回避时长(秒)',
|
||||
cooldownSecondsHint: '默认回避持续时间(1-7200 秒);上游返回明确 reset 时仍优先使用上游时间',
|
||||
saved: '429 默认回避设置保存成功',
|
||||
saveFailed: '保存 429 默认回避设置失败'
|
||||
},
|
||||
streamTimeout: {
|
||||
title: '流超时处理',
|
||||
description: '配置上游响应超时时的账户处理策略,避免问题账户持续被选中',
|
||||
|
||||
@@ -291,6 +291,113 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rate Limit Cooldown (429) Settings -->
|
||||
<div class="card">
|
||||
<div
|
||||
class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t("admin.settings.rateLimit429Cooldown.title") }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.rateLimit429Cooldown.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-5 p-6">
|
||||
<div
|
||||
v-if="rateLimit429CooldownLoading"
|
||||
class="flex items-center gap-2 text-gray-500"
|
||||
>
|
||||
<div
|
||||
class="h-4 w-4 animate-spin rounded-full border-b-2 border-primary-600"
|
||||
></div>
|
||||
{{ t("common.loading") }}
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t("admin.settings.rateLimit429Cooldown.enabled")
|
||||
}}</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.rateLimit429Cooldown.enabledHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="rateLimit429CooldownForm.enabled" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="rateLimit429CooldownForm.enabled"
|
||||
class="space-y-4 border-t border-gray-100 pt-4 dark:border-dark-700"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
"admin.settings.rateLimit429Cooldown.cooldownSeconds",
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
<input
|
||||
v-model.number="rateLimit429CooldownForm.cooldown_seconds"
|
||||
type="number"
|
||||
min="1"
|
||||
max="7200"
|
||||
class="input w-32"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
t(
|
||||
"admin.settings.rateLimit429Cooldown.cooldownSecondsHint",
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex justify-end border-t border-gray-100 pt-4 dark:border-dark-700"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="saveRateLimit429CooldownSettings"
|
||||
:disabled="rateLimit429CooldownSaving"
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
<svg
|
||||
v-if="rateLimit429CooldownSaving"
|
||||
class="mr-1 h-4 w-4 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{{
|
||||
rateLimit429CooldownSaving
|
||||
? t("common.saving")
|
||||
: t("common.save")
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stream Timeout Settings -->
|
||||
<div class="card">
|
||||
<div
|
||||
@@ -5605,6 +5712,14 @@ const overloadCooldownForm = reactive({
|
||||
cooldown_minutes: 10,
|
||||
});
|
||||
|
||||
// Rate Limit Cooldown (429) 状态
|
||||
const rateLimit429CooldownLoading = ref(true);
|
||||
const rateLimit429CooldownSaving = ref(false);
|
||||
const rateLimit429CooldownForm = reactive({
|
||||
enabled: true,
|
||||
cooldown_seconds: 5,
|
||||
});
|
||||
|
||||
// Stream Timeout 状态
|
||||
const streamTimeoutLoading = ref(true);
|
||||
const streamTimeoutSaving = ref(false);
|
||||
@@ -7054,6 +7169,40 @@ async function saveOverloadCooldownSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// Rate Limit Cooldown (429) 方法
|
||||
async function loadRateLimit429CooldownSettings() {
|
||||
rateLimit429CooldownLoading.value = true;
|
||||
try {
|
||||
const settings = await adminAPI.settings.getRateLimit429CooldownSettings();
|
||||
Object.assign(rateLimit429CooldownForm, settings);
|
||||
} catch (_error: unknown) {
|
||||
// Silent fail - settings will use defaults
|
||||
} finally {
|
||||
rateLimit429CooldownLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRateLimit429CooldownSettings() {
|
||||
rateLimit429CooldownSaving.value = true;
|
||||
try {
|
||||
const updated = await adminAPI.settings.updateRateLimit429CooldownSettings({
|
||||
enabled: rateLimit429CooldownForm.enabled,
|
||||
cooldown_seconds: rateLimit429CooldownForm.cooldown_seconds,
|
||||
});
|
||||
Object.assign(rateLimit429CooldownForm, updated);
|
||||
appStore.showSuccess(t("admin.settings.rateLimit429Cooldown.saved"));
|
||||
} catch (error: unknown) {
|
||||
appStore.showError(
|
||||
extractApiErrorMessage(
|
||||
error,
|
||||
t("admin.settings.rateLimit429Cooldown.saveFailed"),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
rateLimit429CooldownSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Stream Timeout 方法
|
||||
async function loadStreamTimeoutSettings() {
|
||||
streamTimeoutLoading.value = true;
|
||||
@@ -7665,6 +7814,7 @@ onMounted(() => {
|
||||
loadSubscriptionGroups();
|
||||
loadAdminApiKey();
|
||||
loadOverloadCooldownSettings();
|
||||
loadRateLimit429CooldownSettings();
|
||||
loadStreamTimeoutSettings();
|
||||
loadRectifierSettings();
|
||||
loadBetaPolicySettings();
|
||||
|
||||
@@ -11,6 +11,8 @@ const {
|
||||
updateWebSearchEmulationConfig,
|
||||
getAdminApiKey,
|
||||
getOverloadCooldownSettings,
|
||||
getRateLimit429CooldownSettings,
|
||||
updateRateLimit429CooldownSettings,
|
||||
getStreamTimeoutSettings,
|
||||
getRectifierSettings,
|
||||
getBetaPolicySettings,
|
||||
@@ -31,6 +33,8 @@ const {
|
||||
updateWebSearchEmulationConfig: vi.fn(),
|
||||
getAdminApiKey: vi.fn(),
|
||||
getOverloadCooldownSettings: vi.fn(),
|
||||
getRateLimit429CooldownSettings: vi.fn(),
|
||||
updateRateLimit429CooldownSettings: vi.fn(),
|
||||
getStreamTimeoutSettings: vi.fn(),
|
||||
getRectifierSettings: vi.fn(),
|
||||
getBetaPolicySettings: vi.fn(),
|
||||
@@ -57,6 +61,8 @@ vi.mock("@/api", () => ({
|
||||
updateWebSearchEmulationConfig,
|
||||
getAdminApiKey,
|
||||
getOverloadCooldownSettings,
|
||||
getRateLimit429CooldownSettings,
|
||||
updateRateLimit429CooldownSettings,
|
||||
getStreamTimeoutSettings,
|
||||
getRectifierSettings,
|
||||
getBetaPolicySettings,
|
||||
@@ -454,6 +460,8 @@ describe("admin SettingsView payment visible method controls", () => {
|
||||
updateWebSearchEmulationConfig.mockReset();
|
||||
getAdminApiKey.mockReset();
|
||||
getOverloadCooldownSettings.mockReset();
|
||||
getRateLimit429CooldownSettings.mockReset();
|
||||
updateRateLimit429CooldownSettings.mockReset();
|
||||
getStreamTimeoutSettings.mockReset();
|
||||
getRectifierSettings.mockReset();
|
||||
getBetaPolicySettings.mockReset();
|
||||
@@ -490,6 +498,11 @@ describe("admin SettingsView payment visible method controls", () => {
|
||||
enabled: true,
|
||||
cooldown_minutes: 10,
|
||||
});
|
||||
getRateLimit429CooldownSettings.mockResolvedValue({
|
||||
enabled: true,
|
||||
cooldown_seconds: 5,
|
||||
});
|
||||
updateRateLimit429CooldownSettings.mockImplementation(async (payload) => payload);
|
||||
getStreamTimeoutSettings.mockResolvedValue({
|
||||
enabled: true,
|
||||
action: "temp_unsched",
|
||||
@@ -690,6 +703,8 @@ describe("admin SettingsView wechat connect controls", () => {
|
||||
updateWebSearchEmulationConfig.mockReset();
|
||||
getAdminApiKey.mockReset();
|
||||
getOverloadCooldownSettings.mockReset();
|
||||
getRateLimit429CooldownSettings.mockReset();
|
||||
updateRateLimit429CooldownSettings.mockReset();
|
||||
getStreamTimeoutSettings.mockReset();
|
||||
getRectifierSettings.mockReset();
|
||||
getBetaPolicySettings.mockReset();
|
||||
@@ -729,6 +744,11 @@ describe("admin SettingsView wechat connect controls", () => {
|
||||
enabled: true,
|
||||
cooldown_minutes: 10,
|
||||
});
|
||||
getRateLimit429CooldownSettings.mockResolvedValue({
|
||||
enabled: true,
|
||||
cooldown_seconds: 5,
|
||||
});
|
||||
updateRateLimit429CooldownSettings.mockImplementation(async (payload) => payload);
|
||||
getStreamTimeoutSettings.mockResolvedValue({
|
||||
enabled: true,
|
||||
action: "temp_unsched",
|
||||
|
||||
Reference in New Issue
Block a user