feat: 添加API Key时间窗口限流功能并移除累计总量限制

- 新增时间窗口限流功能,支持按分钟设置时间窗口
- 支持在时间窗口内限制请求次数和Token使用量
- 移除原有的累计总量限制,只保留时间窗口限制
- Token统计包含所有4种类型:输入、输出、缓存创建、缓存读取
- 前端UI优化,明确显示限流参数的作用范围
- 限流触发时提供友好的错误提示和重置时间

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-07-20 15:58:00 +08:00
parent 0aa986a0d8
commit 088ce266ba
7 changed files with 224 additions and 16 deletions

View File

@@ -117,6 +117,8 @@ const app = createApp({
tokenLimit: '',
description: '',
concurrencyLimit: '',
rateLimitWindow: '',
rateLimitRequests: '',
claudeAccountId: '',
enableModelRestriction: false,
restrictedModels: [],
@@ -158,6 +160,8 @@ const app = createApp({
name: '',
tokenLimit: '',
concurrencyLimit: '',
rateLimitWindow: '',
rateLimitRequests: '',
claudeAccountId: '',
enableModelRestriction: false,
restrictedModels: [],
@@ -1211,6 +1215,8 @@ const app = createApp({
tokenLimit: this.apiKeyForm.tokenLimit && this.apiKeyForm.tokenLimit.trim() ? parseInt(this.apiKeyForm.tokenLimit) : null,
description: this.apiKeyForm.description || '',
concurrencyLimit: this.apiKeyForm.concurrencyLimit && this.apiKeyForm.concurrencyLimit.trim() ? parseInt(this.apiKeyForm.concurrencyLimit) : 0,
rateLimitWindow: this.apiKeyForm.rateLimitWindow && this.apiKeyForm.rateLimitWindow.trim() ? parseInt(this.apiKeyForm.rateLimitWindow) : null,
rateLimitRequests: this.apiKeyForm.rateLimitRequests && this.apiKeyForm.rateLimitRequests.trim() ? parseInt(this.apiKeyForm.rateLimitRequests) : null,
claudeAccountId: this.apiKeyForm.claudeAccountId || null,
enableModelRestriction: this.apiKeyForm.enableModelRestriction,
restrictedModels: this.apiKeyForm.restrictedModels
@@ -1231,7 +1237,7 @@ const app = createApp({
// 关闭创建弹窗并清理表单
this.showCreateApiKeyModal = false;
this.apiKeyForm = { name: '', tokenLimit: '', description: '', concurrencyLimit: '', claudeAccountId: '', enableModelRestriction: false, restrictedModels: [], modelInput: '' };
this.apiKeyForm = { name: '', tokenLimit: '', description: '', concurrencyLimit: '', rateLimitWindow: '', rateLimitRequests: '', claudeAccountId: '', enableModelRestriction: false, restrictedModels: [], modelInput: '' };
// 重新加载API Keys列表
await this.loadApiKeys();
@@ -1275,6 +1281,8 @@ const app = createApp({
name: key.name,
tokenLimit: key.tokenLimit || '',
concurrencyLimit: key.concurrencyLimit || '',
rateLimitWindow: key.rateLimitWindow || '',
rateLimitRequests: key.rateLimitRequests || '',
claudeAccountId: key.claudeAccountId || '',
enableModelRestriction: key.enableModelRestriction || false,
restrictedModels: key.restrictedModels ? [...key.restrictedModels] : [],
@@ -1290,6 +1298,8 @@ const app = createApp({
name: '',
tokenLimit: '',
concurrencyLimit: '',
rateLimitWindow: '',
rateLimitRequests: '',
claudeAccountId: '',
enableModelRestriction: false,
restrictedModels: [],
@@ -1309,6 +1319,8 @@ const app = createApp({
body: JSON.stringify({
tokenLimit: this.editApiKeyForm.tokenLimit && this.editApiKeyForm.tokenLimit.toString().trim() !== '' ? parseInt(this.editApiKeyForm.tokenLimit) : 0,
concurrencyLimit: this.editApiKeyForm.concurrencyLimit && this.editApiKeyForm.concurrencyLimit.toString().trim() !== '' ? parseInt(this.editApiKeyForm.concurrencyLimit) : 0,
rateLimitWindow: this.editApiKeyForm.rateLimitWindow && this.editApiKeyForm.rateLimitWindow.toString().trim() !== '' ? parseInt(this.editApiKeyForm.rateLimitWindow) : 0,
rateLimitRequests: this.editApiKeyForm.rateLimitRequests && this.editApiKeyForm.rateLimitRequests.toString().trim() !== '' ? parseInt(this.editApiKeyForm.rateLimitRequests) : 0,
claudeAccountId: this.editApiKeyForm.claudeAccountId || null,
enableModelRestriction: this.editApiKeyForm.enableModelRestriction,
restrictedModels: this.editApiKeyForm.restrictedModels

View File

@@ -534,6 +534,16 @@
<span v-if="key.concurrencyLimit > 0" class="text-xs text-gray-500">/ {{ key.concurrencyLimit }}</span>
</span>
</div>
<!-- 时间窗口限流 -->
<div v-if="key.rateLimitWindow > 0" class="flex justify-between text-sm">
<span class="text-gray-600">时间窗口:</span>
<span class="font-medium text-indigo-600">{{ key.rateLimitWindow }} 分钟</span>
</div>
<!-- 请求次数限制 -->
<div v-if="key.rateLimitRequests > 0" class="flex justify-between text-sm">
<span class="text-gray-600">请求限制:</span>
<span class="font-medium text-indigo-600">{{ key.rateLimitRequests }} 次/窗口</span>
</div>
<!-- 输入/输出Token -->
<div class="flex justify-between text-xs text-gray-500">
<span>输入: {{ formatNumber((key.usage && key.usage.total && key.usage.total.inputTokens) || 0) }}</span>
@@ -1792,14 +1802,38 @@
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">Token 限制 (可选)</label>
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口 (可选)</label>
<input
v-model="apiKeyForm.rateLimitWindow"
type="number"
min="1"
placeholder="留空表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">设置时间窗口分钟在此时间内限制请求次数或Token使用量</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">请求次数限制 (可选)</label>
<input
v-model="apiKeyForm.rateLimitRequests"
type="number"
min="1"
placeholder="留空表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">在时间窗口内允许的最大请求次数</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口内 Token 限制 (可选)</label>
<input
v-model="apiKeyForm.tokenLimit"
type="number"
placeholder="留空表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">设置此 API Key 的最大 token 使用量</p>
<p class="text-xs text-gray-500 mt-2">设置在时间窗口内的最大 Token 使用量(需同时设置时间窗口)</p>
</div>
<div>
@@ -1951,7 +1985,31 @@
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">Token 限制</label>
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口</label>
<input
v-model="editApiKeyForm.rateLimitWindow"
type="number"
min="1"
placeholder="留空表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">设置时间窗口分钟在此时间内限制请求次数或Token使用量</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">请求次数限制</label>
<input
v-model="editApiKeyForm.rateLimitRequests"
type="number"
min="1"
placeholder="留空表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">在时间窗口内允许的最大请求次数</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口内 Token 限制</label>
<input
v-model="editApiKeyForm.tokenLimit"
type="number"
@@ -1959,7 +2017,7 @@
placeholder="0 表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">设置此 API Key 的最大 token 使用量0 或留空表示无限制</p>
<p class="text-xs text-gray-500 mt-2">设置在时间窗口内的最大 Token 使用量(需同时设置时间窗口)0 或留空表示无限制</p>
</div>
<div>