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:
fawney19
2026-02-22 00:43:41 +08:00
parent 98e60e8f74
commit cf2eee222e
121 changed files with 1652 additions and 1179 deletions

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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'))
}
}

View File

@@ -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
}

View File

@@ -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 列表失败')
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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, '操作失败'), '错误')
}
}

View File

@@ -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)
}

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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"

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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
}> = []

View File

@@ -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 {
// 配置不存在时使用默认值,无需处理

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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)
)
}