mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Fix/api key concurrency runtime miss (#309)
* test(cli): 覆盖 API key 并发等待与超时路径 * feat(scheduler): API key 并发饱和时等待可用槽位 * fix(proxy): 区分 API key 并发受限与真正的 runtime miss * fix(outcome): runtime miss 仅归因真实执行候选 * feat(api-keys): 统一 concurrent_limit 默认值与校验辅助 * feat(admin): 独立 Key 接口支持 concurrent_limit * feat(admin): 用户 API Key 路由支持 concurrent_limit * feat(public): 自助 API Key 路由支持 concurrent_limit * feat(import): 导入与存储层持久化 concurrent_limit * feat(frontend): 同步 API Key concurrent_limit 类型定义 * feat(frontend): 独立 Key 表单支持 concurrent_limit * feat(frontend): 管理员用户 API Key 表单支持 concurrent_limit * feat(frontend): 自助 API Key 页面支持 concurrent_limit * chore(fmt): 统一 runtime 归因相关 Rust 格式 * chore(fmt): 统一 admin API key 路由 Rust 格式 * chore(fmt): 统一 public 路由与相关测试 Rust 格式 * fix(test): 对齐 no-execution usage 归因断言 * test(middleware): 固定 access log tracing 用例线程模型 * fix(frontend): 提取用户 API Key payload 默认并发辅助 * fix(frontend): 保留用户 Key 的 concurrent_limit 默认值 * fix(api-keys): remove hardcoded concurrent limit default --------- Co-authored-by: fawney19 <elky0401@gmail.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import apiClient from './client'
|
||||
import { cachedRequest, buildCacheKey } from '@/utils/cache'
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import type { BillingSummary } from './auth'
|
||||
|
||||
// LDAP 配置导出结构
|
||||
@@ -353,6 +352,7 @@ export interface AdminApiKey {
|
||||
total_tokens?: number | null
|
||||
total_cost_usd?: number
|
||||
rate_limit?: number | null // null = 跟随系统默认,0 = 不限制
|
||||
concurrent_limit?: number | null // null = 跟随系统默认,0 = 不限制
|
||||
allowed_providers?: string[] | null // 允许的提供商列表
|
||||
allowed_api_formats?: string[] | null // 允许的 API 格式列表
|
||||
allowed_models?: string[] | null // 允许的模型列表
|
||||
@@ -370,6 +370,7 @@ export interface CreateStandaloneApiKeyRequest {
|
||||
allowed_api_formats?: string[] | null
|
||||
allowed_models?: string[] | null
|
||||
rate_limit?: number | null // null = 跟随系统默认,0 = 不限制
|
||||
concurrent_limit?: number | null // null = 跟随系统默认,0 = 不限制
|
||||
expires_at?: string | null // RFC3339 时间,null = 永不过期
|
||||
initial_balance_usd: number | null // 初始余额,null = 无限制
|
||||
unlimited_balance?: boolean | null // 编辑时仅切换额度模式,不调整余额数值
|
||||
@@ -594,7 +595,7 @@ export const adminApi = {
|
||||
key: string,
|
||||
value: unknown,
|
||||
description?: string,
|
||||
requestConfig?: AxiosRequestConfig,
|
||||
requestConfig?: Parameters<typeof apiClient.put>[2],
|
||||
): Promise<{ key: string; value: unknown; description?: string }> {
|
||||
const response = await apiClient.put<{ key: string; value: unknown; description?: string }>(
|
||||
`/api/admin/system/configs/${key}`,
|
||||
|
||||
@@ -159,6 +159,7 @@ export interface ApiKey {
|
||||
total_requests?: number
|
||||
total_cost_usd?: number
|
||||
rate_limit?: number | null
|
||||
concurrent_limit?: number | null
|
||||
allowed_providers?: ProviderConfig[]
|
||||
force_capabilities?: Record<string, boolean> | null // 强制能力配置
|
||||
}
|
||||
@@ -220,7 +221,7 @@ export const meApi = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
async createApiKey(data: { name: string; rate_limit?: number }): Promise<ApiKey> {
|
||||
async createApiKey(data: { name: string; rate_limit?: number | null; concurrent_limit?: number | null }): Promise<ApiKey> {
|
||||
const response = await apiClient.post<ApiKey>('/api/users/me/api-keys', data)
|
||||
return response.data
|
||||
},
|
||||
@@ -253,7 +254,7 @@ export const meApi = {
|
||||
|
||||
async updateApiKey(
|
||||
keyId: string,
|
||||
data: { name?: string; rate_limit?: number | null }
|
||||
data: { name?: string; rate_limit?: number | null; concurrent_limit?: number | null }
|
||||
): Promise<ApiKey & { message: string }> {
|
||||
const response = await apiClient.put<ApiKey & { message: string }>(
|
||||
`/api/users/me/api-keys/${keyId}`,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import apiClient from './client'
|
||||
import type { UserSession as SessionRecord } from '@/types/session'
|
||||
|
||||
export interface User {
|
||||
id: string // UUID
|
||||
@@ -53,6 +54,7 @@ export interface ApiKey {
|
||||
is_locked: boolean // 管理员锁定标志
|
||||
is_standalone: boolean // 是否为独立余额Key
|
||||
rate_limit?: number | null // 普通Key: 0 = 不限制,历史 null 视为跟随系统默认
|
||||
concurrent_limit?: number | null // 普通Key: 0 = 不限制并发,历史 null 兼容
|
||||
total_requests?: number // 总请求数
|
||||
total_cost_usd?: number // 总费用
|
||||
}
|
||||
@@ -60,9 +62,10 @@ export interface ApiKey {
|
||||
export interface UpsertUserApiKeyRequest {
|
||||
name?: string
|
||||
rate_limit?: number | null
|
||||
concurrent_limit?: number | null
|
||||
}
|
||||
|
||||
export type { UserSession } from '@/types/session'
|
||||
export type UserSession = SessionRecord
|
||||
|
||||
export const usersApi = {
|
||||
async getAllUsers(): Promise<User[]> {
|
||||
@@ -94,8 +97,8 @@ export const usersApi = {
|
||||
return response.data.api_keys
|
||||
},
|
||||
|
||||
async getUserSessions(userId: string): Promise<UserSession[]> {
|
||||
const response = await apiClient.get<UserSession[]>(`/api/admin/users/${userId}/sessions`)
|
||||
async getUserSessions(userId: string): Promise<SessionRecord[]> {
|
||||
const response = await apiClient.get<SessionRecord[]>(`/api/admin/users/${userId}/sessions`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
|
||||
@@ -203,6 +203,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
for="form-concurrent-limit"
|
||||
class="text-sm font-medium"
|
||||
>并发限制</Label>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<Input
|
||||
v-if="!form.concurrent_limit_inherited"
|
||||
id="form-concurrent-limit"
|
||||
:model-value="form.concurrent_limit ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000"
|
||||
placeholder="0 = 不限制"
|
||||
class="h-10"
|
||||
@update:model-value="(v) => form.concurrent_limit = parseNumberInput(v, { min: 0, max: 10000 })"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="flex h-10 w-full items-center rounded-lg border bg-background px-3 text-sm text-muted-foreground opacity-60"
|
||||
>不限制</span>
|
||||
</div>
|
||||
<Switch
|
||||
v-model="form.concurrent_limit_inherited"
|
||||
class="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
留空表示不限制,填 0 也表示不限制并发
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 额度 -->
|
||||
<div class="space-y-2">
|
||||
<Label class="text-sm font-medium">额度</Label>
|
||||
@@ -288,6 +321,7 @@ export interface StandaloneKeyFormData {
|
||||
unlimited_balance?: boolean
|
||||
expires_at?: string // ISO 日期字符串,如 "2025-12-31",undefined = 永不过期
|
||||
rate_limit?: number | null
|
||||
concurrent_limit?: number | null
|
||||
auto_delete_on_expiry: boolean
|
||||
allowed_providers?: string[] | null
|
||||
allowed_api_formats?: string[] | null
|
||||
@@ -303,6 +337,8 @@ interface StandaloneKeyFormState {
|
||||
expires_at?: string
|
||||
rate_limit_inherited: boolean
|
||||
rate_limit?: number
|
||||
concurrent_limit_inherited: boolean
|
||||
concurrent_limit?: number
|
||||
auto_delete_on_expiry: boolean
|
||||
provider_unrestricted: boolean
|
||||
api_format_unrestricted: boolean
|
||||
@@ -358,6 +394,8 @@ const form = ref<StandaloneKeyFormState>({
|
||||
expires_at: undefined,
|
||||
rate_limit_inherited: true,
|
||||
rate_limit: undefined,
|
||||
concurrent_limit_inherited: true,
|
||||
concurrent_limit: undefined,
|
||||
auto_delete_on_expiry: false,
|
||||
provider_unrestricted: true,
|
||||
api_format_unrestricted: true,
|
||||
@@ -402,6 +440,8 @@ function resetForm() {
|
||||
expires_at: undefined,
|
||||
rate_limit_inherited: true,
|
||||
rate_limit: undefined,
|
||||
concurrent_limit_inherited: true,
|
||||
concurrent_limit: undefined,
|
||||
auto_delete_on_expiry: false,
|
||||
provider_unrestricted: true,
|
||||
api_format_unrestricted: true,
|
||||
@@ -423,6 +463,8 @@ function loadKeyData() {
|
||||
expires_at: props.apiKey.expires_at,
|
||||
rate_limit_inherited: props.apiKey.rate_limit == null,
|
||||
rate_limit: props.apiKey.rate_limit ?? undefined,
|
||||
concurrent_limit_inherited: props.apiKey.concurrent_limit == null,
|
||||
concurrent_limit: props.apiKey.concurrent_limit ?? undefined,
|
||||
auto_delete_on_expiry: props.apiKey.auto_delete_on_expiry,
|
||||
provider_unrestricted: props.apiKey.allowed_providers == null,
|
||||
api_format_unrestricted: props.apiKey.allowed_api_formats == null,
|
||||
@@ -473,6 +515,7 @@ function handleSubmit() {
|
||||
unlimited_balance: form.value.unlimited_balance,
|
||||
expires_at: form.value.expires_at,
|
||||
rate_limit: form.value.rate_limit_inherited ? null : (form.value.rate_limit ?? 0),
|
||||
concurrent_limit: form.value.concurrent_limit_inherited ? null : (form.value.concurrent_limit ?? 0),
|
||||
auto_delete_on_expiry: form.value.auto_delete_on_expiry,
|
||||
allowed_providers: form.value.provider_unrestricted ? null : [...form.value.allowed_providers],
|
||||
allowed_api_formats: form.value.api_format_unrestricted ? null : [...form.value.allowed_api_formats],
|
||||
@@ -503,6 +546,15 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => form.value.concurrent_limit_inherited,
|
||||
(inherited) => {
|
||||
if (!inherited && form.value.concurrent_limit == null) {
|
||||
form.value.concurrent_limit = 0
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
defineExpose({
|
||||
setSaving
|
||||
})
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { buildUserApiKeyMutationPayload } from '@/features/api-keys/utils/userKeyPayload'
|
||||
|
||||
describe('userKeyPayload', () => {
|
||||
it('omits concurrent_limit when the field is left blank', () => {
|
||||
expect(buildUserApiKeyMutationPayload({
|
||||
name: 'writer-key',
|
||||
rate_limit: 30,
|
||||
concurrent_limit: undefined,
|
||||
})).toEqual({
|
||||
name: 'writer-key',
|
||||
rate_limit: 30,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps explicit unlimited concurrent_limit values', () => {
|
||||
expect(buildUserApiKeyMutationPayload({
|
||||
name: 'writer-key',
|
||||
rate_limit: undefined,
|
||||
concurrent_limit: 0,
|
||||
})).toEqual({
|
||||
name: 'writer-key',
|
||||
rate_limit: 0,
|
||||
concurrent_limit: 0,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps positive concurrent_limit values', () => {
|
||||
expect(buildUserApiKeyMutationPayload({
|
||||
name: 'writer-key',
|
||||
rate_limit: 15,
|
||||
concurrent_limit: 4,
|
||||
})).toEqual({
|
||||
name: 'writer-key',
|
||||
rate_limit: 15,
|
||||
concurrent_limit: 4,
|
||||
})
|
||||
})
|
||||
})
|
||||
21
frontend/src/features/api-keys/utils/userKeyPayload.ts
Normal file
21
frontend/src/features/api-keys/utils/userKeyPayload.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface UserApiKeyMutationPayload {
|
||||
name: string
|
||||
rate_limit: number
|
||||
concurrent_limit?: number
|
||||
}
|
||||
|
||||
interface BuildUserApiKeyMutationPayloadInput {
|
||||
name: string
|
||||
rate_limit?: number
|
||||
concurrent_limit?: number
|
||||
}
|
||||
|
||||
export function buildUserApiKeyMutationPayload(
|
||||
input: BuildUserApiKeyMutationPayloadInput,
|
||||
): UserApiKeyMutationPayload {
|
||||
return {
|
||||
name: input.name,
|
||||
rate_limit: input.rate_limit ?? 0,
|
||||
...(input.concurrent_limit === undefined ? {} : { concurrent_limit: input.concurrent_limit }),
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@
|
||||
钱包
|
||||
</TableHead>
|
||||
<TableHead class="w-[190px] h-12 font-semibold">
|
||||
统计/限速
|
||||
统计/限制
|
||||
</TableHead>
|
||||
<TableHead class="w-[110px] h-12 font-semibold">
|
||||
有效期
|
||||
@@ -239,6 +239,22 @@
|
||||
{{ formatRateLimitInheritable(apiKey.rate_limit) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-muted-foreground">
|
||||
<span>并发:</span>
|
||||
<Badge
|
||||
v-if="isConcurrentLimitInherited(apiKey.concurrent_limit) || isConcurrentLimitUnlimited(apiKey.concurrent_limit)"
|
||||
variant="secondary"
|
||||
class="h-5 px-1.5 py-0 text-[10px] font-medium"
|
||||
>
|
||||
{{ formatConcurrentLimitInheritable(apiKey.concurrent_limit) }}
|
||||
</Badge>
|
||||
<span
|
||||
v-else
|
||||
class="font-medium text-foreground"
|
||||
>
|
||||
{{ formatConcurrentLimitInheritable(apiKey.concurrent_limit) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
@@ -409,6 +425,12 @@
|
||||
>
|
||||
{{ formatRateLimitInheritable(apiKey.rate_limit) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="h-5 px-1.5 py-0 text-[10px] font-medium"
|
||||
>
|
||||
{{ formatConcurrentLimitInheritable(apiKey.concurrent_limit) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="apiKey.auto_delete_on_expiry"
|
||||
variant="secondary"
|
||||
@@ -920,6 +942,7 @@ function editApiKey(apiKey: AdminApiKey) {
|
||||
unlimited_balance: isApiKeyUnlimited(apiKey),
|
||||
expires_at: expiresAt,
|
||||
rate_limit: apiKey.rate_limit ?? undefined,
|
||||
concurrent_limit: apiKey.concurrent_limit ?? undefined,
|
||||
auto_delete_on_expiry: apiKey.auto_delete_on_expiry || false,
|
||||
allowed_providers: apiKey.allowed_providers == null ? null : [...apiKey.allowed_providers],
|
||||
allowed_api_formats: apiKey.allowed_api_formats == null ? null : [...apiKey.allowed_api_formats],
|
||||
@@ -961,6 +984,20 @@ function formatApiKeyTotalTokens(apiKey: AdminApiKey): string {
|
||||
return formatTokens(apiKey.total_tokens)
|
||||
}
|
||||
|
||||
function formatConcurrentLimitInheritable(concurrentLimit?: number | null): string {
|
||||
if (concurrentLimit == null) return '不限并发'
|
||||
if (concurrentLimit === 0) return '不限并发'
|
||||
return `${concurrentLimit} 并发`
|
||||
}
|
||||
|
||||
function isConcurrentLimitInherited(concurrentLimit?: number | null): boolean {
|
||||
return concurrentLimit == null
|
||||
}
|
||||
|
||||
function isConcurrentLimitUnlimited(concurrentLimit?: number | null): boolean {
|
||||
return concurrentLimit === 0
|
||||
}
|
||||
|
||||
function formatWalletAmount(value: number | null, nullLabel = '无限制'): string {
|
||||
if (value == null) {
|
||||
return nullLabel
|
||||
@@ -1110,6 +1147,7 @@ async function handleKeyFormSubmit(data: StandaloneKeyFormData) {
|
||||
name: data.name || undefined,
|
||||
unlimited_balance: Boolean(data.unlimited_balance),
|
||||
rate_limit: data.rate_limit ?? null, // undefined = 跟随系统默认,显式传 null
|
||||
concurrent_limit: data.concurrent_limit ?? null,
|
||||
expires_at: serializeExpiryDate(data.expires_at),
|
||||
auto_delete_on_expiry: data.auto_delete_on_expiry,
|
||||
// 空数组表示清除限制(允许全部),后端会将空数组存为 NULL
|
||||
@@ -1139,6 +1177,7 @@ async function handleKeyFormSubmit(data: StandaloneKeyFormData) {
|
||||
name: data.name || undefined,
|
||||
initial_balance_usd: isUnlimited ? null : (data.initial_balance_usd as number),
|
||||
rate_limit: data.rate_limit ?? null, // undefined = 跟随系统默认,显式传 null
|
||||
concurrent_limit: data.concurrent_limit ?? null,
|
||||
expires_at: serializeExpiryDate(data.expires_at),
|
||||
auto_delete_on_expiry: data.auto_delete_on_expiry,
|
||||
// 空数组表示不设置限制(允许全部),后端会将空数组存为 NULL
|
||||
|
||||
@@ -679,6 +679,12 @@
|
||||
>
|
||||
{{ formatRateLimitSimple(apiKey.rate_limit) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ formatConcurrentLimitSimple(apiKey.concurrent_limit) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 mt-0.5">
|
||||
<code class="text-xs font-mono text-muted-foreground">
|
||||
@@ -795,7 +801,7 @@
|
||||
{{ editingUserApiKey ? '编辑 API Key' : '创建 API Key' }}
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ editingUserApiKey ? '更新用户 API Key 的名称和速率限制' : '为用户创建新的 API Key' }}
|
||||
{{ editingUserApiKey ? '更新用户 API Key 的名称、速率限制和并发限制' : '为用户创建新的 API Key' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -834,6 +840,25 @@
|
||||
留空表示不限制
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
for="admin-user-key-concurrent-limit"
|
||||
class="text-sm font-medium"
|
||||
>并发限制</Label>
|
||||
<Input
|
||||
id="admin-user-key-concurrent-limit"
|
||||
:model-value="userApiKeyForm.concurrent_limit ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000"
|
||||
class="h-10"
|
||||
placeholder="0 = 不限并发"
|
||||
@update:model-value="(v) => userApiKeyForm.concurrent_limit = parseNumberInput(v, { min: 0, max: 10000 })"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ editingUserApiKey ? '留空表示保持当前值,填 0 表示不限并发' : '留空表示不限并发,填 0 也表示不限并发' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -1099,6 +1124,7 @@ const editingUserApiKey = ref<ApiKey | null>(null)
|
||||
const userApiKeyForm = ref({
|
||||
name: '',
|
||||
rate_limit: undefined as number | undefined,
|
||||
concurrent_limit: undefined as number | undefined,
|
||||
})
|
||||
|
||||
// 用户统计
|
||||
@@ -1253,6 +1279,13 @@ function formatCurrencyValue(value: number | null, nullLabel = '-'): string {
|
||||
return `$${value.toFixed(2)}`
|
||||
}
|
||||
|
||||
function formatConcurrentLimitSimple(concurrentLimit?: number | null): string {
|
||||
if (concurrentLimit == null || concurrentLimit === 0) {
|
||||
return '不限并发'
|
||||
}
|
||||
return `${concurrentLimit} 并发`
|
||||
}
|
||||
|
||||
function isNegativeWalletValue(value: number | null): boolean {
|
||||
return typeof value === 'number' && value < 0
|
||||
}
|
||||
@@ -1387,6 +1420,7 @@ function openCreateUserApiKeyDialog() {
|
||||
userApiKeyForm.value = {
|
||||
name: `Key-${new Date().toISOString().split('T')[0]}`,
|
||||
rate_limit: undefined,
|
||||
concurrent_limit: undefined,
|
||||
}
|
||||
editingUserApiKey.value = null
|
||||
showUserApiKeyFormDialog.value = true
|
||||
@@ -1397,6 +1431,7 @@ function openEditUserApiKeyDialog(apiKey: ApiKey) {
|
||||
userApiKeyForm.value = {
|
||||
name: apiKey.name || '',
|
||||
rate_limit: apiKey.rate_limit ?? undefined,
|
||||
concurrent_limit: apiKey.concurrent_limit ?? undefined,
|
||||
}
|
||||
showUserApiKeyFormDialog.value = true
|
||||
}
|
||||
@@ -1407,6 +1442,7 @@ function closeUserApiKeyFormDialog() {
|
||||
userApiKeyForm.value = {
|
||||
name: '',
|
||||
rate_limit: undefined,
|
||||
concurrent_limit: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1423,12 +1459,14 @@ async function submitUserApiKeyForm() {
|
||||
await usersStore.updateApiKey(selectedUser.value.id, editingUserApiKey.value.id, {
|
||||
name: userApiKeyForm.value.name,
|
||||
rate_limit: userApiKeyForm.value.rate_limit ?? 0,
|
||||
concurrent_limit: userApiKeyForm.value.concurrent_limit,
|
||||
})
|
||||
success('API Key已更新')
|
||||
} else {
|
||||
const response = await usersStore.createApiKey(selectedUser.value.id, {
|
||||
name: userApiKeyForm.value.name,
|
||||
rate_limit: userApiKeyForm.value.rate_limit ?? 0,
|
||||
concurrent_limit: userApiKeyForm.value.concurrent_limit,
|
||||
})
|
||||
newApiKey.value = response.key || ''
|
||||
showNewApiKeyDialog.value = true
|
||||
|
||||
@@ -174,6 +174,12 @@
|
||||
>
|
||||
{{ formatRateLimitSimple(apiKey.rate_limit) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="h-5 px-2 py-0 text-[10px] font-medium"
|
||||
>
|
||||
{{ formatConcurrentLimitSimple(apiKey.concurrent_limit) }}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -259,6 +265,12 @@
|
||||
>
|
||||
{{ formatRateLimitSimple(apiKey.rate_limit) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-[10px] px-1.5 py-0"
|
||||
>
|
||||
{{ formatConcurrentLimitSimple(apiKey.concurrent_limit) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 flex-shrink-0">
|
||||
<Button
|
||||
@@ -355,7 +367,7 @@
|
||||
{{ editingApiKey ? '编辑 API 密钥' : '创建 API 密钥' }}
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ editingApiKey ? '更新密钥名称和速率限制' : '创建一个新的密钥用于访问 API 服务' }}
|
||||
{{ editingApiKey ? '更新密钥名称、速率限制和并发限制' : '创建一个新的密钥用于访问 API 服务' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -400,6 +412,26 @@
|
||||
留空不限
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
for="key-concurrent-limit"
|
||||
class="text-sm font-semibold"
|
||||
>并发限制</Label>
|
||||
<Input
|
||||
id="key-concurrent-limit"
|
||||
:model-value="newKeyConcurrentLimit ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000"
|
||||
placeholder="0 = 不限并发"
|
||||
class="h-11 border-border/60"
|
||||
@update:model-value="(v) => newKeyConcurrentLimit = parseNumberInput(v, { min: 0, max: 10000 })"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ editingApiKey ? '留空表示保持当前值,填 0 表示不限并发' : '留空表示不限并发,填 0 也表示不限并发' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -542,6 +574,7 @@ const showDeleteDialog = ref(false)
|
||||
|
||||
const newKeyName = ref('')
|
||||
const newKeyRateLimit = ref<number | undefined>(undefined)
|
||||
const newKeyConcurrentLimit = ref<number | undefined>(undefined)
|
||||
const newKeyValue = ref('')
|
||||
const keyToDelete = ref<ApiKey | null>(null)
|
||||
const editingApiKey = ref<ApiKey | null>(null)
|
||||
@@ -573,6 +606,7 @@ function openEditApiKeyDialog(apiKey: ApiKey) {
|
||||
editingApiKey.value = apiKey
|
||||
newKeyName.value = apiKey.name || ''
|
||||
newKeyRateLimit.value = apiKey.rate_limit ?? undefined
|
||||
newKeyConcurrentLimit.value = apiKey.concurrent_limit ?? undefined
|
||||
showCreateDialog.value = true
|
||||
}
|
||||
|
||||
@@ -580,6 +614,7 @@ function openCreateApiKeyDialog() {
|
||||
editingApiKey.value = null
|
||||
newKeyName.value = ''
|
||||
newKeyRateLimit.value = undefined
|
||||
newKeyConcurrentLimit.value = undefined
|
||||
showCreateDialog.value = true
|
||||
}
|
||||
|
||||
@@ -588,6 +623,7 @@ function closeApiKeyDialog() {
|
||||
editingApiKey.value = null
|
||||
newKeyName.value = ''
|
||||
newKeyRateLimit.value = undefined
|
||||
newKeyConcurrentLimit.value = undefined
|
||||
}
|
||||
|
||||
async function saveApiKey() {
|
||||
@@ -602,12 +638,14 @@ async function saveApiKey() {
|
||||
await meApi.updateApiKey(editingApiKey.value.id, {
|
||||
name: newKeyName.value,
|
||||
rate_limit: newKeyRateLimit.value ?? 0,
|
||||
concurrent_limit: newKeyConcurrentLimit.value,
|
||||
})
|
||||
success('API 密钥更新成功')
|
||||
} else {
|
||||
const newKey = await meApi.createApiKey({
|
||||
name: newKeyName.value,
|
||||
rate_limit: newKeyRateLimit.value ?? 0,
|
||||
concurrent_limit: newKeyConcurrentLimit.value,
|
||||
})
|
||||
newKeyValue.value = newKey.key || ''
|
||||
showKeyDialog.value = true
|
||||
@@ -711,6 +749,13 @@ function formatNumber(num: number | undefined | null): string {
|
||||
return num.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
function formatConcurrentLimitSimple(concurrentLimit?: number | null): string {
|
||||
if (concurrentLimit == null || concurrentLimit === 0) {
|
||||
return '不限并发'
|
||||
}
|
||||
return `${concurrentLimit} 并发`
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
|
||||
Reference in New Issue
Block a user