mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat(rate-limit): 实现分层 RPM 限速,支持系统默认/用户/独立Key三级配置
- 新增用户级 rate_limit 字段,支持系统默认/用户自定义/不限制三种模式 - 独立 Key 的 rate_limit 语义调整:null=跟随系统默认,0=不限制,>0=自定义 - 实现 UserRpmLimiter 基于 Redis sliding window 的 RPM 限速引擎 - Pipeline 请求流程集成用户级 RPM 检查 - 管理后台和用户面板新增 RPM 限速配置与实时状态查看 - 系统设置新增全局默认 RPM 配置项 - 迁移脚本回填现有 API Key 的 rate_limit 默认值 - 新增用户/Key RPM 状态监控 API 和前端展示 Closes #231 Co-authored-by: LewisPen <LewisPen@nyadoo.com>
This commit is contained in:
@@ -86,6 +86,7 @@ export interface UserExport {
|
||||
allowed_providers?: string[] | null
|
||||
allowed_api_formats?: string[] | null
|
||||
allowed_models?: string[] | null
|
||||
rate_limit?: number | null // null = 跟随系统默认,0 = 不限制
|
||||
model_capability_settings?: Record<string, Record<string, boolean>>
|
||||
unlimited?: boolean
|
||||
wallet?: BillingSummary | null
|
||||
@@ -102,7 +103,7 @@ export interface UserApiKeyExport {
|
||||
allowed_providers?: string[] | null
|
||||
allowed_api_formats?: string[] | null
|
||||
allowed_models?: string[] | null
|
||||
rate_limit?: number | null // null = 无限制
|
||||
rate_limit?: number | null // legacy/null 兼容;1.3+ standalone null = 跟随系统默认
|
||||
concurrent_limit?: number | null
|
||||
force_capabilities?: Record<string, boolean>
|
||||
is_active: boolean
|
||||
@@ -349,7 +350,7 @@ export interface AdminApiKey {
|
||||
total_requests?: number
|
||||
total_tokens?: number
|
||||
total_cost_usd?: number
|
||||
rate_limit?: number | null // null = 无限制
|
||||
rate_limit?: number | null // null = 跟随系统默认,0 = 不限制
|
||||
allowed_providers?: string[] | null // 允许的提供商列表
|
||||
allowed_api_formats?: string[] | null // 允许的 API 格式列表
|
||||
allowed_models?: string[] | null // 允许的模型列表
|
||||
@@ -365,7 +366,7 @@ export interface CreateStandaloneApiKeyRequest {
|
||||
allowed_providers?: string[] | null
|
||||
allowed_api_formats?: string[] | null
|
||||
allowed_models?: string[] | null
|
||||
rate_limit?: number | null // null = 无限制
|
||||
rate_limit?: number | null // null = 跟随系统默认,0 = 不限制
|
||||
expires_at?: string | null // ISO 日期字符串,如 "2025-12-31",null = 永不过期
|
||||
initial_balance_usd: number | null // 初始余额,null = 无限制
|
||||
unlimited_balance?: boolean | null // 编辑时仅切换额度模式,不调整余额数值
|
||||
|
||||
@@ -142,6 +142,7 @@ export interface ApiKey {
|
||||
created_at: string
|
||||
total_requests?: number
|
||||
total_cost_usd?: number
|
||||
rate_limit?: number | null
|
||||
allowed_providers?: ProviderConfig[]
|
||||
force_capabilities?: Record<string, boolean> | null // 强制能力配置
|
||||
}
|
||||
@@ -181,8 +182,8 @@ export const meApi = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
async createApiKey(name: string): Promise<ApiKey> {
|
||||
const response = await apiClient.post<ApiKey>('/api/users/me/api-keys', { name })
|
||||
async createApiKey(data: { name: string; rate_limit?: number }): Promise<ApiKey> {
|
||||
const response = await apiClient.post<ApiKey>('/api/users/me/api-keys', data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -212,6 +213,17 @@ export const meApi = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
async updateApiKey(
|
||||
keyId: string,
|
||||
data: { name?: string; rate_limit?: number | null }
|
||||
): Promise<ApiKey & { message: string }> {
|
||||
const response = await apiClient.put<ApiKey & { message: string }>(
|
||||
`/api/users/me/api-keys/${keyId}`,
|
||||
data
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 使用统计
|
||||
async getUsage(params?: {
|
||||
start_date?: string
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface User {
|
||||
allowed_providers: string[] | null // 允许使用的提供商 ID 列表
|
||||
allowed_api_formats: string[] | null // 允许使用的 API 格式列表
|
||||
allowed_models: string[] | null // 允许使用的模型名称列表
|
||||
rate_limit?: number | null // null = 跟随系统默认,0 = 不限制
|
||||
created_at: string
|
||||
updated_at?: string
|
||||
last_login_at?: string | null
|
||||
@@ -25,6 +26,7 @@ export interface CreateUserRequest {
|
||||
allowed_providers?: string[] | null
|
||||
allowed_api_formats?: string[] | null
|
||||
allowed_models?: string[] | null
|
||||
rate_limit?: number | null
|
||||
}
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
@@ -36,6 +38,7 @@ export interface UpdateUserRequest {
|
||||
allowed_providers?: string[] | null
|
||||
allowed_api_formats?: string[] | null
|
||||
allowed_models?: string[] | null
|
||||
rate_limit?: number | null
|
||||
}
|
||||
|
||||
export interface ApiKey {
|
||||
@@ -49,11 +52,16 @@ export interface ApiKey {
|
||||
is_active: boolean
|
||||
is_locked: boolean // 管理员锁定标志
|
||||
is_standalone: boolean // 是否为独立余额Key
|
||||
rate_limit?: number // 速率限制(请求/分钟)
|
||||
rate_limit?: number | null // 普通Key: 0 = 不限制,历史 null 视为跟随系统默认
|
||||
total_requests?: number // 总请求数
|
||||
total_cost_usd?: number // 总费用
|
||||
}
|
||||
|
||||
export interface UpsertUserApiKeyRequest {
|
||||
name?: string
|
||||
rate_limit?: number | null
|
||||
}
|
||||
|
||||
export const usersApi = {
|
||||
async getAllUsers(): Promise<User[]> {
|
||||
const response = await apiClient.get<User[]>('/api/admin/users')
|
||||
@@ -84,8 +92,26 @@ export const usersApi = {
|
||||
return response.data.api_keys
|
||||
},
|
||||
|
||||
async createApiKey(userId: string, name?: string): Promise<ApiKey & { key: string }> {
|
||||
const response = await apiClient.post<ApiKey & { key: string }>(`/api/admin/users/${userId}/api-keys`, { name })
|
||||
async createApiKey(
|
||||
userId: string,
|
||||
data: UpsertUserApiKeyRequest
|
||||
): Promise<ApiKey & { key: string }> {
|
||||
const response = await apiClient.post<ApiKey & { key: string }>(
|
||||
`/api/admin/users/${userId}/api-keys`,
|
||||
data
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async updateApiKey(
|
||||
userId: string,
|
||||
keyId: string,
|
||||
data: UpsertUserApiKeyRequest
|
||||
): Promise<ApiKey & { message: string }> {
|
||||
const response = await apiClient.put<ApiKey & { message: string }>(
|
||||
`/api/admin/users/${userId}/api-keys/${keyId}`,
|
||||
data
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
|
||||
@@ -96,23 +96,6 @@
|
||||
{{ form.expires_at ? '到期后' + (form.auto_delete_on_expiry ? '自动删除' : '仅禁用') + '(当天 23:59 失效)' : '留空表示永不过期' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
for="form-rate-limit"
|
||||
class="text-sm font-medium"
|
||||
>速率限制 (请求/分钟)</Label>
|
||||
<Input
|
||||
id="form-rate-limit"
|
||||
:model-value="form.rate_limit ?? ''"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000"
|
||||
placeholder="留空不限制"
|
||||
class="h-10"
|
||||
@update:model-value="(v) => form.rate_limit = parseNumberInput(v, { min: 1, max: 10000 })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:访问限制 -->
|
||||
@@ -190,6 +173,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
for="form-rate-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.rate_limit_inherited"
|
||||
id="form-rate-limit"
|
||||
:model-value="form.rate_limit ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000"
|
||||
placeholder="0 = 不限速"
|
||||
class="h-10"
|
||||
@update:model-value="(v) => form.rate_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.rate_limit_inherited"
|
||||
class="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 额度 -->
|
||||
<div class="space-y-2">
|
||||
<Label class="text-sm font-medium">额度</Label>
|
||||
@@ -267,7 +280,7 @@ export interface StandaloneKeyFormData {
|
||||
initial_balance_usd?: number
|
||||
unlimited_balance?: boolean
|
||||
expires_at?: string // ISO 日期字符串,如 "2025-12-31",undefined = 永不过期
|
||||
rate_limit?: number
|
||||
rate_limit?: number | null
|
||||
auto_delete_on_expiry: boolean
|
||||
allowed_providers?: string[] | null
|
||||
allowed_api_formats?: string[] | null
|
||||
@@ -280,6 +293,7 @@ interface StandaloneKeyFormState {
|
||||
initial_balance_usd?: number
|
||||
unlimited_balance?: boolean
|
||||
expires_at?: string
|
||||
rate_limit_inherited: boolean
|
||||
rate_limit?: number
|
||||
auto_delete_on_expiry: boolean
|
||||
provider_unrestricted: boolean
|
||||
@@ -333,6 +347,7 @@ const form = ref<StandaloneKeyFormState>({
|
||||
initial_balance_usd: 10,
|
||||
unlimited_balance: false,
|
||||
expires_at: undefined,
|
||||
rate_limit_inherited: true,
|
||||
rate_limit: undefined,
|
||||
auto_delete_on_expiry: false,
|
||||
provider_unrestricted: true,
|
||||
@@ -356,6 +371,7 @@ function resetForm() {
|
||||
initial_balance_usd: 10,
|
||||
unlimited_balance: false,
|
||||
expires_at: undefined,
|
||||
rate_limit_inherited: true,
|
||||
rate_limit: undefined,
|
||||
auto_delete_on_expiry: false,
|
||||
provider_unrestricted: true,
|
||||
@@ -375,7 +391,8 @@ function loadKeyData() {
|
||||
initial_balance_usd: props.apiKey.initial_balance_usd,
|
||||
unlimited_balance: props.apiKey.initial_balance_usd == null,
|
||||
expires_at: props.apiKey.expires_at,
|
||||
rate_limit: props.apiKey.rate_limit,
|
||||
rate_limit_inherited: props.apiKey.rate_limit == null,
|
||||
rate_limit: props.apiKey.rate_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,
|
||||
@@ -425,7 +442,7 @@ function handleSubmit() {
|
||||
initial_balance_usd: form.value.initial_balance_usd,
|
||||
unlimited_balance: form.value.unlimited_balance,
|
||||
expires_at: form.value.expires_at,
|
||||
rate_limit: form.value.rate_limit,
|
||||
rate_limit: form.value.rate_limit_inherited ? null : (form.value.rate_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],
|
||||
|
||||
@@ -256,6 +256,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
for="form-rate-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.rate_limit_inherited"
|
||||
id="form-rate-limit"
|
||||
:model-value="form.rate_limit ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000"
|
||||
placeholder="0 = 不限速"
|
||||
class="h-10"
|
||||
@update:model-value="(v) => form.rate_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.rate_limit_inherited"
|
||||
class="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 额度 -->
|
||||
<div class="space-y-2">
|
||||
<Label class="text-sm font-medium">额度</Label>
|
||||
@@ -352,6 +382,7 @@ export interface UserFormData {
|
||||
allowed_providers?: string[] | null
|
||||
allowed_api_formats?: string[] | null
|
||||
allowed_models?: string[] | null
|
||||
rate_limit?: number | null
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -406,9 +437,11 @@ const form = ref({
|
||||
provider_unrestricted: true,
|
||||
api_format_unrestricted: true,
|
||||
model_unrestricted: true,
|
||||
rate_limit_inherited: true,
|
||||
allowed_providers: [] as string[],
|
||||
allowed_api_formats: [] as string[],
|
||||
allowed_models: [] as string[],
|
||||
rate_limit: undefined as number | undefined,
|
||||
})
|
||||
|
||||
function createFieldNonce(): string {
|
||||
@@ -429,9 +462,11 @@ function resetForm() {
|
||||
provider_unrestricted: true,
|
||||
api_format_unrestricted: true,
|
||||
model_unrestricted: true,
|
||||
rate_limit_inherited: true,
|
||||
allowed_providers: [],
|
||||
allowed_api_formats: [],
|
||||
allowed_models: [],
|
||||
rate_limit: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,9 +486,11 @@ function loadUserData() {
|
||||
provider_unrestricted: props.user.allowed_providers == null,
|
||||
api_format_unrestricted: props.user.allowed_api_formats == null,
|
||||
model_unrestricted: props.user.allowed_models == null,
|
||||
rate_limit_inherited: props.user.rate_limit == null,
|
||||
allowed_providers: props.user.allowed_providers ? [...props.user.allowed_providers] : [],
|
||||
allowed_api_formats: props.user.allowed_api_formats ? [...props.user.allowed_api_formats] : [],
|
||||
allowed_models: props.user.allowed_models ? [...props.user.allowed_models] : [],
|
||||
rate_limit: props.user.rate_limit ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,6 +580,7 @@ async function handleSubmit() {
|
||||
allowed_models: form.value.model_unrestricted
|
||||
? null
|
||||
: [...form.value.allowed_models],
|
||||
rate_limit: form.value.rate_limit_inherited ? null : (form.value.rate_limit ?? 0),
|
||||
}
|
||||
|
||||
if (isEditMode.value && props.user?.id) {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { usersApi, type User, type CreateUserRequest, type UpdateUserRequest, type ApiKey } from '@/api/users'
|
||||
import {
|
||||
usersApi,
|
||||
type User,
|
||||
type CreateUserRequest,
|
||||
type UpdateUserRequest,
|
||||
type ApiKey,
|
||||
type UpsertUserApiKeyRequest
|
||||
} from '@/api/users'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
export const useUsersStore = defineStore('users', () => {
|
||||
@@ -84,15 +91,28 @@ export const useUsersStore = defineStore('users', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function createApiKey(userId: string, name?: string): Promise<ApiKey> {
|
||||
async function createApiKey(userId: string, data: UpsertUserApiKeyRequest): Promise<ApiKey> {
|
||||
try {
|
||||
return await usersApi.createApiKey(userId, name)
|
||||
return await usersApi.createApiKey(userId, data)
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '创建 API Key 失败')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function updateApiKey(
|
||||
userId: string,
|
||||
keyId: string,
|
||||
data: UpsertUserApiKeyRequest
|
||||
): Promise<ApiKey> {
|
||||
try {
|
||||
return await usersApi.updateApiKey(userId, keyId, data)
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '更新 API Key 失败')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteApiKey(userId: string, keyId: string) {
|
||||
try {
|
||||
await usersApi.deleteApiKey(userId, keyId)
|
||||
@@ -121,6 +141,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
deleteUser,
|
||||
getUserApiKeys,
|
||||
createApiKey,
|
||||
updateApiKey,
|
||||
deleteApiKey,
|
||||
getFullApiKey
|
||||
}
|
||||
|
||||
@@ -159,3 +159,25 @@ export function formatHitRate(rate: number | undefined): string {
|
||||
if (typeof rate !== 'number' || Number.isNaN(rate)) return '-'
|
||||
return `${rate.toFixed(2)}%`
|
||||
}
|
||||
|
||||
// Rate limit formatting (supports "inherit" semantics: null = inherit system default)
|
||||
export function formatRateLimitInheritable(rateLimit?: number | null): string {
|
||||
if (rateLimit == null) return '跟随系统'
|
||||
if (rateLimit === 0) return '不限速'
|
||||
return `${rateLimit}/min`
|
||||
}
|
||||
|
||||
// Rate limit formatting (simple: null/0 both mean unlimited)
|
||||
export function formatRateLimitSimple(rateLimit?: number | null): string {
|
||||
if (rateLimit == null || rateLimit === 0) return '不限速'
|
||||
return `${rateLimit}/min`
|
||||
}
|
||||
|
||||
// Rate limit state helpers
|
||||
export function isRateLimitInherited(rateLimit?: number | null): boolean {
|
||||
return rateLimit == null
|
||||
}
|
||||
|
||||
export function isRateLimitUnlimited(rateLimit?: number | null): boolean {
|
||||
return rateLimit === 0
|
||||
}
|
||||
|
||||
@@ -105,8 +105,8 @@
|
||||
<TableHead class="w-[240px] h-12 font-semibold">
|
||||
钱包
|
||||
</TableHead>
|
||||
<TableHead class="w-[130px] h-12 font-semibold">
|
||||
使用统计
|
||||
<TableHead class="w-[190px] h-12 font-semibold">
|
||||
统计/限速
|
||||
</TableHead>
|
||||
<TableHead class="w-[110px] h-12 font-semibold">
|
||||
有效期
|
||||
@@ -221,7 +221,23 @@
|
||||
请求: <span class="font-medium text-foreground">{{ (apiKey.total_requests || 0).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground">
|
||||
速率: <span class="font-medium text-foreground">{{ apiKey.rate_limit ? `${apiKey.rate_limit}/min` : '未设置' }}</span>
|
||||
Tokens: <span class="font-medium text-foreground">{{ formatTokens(apiKey.total_tokens || 0) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-muted-foreground">
|
||||
<span>限速:</span>
|
||||
<Badge
|
||||
v-if="isRateLimitInherited(apiKey.rate_limit) || isRateLimitUnlimited(apiKey.rate_limit)"
|
||||
variant="secondary"
|
||||
class="h-5 px-1.5 py-0 text-[10px] font-medium"
|
||||
>
|
||||
{{ formatRateLimitInheritable(apiKey.rate_limit) }}
|
||||
</Badge>
|
||||
<span
|
||||
v-else
|
||||
class="font-medium text-foreground"
|
||||
>
|
||||
{{ formatRateLimitInheritable(apiKey.rate_limit) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -387,6 +403,12 @@
|
||||
>
|
||||
{{ walletStatusLabel(getApiKeyWalletStatus(apiKey.id)) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="h-5 px-1.5 py-0 text-[10px] font-medium"
|
||||
>
|
||||
{{ formatRateLimitInheritable(apiKey.rate_limit) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="apiKey.auto_delete_on_expiry"
|
||||
variant="secondary"
|
||||
@@ -429,14 +451,6 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2.5 text-xs">
|
||||
<div class="rounded-lg border border-border/50 bg-background/70 p-2.5">
|
||||
<div class="mb-1 text-muted-foreground">
|
||||
速率限制
|
||||
</div>
|
||||
<div class="font-semibold text-foreground">
|
||||
{{ apiKey.rate_limit ? `${apiKey.rate_limit}/min` : '未设置' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-border/50 bg-background/70 p-2.5">
|
||||
<div class="mb-1 text-muted-foreground">
|
||||
请求次数
|
||||
@@ -445,6 +459,14 @@
|
||||
{{ (apiKey.total_requests || 0).toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-border/50 bg-background/70 p-2.5">
|
||||
<div class="mb-1 text-muted-foreground">
|
||||
Tokens
|
||||
</div>
|
||||
<div class="font-semibold text-foreground">
|
||||
{{ formatTokens(apiKey.total_tokens || 0) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2 rounded-lg border border-border/50 bg-background/70 p-2.5">
|
||||
<div class="mb-1 text-muted-foreground">
|
||||
有效期
|
||||
@@ -663,6 +685,7 @@ import {
|
||||
|
||||
import { StandaloneKeyFormDialog, type StandaloneKeyFormData } from '@/features/api-keys'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { formatTokens, formatRateLimitInheritable, isRateLimitInherited, isRateLimitUnlimited } from '@/utils/format'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
const { success, error } = useToast()
|
||||
@@ -1028,7 +1051,7 @@ async function handleKeyFormSubmit(data: StandaloneKeyFormData) {
|
||||
const updateData: Partial<CreateStandaloneApiKeyRequest> = {
|
||||
name: data.name || undefined,
|
||||
unlimited_balance: Boolean(data.unlimited_balance),
|
||||
rate_limit: data.rate_limit ?? null, // undefined = 无限制,显式传 null
|
||||
rate_limit: data.rate_limit ?? null, // undefined = 跟随系统默认,显式传 null
|
||||
expires_at: data.expires_at || null, // undefined/空 = 永不过期
|
||||
auto_delete_on_expiry: data.auto_delete_on_expiry,
|
||||
// 空数组表示清除限制(允许全部),后端会将空数组存为 NULL
|
||||
@@ -1057,7 +1080,7 @@ async function handleKeyFormSubmit(data: StandaloneKeyFormData) {
|
||||
const createData: CreateStandaloneApiKeyRequest = {
|
||||
name: data.name || undefined,
|
||||
initial_balance_usd: isUnlimited ? null : (data.initial_balance_usd as number),
|
||||
rate_limit: data.rate_limit ?? null, // undefined = 无限制,显式传 null
|
||||
rate_limit: data.rate_limit ?? null, // undefined = 跟随系统默认,显式传 null
|
||||
expires_at: data.expires_at || null, // undefined/空 = 永不过期
|
||||
auto_delete_on_expiry: data.auto_delete_on_expiry,
|
||||
// 空数组表示不设置限制(允许全部),后端会将空数组存为 NULL
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
钱包
|
||||
</TableHead>
|
||||
<TableHead class="w-[170px] h-12 font-semibold">
|
||||
使用统计
|
||||
统计/限速
|
||||
</TableHead>
|
||||
<TableHead class="w-[110px] h-12 font-semibold">
|
||||
创建时间
|
||||
@@ -258,26 +258,42 @@
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<div
|
||||
v-if="userStats[user.id]"
|
||||
class="space-y-1 text-xs"
|
||||
>
|
||||
<div class="flex items-center text-muted-foreground">
|
||||
<span class="w-14">请求:</span>
|
||||
<span class="font-medium text-foreground">{{ formatNumber(userStats[user.id]?.request_count) }}</span>
|
||||
<div class="space-y-1 text-xs">
|
||||
<template v-if="userStats[user.id]">
|
||||
<div class="flex items-center text-muted-foreground">
|
||||
<span class="w-14">请求:</span>
|
||||
<span class="font-medium text-foreground">{{ formatNumber(userStats[user.id]?.request_count) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center text-muted-foreground">
|
||||
<span class="w-14">Tokens:</span>
|
||||
<span class="font-medium text-foreground">{{ formatTokens(userStats[user.id]?.total_tokens ?? 0) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center text-muted-foreground"
|
||||
>
|
||||
<span class="w-14">统计:</span>
|
||||
<span v-if="loadingStats">加载中...</span>
|
||||
<span v-else>无数据</span>
|
||||
</div>
|
||||
<div class="flex items-center text-muted-foreground">
|
||||
<span class="w-14">Tokens:</span>
|
||||
<span class="font-medium text-foreground">{{ formatTokens(userStats[user.id]?.total_tokens ?? 0) }}</span>
|
||||
<span class="w-14">限速:</span>
|
||||
<Badge
|
||||
v-if="isRateLimitInherited(user.rate_limit) || isRateLimitUnlimited(user.rate_limit)"
|
||||
variant="secondary"
|
||||
class="h-5 px-1.5 py-0 text-[10px] font-medium"
|
||||
>
|
||||
{{ formatRateLimitInheritable(user.rate_limit) }}
|
||||
</Badge>
|
||||
<span
|
||||
v-else
|
||||
class="font-medium text-foreground"
|
||||
>
|
||||
{{ formatRateLimitInheritable(user.rate_limit) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
<span v-if="loadingStats">加载中...</span>
|
||||
<span v-else>无数据</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-xs text-muted-foreground">
|
||||
{{ formatDate(user.created_at) }}
|
||||
@@ -436,6 +452,12 @@
|
||||
>
|
||||
{{ walletStatusLabel(getUserWalletStatus(user.id)) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="h-5 px-1.5 py-0 text-[10px] font-medium"
|
||||
>
|
||||
{{ formatRateLimitInheritable(user.rate_limit) }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-border/60 bg-muted/40 p-3.5">
|
||||
@@ -633,6 +655,12 @@
|
||||
>
|
||||
独立余额
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ formatRateLimitSimple(apiKey.rate_limit) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 mt-0.5">
|
||||
<code class="text-xs font-mono text-muted-foreground">
|
||||
@@ -658,6 +686,15 @@
|
||||
${{ (apiKey.total_cost_usd || 0).toFixed(4) }}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="编辑"
|
||||
@click="openEditUserApiKeyDialog(apiKey)"
|
||||
>
|
||||
<SquarePen class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -718,13 +755,87 @@
|
||||
<Button
|
||||
class="h-10 px-5"
|
||||
:disabled="creatingApiKey"
|
||||
@click="createApiKey"
|
||||
@click="openCreateUserApiKeyDialog"
|
||||
>
|
||||
{{ creatingApiKey ? '创建中...' : '创建' }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
v-model="showUserApiKeyFormDialog"
|
||||
size="lg"
|
||||
>
|
||||
<template #header>
|
||||
<div class="border-b border-border px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-kraft/10 flex-shrink-0">
|
||||
<Key class="h-5 w-5 text-kraft" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
||||
{{ editingUserApiKey ? '编辑 API Key' : '创建 API Key' }}
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ editingUserApiKey ? '更新用户 API Key 的名称和速率限制' : '为用户创建新的 API Key' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
for="admin-user-key-name"
|
||||
class="text-sm font-medium"
|
||||
>密钥名称</Label>
|
||||
<Input
|
||||
id="admin-user-key-name"
|
||||
v-model="userApiKeyForm.name"
|
||||
class="h-10"
|
||||
placeholder="例如:生产环境 Key"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
for="admin-user-key-rate-limit"
|
||||
class="text-sm font-medium"
|
||||
>速率限制 (请求/分钟)</Label>
|
||||
<Input
|
||||
id="admin-user-key-rate-limit"
|
||||
:model-value="userApiKeyForm.rate_limit ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000"
|
||||
class="h-10"
|
||||
placeholder="留空不限"
|
||||
@update:model-value="(v) => userApiKeyForm.rate_limit = parseNumberInput(v, { min: 0, max: 10000 })"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
留空表示不限制
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="h-10 px-5"
|
||||
@click="closeUserApiKeyFormDialog"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
class="h-10 px-5"
|
||||
:disabled="creatingApiKey"
|
||||
@click="submitUserApiKeyForm"
|
||||
>
|
||||
{{ creatingApiKey ? (editingUserApiKey ? '保存中...' : '创建中...') : (editingUserApiKey ? '保存' : '创建') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<WalletOpsDrawer
|
||||
:open="showWalletActionDialogState"
|
||||
:wallet="walletActionTarget?.wallet || null"
|
||||
@@ -849,6 +960,8 @@ import {
|
||||
import UserFormDialog, { type UserFormData } from '@/features/users/components/UserFormDialog.vue'
|
||||
import WalletOpsDrawer from '@/features/wallet/components/WalletOpsDrawer.vue'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { formatTokens, formatRateLimitInheritable, formatRateLimitSimple, isRateLimitInherited, isRateLimitUnlimited } from '@/utils/format'
|
||||
import { parseNumberInput } from '@/utils/form'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
const { success, error } = useToast()
|
||||
@@ -864,11 +977,17 @@ const userFormDialogRef = ref<InstanceType<typeof UserFormDialog>>()
|
||||
// API Keys 对话框状态
|
||||
const showApiKeysDialog = ref(false)
|
||||
const showNewApiKeyDialog = ref(false)
|
||||
const showUserApiKeyFormDialog = ref(false)
|
||||
const selectedUser = ref<User | null>(null)
|
||||
const userApiKeys = ref<ApiKey[]>([])
|
||||
const newApiKey = ref('')
|
||||
const creatingApiKey = ref(false)
|
||||
const apiKeyInput = ref<HTMLInputElement>()
|
||||
const editingUserApiKey = ref<ApiKey | null>(null)
|
||||
const userApiKeyForm = ref({
|
||||
name: '',
|
||||
rate_limit: undefined as number | undefined,
|
||||
})
|
||||
|
||||
// 用户统计
|
||||
const userStats = ref<Record<string, UsageByUser>>({})
|
||||
@@ -979,15 +1098,6 @@ async function loadUserWallets() {
|
||||
}
|
||||
}
|
||||
|
||||
function formatTokens(tokens: number): string {
|
||||
if (tokens >= 1000000) {
|
||||
return `${(tokens / 1000000).toFixed(1)}M`
|
||||
} else if (tokens >= 1000) {
|
||||
return `${(tokens / 1000).toFixed(1)}K`
|
||||
}
|
||||
return tokens.toString()
|
||||
}
|
||||
|
||||
function formatNumber(value?: number | null): string {
|
||||
const numericValue = typeof value === 'number' && Number.isFinite(value) ? value : 0
|
||||
return numericValue.toLocaleString()
|
||||
@@ -1071,7 +1181,8 @@ function editUser(user: User) {
|
||||
is_active: user.is_active,
|
||||
allowed_providers: user.allowed_providers == null ? null : [...user.allowed_providers],
|
||||
allowed_api_formats: user.allowed_api_formats == null ? null : [...user.allowed_api_formats],
|
||||
allowed_models: user.allowed_models == null ? null : [...user.allowed_models]
|
||||
allowed_models: user.allowed_models == null ? null : [...user.allowed_models],
|
||||
rate_limit: user.rate_limit ?? null
|
||||
}
|
||||
showUserFormDialog.value = true
|
||||
}
|
||||
@@ -1093,7 +1204,8 @@ async function handleUserFormSubmit(data: UserFormData & { password?: string; un
|
||||
role: data.role,
|
||||
allowed_providers: data.allowed_providers,
|
||||
allowed_api_formats: data.allowed_api_formats,
|
||||
allowed_models: data.allowed_models
|
||||
allowed_models: data.allowed_models,
|
||||
rate_limit: data.rate_limit ?? null
|
||||
}
|
||||
if (data.password) {
|
||||
updateData.password = data.password
|
||||
@@ -1112,7 +1224,8 @@ async function handleUserFormSubmit(data: UserFormData & { password?: string; un
|
||||
role: data.role,
|
||||
allowed_providers: data.allowed_providers,
|
||||
allowed_api_formats: data.allowed_api_formats,
|
||||
allowed_models: data.allowed_models
|
||||
allowed_models: data.allowed_models,
|
||||
rate_limit: data.rate_limit ?? null
|
||||
})
|
||||
// 如果创建时指定为禁用,则更新状态
|
||||
if (data.is_active === false && newUser) {
|
||||
@@ -1145,20 +1258,61 @@ async function loadUserApiKeys(userId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function createApiKey() {
|
||||
function openCreateUserApiKeyDialog() {
|
||||
userApiKeyForm.value = {
|
||||
name: `Key-${new Date().toISOString().split('T')[0]}`,
|
||||
rate_limit: undefined,
|
||||
}
|
||||
editingUserApiKey.value = null
|
||||
showUserApiKeyFormDialog.value = true
|
||||
}
|
||||
|
||||
function openEditUserApiKeyDialog(apiKey: ApiKey) {
|
||||
editingUserApiKey.value = apiKey
|
||||
userApiKeyForm.value = {
|
||||
name: apiKey.name || '',
|
||||
rate_limit: apiKey.rate_limit ?? undefined,
|
||||
}
|
||||
showUserApiKeyFormDialog.value = true
|
||||
}
|
||||
|
||||
function closeUserApiKeyFormDialog() {
|
||||
showUserApiKeyFormDialog.value = false
|
||||
editingUserApiKey.value = null
|
||||
userApiKeyForm.value = {
|
||||
name: '',
|
||||
rate_limit: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
async function submitUserApiKeyForm() {
|
||||
if (!selectedUser.value) return
|
||||
if (!userApiKeyForm.value.name.trim()) {
|
||||
error('请输入密钥名称', editingUserApiKey.value ? '更新 API Key 失败' : '创建 API Key 失败')
|
||||
return
|
||||
}
|
||||
|
||||
creatingApiKey.value = true
|
||||
try {
|
||||
const response = await usersStore.createApiKey(
|
||||
selectedUser.value.id,
|
||||
`Key-${new Date().toISOString().split('T')[0]}`
|
||||
)
|
||||
newApiKey.value = response.key || ''
|
||||
showNewApiKeyDialog.value = true
|
||||
if (editingUserApiKey.value) {
|
||||
await usersStore.updateApiKey(selectedUser.value.id, editingUserApiKey.value.id, {
|
||||
name: userApiKeyForm.value.name,
|
||||
rate_limit: userApiKeyForm.value.rate_limit ?? 0,
|
||||
})
|
||||
success('API Key已更新')
|
||||
} else {
|
||||
const response = await usersStore.createApiKey(selectedUser.value.id, {
|
||||
name: userApiKeyForm.value.name,
|
||||
rate_limit: userApiKeyForm.value.rate_limit ?? 0,
|
||||
})
|
||||
newApiKey.value = response.key || ''
|
||||
showNewApiKeyDialog.value = true
|
||||
success('API Key创建成功')
|
||||
}
|
||||
await loadUserApiKeys(selectedUser.value.id)
|
||||
closeUserApiKeyFormDialog()
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '未知错误'), '创建 API Key 失败')
|
||||
error(parseApiError(err, '未知错误'), editingUserApiKey.value ? '更新 API Key 失败' : '创建 API Key 失败')
|
||||
} finally {
|
||||
creatingApiKey.value = false
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
for="rate-limit"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
每分钟请求限制
|
||||
默认速率限制 (请求/分钟)
|
||||
</Label>
|
||||
<Input
|
||||
id="rate-limit"
|
||||
@@ -50,7 +50,7 @@
|
||||
@update:model-value="$emit('update:rateLimitPerMinute', Number($event))"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
0 表示不限制
|
||||
0 表示默认不限制;未单独配置的用户和独立 Key 会跟随这里
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="创建新 API Key"
|
||||
@click="showCreateDialog = true"
|
||||
@click="openCreateApiKeyDialog"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -56,7 +56,7 @@
|
||||
<Button
|
||||
size="lg"
|
||||
class="shadow-lg shadow-primary/20"
|
||||
@click="showCreateDialog = true"
|
||||
@click="openCreateApiKeyDialog"
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
创建新 API Key
|
||||
@@ -157,17 +157,23 @@
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<Badge
|
||||
:variant="apiKey.is_active ? 'success' : 'secondary'"
|
||||
class="font-medium px-3 py-1"
|
||||
class="h-5 px-2 py-0 text-[10px] font-medium"
|
||||
>
|
||||
{{ apiKey.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="apiKey.is_locked"
|
||||
variant="warning"
|
||||
class="font-medium text-[10px]"
|
||||
class="h-5 px-2 py-0 text-[10px] font-medium"
|
||||
>
|
||||
已锁定
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="h-5 px-2 py-0 text-[10px] font-medium"
|
||||
>
|
||||
{{ formatRateLimitSimple(apiKey.rate_limit) }}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -179,6 +185,16 @@
|
||||
<!-- 操作按钮 -->
|
||||
<TableCell class="py-4">
|
||||
<div class="flex justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:title="apiKey.is_locked ? '已锁定' : '编辑'"
|
||||
:disabled="apiKey.is_locked"
|
||||
@click="openEditApiKeyDialog(apiKey)"
|
||||
>
|
||||
<SquarePen class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -237,8 +253,24 @@
|
||||
>
|
||||
已锁定
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-[10px] px-1.5 py-0"
|
||||
>
|
||||
{{ formatRateLimitSimple(apiKey.rate_limit) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 flex-shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:title="apiKey.is_locked ? '已锁定' : '编辑'"
|
||||
:disabled="apiKey.is_locked"
|
||||
@click="openEditApiKeyDialog(apiKey)"
|
||||
>
|
||||
<SquarePen class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -288,6 +320,10 @@
|
||||
<span class="text-foreground font-medium">
|
||||
{{ formatNumber(apiKey.total_requests || 0) }} 次
|
||||
</span>
|
||||
<span class="text-muted-foreground">•</span>
|
||||
<span class="text-muted-foreground">
|
||||
{{ formatRateLimitSimple(apiKey.rate_limit) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -316,10 +352,10 @@
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
||||
创建 API 密钥
|
||||
{{ editingApiKey ? '编辑 API 密钥' : '创建 API 密钥' }}
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
创建一个新的密钥用于访问 API 服务
|
||||
{{ editingApiKey ? '更新密钥名称和速率限制' : '创建一个新的密钥用于访问 API 服务' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -344,26 +380,46 @@
|
||||
给密钥起一个有意义的名称方便识别
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
for="key-rate-limit"
|
||||
class="text-sm font-semibold"
|
||||
>速率限制 (请求/分钟)</Label>
|
||||
<Input
|
||||
id="key-rate-limit"
|
||||
:model-value="newKeyRateLimit ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000"
|
||||
placeholder="留空不限"
|
||||
class="h-11 border-border/60"
|
||||
@update:model-value="(v) => newKeyRateLimit = parseNumberInput(v, { min: 0, max: 10000 })"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
留空不限
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="h-11 px-6"
|
||||
@click="showCreateDialog = false"
|
||||
@click="closeApiKeyDialog"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
class="h-11 px-6 shadow-lg shadow-primary/20"
|
||||
:disabled="creating"
|
||||
@click="createApiKey"
|
||||
@click="saveApiKey"
|
||||
>
|
||||
<Loader2
|
||||
v-if="creating"
|
||||
class="animate-spin h-4 w-4 mr-2"
|
||||
/>
|
||||
{{ creating ? '创建中...' : '创建' }}
|
||||
{{ creating ? (editingApiKey ? '保存中...' : '创建中...') : (editingApiKey ? '保存' : '创建') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -455,10 +511,12 @@ import {
|
||||
TableRow
|
||||
} from '@/components/ui'
|
||||
import RefreshButton from '@/components/ui/refresh-button.vue'
|
||||
import { Plus, Key, Copy, Trash2, Loader2, Activity, CheckCircle, Power } from 'lucide-vue-next'
|
||||
import { Plus, Key, Copy, Trash2, Loader2, Activity, CheckCircle, Power, SquarePen } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { log } from '@/utils/logger'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { formatRateLimitSimple } from '@/utils/format'
|
||||
import { parseNumberInput } from '@/utils/form'
|
||||
import { getErrorStatus } from '@/types/api-error'
|
||||
import { computed } from 'vue'
|
||||
|
||||
@@ -483,8 +541,10 @@ const showKeyDialog = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
|
||||
const newKeyName = ref('')
|
||||
const newKeyRateLimit = ref<number | undefined>(undefined)
|
||||
const newKeyValue = ref('')
|
||||
const keyToDelete = ref<ApiKey | null>(null)
|
||||
const editingApiKey = ref<ApiKey | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
loadApiKeys()
|
||||
@@ -509,7 +569,28 @@ async function loadApiKeys() {
|
||||
}
|
||||
}
|
||||
|
||||
async function createApiKey() {
|
||||
function openEditApiKeyDialog(apiKey: ApiKey) {
|
||||
editingApiKey.value = apiKey
|
||||
newKeyName.value = apiKey.name || ''
|
||||
newKeyRateLimit.value = apiKey.rate_limit ?? undefined
|
||||
showCreateDialog.value = true
|
||||
}
|
||||
|
||||
function openCreateApiKeyDialog() {
|
||||
editingApiKey.value = null
|
||||
newKeyName.value = ''
|
||||
newKeyRateLimit.value = undefined
|
||||
showCreateDialog.value = true
|
||||
}
|
||||
|
||||
function closeApiKeyDialog() {
|
||||
showCreateDialog.value = false
|
||||
editingApiKey.value = null
|
||||
newKeyName.value = ''
|
||||
newKeyRateLimit.value = undefined
|
||||
}
|
||||
|
||||
async function saveApiKey() {
|
||||
if (!newKeyName.value.trim()) {
|
||||
showError('请输入密钥名称')
|
||||
return
|
||||
@@ -517,16 +598,26 @@ async function createApiKey() {
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
const newKey = await meApi.createApiKey(newKeyName.value)
|
||||
newKeyValue.value = newKey.key || ''
|
||||
showCreateDialog.value = false
|
||||
showKeyDialog.value = true
|
||||
newKeyName.value = ''
|
||||
if (editingApiKey.value) {
|
||||
await meApi.updateApiKey(editingApiKey.value.id, {
|
||||
name: newKeyName.value,
|
||||
rate_limit: newKeyRateLimit.value ?? 0,
|
||||
})
|
||||
success('API 密钥更新成功')
|
||||
} else {
|
||||
const newKey = await meApi.createApiKey({
|
||||
name: newKeyName.value,
|
||||
rate_limit: newKeyRateLimit.value ?? 0,
|
||||
})
|
||||
newKeyValue.value = newKey.key || ''
|
||||
showKeyDialog.value = true
|
||||
success('API 密钥创建成功')
|
||||
}
|
||||
closeApiKeyDialog()
|
||||
await loadApiKeys()
|
||||
success('API 密钥创建成功')
|
||||
} catch (error) {
|
||||
log.error('创建 API 密钥失败:', error)
|
||||
showError('创建 API 密钥失败')
|
||||
log.error(editingApiKey.value ? '更新 API 密钥失败:' : '创建 API 密钥失败:', error)
|
||||
showError(editingApiKey.value ? '更新 API 密钥失败' : '创建 API 密钥失败')
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user