mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
refactor: 前端全面替换 any 为 unknown 并统一错误处理,后端用量记录补写请求头/体
- 前端 API 层、stores、conversation 解析器、组件全面替换 any 为 unknown/具体类型 - 错误处理统一使用 parseApiError/getErrorStatus 替代 err.response?.data?.detail 模式 - 后端 handler/TaskService/UsageLifecycle/StreamTracker 链路传递 request_headers/request_body - streaming/pending 状态更新时可补写客户端和提供商的请求头及请求体 - 新增 TaskService 和 UsageService 相关测试
This commit is contained in:
@@ -732,6 +732,7 @@ import {
|
||||
|
||||
import { StandaloneKeyFormDialog, type StandaloneKeyFormData } from '@/features/api-keys'
|
||||
import { parseNumberInput } from '@/utils/form'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
const { success, error } = useToast()
|
||||
@@ -843,9 +844,9 @@ async function loadApiKeys() {
|
||||
})
|
||||
apiKeys.value = response.api_keys
|
||||
total.value = response.total
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('加载独立Keys失败:', err)
|
||||
error(err.response?.data?.detail || '加载独立 Keys 失败')
|
||||
error(parseApiError(err, '加载独立 Keys 失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -864,9 +865,9 @@ async function toggleApiKey(apiKey: AdminApiKey) {
|
||||
apiKeys.value[index].is_active = response.is_active
|
||||
}
|
||||
success(response.message)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('切换密钥状态失败:', err)
|
||||
error(err.response?.data?.detail || '操作失败')
|
||||
error(parseApiError(err, '操作失败'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -878,9 +879,9 @@ async function toggleLockApiKey(apiKey: AdminApiKey) {
|
||||
apiKeys.value[index].is_locked = response.is_locked
|
||||
}
|
||||
success(response.message)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('切换密钥锁定状态失败:', err)
|
||||
error(err.response?.data?.detail || '操作失败')
|
||||
error(parseApiError(err, '操作失败'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -897,9 +898,9 @@ async function deleteApiKey(apiKey: AdminApiKey) {
|
||||
apiKeys.value = apiKeys.value.filter(k => k.id !== apiKey.id)
|
||||
total.value = total.value - 1
|
||||
success(response.message)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('删除密钥失败:', err)
|
||||
error(err.response?.data?.detail || '删除失败')
|
||||
error(parseApiError(err, '删除失败'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -962,9 +963,9 @@ async function handleAddBalance() {
|
||||
const action = addBalanceAmount.value > 0 ? '增加' : '扣除'
|
||||
const amount = Math.abs(addBalanceAmount.value).toFixed(2)
|
||||
success(response.message || `余额${action}成功,${action} $${amount}`)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('余额调整失败:', err)
|
||||
error(err.response?.data?.detail || '调整失败')
|
||||
error(parseApiError(err, '调整失败'))
|
||||
} finally {
|
||||
addingBalance.value = false
|
||||
}
|
||||
@@ -1130,9 +1131,9 @@ async function handleKeyFormSubmit(data: StandaloneKeyFormData) {
|
||||
}
|
||||
closeKeyFormDialog()
|
||||
await loadApiKeys()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('保存独立Key失败:', err)
|
||||
error(err.response?.data?.detail || '保存失败')
|
||||
error(parseApiError(err, '保存失败'))
|
||||
} finally {
|
||||
keyFormDialogRef.value?.setSaving(false)
|
||||
}
|
||||
|
||||
@@ -882,6 +882,7 @@ import {
|
||||
ExternalLink,
|
||||
Copy,
|
||||
} from 'lucide-vue-next'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
const { toast } = useToast()
|
||||
const { copyToClipboard } = useClipboard()
|
||||
@@ -921,10 +922,10 @@ async function fetchTasks() {
|
||||
})
|
||||
tasks.value = response.items
|
||||
total.value = response.total
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast({
|
||||
title: '获取任务列表失败',
|
||||
description: error.message,
|
||||
description: error instanceof Error ? error.message : String(error),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -937,7 +938,7 @@ async function fetchStats() {
|
||||
try {
|
||||
stats.value = await asyncTasksApi.getStats()
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch stats:', error)
|
||||
log.error('Failed to fetch stats', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -946,10 +947,10 @@ async function openTaskDetail(task: AsyncTaskItem) {
|
||||
try {
|
||||
selectedTask.value = await asyncTasksApi.getDetail(task.id)
|
||||
showDetail.value = true
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast({
|
||||
title: '获取任务详情失败',
|
||||
description: error.message,
|
||||
description: error instanceof Error ? error.message : String(error),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -960,10 +961,10 @@ async function refreshTaskDetail() {
|
||||
if (!selectedTask.value) return
|
||||
try {
|
||||
selectedTask.value = await asyncTasksApi.getDetail(selectedTask.value.id)
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast({
|
||||
title: '刷新失败',
|
||||
description: error.message,
|
||||
description: error instanceof Error ? error.message : String(error),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -1023,10 +1024,10 @@ async function openUsageRecord(task: AsyncTaskItem) {
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast({
|
||||
title: '获取任务信息失败',
|
||||
description: error.message,
|
||||
description: error instanceof Error ? error.message : String(error),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -1045,10 +1046,10 @@ async function cancelTask(task: AsyncTaskItem | AsyncTaskDetail) {
|
||||
if (showDetail.value) {
|
||||
closeDetail()
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast({
|
||||
title: '取消任务失败',
|
||||
description: error.message,
|
||||
description: error instanceof Error ? error.message : String(error),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -1164,7 +1165,7 @@ function calcDuration(startStr: string, endStr: string): string {
|
||||
|
||||
|
||||
// 格式化 JSON
|
||||
function formatJson(obj: any): string {
|
||||
function formatJson(obj: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(obj, null, 2)
|
||||
} catch {
|
||||
|
||||
@@ -168,29 +168,29 @@
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="log in logs"
|
||||
:key="log.id"
|
||||
v-for="entry in logs"
|
||||
:key="entry.id"
|
||||
class="cursor-pointer border-b border-border/40 hover:bg-muted/30 transition-colors"
|
||||
@mousedown="handleMouseDown"
|
||||
@click="handleRowClick($event, log)"
|
||||
@click="handleRowClick($event, entry)"
|
||||
>
|
||||
<TableCell class="text-xs py-4">
|
||||
{{ formatDateTime(log.created_at) }}
|
||||
{{ formatDateTime(entry.created_at) }}
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="py-4">
|
||||
<div
|
||||
v-if="log.user_id"
|
||||
v-if="entry.user_id"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<span class="text-sm font-medium">
|
||||
{{ log.user_email || `用户 ${log.user_id}` }}
|
||||
{{ entry.user_email || `用户 ${entry.user_id}` }}
|
||||
</span>
|
||||
<span
|
||||
v-if="log.user_username"
|
||||
v-if="entry.user_username"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
{{ log.user_username }}
|
||||
{{ entry.user_username }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
@@ -200,39 +200,39 @@
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="py-4">
|
||||
<Badge :variant="getEventTypeBadgeVariant(log.event_type)">
|
||||
<Badge :variant="getEventTypeBadgeVariant(entry.event_type)">
|
||||
<component
|
||||
:is="getEventTypeIcon(log.event_type)"
|
||||
:is="getEventTypeIcon(entry.event_type)"
|
||||
class="h-3 w-3 mr-1"
|
||||
/>
|
||||
{{ getEventTypeLabel(log.event_type) }}
|
||||
{{ getEventTypeLabel(entry.event_type) }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
class="max-w-xs truncate py-4"
|
||||
:title="log.description"
|
||||
:title="entry.description"
|
||||
>
|
||||
{{ log.description || '无描述' }}
|
||||
{{ entry.description || '无描述' }}
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="py-4">
|
||||
<span
|
||||
v-if="log.ip_address"
|
||||
v-if="entry.ip_address"
|
||||
class="flex items-center text-sm"
|
||||
>
|
||||
<Globe class="h-3 w-3 mr-1 text-muted-foreground" />
|
||||
{{ log.ip_address }}
|
||||
{{ entry.ip_address }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="py-4">
|
||||
<Badge
|
||||
v-if="log.status_code"
|
||||
:variant="getStatusCodeVariant(log.status_code)"
|
||||
v-if="entry.status_code"
|
||||
:variant="getStatusCodeVariant(entry.status_code)"
|
||||
>
|
||||
{{ log.status_code }}
|
||||
{{ entry.status_code }}
|
||||
</Badge>
|
||||
<span v-else>-</span>
|
||||
</TableCell>
|
||||
@@ -455,7 +455,7 @@ interface AuditLog {
|
||||
ip_address?: string
|
||||
status_code?: number
|
||||
error_message?: string
|
||||
metadata?: any
|
||||
metadata?: Record<string, unknown>
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -657,7 +657,7 @@ function getEventTypeLabel(eventType: string): string {
|
||||
}
|
||||
|
||||
function getEventTypeIcon(eventType: string) {
|
||||
const icons: Record<string, any> = {
|
||||
const icons: Record<string, unknown> = {
|
||||
'login_success': CheckCircle,
|
||||
'login_failed': XCircle,
|
||||
'logout': User,
|
||||
|
||||
@@ -486,6 +486,7 @@ import { PageHeader, PageContainer, CardSection } from '@/components/layout'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { adminApi, type EmailTemplateInfo } from '@/api/admin'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
const { success, error } = useToast()
|
||||
@@ -764,7 +765,7 @@ async function loadEmailConfig() {
|
||||
smtpPasswordIsSet.value = response.is_set === true
|
||||
// 不设置 smtp_password 的值,保持为 null
|
||||
} else if (response.value !== null && response.value !== undefined) {
|
||||
(emailConfig.value as any)[key] = response.value
|
||||
(emailConfig.value as Record<string, unknown>)[key] = response.value
|
||||
}
|
||||
} catch {
|
||||
// 配置不存在时使用默认值,无需处理
|
||||
@@ -867,10 +868,9 @@ async function handleTestSmtp() {
|
||||
} else {
|
||||
error(result.message || '未知错误', 'SMTP 连接测试失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('SMTP 连接测试失败:', err)
|
||||
const errMsg = err.response?.data?.detail || err.message || '未知错误'
|
||||
error(errMsg, 'SMTP 连接测试失败')
|
||||
error(parseApiError(err, '未知错误'), 'SMTP 连接测试失败')
|
||||
} finally {
|
||||
testSmtpLoading.value = false
|
||||
}
|
||||
|
||||
@@ -436,14 +436,16 @@ import {
|
||||
Music,
|
||||
Upload
|
||||
} from 'lucide-vue-next'
|
||||
import { geminiFilesApi } from '@/api/gemini-files'
|
||||
import { geminiFilesApi, type FileMappingStatsResponse, type FileMappingResponse, type CapableKeyResponse } from '@/api/gemini-files'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
const { toast } = useToast()
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const stats = ref<any>(null)
|
||||
const mappings = ref<any[]>([])
|
||||
const stats = ref<FileMappingStatsResponse | null>(null)
|
||||
const mappings = ref<FileMappingResponse[]>([])
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 20
|
||||
@@ -454,7 +456,7 @@ const includeExpired = ref(false)
|
||||
const uploading = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
const capableKeys = ref<any[]>([])
|
||||
const capableKeys = ref<CapableKeyResponse[]>([])
|
||||
const selectedKeyIds = ref<string[]>([])
|
||||
|
||||
// 计算属性
|
||||
@@ -483,8 +485,8 @@ async function fetchCapableKeys() {
|
||||
if (selectedKeyIds.value.length === 0 && keys.length > 0) {
|
||||
selectedKeyIds.value = keys.map(k => k.id)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to fetch capable keys:', error)
|
||||
} catch (error: unknown) {
|
||||
log.error('Failed to fetch capable keys', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,10 +511,10 @@ async function fetchStats() {
|
||||
try {
|
||||
const data = await geminiFilesApi.getStats()
|
||||
stats.value = data
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast({
|
||||
title: '获取统计失败',
|
||||
description: error.message,
|
||||
description: error instanceof Error ? error.message : String(error),
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
@@ -529,10 +531,10 @@ async function fetchMappings() {
|
||||
})
|
||||
mappings.value = data.items
|
||||
total.value = data.total
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast({
|
||||
title: '获取文件列表失败',
|
||||
description: error.message,
|
||||
description: error instanceof Error ? error.message : String(error),
|
||||
variant: 'destructive'
|
||||
})
|
||||
} finally {
|
||||
@@ -540,7 +542,7 @@ async function fetchMappings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMapping(mapping: any) {
|
||||
async function deleteMapping(mapping: FileMappingResponse) {
|
||||
if (!confirm(`确定要删除映射 "${mapping.file_name}" 吗?\n\n注意:这只会删除映射记录,不会删除 Google 上的实际文件。`)) {
|
||||
return
|
||||
}
|
||||
@@ -552,10 +554,10 @@ async function deleteMapping(mapping: any) {
|
||||
description: `已删除映射 ${mapping.file_name}`
|
||||
})
|
||||
await fetchData()
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast({
|
||||
title: '删除失败',
|
||||
description: error.message,
|
||||
description: error instanceof Error ? error.message : String(error),
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
@@ -573,10 +575,10 @@ async function cleanupExpired() {
|
||||
description: `已清理 ${result.deleted_count} 条过期映射`
|
||||
})
|
||||
await fetchData()
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast({
|
||||
title: '清理失败',
|
||||
description: error.message,
|
||||
description: error instanceof Error ? error.message : String(error),
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
@@ -617,10 +619,10 @@ async function uploadFile(file: globalThis.File) {
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast({
|
||||
title: '上传失败',
|
||||
description: error.response?.data?.detail || error.message,
|
||||
description: parseApiError(error, '上传失败'),
|
||||
variant: 'destructive'
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -333,6 +333,7 @@ import {
|
||||
import { blacklistApi, whitelistApi, type BlacklistStats, type WhitelistResponse } from '@/api/security'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
const { success, error } = useToast()
|
||||
const { confirmDanger } = useConfirm()
|
||||
@@ -368,8 +369,8 @@ async function loadBlacklistStats() {
|
||||
loadingBlacklist.value = true
|
||||
try {
|
||||
blacklistStats.value = await blacklistApi.getStats()
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.detail || '无法获取黑名单统计')
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '无法获取黑名单统计'))
|
||||
} finally {
|
||||
loadingBlacklist.value = false
|
||||
}
|
||||
@@ -382,8 +383,8 @@ async function loadWhitelist() {
|
||||
loadingWhitelist.value = true
|
||||
try {
|
||||
whitelistData.value = await whitelistApi.getList()
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.detail || '无法获取白名单列表')
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '无法获取白名单列表'))
|
||||
} finally {
|
||||
loadingWhitelist.value = false
|
||||
}
|
||||
@@ -405,8 +406,8 @@ async function handleAddToBlacklist() {
|
||||
showAddBlacklistDialog.value = false
|
||||
blacklistForm.value = { ip_address: '', reason: '', ttl: undefined }
|
||||
await loadBlacklistStats()
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.detail || '无法添加 IP 到黑名单')
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '无法添加 IP 到黑名单'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,8 +425,8 @@ async function handleAddToWhitelist() {
|
||||
showAddWhitelistDialog.value = false
|
||||
whitelistForm.value = { ip_address: '' }
|
||||
await loadWhitelist()
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.detail || '无法添加 IP 到白名单')
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '无法添加 IP 到白名单'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,8 +447,8 @@ async function handleRemoveFromWhitelist(ip: string) {
|
||||
success(`IP ${ip} 已从白名单移除`)
|
||||
|
||||
await loadWhitelist()
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.detail || '无法从白名单移除 IP')
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '无法从白名单移除 IP'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -243,6 +243,7 @@ import { PageContainer, PageHeader, CardSection } from '@/components/layout'
|
||||
import { Button, Input, Label, Switch } from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { adminApi, type LdapConfigUpdateRequest } from '@/api/admin'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
const { success, error } = useToast()
|
||||
|
||||
@@ -291,7 +292,7 @@ async function loadConfig() {
|
||||
hasPassword.value = !!response.has_bind_password
|
||||
} catch (err) {
|
||||
error('加载 LDAP 配置失败')
|
||||
console.error('加载 LDAP 配置失败:', err)
|
||||
log.error('加载 LDAP 配置失败', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -328,7 +329,7 @@ async function handleSave() {
|
||||
ldapConfig.value.bind_password = ''
|
||||
} catch (err) {
|
||||
error('保存 LDAP 配置失败')
|
||||
console.error('保存 LDAP 配置失败:', err)
|
||||
log.error('保存 LDAP 配置失败', err)
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
@@ -359,7 +360,7 @@ async function handleTestConnection() {
|
||||
}
|
||||
} catch (err) {
|
||||
error('LDAP 连接测试失败')
|
||||
console.error('LDAP 连接测试失败:', err)
|
||||
log.error('LDAP 连接测试失败', err)
|
||||
} finally {
|
||||
testLoading.value = false
|
||||
}
|
||||
|
||||
@@ -541,9 +541,31 @@ import {
|
||||
type GlobalModelResponse,
|
||||
} from '@/api/global-models'
|
||||
import { log } from '@/utils/logger'
|
||||
import { getProvidersSummary } from '@/api/endpoints/providers'
|
||||
import { getProvidersSummary, type ProviderWithEndpointsSummary } from '@/api/endpoints/providers'
|
||||
import { getAllCapabilities, type CapabilityDefinition } from '@/api/endpoints'
|
||||
|
||||
|
||||
interface ModelProviderDisplay {
|
||||
id: string
|
||||
model_id?: string | null
|
||||
name: string
|
||||
provider_type: string
|
||||
target_model: string
|
||||
is_active: boolean
|
||||
input_price_per_1m?: number | null
|
||||
output_price_per_1m?: number | null
|
||||
cache_creation_price_per_1m?: number | null
|
||||
cache_read_price_per_1m?: number | null
|
||||
cache_1h_creation_price_per_1m?: number | null
|
||||
price_per_request?: number | null
|
||||
effective_tiered_pricing?: unknown
|
||||
tier_count?: number
|
||||
supports_vision?: boolean | null
|
||||
supports_function_calling?: boolean | null
|
||||
supports_streaming?: boolean | null
|
||||
supports_extended_thinking?: boolean | null
|
||||
}
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const { copyToClipboard } = useClipboard()
|
||||
|
||||
@@ -558,7 +580,7 @@ const editingModel = ref<GlobalModelResponse | null>(null)
|
||||
|
||||
// 数据
|
||||
const globalModels = ref<GlobalModelResponse[]>([])
|
||||
const providers = ref<any[]>([])
|
||||
const providers = ref<ProviderWithEndpointsSummary[]>([])
|
||||
const capabilities = ref<CapabilityDefinition[]>([])
|
||||
|
||||
// 模型目录分页
|
||||
@@ -566,13 +588,13 @@ const catalogCurrentPage = ref(1)
|
||||
const catalogPageSize = ref(20)
|
||||
|
||||
// 选中模型的详细数据
|
||||
const selectedModelProviders = ref<any[]>([])
|
||||
const selectedModelProviders = ref<ModelProviderDisplay[]>([])
|
||||
const loadingModelProviders = ref(false)
|
||||
|
||||
// 批量添加关联提供商
|
||||
const batchAddProvidersDialogOpen = ref(false)
|
||||
const submittingBatchProviders = ref(false)
|
||||
const providerOptions = ref<any[]>([])
|
||||
const providerOptions = ref<ProviderWithEndpointsSummary[]>([])
|
||||
const loadingProviderOptions = ref(false)
|
||||
|
||||
// 单列勾选模式所需状态
|
||||
@@ -582,7 +604,7 @@ const initialBatchProviderIds = ref<Set<string>>(new Set())
|
||||
|
||||
// 编辑提供商模型
|
||||
const editProviderDialogOpen = ref(false)
|
||||
const editingProvider = ref<any>(null)
|
||||
const editingProvider = ref<ModelProviderDisplay | null>(null)
|
||||
|
||||
// 将 provider 数据转换为 Model 类型供 ProviderModelFormDialog 使用
|
||||
const editingProviderModel = computed<Model | null>(() => {
|
||||
@@ -771,7 +793,7 @@ function toggleAllBatchProviders() {
|
||||
|
||||
// 同步初始选择状态
|
||||
function syncBatchProviderSelection() {
|
||||
const existingIds = new Set(selectedModelProviders.value.map((p: any) => p.id))
|
||||
const existingIds = new Set(selectedModelProviders.value.map((p) => p.id))
|
||||
selectedBatchProviderIds.value = new Set(existingIds)
|
||||
initialBatchProviderIds.value = new Set(existingIds)
|
||||
}
|
||||
@@ -789,7 +811,7 @@ async function saveBatchProviderChanges() {
|
||||
if (batchProvidersToRemove.value.length > 0) {
|
||||
const { deleteModel } = await import('@/api/endpoints')
|
||||
const removePromises = batchProvidersToRemove.value.map(async (providerId) => {
|
||||
const existingProvider = selectedModelProviders.value.find((p: any) => p.id === providerId)
|
||||
const existingProvider = selectedModelProviders.value.find((p) => p.id === providerId)
|
||||
if (existingProvider && existingProvider.model_id) {
|
||||
return deleteModel(providerId, existingProvider.model_id)
|
||||
}
|
||||
@@ -832,7 +854,7 @@ async function saveBatchProviderChanges() {
|
||||
// 刷新路由数据
|
||||
modelDetailDrawerRef.value?.refreshRoutingData?.()
|
||||
closeBatchAddProvidersDialog()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '保存失败'), '错误')
|
||||
} finally {
|
||||
submittingBatchProviders.value = false
|
||||
@@ -890,9 +912,9 @@ async function loadGlobalModels() {
|
||||
const response = await listGlobalModels()
|
||||
// API 返回 { models: [...], total: number }
|
||||
globalModels.value = response.models || []
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('加载模型失败:', err)
|
||||
showError(err.response?.data?.detail || err.message, '加载模型失败')
|
||||
showError(parseApiError(err, '加载模型失败'), '加载模型失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -967,7 +989,7 @@ async function loadModelProviders(_globalModelId: string) {
|
||||
supports_function_calling: p.supports_function_calling,
|
||||
supports_streaming: p.supports_streaming
|
||||
}))
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('加载关联提供商失败:', err)
|
||||
showError(parseApiError(err, '加载关联提供商失败'), '错误')
|
||||
selectedModelProviders.value = []
|
||||
@@ -984,7 +1006,7 @@ async function ensureProviderOptions() {
|
||||
try {
|
||||
loadingProviderOptions.value = true
|
||||
providerOptions.value = await getProvidersSummary()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const message = parseApiError(err, '加载 Provider 列表失败')
|
||||
showError(message, '错误')
|
||||
} finally {
|
||||
@@ -1029,7 +1051,7 @@ async function linkProvidersToModel(providerIds: string[]) {
|
||||
await loadModelProviders(selectedModel.value.id)
|
||||
await loadGlobalModels()
|
||||
modelDetailDrawerRef.value?.refreshRoutingData?.()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '关联失败'), '错误')
|
||||
}
|
||||
}
|
||||
@@ -1060,7 +1082,7 @@ function handleDrawerOpenChange(value: boolean) {
|
||||
}
|
||||
|
||||
// 编辑提供商模型
|
||||
function openEditProviderImplementation(provider: any) {
|
||||
function openEditProviderImplementation(provider: ModelProviderDisplay) {
|
||||
editingProvider.value = provider
|
||||
editProviderDialogOpen.value = true
|
||||
}
|
||||
@@ -1081,7 +1103,7 @@ async function handleEditProviderSaved() {
|
||||
}
|
||||
|
||||
// 切换关联提供商状态
|
||||
async function toggleProviderStatus(provider: any) {
|
||||
async function toggleProviderStatus(provider: ModelProviderDisplay) {
|
||||
if (!provider.model_id) {
|
||||
showError('缺少模型 ID')
|
||||
return
|
||||
@@ -1095,13 +1117,13 @@ async function toggleProviderStatus(provider: any) {
|
||||
success(newStatus ? '已启用此关联提供商' : '已停用此关联提供商')
|
||||
// 刷新路由数据
|
||||
modelDetailDrawerRef.value?.refreshRoutingData?.()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '更新状态失败'))
|
||||
}
|
||||
}
|
||||
|
||||
// 删除关联提供商
|
||||
async function confirmDeleteProviderImplementation(provider: any) {
|
||||
async function confirmDeleteProviderImplementation(provider: ModelProviderDisplay) {
|
||||
if (!provider.model_id) {
|
||||
showError('缺少模型 ID')
|
||||
return
|
||||
@@ -1123,7 +1145,7 @@ async function confirmDeleteProviderImplementation(provider: any) {
|
||||
}
|
||||
// 刷新路由数据
|
||||
modelDetailDrawerRef.value?.refreshRoutingData?.()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '删除模型失败'))
|
||||
}
|
||||
}
|
||||
@@ -1167,8 +1189,8 @@ async function deleteModel(model: GlobalModelResponse) {
|
||||
selectedModel.value = null
|
||||
}
|
||||
await loadGlobalModels()
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || err.message, '删除失败')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '删除失败'), '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1177,8 +1199,8 @@ async function toggleModelStatus(model: GlobalModelResponse) {
|
||||
await updateGlobalModel(model.id, { is_active: !model.is_active })
|
||||
model.is_active = !model.is_active
|
||||
success(model.is_active ? '模型已启用' : '模型已停用')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || err.message, '操作失败')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '操作失败'), '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1189,8 +1211,8 @@ async function refreshData() {
|
||||
async function loadProviders() {
|
||||
try {
|
||||
providers.value = await getProvidersSummary()
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || err.message, '加载 Provider 列表失败')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载 Provider 列表失败'), '加载 Provider 列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ const filteredBuiltinTools = computed(() => {
|
||||
|
||||
// 获取分类图标
|
||||
function getCategoryIcon(category: string) {
|
||||
const icons: Record<string, any> = {
|
||||
const icons: Record<string, unknown> = {
|
||||
auth: Users,
|
||||
monitoring: Gauge,
|
||||
security: Shield,
|
||||
|
||||
@@ -293,7 +293,7 @@ function parseScopes(input: string): string[] | null {
|
||||
return parts.length ? parts : null
|
||||
}
|
||||
|
||||
function parseJsonOrNull(input: string): Record<string, any> | null {
|
||||
function parseJsonOrNull(input: string): Record<string, unknown> | null {
|
||||
const raw = input.trim()
|
||||
if (!raw) return null
|
||||
return JSON.parse(raw)
|
||||
@@ -387,7 +387,7 @@ async function loadAll() {
|
||||
if (selectedType.value) {
|
||||
syncFormFromSelected()
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('加载 OAuth 配置失败:', err)
|
||||
showError(getErrorMessage(err, '加载失败'))
|
||||
} finally {
|
||||
@@ -420,7 +420,7 @@ async function handleSave() {
|
||||
await oauthApi.admin.upsertProviderConfig(selectedType.value, payload)
|
||||
success('保存成功')
|
||||
await loadAll()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(getErrorMessage(err, '保存失败'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
@@ -441,7 +441,7 @@ async function handleTest() {
|
||||
}
|
||||
lastTestResult.value = await oauthApi.admin.testProviderConfig(selectedType.value, testPayload)
|
||||
success('测试完成')
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(getErrorMessage(err, '测试失败'))
|
||||
} finally {
|
||||
testing.value = false
|
||||
|
||||
@@ -217,6 +217,7 @@ import {
|
||||
type ProviderWithEndpointsSummary,
|
||||
} from '@/api/endpoints'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
const { error: showError, success: showSuccess } = useToast()
|
||||
const { confirmDanger } = useConfirm()
|
||||
@@ -303,8 +304,8 @@ async function saveDescription(_event: Event, provider: ProviderWithEndpointsSum
|
||||
target.description = trimmed || undefined
|
||||
}
|
||||
cancelEditDescription()
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '更新备注失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '更新备注失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,8 +354,8 @@ async function loadProviders() {
|
||||
providers.value = await getProvidersSummary()
|
||||
// 异步加载配置了 ops 的 provider 的余额数据
|
||||
loadBalances(providers.value)
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载提供商列表失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载提供商列表失败'), '错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -434,8 +435,8 @@ async function handleDeleteProvider(provider: ProviderWithEndpointsSummary) {
|
||||
await deleteProvider(provider.id)
|
||||
showSuccess('提供商已删除')
|
||||
loadProviders()
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '删除提供商失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '删除提供商失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,8 +456,8 @@ async function toggleProviderStatus(provider: ProviderWithEndpointsSummary) {
|
||||
}
|
||||
|
||||
showSuccess(newStatus ? '提供商已启用' : '提供商已停用')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '操作失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -587,6 +587,7 @@ import {
|
||||
} from '@/components/ui'
|
||||
|
||||
import { Search, Trash2, Plus, SquarePen, Activity, Loader2, Settings, Copy } from 'lucide-vue-next'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { formatRegion } from '@/utils/region'
|
||||
import HardwareTooltip from './components/HardwareTooltip.vue'
|
||||
|
||||
@@ -678,8 +679,8 @@ async function handleTestUrl() {
|
||||
} else {
|
||||
toastError(`连通性测试失败: ${result.error || '未知错误'}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
toastError(err.response?.data?.error?.message || '测试请求失败')
|
||||
} catch (err: unknown) {
|
||||
toastError(parseApiError(err, '测试请求失败'))
|
||||
} finally {
|
||||
testingUrl.value = false
|
||||
}
|
||||
@@ -689,8 +690,8 @@ async function copyHmacKey() {
|
||||
try {
|
||||
const { proxy_hmac_key } = await proxyNodesApi.getHmacKey()
|
||||
await copyToClipboard(proxy_hmac_key)
|
||||
} catch (err: any) {
|
||||
toastError(err.response?.data?.error?.message || err.response?.data?.detail || '获取 HMAC Key 失败')
|
||||
} catch (err: unknown) {
|
||||
toastError(parseApiError(err, '获取 HMAC Key 失败'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -730,8 +731,8 @@ async function handleUpdateManualNode() {
|
||||
success('代理节点已更新')
|
||||
handleDialogClose(false)
|
||||
await store.fetchNodes()
|
||||
} catch (err: any) {
|
||||
toastError(err.response?.data?.error?.message || err.response?.data?.detail || '更新失败')
|
||||
} catch (err: unknown) {
|
||||
toastError(parseApiError(err, '更新失败'))
|
||||
} finally {
|
||||
addingNode.value = false
|
||||
}
|
||||
@@ -751,8 +752,8 @@ async function handleAddManualNode() {
|
||||
})
|
||||
success('代理节点已添加')
|
||||
handleDialogClose(false)
|
||||
} catch (err: any) {
|
||||
toastError(err.response?.data?.error?.message || err.response?.data?.detail || '添加失败')
|
||||
} catch (err: unknown) {
|
||||
toastError(parseApiError(err, '添加失败'))
|
||||
} finally {
|
||||
addingNode.value = false
|
||||
}
|
||||
@@ -807,8 +808,8 @@ async function handleSaveConfig() {
|
||||
success('远程配置已保存,将在下次心跳时生效')
|
||||
handleConfigDialogClose(false)
|
||||
await store.fetchNodes()
|
||||
} catch (err: any) {
|
||||
toastError(err.response?.data?.error?.message || err.response?.data?.detail || '保存失败')
|
||||
} catch (err: unknown) {
|
||||
toastError(parseApiError(err, '保存失败'))
|
||||
} finally {
|
||||
savingConfig.value = false
|
||||
}
|
||||
@@ -830,8 +831,8 @@ async function handleDelete(node: ProxyNode) {
|
||||
} else {
|
||||
success('代理节点已删除')
|
||||
}
|
||||
} catch (err: any) {
|
||||
toastError(err.response?.data?.error?.message || '删除失败')
|
||||
} catch (err: unknown) {
|
||||
toastError(parseApiError(err, '删除失败'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,8 +849,8 @@ async function handleTest(node: ProxyNode) {
|
||||
} else {
|
||||
toastError(`连通性测试失败: ${result.error || '未知错误'}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
toastError(err.response?.data?.error?.message || '测试请求失败')
|
||||
} catch (err: unknown) {
|
||||
toastError(parseApiError(err, '测试请求失败'))
|
||||
} finally {
|
||||
testingNodes.value.delete(node.id)
|
||||
}
|
||||
|
||||
@@ -166,11 +166,23 @@ const compareUserId = ref<string>('__none__')
|
||||
const leaderboard = ref<LeaderboardItem[]>([])
|
||||
const leaderboardLoading = ref(false)
|
||||
|
||||
const userSummary = ref<any | null>(null)
|
||||
interface UsageSummary {
|
||||
total_requests: number
|
||||
total_tokens: number
|
||||
total_cost: number
|
||||
error_rate: number
|
||||
}
|
||||
|
||||
interface TimeSeriesItem {
|
||||
date: string
|
||||
total_cost: number
|
||||
}
|
||||
|
||||
const userSummary = ref<UsageSummary | null>(null)
|
||||
const summaryLoading = ref(false)
|
||||
|
||||
const series = ref<any[]>([])
|
||||
const comparisonSeries = ref<any[]>([])
|
||||
const series = ref<TimeSeriesItem[]>([])
|
||||
const comparisonSeries = ref<TimeSeriesItem[]>([])
|
||||
const seriesLoading = ref(false)
|
||||
|
||||
function buildTimeRangeParams() {
|
||||
|
||||
@@ -719,6 +719,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useUsersStore } from '@/stores/users'
|
||||
import type { User, ApiKey } from '@/api/users'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
@@ -767,6 +768,7 @@ import {
|
||||
|
||||
// 功能组件
|
||||
import UserFormDialog, { type UserFormData } from '@/features/users/components/UserFormDialog.vue'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
const { success, error } = useToast()
|
||||
@@ -782,8 +784,8 @@ const userFormDialogRef = ref<InstanceType<typeof UserFormDialog>>()
|
||||
// API Keys 对话框状态
|
||||
const showApiKeysDialog = ref(false)
|
||||
const showNewApiKeyDialog = ref(false)
|
||||
const selectedUser = ref<any>(null)
|
||||
const userApiKeys = ref<any[]>([])
|
||||
const selectedUser = ref<User | null>(null)
|
||||
const userApiKeys = ref<ApiKey[]>([])
|
||||
const newApiKey = ref('')
|
||||
const creatingApiKey = ref(false)
|
||||
const apiKeyInput = ref<HTMLInputElement>()
|
||||
@@ -861,7 +863,7 @@ async function loadUserStats() {
|
||||
loadingStats.value = true
|
||||
try {
|
||||
const data = await usageApi.getUsageByUser()
|
||||
userStats.value = data.reduce((acc: any, stat: any) => {
|
||||
userStats.value = data.reduce((acc: Record<string, UsageByUser>, stat: UsageByUser) => {
|
||||
acc[stat.user_id] = stat
|
||||
return acc
|
||||
}, {})
|
||||
@@ -886,7 +888,7 @@ function formatNumber(value?: number | null): string {
|
||||
return numericValue.toLocaleString()
|
||||
}
|
||||
|
||||
async function toggleUserStatus(user: any) {
|
||||
async function toggleUserStatus(user: User) {
|
||||
const action = user.is_active ? '禁用' : '启用'
|
||||
const confirmed = await confirmDanger(
|
||||
`确定要${action}用户 ${user.username} 吗?`,
|
||||
@@ -899,8 +901,8 @@ async function toggleUserStatus(user: any) {
|
||||
try {
|
||||
await usersStore.updateUser(user.id, { is_active: !user.is_active })
|
||||
success(`用户已${action}`)
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', `${action}用户失败`)
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '未知错误'), `${action}用户失败`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -911,7 +913,7 @@ function openCreateDialog() {
|
||||
showUserFormDialog.value = true
|
||||
}
|
||||
|
||||
function editUser(user: any) {
|
||||
function editUser(user: User) {
|
||||
// 创建数组副本,避免与 store 数据共享引用
|
||||
editingUser.value = {
|
||||
id: user.id,
|
||||
@@ -937,7 +939,7 @@ async function handleUserFormSubmit(data: UserFormData & { password?: string })
|
||||
try {
|
||||
if (data.id) {
|
||||
// 更新用户
|
||||
const updateData: any = {
|
||||
const updateData: Record<string, unknown> = {
|
||||
username: data.username,
|
||||
email: data.email || undefined,
|
||||
quota_usd: data.quota_usd,
|
||||
@@ -955,10 +957,10 @@ async function handleUserFormSubmit(data: UserFormData & { password?: string })
|
||||
// 创建用户
|
||||
const newUser = await usersStore.createUser({
|
||||
username: data.username,
|
||||
password: data.password!,
|
||||
password: data.password ?? '',
|
||||
email: data.email || undefined,
|
||||
quota_usd: data.quota_usd,
|
||||
unlimited: (data as any).unlimited,
|
||||
unlimited: (data as Record<string, unknown>).unlimited as boolean | undefined,
|
||||
role: data.role,
|
||||
allowed_providers: data.allowed_providers,
|
||||
allowed_api_formats: data.allowed_api_formats,
|
||||
@@ -971,15 +973,15 @@ async function handleUserFormSubmit(data: UserFormData & { password?: string })
|
||||
success('用户创建成功')
|
||||
}
|
||||
closeUserFormDialog()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const title = data.id ? '更新用户失败' : '创建用户失败'
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', title)
|
||||
error(parseApiError(err, '未知错误'), title)
|
||||
} finally {
|
||||
userFormDialogRef.value?.setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function manageApiKeys(user: any) {
|
||||
async function manageApiKeys(user: User) {
|
||||
selectedUser.value = user
|
||||
showApiKeysDialog.value = true
|
||||
await loadUserApiKeys(user.id)
|
||||
@@ -1006,8 +1008,8 @@ async function createApiKey() {
|
||||
newApiKey.value = response.key || ''
|
||||
showNewApiKeyDialog.value = true
|
||||
await loadUserApiKeys(selectedUser.value.id)
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', '创建 API Key 失败')
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '未知错误'), '创建 API Key 失败')
|
||||
} finally {
|
||||
creatingApiKey.value = false
|
||||
}
|
||||
@@ -1026,7 +1028,7 @@ async function closeNewApiKeyDialog() {
|
||||
newApiKey.value = ''
|
||||
}
|
||||
|
||||
async function deleteApiKey(apiKey: any) {
|
||||
async function deleteApiKey(apiKey: ApiKey) {
|
||||
const confirmed = await confirmDanger(
|
||||
`确定要删除这个API Key吗?\n\n${apiKey.key_display || 'sk-****'}\n\n此操作无法撤销。`,
|
||||
'删除 API Key'
|
||||
@@ -1038,12 +1040,12 @@ async function deleteApiKey(apiKey: any) {
|
||||
await usersStore.deleteApiKey(selectedUser.value.id, apiKey.id)
|
||||
await loadUserApiKeys(selectedUser.value.id)
|
||||
success('API Key已删除')
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', '删除 API Key 失败')
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '未知错误'), '删除 API Key 失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleLockApiKey(apiKey: any) {
|
||||
async function toggleLockApiKey(apiKey: ApiKey) {
|
||||
try {
|
||||
const response = await adminApi.toggleLockApiKey(apiKey.id)
|
||||
// 更新本地状态
|
||||
@@ -1052,24 +1054,24 @@ async function toggleLockApiKey(apiKey: any) {
|
||||
userApiKeys.value[index].is_locked = response.is_locked
|
||||
}
|
||||
success(response.message)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('切换密钥锁定状态失败:', err)
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '操作失败', '锁定/解锁失败')
|
||||
error(parseApiError(err, '操作失败'), '锁定/解锁失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function copyFullKey(apiKey: any) {
|
||||
async function copyFullKey(apiKey: ApiKey) {
|
||||
try {
|
||||
// 调用后端 API 获取完整密钥
|
||||
const response = await adminApi.getFullApiKey(apiKey.id)
|
||||
await copyToClipboard(response.key)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('复制密钥失败:', err)
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', '复制密钥失败')
|
||||
error(parseApiError(err, '未知错误'), '复制密钥失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function resetQuota(user: any) {
|
||||
async function resetQuota(user: User) {
|
||||
const confirmed = await confirmWarning(
|
||||
`确定要重置用户 ${user.username} 的配额使用量吗?\n\n这将把已使用金额重置为0。`,
|
||||
'重置配额'
|
||||
@@ -1080,12 +1082,12 @@ async function resetQuota(user: any) {
|
||||
try {
|
||||
await usersStore.resetUserQuota(user.id)
|
||||
success('配额已重置')
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', '重置配额失败')
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '未知错误'), '重置配额失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(user: any) {
|
||||
async function deleteUser(user: User) {
|
||||
const confirmed = await confirmDanger(
|
||||
`确定要删除用户 ${user.username} 吗?\n\n此操作将删除:\n• 用户账户\n• 所有API密钥\n• 所有使用记录\n\n此操作无法撤销!`,
|
||||
'删除用户'
|
||||
@@ -1096,8 +1098,8 @@ async function deleteUser(user: any) {
|
||||
try {
|
||||
await usersStore.deleteUser(user.id)
|
||||
success('用户已删除')
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', '删除用户失败')
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '未知错误'), '删除用户失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,24 +2,23 @@
|
||||
import type { ProxyNode } from '@/api/proxy-nodes'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Cpu } from 'lucide-vue-next'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{ node: ProxyNode }>()
|
||||
const open = ref(false)
|
||||
|
||||
const hardwareInfo = computed<Record<string, any> | null>(() => {
|
||||
const hardwareInfo = computed<Record<string, unknown> | null>(() => {
|
||||
const info = props.node.hardware_info
|
||||
if (info == null) return null
|
||||
if (typeof info === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(info)
|
||||
if (parsed && typeof parsed === 'object') return parsed as Record<string, any>
|
||||
if (parsed && typeof parsed === 'object') return parsed as Record<string, unknown>
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
if (typeof info === 'object') return info as Record<string, any>
|
||||
if (typeof info === 'object') return info as Record<string, unknown>
|
||||
return {}
|
||||
})
|
||||
|
||||
@@ -27,16 +26,18 @@ const hardwareRows = computed(() => {
|
||||
const info = hardwareInfo.value ?? {}
|
||||
const rows: Array<{ label: string; value: string }> = []
|
||||
|
||||
const cpuCores = pickNumber(info.cpu_cores, info.cpu_count, info.cpu?.cores)
|
||||
const cpuObj = info.cpu as Record<string, unknown> | undefined
|
||||
const cpuCores = pickNumber(info.cpu_cores, info.cpu_count, cpuObj?.cores)
|
||||
if (cpuCores != null) {
|
||||
rows.push({ label: 'CPU', value: `${cpuCores} cores` })
|
||||
}
|
||||
|
||||
const memObj = info.memory as Record<string, unknown> | undefined
|
||||
const memoryMb = pickNumber(
|
||||
info.total_memory_mb,
|
||||
info.memory_total_mb,
|
||||
info.memory_mb,
|
||||
info.memory?.total_mb
|
||||
memObj?.total_mb
|
||||
)
|
||||
if (memoryMb != null) {
|
||||
rows.push({ label: 'RAM', value: formatMemory(memoryMb) })
|
||||
@@ -62,11 +63,6 @@ const hardwareRows = computed(() => {
|
||||
return rows
|
||||
})
|
||||
|
||||
const tooltipTitle = computed(() => {
|
||||
if (hardwareRows.value.length === 0) return '暂无硬件信息上报'
|
||||
return hardwareRows.value.map(row => `${row.label}: ${row.value}`).join(' | ')
|
||||
})
|
||||
|
||||
const showHardwareInfo = computed(
|
||||
() =>
|
||||
!props.node.is_manual
|
||||
@@ -103,13 +99,7 @@ function pickString(...values: unknown[]): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
function toggleTooltip() {
|
||||
open.value = !open.value
|
||||
}
|
||||
|
||||
function handleOpenChange(value: boolean) {
|
||||
open.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -117,22 +107,14 @@ function handleOpenChange(value: boolean) {
|
||||
v-if="showHardwareInfo"
|
||||
:delay-duration="0"
|
||||
>
|
||||
<Tooltip
|
||||
:open="open"
|
||||
@update:open="handleOpenChange"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
<span
|
||||
aria-label="硬件信息"
|
||||
:title="tooltipTitle"
|
||||
class="inline-flex items-center justify-center rounded-sm p-0.5 hover:bg-muted/60 transition-colors cursor-help"
|
||||
@click.stop="toggleTooltip"
|
||||
@keydown.enter.prevent.stop="toggleTooltip"
|
||||
@keydown.space.prevent.stop="toggleTooltip"
|
||||
>
|
||||
<Cpu class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
<li>全局模型: {{ importPreview.global_models?.length || 0 }} 个</li>
|
||||
<li>提供商: {{ importPreview.providers?.length || 0 }} 个</li>
|
||||
<li>
|
||||
端点: {{ importPreview.providers?.reduce((sum: number, p: any) => sum + (p.endpoints?.length || 0), 0) }} 个
|
||||
端点: {{ importPreview.providers?.reduce((sum: number, p: { endpoints?: unknown[] }) => sum + (p.endpoints?.length || 0), 0) }} 个
|
||||
</li>
|
||||
<li>
|
||||
API Keys: {{ importPreview.providers?.reduce((sum: number, p: any) => sum + (p.api_keys?.length || 0), 0) }} 个
|
||||
API Keys: {{ importPreview.providers?.reduce((sum: number, p: { api_keys?: unknown[] }) => sum + (p.api_keys?.length || 0), 0) }} 个
|
||||
</li>
|
||||
<li v-if="importPreview.ldap_config">
|
||||
LDAP 配置: 1 个
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<ul class="space-y-1 text-muted-foreground">
|
||||
<li>用户: {{ importUsersPreview.users?.length || 0 }} 个</li>
|
||||
<li>
|
||||
API Keys: {{ importUsersPreview.users?.reduce((sum: number, u: any) => sum + (u.api_keys?.length || 0), 0) }} 个
|
||||
API Keys: {{ importUsersPreview.users?.reduce((sum: number, u: { api_keys?: unknown[] }) => sum + (u.api_keys?.length || 0), 0) }} 个
|
||||
</li>
|
||||
<li v-if="importUsersPreview.standalone_keys?.length">
|
||||
独立余额 Keys: {{ importUsersPreview.standalone_keys.length }} 个
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type UsersExportData,
|
||||
type UsersImportResponse,
|
||||
} from '@/api/admin'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { log } from '@/utils/logger'
|
||||
import type { SystemConfig } from './useSystemConfig'
|
||||
|
||||
@@ -117,8 +118,8 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
||||
mergeModeSelectOpen.value = false
|
||||
importResultDialogOpen.value = true
|
||||
success('配置导入成功')
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.detail || '导入配置失败')
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '导入配置失败'))
|
||||
log.error('导入配置失败:', err)
|
||||
} finally {
|
||||
importLoading.value = false
|
||||
@@ -199,8 +200,8 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
||||
usersMergeModeSelectOpen.value = false
|
||||
importUsersResultDialogOpen.value = true
|
||||
success('用户数据导入成功')
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.detail || '导入用户数据失败')
|
||||
} catch (err: unknown) {
|
||||
error(parseApiError(err, '导入用户数据失败'))
|
||||
log.error('导入用户数据失败:', err)
|
||||
} finally {
|
||||
importUsersLoading.value = false
|
||||
|
||||
@@ -167,7 +167,7 @@ export function useScheduledTasks(systemConfig: Ref<SystemConfig>) {
|
||||
async function handleQuotaResetConfigSave() {
|
||||
const configItems: Array<{
|
||||
key: string
|
||||
value: any
|
||||
value: unknown
|
||||
description: string
|
||||
onSuccess: () => void
|
||||
}> = []
|
||||
|
||||
@@ -213,7 +213,7 @@ export function useSystemConfig() {
|
||||
try {
|
||||
const response = await adminApi.getSystemConfig(key)
|
||||
if (response.value !== null && response.value !== undefined) {
|
||||
;(systemConfig.value as any)[key] = response.value
|
||||
;(systemConfig.value as Record<string, unknown>)[key] = response.value
|
||||
}
|
||||
} catch {
|
||||
// 配置不存在时使用默认值,无需处理
|
||||
|
||||
@@ -773,10 +773,12 @@
|
||||
{{ formatFullDate(selectedAnnouncement.created_at) }}
|
||||
</div>
|
||||
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="prose prose-sm dark:prose-invert max-w-none"
|
||||
v-html="renderMarkdown(selectedAnnouncement.content)"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -998,7 +1000,7 @@ const loadingAnnouncements = ref(false)
|
||||
const selectedAnnouncement = ref<Announcement | null>(null)
|
||||
const detailDialogOpen = ref(false)
|
||||
|
||||
const iconMap: Record<string, any> = {
|
||||
const iconMap: Record<string, unknown> = {
|
||||
Users, Activity, TrendingUp, DollarSign, Key, Hash, Database
|
||||
}
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@ async function pollActiveRequests() {
|
||||
if (shouldApply && record.status !== update.status) {
|
||||
record.status = update.status
|
||||
}
|
||||
if (shouldApply && (update.status === 'completed' || update.status === 'failed')) {
|
||||
if (shouldApply && ['completed', 'failed', 'cancelled'].includes(update.status)) {
|
||||
shouldRefresh = true
|
||||
}
|
||||
|
||||
|
||||
@@ -527,10 +527,12 @@
|
||||
<span>{{ formatFullDate(viewingAnnouncement.created_at) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="prose prose-sm dark:prose-invert max-w-none"
|
||||
v-html="renderMarkdown(viewingAnnouncement.content)"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
|
||||
@@ -545,6 +545,7 @@ import {
|
||||
} from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { log } from '@/utils/logger'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
|
||||
@@ -640,13 +641,9 @@ async function loadTokens() {
|
||||
if (tokens.value.length === 0 && currentPage.value > 1) {
|
||||
currentPage.value = 1
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('加载 Management Tokens 失败:', err)
|
||||
if (!err.response) {
|
||||
showError('无法连接到服务器')
|
||||
} else {
|
||||
showError(`加载失败:${err.response?.data?.detail || err.message}`)
|
||||
}
|
||||
showError(parseApiError(err, '加载失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -717,12 +714,9 @@ async function saveToken() {
|
||||
|
||||
closeDialog()
|
||||
await loadTokens()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('保存 Token 失败:', err)
|
||||
const message = err.response?.data?.error?.message
|
||||
|| err.response?.data?.detail
|
||||
|| '保存失败'
|
||||
showError(message)
|
||||
showError(parseApiError(err, '保存失败'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -738,7 +732,7 @@ async function toggleToken(token: ManagementToken) {
|
||||
tokens.value[index] = result.data
|
||||
}
|
||||
success(result.data.is_active ? '令牌已启用' : '令牌已禁用')
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('切换状态失败:', err)
|
||||
showError('操作失败')
|
||||
}
|
||||
@@ -760,7 +754,7 @@ async function deleteToken() {
|
||||
showDeleteDialog.value = false
|
||||
success('令牌已删除')
|
||||
await loadTokens()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('删除 Token 失败:', err)
|
||||
showError('删除失败')
|
||||
} finally {
|
||||
@@ -787,7 +781,7 @@ async function regenerateToken() {
|
||||
showTokenDialog.value = true
|
||||
await loadTokens()
|
||||
success('令牌已重新生成')
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('重新生成失败:', err)
|
||||
showError('重新生成失败')
|
||||
} finally {
|
||||
|
||||
@@ -292,6 +292,7 @@ import {
|
||||
import UserModelDetailDrawer from './components/UserModelDetailDrawer.vue'
|
||||
import { useRowClick } from '@/composables/useRowClick'
|
||||
import { log } from '@/utils/logger'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
const { error: showError } = useToast()
|
||||
const { copyToClipboard } = useClipboard()
|
||||
@@ -440,9 +441,9 @@ async function loadModels() {
|
||||
// 使用用户认证端点,只获取用户有权限使用的模型
|
||||
const response = await meApi.getAvailableModels({ limit: 1000 })
|
||||
models.value = (response.models || []) as PublicGlobalModel[]
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('加载模型失败:', err)
|
||||
showError(err.response?.data?.detail || err.message, '加载模型失败')
|
||||
showError(parseApiError(err, ''), '加载模型失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -498,6 +498,8 @@ import RefreshButton from '@/components/ui/refresh-button.vue'
|
||||
import { Plus, Key, Copy, Trash2, Loader2, Activity, CheckCircle, Power, Check } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { log } from '@/utils/logger'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { getErrorStatus } from '@/types/api-error'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
@@ -548,14 +550,15 @@ async function loadApiKeys() {
|
||||
loading.value = true
|
||||
try {
|
||||
apiKeys.value = await meApi.getApiKeys()
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
log.error('加载 API 密钥失败:', error)
|
||||
if (!error.response) {
|
||||
const status = getErrorStatus(error)
|
||||
if (status === undefined) {
|
||||
showError('无法连接到服务器,请检查后端服务是否运行')
|
||||
} else if (error.response.status === 401) {
|
||||
} else if (status === 401) {
|
||||
showError('认证失败,请重新登录')
|
||||
} else {
|
||||
showError(`加载 API 密钥失败:${ error.response?.data?.detail || error.message}`)
|
||||
showError(parseApiError(error, '加载 API 密钥失败'))
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -596,7 +599,7 @@ async function deleteApiKey() {
|
||||
deleting.value = true
|
||||
try {
|
||||
await meApi.deleteApiKey(keyToDelete.value.id)
|
||||
apiKeys.value = apiKeys.value.filter(k => k.id !== keyToDelete.value!.id)
|
||||
apiKeys.value = apiKeys.value.filter(k => k.id !== keyToDelete.value?.id)
|
||||
showDeleteDialog.value = false
|
||||
success('API 密钥已删除')
|
||||
} catch (error) {
|
||||
|
||||
@@ -174,10 +174,12 @@
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-border bg-muted/30 p-4"
|
||||
>
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="oauth-icon shrink-0"
|
||||
v-html="getOAuthIcon(link.provider_type)"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-medium truncate">
|
||||
{{ link.display_name }}
|
||||
@@ -204,10 +206,12 @@
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-dashed border-border p-4 hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="oauth-icon shrink-0"
|
||||
v-html="getOAuthIcon(p.provider_type)"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-medium truncate">
|
||||
{{ p.display_name }}
|
||||
@@ -462,7 +466,7 @@ import { useToast } from '@/composables/useToast'
|
||||
import { formatCurrency } from '@/utils/format'
|
||||
import { getApiUrl } from '@/utils/url'
|
||||
import { log } from '@/utils/logger'
|
||||
import { getErrorMessage } from '@/types/api-error'
|
||||
import { getErrorMessage, getErrorStatus } from '@/types/api-error'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const route = useRoute()
|
||||
@@ -601,8 +605,8 @@ async function loadOAuthBindings() {
|
||||
])
|
||||
oauthLinks.value = links
|
||||
bindableProviders.value = providers
|
||||
} catch (err: any) {
|
||||
if (err?.response?.status === 503) {
|
||||
} catch (err: unknown) {
|
||||
if (getErrorStatus(err) === 503) {
|
||||
oauthUnavailable.value = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -388,7 +388,7 @@ interface Props {
|
||||
function getModelUserConfigurableCapabilities(): CapabilityDefinition[] {
|
||||
if (!props.model?.supported_capabilities || !props.userConfigurableCapabilities) return []
|
||||
return props.userConfigurableCapabilities.filter(cap =>
|
||||
props.model!.supported_capabilities!.includes(cap.name)
|
||||
props.model?.supported_capabilities?.includes(cap.name)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user