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:
Wesley Liddick
2026-05-05 19:46:11 +08:00
committed by GitHub
15 changed files with 520 additions and 12 deletions
+150
View File
@@ -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",