mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10: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:
@@ -28,8 +28,8 @@ export interface OAuthProviderExport {
|
||||
scopes?: string[] | null
|
||||
redirect_uri: string
|
||||
frontend_callback_url: string
|
||||
attribute_mapping?: any
|
||||
extra_config?: any
|
||||
attribute_mapping?: Record<string, unknown>
|
||||
extra_config?: Record<string, unknown>
|
||||
is_enabled?: boolean
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export interface UserExport {
|
||||
allowed_providers?: string[] | null
|
||||
allowed_api_formats?: string[] | null
|
||||
allowed_models?: string[] | null
|
||||
model_capability_settings?: any
|
||||
model_capability_settings?: Record<string, Record<string, boolean>>
|
||||
quota_usd?: number | null
|
||||
used_usd?: number
|
||||
total_usd?: number
|
||||
@@ -79,7 +79,7 @@ export interface UserApiKeyExport {
|
||||
allowed_models?: string[] | null
|
||||
rate_limit?: number | null // null = 无限制
|
||||
concurrent_limit?: number | null
|
||||
force_capabilities?: any
|
||||
force_capabilities?: Record<string, boolean>
|
||||
is_active: boolean
|
||||
expires_at?: string | null
|
||||
auto_delete_on_expiry?: boolean
|
||||
@@ -94,9 +94,9 @@ export interface GlobalModelExport {
|
||||
name: string
|
||||
display_name: string
|
||||
default_price_per_request?: number | null
|
||||
default_tiered_pricing: any
|
||||
default_tiered_pricing: Record<string, unknown>
|
||||
supported_capabilities?: string[] | null
|
||||
config?: any
|
||||
config?: Record<string, unknown>
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
@@ -112,8 +112,8 @@ export interface ProviderExport {
|
||||
is_active: boolean
|
||||
concurrent_limit?: number | null
|
||||
max_retries?: number | null
|
||||
proxy?: any
|
||||
config?: any
|
||||
proxy?: Record<string, unknown>
|
||||
config?: Record<string, unknown>
|
||||
endpoints: EndpointExport[]
|
||||
api_keys: ProviderKeyExport[]
|
||||
models: ModelExport[]
|
||||
@@ -122,12 +122,12 @@ export interface ProviderExport {
|
||||
export interface EndpointExport {
|
||||
api_format: string
|
||||
base_url: string
|
||||
headers?: any
|
||||
headers?: Record<string, unknown>
|
||||
max_retries?: number
|
||||
is_active: boolean
|
||||
custom_path?: string | null
|
||||
config?: any
|
||||
proxy?: any
|
||||
config?: Record<string, unknown>
|
||||
proxy?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ProviderKeyExport {
|
||||
@@ -139,8 +139,8 @@ export interface ProviderKeyExport {
|
||||
internal_priority?: number
|
||||
global_priority_by_format?: Record<string, number> | null
|
||||
rpm_limit?: number | null
|
||||
allowed_models?: any
|
||||
capabilities?: any
|
||||
allowed_models?: string[] | null
|
||||
capabilities?: Record<string, boolean>
|
||||
cache_ttl_minutes?: number
|
||||
max_probe_interval_minutes?: number
|
||||
is_active: boolean
|
||||
@@ -149,16 +149,16 @@ export interface ProviderKeyExport {
|
||||
export interface ModelExport {
|
||||
global_model_name: string | null
|
||||
provider_model_name: string
|
||||
provider_model_mappings?: any
|
||||
provider_model_mappings?: Record<string, unknown>
|
||||
price_per_request?: number | null
|
||||
tiered_pricing?: any
|
||||
tiered_pricing?: Record<string, unknown>
|
||||
supports_vision?: boolean | null
|
||||
supports_function_calling?: boolean | null
|
||||
supports_streaming?: boolean | null
|
||||
supports_extended_thinking?: boolean | null
|
||||
supports_image_generation?: boolean | null
|
||||
is_active: boolean
|
||||
config?: any
|
||||
config?: Record<string, unknown>
|
||||
}
|
||||
|
||||
// 邮件模板接口
|
||||
@@ -533,14 +533,14 @@ export const adminApi = {
|
||||
|
||||
// 系统配置相关
|
||||
// 获取所有系统配置
|
||||
async getAllSystemConfigs(): Promise<any[]> {
|
||||
const response = await apiClient.get<any[]>('/api/admin/system/configs')
|
||||
async getAllSystemConfigs(): Promise<Array<{ key: string; value: unknown; description?: string }>> {
|
||||
const response = await apiClient.get<Array<{ key: string; value: unknown; description?: string }>>('/api/admin/system/configs')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取特定系统配置
|
||||
async getSystemConfig(key: string): Promise<{ key: string; value: any }> {
|
||||
const response = await apiClient.get<{ key: string; value: any }>(
|
||||
async getSystemConfig(key: string): Promise<{ key: string; value: unknown }> {
|
||||
const response = await apiClient.get<{ key: string; value: unknown }>(
|
||||
`/api/admin/system/configs/${key}`
|
||||
)
|
||||
return response.data
|
||||
@@ -549,10 +549,10 @@ export const adminApi = {
|
||||
// 更新系统配置
|
||||
async updateSystemConfig(
|
||||
key: string,
|
||||
value: any,
|
||||
value: unknown,
|
||||
description?: string
|
||||
): Promise<{ key: string; value: any; description?: string }> {
|
||||
const response = await apiClient.put<{ key: string; value: any; description?: string }>(
|
||||
): Promise<{ key: string; value: unknown; description?: string }> {
|
||||
const response = await apiClient.put<{ key: string; value: unknown; description?: string }>(
|
||||
`/api/admin/system/configs/${key}`,
|
||||
{ value, description }
|
||||
)
|
||||
@@ -568,8 +568,8 @@ export const adminApi = {
|
||||
},
|
||||
|
||||
// 获取系统统计
|
||||
async getSystemStats(): Promise<any> {
|
||||
const response = await apiClient.get<any>('/api/admin/system/stats')
|
||||
async getSystemStats(): Promise<Record<string, unknown>> {
|
||||
const response = await apiClient.get<Record<string, unknown>>('/api/admin/system/stats')
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -621,7 +621,7 @@ export const adminApi = {
|
||||
},
|
||||
|
||||
// 测试 SMTP 连接,支持传入未保存的配置
|
||||
async testSmtpConnection(config: Record<string, any> = {}): Promise<{ success: boolean; message: string }> {
|
||||
async testSmtpConnection(config: Record<string, unknown> = {}): Promise<{ success: boolean; message: string }> {
|
||||
const response = await apiClient.post<{ success: boolean; message: string }>(
|
||||
'/api/admin/system/smtp/test',
|
||||
config
|
||||
@@ -860,8 +860,8 @@ export const adminApi = {
|
||||
user_id?: string
|
||||
model?: string
|
||||
provider_name?: string
|
||||
}): Promise<any[]> {
|
||||
const response = await apiClient.get<any[]>('/api/admin/stats/time-series', { params })
|
||||
}): Promise<Array<Record<string, unknown>>> {
|
||||
const response = await apiClient.get<Array<Record<string, unknown>>>('/api/admin/stats/time-series', { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ export interface AsyncTaskRequestMetadata {
|
||||
user_agent: string
|
||||
request_id: string
|
||||
request_headers?: Record<string, string>
|
||||
poll_raw_response?: any // 轮询完成时的原始响应
|
||||
billing_snapshot?: any // 计费快照
|
||||
poll_raw_response?: unknown // 轮询完成时的原始响应
|
||||
billing_snapshot?: unknown // 计费快照
|
||||
}
|
||||
|
||||
// 异步任务详情
|
||||
@@ -68,8 +68,8 @@ export interface AsyncTaskDetail extends AsyncTaskItem {
|
||||
client_api_format: string
|
||||
provider_api_format: string
|
||||
format_converted: boolean
|
||||
original_request_body: any
|
||||
converted_request_body: any
|
||||
original_request_body: unknown
|
||||
converted_request_body: unknown
|
||||
size: string | null
|
||||
video_urls: string[] | null
|
||||
thumbnail_url: string | null
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface AuditLog {
|
||||
ip_address?: string
|
||||
status_code?: number
|
||||
error_message?: string
|
||||
metadata?: any
|
||||
metadata?: Record<string, unknown>
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface PaginationMeta {
|
||||
export interface AuditLogsResponse {
|
||||
items: AuditLog[]
|
||||
meta: PaginationMeta
|
||||
filters?: Record<string, any>
|
||||
filters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AuditFilters {
|
||||
@@ -33,19 +33,19 @@ export interface AuditFilters {
|
||||
offset?: number
|
||||
}
|
||||
|
||||
function normalizeAuditResponse(data: any): AuditLogsResponse {
|
||||
const items: AuditLog[] = data.items ?? data.logs ?? []
|
||||
const meta: PaginationMeta = data.meta ?? {
|
||||
total: data.total ?? items.length,
|
||||
limit: data.limit ?? items.length,
|
||||
offset: data.offset ?? 0,
|
||||
count: data.count ?? items.length
|
||||
function normalizeAuditResponse(data: Record<string, unknown>): AuditLogsResponse {
|
||||
const items: AuditLog[] = (data.items ?? data.logs ?? []) as AuditLog[]
|
||||
const meta: PaginationMeta = (data.meta as PaginationMeta) ?? {
|
||||
total: (data.total as number) ?? items.length,
|
||||
limit: (data.limit as number) ?? items.length,
|
||||
offset: (data.offset as number) ?? 0,
|
||||
count: (data.count as number) ?? items.length
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
meta,
|
||||
filters: data.filters
|
||||
filters: data.filters as Record<string, unknown> | undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export const auditApi = {
|
||||
|
||||
// 分析用户行为 (管理员)
|
||||
async analyzeUserBehavior(userId: number, days: number = 7): Promise<{
|
||||
analysis: any
|
||||
analysis: Record<string, unknown>
|
||||
recommendations: string[]
|
||||
}> {
|
||||
const response = await apiClient.get(`/api/admin/monitoring/user-behavior/${userId}`, {
|
||||
|
||||
@@ -63,11 +63,13 @@ function createDemoAdapter(defaultAdapter: AxiosAdapter) {
|
||||
mockResponse.config = config
|
||||
return mockResponse
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
// Mock 错误需要附加 config,否则 handleResponseError 会崩溃
|
||||
if (error.response) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
error.config = config
|
||||
error.response.config = config
|
||||
if (error.response) {
|
||||
error.response.config = config
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
@@ -130,14 +132,18 @@ class ApiClient {
|
||||
/**
|
||||
* 处理响应错误
|
||||
*/
|
||||
private async handleResponseError(error: any): Promise<any> {
|
||||
const originalRequest = error.config
|
||||
|
||||
private async handleResponseError(error: unknown): Promise<never> {
|
||||
// 请求被取消
|
||||
if (axios.isCancel(error)) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (!axios.isAxiosError(error)) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
const originalRequest = error.config
|
||||
|
||||
// 网络错误或服务器不可达
|
||||
if (!error.response) {
|
||||
log.warn('Network error or server unreachable', error.message)
|
||||
@@ -160,18 +166,18 @@ class ApiClient {
|
||||
/**
|
||||
* 处理401认证错误
|
||||
*/
|
||||
private async handle401Error(error: any, originalRequest: any): Promise<any> {
|
||||
private async handle401Error(error: import('axios').AxiosError, originalRequest: InternalAxiosRequestConfig & { _retry?: boolean; _retryCount?: number } | undefined): Promise<AxiosResponse> {
|
||||
// 如果不需要认证,直接返回错误
|
||||
if (isPublicEndpoint(originalRequest?.url, originalRequest?.method)) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 如果已经重试过,不再重试
|
||||
if (originalRequest._retry) {
|
||||
if (!originalRequest || originalRequest._retry) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
const errorDetail = error.response?.data?.detail || ''
|
||||
const errorDetail = (error.response?.data as Record<string, unknown>)?.detail as string || ''
|
||||
log.debug('Got 401 error, attempting token refresh', { errorDetail })
|
||||
|
||||
// 检查是否为业务相关的401错误(用户被禁用/删除等)
|
||||
@@ -221,9 +227,9 @@ class ApiClient {
|
||||
*/
|
||||
private async refreshTokenAndRetry(
|
||||
refreshToken: string,
|
||||
originalRequest: any,
|
||||
originalError: any
|
||||
): Promise<any> {
|
||||
originalRequest: InternalAxiosRequestConfig,
|
||||
originalError: import('axios').AxiosError
|
||||
): Promise<AxiosResponse> {
|
||||
this.isRefreshing = true
|
||||
this.refreshPromise = this.refreshToken(refreshToken)
|
||||
|
||||
@@ -237,8 +243,8 @@ class ApiClient {
|
||||
// 重试原始请求
|
||||
originalRequest.headers.Authorization = `Bearer ${response.data.access_token}`
|
||||
return this.client.request(originalRequest)
|
||||
} catch (refreshError: any) {
|
||||
log.error('Token refresh failed', refreshError)
|
||||
} catch (refreshError: unknown) {
|
||||
log.error('Token refresh failed', refreshError instanceof Error ? refreshError.message : String(refreshError))
|
||||
this.isRefreshing = false
|
||||
this.refreshPromise = null
|
||||
this.clearAuth()
|
||||
@@ -282,27 +288,27 @@ class ApiClient {
|
||||
}
|
||||
|
||||
// 以下方法直接委托给 axios client,Demo 模式由 adapter 统一处理
|
||||
async request<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
async request<T = unknown>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.request<T>(config)
|
||||
}
|
||||
|
||||
async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
async get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.get<T>(url, config)
|
||||
}
|
||||
|
||||
async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
async post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.post<T>(url, data, config)
|
||||
}
|
||||
|
||||
async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
async put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.put<T>(url, data, config)
|
||||
}
|
||||
|
||||
async patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
async patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.patch<T>(url, data, config)
|
||||
}
|
||||
|
||||
async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
async delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.delete<T>(url, config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,15 +165,15 @@ export interface RequestDetail {
|
||||
error_message?: string
|
||||
response_time_ms: number
|
||||
created_at: string
|
||||
request_headers?: Record<string, any>
|
||||
request_body?: Record<string, any>
|
||||
provider_request_headers?: Record<string, any>
|
||||
provider_request_body?: Record<string, any>
|
||||
response_headers?: Record<string, any>
|
||||
client_response_headers?: Record<string, any>
|
||||
response_body?: Record<string, any>
|
||||
client_response_body?: Record<string, any>
|
||||
metadata?: Record<string, any>
|
||||
request_headers?: Record<string, unknown>
|
||||
request_body?: Record<string, unknown>
|
||||
provider_request_headers?: Record<string, unknown>
|
||||
provider_request_body?: Record<string, unknown>
|
||||
response_headers?: Record<string, unknown>
|
||||
client_response_headers?: Record<string, unknown>
|
||||
response_body?: Record<string, unknown>
|
||||
client_response_body?: Record<string, unknown>
|
||||
metadata?: Record<string, unknown>
|
||||
// 阶梯计费信息
|
||||
tiered_pricing?: {
|
||||
total_input_context: number // 总输入上下文 (input + cache_read)
|
||||
@@ -211,7 +211,7 @@ export interface CurlData {
|
||||
url: string
|
||||
method: string
|
||||
headers: Record<string, string>
|
||||
body: Record<string, any>
|
||||
body: Record<string, unknown>
|
||||
curl: string
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ export interface ReplayRequest {
|
||||
provider_id?: string
|
||||
endpoint_id?: string
|
||||
api_key_id?: string
|
||||
body_override?: Record<string, any>
|
||||
body_override?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ReplayResponse {
|
||||
@@ -227,7 +227,7 @@ export interface ReplayResponse {
|
||||
provider: string
|
||||
status_code: number
|
||||
response_headers: Record<string, string>
|
||||
response_body: Record<string, any>
|
||||
response_body: Record<string, unknown>
|
||||
response_time_ms: number
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function createEndpoint(
|
||||
body_rules?: BodyRule[]
|
||||
max_retries?: number
|
||||
is_active?: boolean
|
||||
config?: Record<string, any>
|
||||
config?: Record<string, unknown>
|
||||
proxy?: ProxyConfig | null
|
||||
format_acceptance_config?: FormatAcceptanceConfig | null
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export async function updateEndpoint(
|
||||
body_rules: BodyRule[] | null
|
||||
max_retries: number
|
||||
is_active: boolean
|
||||
config: Record<string, any>
|
||||
config: Record<string, unknown> | null
|
||||
proxy: ProxyConfig | null
|
||||
format_acceptance_config: FormatAcceptanceConfig | null
|
||||
}>
|
||||
|
||||
@@ -59,7 +59,7 @@ export interface RevealKeyResult {
|
||||
auth_type: 'api_key' | 'vertex_ai' | 'oauth'
|
||||
api_key?: string
|
||||
refresh_token?: string
|
||||
auth_config?: string | Record<string, any>
|
||||
auth_config?: string | Record<string, unknown>
|
||||
}
|
||||
|
||||
export async function revealEndpointKey(keyId: string): Promise<RevealKeyResult> {
|
||||
@@ -70,7 +70,7 @@ export async function revealEndpointKey(keyId: string): Promise<RevealKeyResult>
|
||||
/**
|
||||
* 导出 OAuth Key 凭据(扁平 JSON,用于跨实例迁移)
|
||||
*/
|
||||
export async function exportKey(keyId: string): Promise<Record<string, any>> {
|
||||
export async function exportKey(keyId: string): Promise<Record<string, unknown>> {
|
||||
const response = await client.get(`/api/admin/endpoints/keys/${keyId}/export`)
|
||||
return response.data
|
||||
}
|
||||
@@ -104,7 +104,7 @@ export async function addProviderKey(
|
||||
api_formats: string[] // 支持的 API 格式列表(必填)
|
||||
api_key: string
|
||||
auth_type?: 'api_key' | 'vertex_ai' | 'oauth' // 认证类型
|
||||
auth_config?: Record<string, any> // 认证配置(Vertex AI Service Account JSON)
|
||||
auth_config?: Record<string, unknown> // 认证配置(Vertex AI Service Account JSON)
|
||||
name: string
|
||||
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
|
||||
internal_priority?: number
|
||||
@@ -132,7 +132,7 @@ export async function updateProviderKey(
|
||||
api_formats: string[] // 支持的 API 格式列表
|
||||
api_key: string
|
||||
auth_type: 'api_key' | 'vertex_ai' | 'oauth' // 认证类型
|
||||
auth_config: Record<string, any> // 认证配置(Vertex AI Service Account JSON)
|
||||
auth_config: Record<string, unknown> // 认证配置(Vertex AI Service Account JSON)
|
||||
name: string
|
||||
rate_multipliers: Record<string, number> | null // 按 API 格式的成本倍率
|
||||
internal_priority: number
|
||||
@@ -175,7 +175,7 @@ export interface RefreshQuotaResult {
|
||||
key_name: string
|
||||
status: 'success' | 'no_metadata' | 'error'
|
||||
// Codex: 额度字段为扁平结构;Antigravity: 返回 { antigravity: { quota_by_model: ... } }
|
||||
metadata?: Record<string, any>
|
||||
metadata?: Record<string, unknown>
|
||||
message?: string
|
||||
status_code?: number
|
||||
}>
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
ModelUpdate,
|
||||
ModelCatalogResponse,
|
||||
ProviderAvailableSourceModelsResponse,
|
||||
UpstreamModel,
|
||||
ImportFromUpstreamResponse,
|
||||
} from './types'
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface Model {
|
||||
global_model_id?: string // 关联的 GlobalModel ID
|
||||
provider_model_name: string // Provider 侧的主模型名称
|
||||
provider_model_mappings?: ProviderModelMapping[] | null // 模型名称映射列表(带优先级)
|
||||
config?: Record<string, any> | null // 额外配置(如 billing/video 等)
|
||||
config?: Record<string, unknown> | null // 额外配置(如 billing/video 等)
|
||||
// 原始配置值(可能为空,为空时使用 GlobalModel 默认值)
|
||||
price_per_request?: number | null // 按次计费价格
|
||||
tiered_pricing?: TieredPricingConfig | null // 阶梯计费配置
|
||||
@@ -56,7 +56,7 @@ export interface Model {
|
||||
global_model_name?: string
|
||||
global_model_display_name?: string
|
||||
// 有效配置(合并 Model 和 GlobalModel 的 config)
|
||||
effective_config?: Record<string, any> | null
|
||||
effective_config?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface ModelCreate {
|
||||
@@ -73,7 +73,7 @@ export interface ModelCreate {
|
||||
supports_extended_thinking?: boolean
|
||||
supports_image_generation?: boolean
|
||||
is_active?: boolean
|
||||
config?: Record<string, any>
|
||||
config?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ModelUpdate {
|
||||
@@ -89,7 +89,7 @@ export interface ModelUpdate {
|
||||
supports_image_generation?: boolean
|
||||
is_active?: boolean
|
||||
is_available?: boolean
|
||||
config?: Record<string, any> | null
|
||||
config?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface ModelCapabilities {
|
||||
@@ -183,7 +183,7 @@ export interface GlobalModelCreate {
|
||||
// Key 能力配置 - 模型支持的能力列表
|
||||
supported_capabilities?: string[]
|
||||
// 模型配置(JSON格式)- 包含能力、规格、元信息等
|
||||
config?: Record<string, any>
|
||||
config?: Record<string, unknown>
|
||||
is_active?: boolean
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ export interface GlobalModelUpdate {
|
||||
// Key 能力配置 - 模型支持的能力列表
|
||||
supported_capabilities?: string[] | null
|
||||
// 模型配置(JSON格式)- 包含能力、规格、元信息等
|
||||
config?: Record<string, any> | null
|
||||
config?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface GlobalModelResponse {
|
||||
@@ -212,7 +212,7 @@ export interface GlobalModelResponse {
|
||||
// Key 能力配置 - 模型支持的能力列表
|
||||
supported_capabilities?: string[] | null
|
||||
// 模型配置(JSON格式)
|
||||
config?: Record<string, any> | null
|
||||
config?: Record<string, unknown> | null
|
||||
// 统计数据
|
||||
provider_count?: number
|
||||
active_provider_count?: number
|
||||
|
||||
@@ -52,7 +52,7 @@ export type HeaderRule = HeaderRuleSet | HeaderRuleDrop | HeaderRuleRename
|
||||
export interface BodyRuleSet {
|
||||
action: 'set'
|
||||
path: string
|
||||
value: any
|
||||
value: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +87,7 @@ export interface BodyRuleRename {
|
||||
export interface BodyRuleAppend {
|
||||
action: 'append'
|
||||
path: string
|
||||
value: any
|
||||
value: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +101,7 @@ export interface BodyRuleInsert {
|
||||
action: 'insert'
|
||||
path: string
|
||||
index: number
|
||||
value: any
|
||||
value: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +132,7 @@ export type BodyRuleConditionOp =
|
||||
export interface BodyRuleCondition {
|
||||
path: string
|
||||
op: BodyRuleConditionOp
|
||||
value?: any // exists / not_exists 不需要 value
|
||||
value?: unknown // exists / not_exists 不需要 value
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,7 +174,7 @@ export interface ProviderEndpoint {
|
||||
body_rules?: BodyRule[] // 请求体规则列表,支持 set/drop/rename 操作
|
||||
max_retries: number
|
||||
is_active: boolean
|
||||
config?: Record<string, any>
|
||||
config?: Record<string, unknown>
|
||||
proxy?: ProxyConfig | null
|
||||
// 格式转换配置
|
||||
format_acceptance_config?: FormatAcceptanceConfig | null
|
||||
@@ -352,7 +352,7 @@ export interface EndpointAPIKeyUpdate {
|
||||
name?: string
|
||||
api_key?: string // 仅在需要更新时提供
|
||||
auth_type?: 'api_key' | 'vertex_ai' | 'oauth' // 认证类型
|
||||
auth_config?: Record<string, any> // 认证配置(Vertex AI Service Account JSON)
|
||||
auth_config?: Record<string, unknown> // 认证配置(Vertex AI Service Account JSON)
|
||||
rate_multipliers?: Record<string, number> | null // 按 API 格式的成本倍率
|
||||
internal_priority?: number
|
||||
global_priority_by_format?: Record<string, number> | null // 按 API 格式的全局优先级
|
||||
@@ -492,7 +492,7 @@ export interface HealthStatus {
|
||||
key_consecutive_failures?: number
|
||||
key_last_failure_at?: string
|
||||
key_is_active?: boolean
|
||||
key_statistics?: Record<string, any>
|
||||
key_statistics?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface HealthSummary {
|
||||
@@ -537,6 +537,6 @@ export interface AdaptiveStatsResponse {
|
||||
old_limit: number
|
||||
new_limit: number
|
||||
reason: string
|
||||
[key: string]: any
|
||||
[key: string]: unknown
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface UserPreferences {
|
||||
avatar_url?: string
|
||||
bio?: string
|
||||
default_provider_id?: string // UUID
|
||||
default_provider?: any
|
||||
default_provider?: Record<string, unknown>
|
||||
theme: string
|
||||
language: string
|
||||
timezone?: string
|
||||
@@ -216,7 +216,7 @@ export const meApi = {
|
||||
async getActiveRequests(ids?: string): Promise<{
|
||||
requests: Array<{
|
||||
id: string
|
||||
status: 'pending' | 'streaming' | 'completed' | 'failed'
|
||||
status: 'pending' | 'streaming' | 'completed' | 'failed' | 'cancelled'
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
cache_creation_input_tokens?: number | null
|
||||
@@ -237,7 +237,7 @@ export const meApi = {
|
||||
},
|
||||
|
||||
// 获取可用的提供商
|
||||
async getAvailableProviders(): Promise<any[]> {
|
||||
async getAvailableProviders(): Promise<Array<Record<string, unknown>>> {
|
||||
const response = await apiClient.get('/api/users/me/providers')
|
||||
return response.data
|
||||
},
|
||||
@@ -256,7 +256,7 @@ export const meApi = {
|
||||
default_price_per_request: number | null
|
||||
default_tiered_pricing: TieredPricingConfig | null
|
||||
supported_capabilities: string[] | null
|
||||
config: Record<string, any> | null
|
||||
config: Record<string, unknown> | null
|
||||
}>
|
||||
total: number
|
||||
}> {
|
||||
@@ -265,7 +265,7 @@ export const meApi = {
|
||||
},
|
||||
|
||||
// 获取端点状态(不包含敏感信息)
|
||||
async getEndpointStatus(): Promise<any[]> {
|
||||
async getEndpointStatus(): Promise<Array<Record<string, unknown>>> {
|
||||
const response = await apiClient.get('/api/users/me/endpoint-status')
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -44,8 +44,8 @@ export interface OAuthProviderAdminConfig {
|
||||
scopes?: string[] | null
|
||||
redirect_uri: string
|
||||
frontend_callback_url: string
|
||||
attribute_mapping?: Record<string, any> | null
|
||||
extra_config?: Record<string, any> | null
|
||||
attribute_mapping?: Record<string, unknown> | null
|
||||
extra_config?: Record<string, unknown> | null
|
||||
is_enabled: boolean
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ export interface OAuthProviderUpsertRequest {
|
||||
scopes?: string[] | null
|
||||
redirect_uri: string
|
||||
frontend_callback_url: string
|
||||
attribute_mapping?: Record<string, any> | null
|
||||
extra_config?: Record<string, any> | null
|
||||
attribute_mapping?: Record<string, unknown> | null
|
||||
extra_config?: Record<string, unknown> | null
|
||||
is_enabled: boolean
|
||||
force?: boolean
|
||||
}
|
||||
|
||||
@@ -45,17 +45,17 @@ export interface ArchitectureInfo {
|
||||
architecture_id: string
|
||||
display_name: string
|
||||
description: string
|
||||
credentials_schema: Record<string, any>
|
||||
credentials_schema: Record<string, unknown>
|
||||
supported_auth_types: Array<{
|
||||
type: string
|
||||
display_name: string
|
||||
credentials_schema?: Record<string, any>
|
||||
credentials_schema?: Record<string, unknown>
|
||||
}>
|
||||
supported_actions: Array<{
|
||||
type: string
|
||||
display_name: string
|
||||
description: string
|
||||
config_schema: Record<string, any>
|
||||
config_schema: Record<string, unknown>
|
||||
}>
|
||||
default_connector: string | null
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export interface BalanceInfo {
|
||||
total_available: number | null
|
||||
expires_at: string | null
|
||||
currency: string
|
||||
extra: Record<string, any> & {
|
||||
extra: Record<string, unknown> & {
|
||||
// Anyrouter 签到信息
|
||||
checkin_success?: boolean | null // true=成功, false=失败, null=已签到/跳过
|
||||
checkin_message?: string
|
||||
@@ -98,7 +98,7 @@ export interface CheckinInfo {
|
||||
streak_days: number | null
|
||||
next_reward: number | null
|
||||
message: string | null
|
||||
extra: Record<string, any>
|
||||
extra: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** 操作结果响应 */
|
||||
@@ -115,14 +115,14 @@ export interface ActionResultResponse {
|
||||
/** 连接器配置请求 */
|
||||
export interface ConnectorConfigRequest {
|
||||
auth_type: ConnectorAuthType
|
||||
config: Record<string, any>
|
||||
credentials: Record<string, any>
|
||||
config: Record<string, unknown>
|
||||
credentials: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** 操作配置请求 */
|
||||
export interface ActionConfigRequest {
|
||||
enabled: boolean
|
||||
config: Record<string, any>
|
||||
config: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** 保存配置请求 */
|
||||
@@ -136,12 +136,12 @@ export interface SaveConfigRequest {
|
||||
|
||||
/** 连接请求 */
|
||||
export interface ConnectRequest {
|
||||
credentials?: Record<string, any>
|
||||
credentials?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** 执行操作请求 */
|
||||
export interface ExecuteActionRequest {
|
||||
config?: Record<string, any>
|
||||
config?: Record<string, unknown>
|
||||
}
|
||||
|
||||
// ==================== API Functions ====================
|
||||
@@ -186,8 +186,8 @@ export interface ProviderOpsConfigResponse {
|
||||
base_url?: string
|
||||
connector?: {
|
||||
auth_type: string
|
||||
config: Record<string, any>
|
||||
credentials: Record<string, any>
|
||||
config: Record<string, unknown>
|
||||
credentials: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,9 +339,9 @@ export interface VerifyAuthResponse {
|
||||
quota?: number
|
||||
used_quota?: number
|
||||
request_count?: number
|
||||
extra?: Record<string, any>
|
||||
extra?: Record<string, unknown>
|
||||
}
|
||||
updated_credentials?: Record<string, any>
|
||||
updated_credentials?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface ProxyNode {
|
||||
proxy_username?: string
|
||||
proxy_password?: string // 脱敏后的密码
|
||||
// 硬件信息(aether-proxy 节点)
|
||||
hardware_info: Record<string, any> | null
|
||||
hardware_info: Record<string, unknown> | null
|
||||
estimated_max_concurrency: number | null
|
||||
// 远程配置(aether-proxy 节点)
|
||||
remote_config: ProxyNodeRemoteConfig | null
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface PublicGlobalModel {
|
||||
// Key 能力支持
|
||||
supported_capabilities: string[] | null
|
||||
// 模型配置(JSON)
|
||||
config: Record<string, any> | null
|
||||
config: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface PublicGlobalModelListResponse {
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface CandidateRecord {
|
||||
error_message?: string
|
||||
latency_ms?: number
|
||||
concurrent_requests?: number
|
||||
extra_data?: Record<string, any>
|
||||
extra_data?: Record<string, unknown>
|
||||
created_at: string
|
||||
started_at?: string
|
||||
finished_at?: string
|
||||
|
||||
@@ -182,7 +182,7 @@ export const usageApi = {
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<{
|
||||
records: any[]
|
||||
records: Array<Record<string, unknown>>
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
@@ -198,7 +198,7 @@ export const usageApi = {
|
||||
async getActiveRequests(ids?: string[]): Promise<{
|
||||
requests: Array<{
|
||||
id: string
|
||||
status: 'pending' | 'streaming' | 'completed' | 'failed'
|
||||
status: 'pending' | 'streaming' | 'completed' | 'failed' | 'cancelled'
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
cache_creation_input_tokens?: number | null
|
||||
|
||||
@@ -101,7 +101,7 @@ export const usersApi = {
|
||||
},
|
||||
|
||||
// 管理员统计
|
||||
async getUsageStats(): Promise<any> {
|
||||
async getUsageStats(): Promise<Record<string, unknown>> {
|
||||
const response = await apiClient.get('/api/admin/usage/stats')
|
||||
return response.data
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<pre><code
|
||||
:class="`language-${language}`"
|
||||
v-html="highlightedCode"
|
||||
:class="`language-${language}`"
|
||||
v-html="highlightedCode"
|
||||
/></pre>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -55,10 +55,12 @@
|
||||
<div class="text-left text-xs font-medium text-muted-foreground mb-2">
|
||||
更新内容
|
||||
</div>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="w-full max-h-48 overflow-y-auto rounded-lg bg-muted/50 p-3 text-left text-sm text-foreground/80 prose prose-sm dark:prose-invert prose-p:my-1 prose-ul:my-1 prose-li:my-0"
|
||||
v-html="renderedReleaseNotes"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
|
||||
<!-- Description (fallback when no release notes) -->
|
||||
|
||||
@@ -42,6 +42,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
subtitle: undefined,
|
||||
loading: false
|
||||
})
|
||||
|
||||
@@ -81,7 +82,7 @@ const chartOptions = computed(() => ({
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context: any) => {
|
||||
label: (context: { parsed?: { y?: number }; dataset: { label?: string } }) => {
|
||||
const value = context.parsed?.y ?? 0
|
||||
return `${context.dataset.label}: ${formatCurrency(value)}`
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
subtitle: undefined,
|
||||
loading: false
|
||||
})
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
subtitle: undefined,
|
||||
loading: false
|
||||
})
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ interface Props {
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
subtitle: undefined,
|
||||
loading: false
|
||||
})
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { computed } from 'vue'
|
||||
interface Props {
|
||||
class?: string
|
||||
src?: string
|
||||
alt: string
|
||||
alt?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
@@ -76,6 +76,7 @@ const warnPasswordType = import.meta.env.DEV
|
||||
return () => {
|
||||
if (!warned) {
|
||||
warned = true
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'[Input] type="password" 已被弃用,请使用 masked 属性代替。\n' +
|
||||
'示例:<Input v-model="apiKey" masked />\n' +
|
||||
|
||||
@@ -94,7 +94,8 @@ interface Emits {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
pageSize: 20,
|
||||
pageSizeOptions: () => [10, 20, 50, 100],
|
||||
showPageSizeSelector: true
|
||||
showPageSizeSelector: true,
|
||||
cacheKey: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
@@ -9,6 +9,7 @@ const props = withDefaults(defineProps<{
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
}>(), {
|
||||
class: undefined,
|
||||
align: 'center',
|
||||
side: 'bottom',
|
||||
sideOffset: 4,
|
||||
|
||||
@@ -15,8 +15,12 @@ interface Props {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
defaultValue: undefined,
|
||||
modelValue: undefined,
|
||||
open: undefined,
|
||||
dir: undefined,
|
||||
name: undefined,
|
||||
autocomplete: undefined,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -392,7 +392,7 @@ async function loadAccessRestrictionOptions() {
|
||||
])
|
||||
providers.value = providersData
|
||||
globalModels.value = modelsData.models || []
|
||||
allApiFormats.value = formatsData.formats?.map((f: any) => f.value) || []
|
||||
allApiFormats.value = formatsData.formats?.map((f: { value: string }) => f.value) || []
|
||||
} catch (err) {
|
||||
log.error('加载访问限制选项失败:', err)
|
||||
}
|
||||
|
||||
@@ -60,10 +60,12 @@
|
||||
class="oauth-btn"
|
||||
@click="handleOAuthLogin(oauthProviders[0].provider_type)"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
class="oauth-icon"
|
||||
v-html="getOAuthIcon(oauthProviders[0].provider_type)"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span>使用 {{ oauthProviders[0].display_name }} 登录</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -83,10 +85,12 @@
|
||||
:title="p.display_name"
|
||||
@click="handleOAuthLogin(p.provider_type)"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
class="oauth-icon-lg"
|
||||
v-html="getOAuthIcon(p.provider_type)"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -222,6 +222,7 @@
|
||||
import { ref, computed, watch, onUnmounted, nextTick } from 'vue'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { Dialog } from '@/components/ui'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Input from '@/components/ui/input.vue'
|
||||
@@ -576,12 +577,8 @@ const handleSendCode = async () => {
|
||||
} else {
|
||||
showError(response.message || '请稍后重试', '发送失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMsg = error.response?.data?.detail
|
||||
|| error.response?.data?.error?.message
|
||||
|| error.message
|
||||
|| '网络错误,请重试'
|
||||
showError(errorMsg, '发送失败')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '网络错误,请重试'), '发送失败')
|
||||
} finally {
|
||||
isSendingCode.value = false
|
||||
}
|
||||
@@ -609,13 +606,9 @@ const handleCodeComplete = async (code: string) => {
|
||||
// Clear the code input
|
||||
clearCodeInputs()
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
verificationError.value = true
|
||||
const errorMsg = error.response?.data?.detail
|
||||
|| error.response?.data?.error?.message
|
||||
|| error.message
|
||||
|| '验证码错误,请重试'
|
||||
showError(errorMsg, '验证失败')
|
||||
showError(parseApiError(error, '验证码错误,请重试'), '验证失败')
|
||||
// Clear the code input
|
||||
clearCodeInputs()
|
||||
} finally {
|
||||
@@ -662,12 +655,8 @@ const handleSubmit = async () => {
|
||||
|
||||
emit('success')
|
||||
isOpen.value = false
|
||||
} catch (error: any) {
|
||||
const errorMsg = error.response?.data?.detail
|
||||
|| error.response?.data?.error?.message
|
||||
|| error.message
|
||||
|| '注册失败,请重试'
|
||||
showError(errorMsg, '注册失败')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '注册失败,请重试'), '注册失败')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
@@ -66,21 +66,21 @@
|
||||
class="bg-muted/30"
|
||||
>
|
||||
<div
|
||||
v-for="model in group.models"
|
||||
:key="model.modelId"
|
||||
v-for="item in group.models"
|
||||
:key="item.modelId"
|
||||
class="flex flex-col gap-0.5 pl-7 pr-2.5 py-1.5 cursor-pointer text-xs border-t"
|
||||
:class="selectedModel?.modelId === model.modelId && selectedModel?.providerId === model.providerId
|
||||
:class="selectedModel?.modelId === item.modelId && selectedModel?.providerId === item.providerId
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-muted'"
|
||||
@click="selectModel(model)"
|
||||
@click="selectModel(item)"
|
||||
>
|
||||
<span class="truncate font-medium">{{ model.modelName }}</span>
|
||||
<span class="truncate font-medium">{{ item.modelName }}</span>
|
||||
<span
|
||||
class="truncate text-[10px]"
|
||||
:class="selectedModel?.modelId === model.modelId && selectedModel?.providerId === model.providerId
|
||||
:class="selectedModel?.modelId === item.modelId && selectedModel?.providerId === item.providerId
|
||||
? 'text-primary-foreground/70'
|
||||
: 'text-muted-foreground'"
|
||||
>{{ model.modelId }}</span>
|
||||
>{{ item.modelId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -363,6 +363,7 @@ import { useToast } from '@/composables/useToast'
|
||||
import { useFormDialog } from '@/composables/useFormDialog'
|
||||
import { parseNumberInput, sortResolutionEntries } from '@/utils/form'
|
||||
import { log } from '@/utils/logger'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import TieredPricingEditor from './TieredPricingEditor.vue'
|
||||
import {
|
||||
getModelsDevList,
|
||||
@@ -436,7 +437,7 @@ const groupedModels = computed(() => {
|
||||
models: []
|
||||
})
|
||||
}
|
||||
groups.get(model.providerId)!.models.push(model)
|
||||
groups.get(model.providerId)?.models.push(model)
|
||||
}
|
||||
|
||||
// 转换为数组并排序
|
||||
@@ -506,7 +507,7 @@ interface FormData {
|
||||
display_name: string
|
||||
default_price_per_request?: number
|
||||
supported_capabilities?: string[]
|
||||
config?: Record<string, any>
|
||||
config?: Record<string, unknown>
|
||||
is_active?: boolean
|
||||
}
|
||||
|
||||
@@ -524,7 +525,7 @@ const form = ref<FormData>(defaultForm())
|
||||
const KEEP_FALSE_CONFIG_KEYS = new Set(['streaming'])
|
||||
|
||||
// 设置 config 字段
|
||||
function setConfigField(key: string, value: any) {
|
||||
function setConfigField(key: string, value: unknown) {
|
||||
if (!form.value.config) {
|
||||
form.value.config = {}
|
||||
}
|
||||
@@ -535,41 +536,41 @@ function setConfigField(key: string, value: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function getNested(obj: any, path: string): any {
|
||||
function getNested(obj: unknown, path: string): unknown {
|
||||
if (!obj || typeof obj !== 'object') return undefined
|
||||
const parts = path.split('.').filter(Boolean)
|
||||
let cur: any = obj
|
||||
let cur = obj as Record<string, unknown>
|
||||
for (const p of parts) {
|
||||
if (!cur || typeof cur !== 'object') return undefined
|
||||
cur = cur[p]
|
||||
cur = cur[p] as Record<string, unknown>
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
function setNested(obj: any, path: string, value: any) {
|
||||
function setNested(obj: unknown, path: string, value: unknown) {
|
||||
if (!obj || typeof obj !== 'object') return
|
||||
const parts = path.split('.').filter(Boolean)
|
||||
if (parts.length === 0) return
|
||||
let cur: any = obj
|
||||
let cur = obj as Record<string, unknown>
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const p = parts[i]
|
||||
if (!cur[p] || typeof cur[p] !== 'object') {
|
||||
cur[p] = {}
|
||||
}
|
||||
cur = cur[p]
|
||||
cur = cur[p] as Record<string, unknown>
|
||||
}
|
||||
cur[parts[parts.length - 1]] = value
|
||||
}
|
||||
|
||||
function deleteNested(obj: any, path: string) {
|
||||
function deleteNested(obj: unknown, path: string) {
|
||||
if (!obj || typeof obj !== 'object') return
|
||||
const parts = path.split('.').filter(Boolean)
|
||||
if (parts.length === 0) return
|
||||
let cur: any = obj
|
||||
let cur = obj as Record<string, unknown>
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const p = parts[i]
|
||||
if (!cur[p] || typeof cur[p] !== 'object') return
|
||||
cur = cur[p]
|
||||
cur = cur[p] as Record<string, unknown>
|
||||
}
|
||||
delete cur[parts[parts.length - 1]]
|
||||
}
|
||||
@@ -577,9 +578,9 @@ function deleteNested(obj: any, path: string) {
|
||||
function pruneEmptyBillingConfig() {
|
||||
const cfg = form.value.config
|
||||
if (!cfg || typeof cfg !== 'object') return
|
||||
const billing = cfg.billing
|
||||
const billing = cfg.billing as Record<string, unknown> | undefined
|
||||
if (!billing || typeof billing !== 'object') return
|
||||
const video = billing.video
|
||||
const video = billing.video as Record<string, unknown> | undefined
|
||||
if (video && typeof video === 'object' && Object.keys(video).length === 0) {
|
||||
delete billing.video
|
||||
}
|
||||
@@ -611,7 +612,7 @@ function loadVideoPricingFromConfig() {
|
||||
const raw = getNested(cfg, 'billing.video.price_per_second_by_resolution')
|
||||
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
// 按分辨率从低到高排序
|
||||
const sortedEntries = sortResolutionEntries(Object.entries(raw))
|
||||
const sortedEntries = sortResolutionEntries(Object.entries(raw as Record<string, unknown>))
|
||||
videoResolutionPrices.value = sortedEntries.map(([k, v]) => ({
|
||||
resolution: String(k),
|
||||
price_per_second: typeof v === 'number' ? v : undefined,
|
||||
@@ -722,7 +723,7 @@ function selectModel(model: ModelsDevModelItem) {
|
||||
form.value.display_name = model.modelName
|
||||
|
||||
// 构建 config
|
||||
const config: Record<string, any> = {
|
||||
const config: Record<string, unknown> = {
|
||||
streaming: true,
|
||||
}
|
||||
if (model.supportsVision) config.vision = true
|
||||
@@ -848,8 +849,8 @@ async function handleSubmit() {
|
||||
success('模型更新成功')
|
||||
} else {
|
||||
const createData: GlobalModelCreate = {
|
||||
name: form.value.name!,
|
||||
display_name: form.value.display_name!,
|
||||
name: form.value.name ?? '',
|
||||
display_name: form.value.display_name ?? '',
|
||||
config: cleanConfig,
|
||||
default_price_per_request: form.value.default_price_per_request ?? undefined,
|
||||
default_tiered_pricing: finalTieredPricing,
|
||||
@@ -861,8 +862,8 @@ async function handleSubmit() {
|
||||
}
|
||||
emit('update:open', false)
|
||||
emit('success')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || err.message, isEditMode.value ? '更新失败' : '创建失败')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, isEditMode.value ? '更新失败' : '创建失败'), isEditMode.value ? '更新失败' : '创建失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
@@ -469,6 +469,7 @@ import TableCell from '@/components/ui/table-cell.vue'
|
||||
import RoutingTab from './RoutingTab.vue'
|
||||
import ModelMappingsTab from './ModelMappingsTab.vue'
|
||||
import { sortResolutionEntries } from '@/utils/form'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { getGlobalModelRoutingPreview } from '@/api/global-models'
|
||||
|
||||
// 使用外部类型定义
|
||||
@@ -479,15 +480,16 @@ import type { RoutingProviderInfo } from '@/api/global-models'
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
hasBlockingDialogOpen: false,
|
||||
capabilities: () => [],
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
'update:open': [value: boolean]
|
||||
'editModel': [model: GlobalModelResponse]
|
||||
'toggleModelStatus': [model: GlobalModelResponse]
|
||||
'addProvider': []
|
||||
'editProvider': [provider: any]
|
||||
'deleteProvider': [provider: any]
|
||||
'toggleProviderStatus': [provider: any]
|
||||
'editProvider': [provider: Record<string, unknown>]
|
||||
'deleteProvider': [provider: Record<string, unknown>]
|
||||
'toggleProviderStatus': [provider: Record<string, unknown>]
|
||||
'refreshModel': []
|
||||
'linkProvider': [providerId: string]
|
||||
'linkProviders': [providerIds: string[]]
|
||||
@@ -520,8 +522,8 @@ async function loadRoutingData() {
|
||||
|
||||
try {
|
||||
routingData.value = await getGlobalModelRoutingPreview(props.model.id)
|
||||
} catch (err: any) {
|
||||
routingError.value = err.response?.data?.detail || '加载失败'
|
||||
} catch (err: unknown) {
|
||||
routingError.value = parseApiError(err, '加载失败')
|
||||
} finally {
|
||||
routingLoading.value = false
|
||||
}
|
||||
|
||||
@@ -606,6 +606,7 @@ import {
|
||||
import { API_FORMAT_ORDER } from '@/api/endpoints/types'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { recoverKeyHealth } from '@/api/endpoints/health'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useCountdownTimer, getProbeCountdown } from '@/composables/useCountdownTimer'
|
||||
import { MAX_MODEL_NAME_LENGTH, createLRURegexCache, getCompiledModelMappingRegex } from '@/features/models/utils/model-mapping-regex'
|
||||
@@ -696,7 +697,8 @@ const apiFormatGroups = computed<ApiFormatGroup[]>(() => {
|
||||
formatMap.set(format, { providers: [], allKeys: [] })
|
||||
}
|
||||
|
||||
const data = formatMap.get(format)!
|
||||
const data = formatMap.get(format)
|
||||
if (!data) continue
|
||||
|
||||
// 添加 provider entry
|
||||
data.providers.push({
|
||||
@@ -739,7 +741,7 @@ const apiFormatGroups = computed<ApiFormatGroup[]>(() => {
|
||||
if (!keyGroupMap.has(priority)) {
|
||||
keyGroupMap.set(priority, [])
|
||||
}
|
||||
keyGroupMap.get(priority)!.push(keyEntry)
|
||||
keyGroupMap.get(priority)?.push(keyEntry)
|
||||
}
|
||||
|
||||
// 转换为分组数组并排序
|
||||
@@ -847,8 +849,8 @@ async function loadRoutingData() {
|
||||
|
||||
internalRoutingData.value = data
|
||||
compiledGlobalModelMappingRegexes.value = compiled
|
||||
} catch (err: any) {
|
||||
internalError.value = err.response?.data?.detail || '加载失败'
|
||||
} catch (err: unknown) {
|
||||
internalError.value = parseApiError(err, '加载失败')
|
||||
} finally {
|
||||
internalLoading.value = false
|
||||
}
|
||||
@@ -977,7 +979,7 @@ function getKeyPriorityGroups(keys: RoutingKeyInfo[]): KeyPriorityGroup[] {
|
||||
keys: []
|
||||
})
|
||||
}
|
||||
groups.get(priority)!.keys.push(key)
|
||||
groups.get(priority)?.keys.push(key)
|
||||
}
|
||||
|
||||
return Array.from(groups.values()).sort((a, b) => {
|
||||
@@ -1121,8 +1123,8 @@ async function handleRecoverKey(keyId: string, apiFormat: string) {
|
||||
// 通知父组件刷新数据
|
||||
emit('refresh')
|
||||
showSuccess(result.message || 'Key 已恢复')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || 'Key 恢复失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, 'Key 恢复失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,10 +48,12 @@ export function createLRURegexCache(maxSize: number): LRURegexCache {
|
||||
return {
|
||||
get: (key: string) => {
|
||||
if (!cache.has(key)) return undefined
|
||||
const value = cache.get(key)!
|
||||
cache.delete(key)
|
||||
cache.set(key, value)
|
||||
return value
|
||||
const value = cache.get(key)
|
||||
if (value !== undefined) {
|
||||
cache.delete(key)
|
||||
cache.set(key, value)
|
||||
}
|
||||
return value ?? null
|
||||
},
|
||||
set: (key: string, value: RegExp | null) => {
|
||||
if (cache.has(key)) {
|
||||
|
||||
@@ -190,14 +190,14 @@ function propertyToField(
|
||||
export function buildRequestFromSchema(
|
||||
schema: CredentialsSchema,
|
||||
architectureId: string,
|
||||
formData: Record<string, any>,
|
||||
formData: Record<string, unknown>,
|
||||
providerWebsite?: string,
|
||||
): SaveConfigRequest {
|
||||
const baseUrl = formData.base_url || providerWebsite || schema['x-default-base-url'] || ''
|
||||
const baseUrl = (formData.base_url as string) || providerWebsite || schema['x-default-base-url'] || ''
|
||||
const authType = schema['x-auth-type'] || 'api_key'
|
||||
|
||||
// 构建 credentials:除 base_url 和代理字段外的所有 schema 属性
|
||||
const credentials: Record<string, any> = {}
|
||||
const credentials: Record<string, unknown> = {}
|
||||
for (const key of Object.keys(schema.properties)) {
|
||||
if (key === 'base_url') continue
|
||||
const v = formData[key]
|
||||
@@ -227,18 +227,21 @@ export function buildRequestFromSchema(
|
||||
*/
|
||||
export function parseConfigFromSchema(
|
||||
schema: CredentialsSchema,
|
||||
config: any,
|
||||
): Record<string, any> {
|
||||
const proxyData = parseProxyConfig(config?.connector?.config)
|
||||
const result: Record<string, any> = {
|
||||
base_url: config?.base_url || '',
|
||||
config: Record<string, unknown> | null | undefined,
|
||||
): Record<string, unknown> {
|
||||
const connector = config?.connector as Record<string, unknown> | undefined
|
||||
const connectorConfig = connector?.config as Record<string, unknown> | undefined
|
||||
const proxyData = parseProxyConfig(connectorConfig)
|
||||
const result: Record<string, unknown> = {
|
||||
base_url: (config?.base_url as string) || '',
|
||||
...proxyData,
|
||||
}
|
||||
|
||||
// 从 credentials 中提取各 schema 属性
|
||||
const credentials = connector?.credentials as Record<string, unknown> | undefined
|
||||
for (const key of Object.keys(schema.properties)) {
|
||||
if (key === 'base_url') continue
|
||||
result[key] = config?.connector?.credentials?.[key] || ''
|
||||
result[key] = credentials?.[key] || ''
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -246,13 +249,18 @@ export function parseConfigFromSchema(
|
||||
|
||||
// ==================== 验证 ====================
|
||||
|
||||
/** 安全获取字符串值并 trim(表单字段值可能为 string 或其他类型) */
|
||||
function trimValue(v: unknown): string {
|
||||
return typeof v === 'string' ? v.trim() : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 schema 验证表单数据
|
||||
* @returns 错误消息,无错误返回 null
|
||||
*/
|
||||
export function validateFromSchema(
|
||||
schema: CredentialsSchema,
|
||||
formData: Record<string, any>,
|
||||
formData: Record<string, unknown>,
|
||||
): string | null {
|
||||
const validations = schema['x-validation']
|
||||
if (!validations) return null
|
||||
@@ -262,7 +270,7 @@ export function validateFromSchema(
|
||||
case 'required': {
|
||||
if (!rule.fields) break
|
||||
for (const field of rule.fields) {
|
||||
if (!formData[field]?.trim?.()) {
|
||||
if (!trimValue(formData[field])) {
|
||||
return rule.message
|
||||
}
|
||||
}
|
||||
@@ -270,7 +278,7 @@ export function validateFromSchema(
|
||||
}
|
||||
case 'any_required': {
|
||||
if (!rule.fields) break
|
||||
const hasAny = rule.fields.some((f) => !!formData[f]?.trim?.())
|
||||
const hasAny = rule.fields.some((f) => !!trimValue(formData[f]))
|
||||
if (!hasAny) {
|
||||
return rule.message
|
||||
}
|
||||
@@ -282,12 +290,12 @@ export function validateFromSchema(
|
||||
const thenFields = rule.then
|
||||
if (!ifField || !thenFields) break
|
||||
|
||||
const ifHasValue = !!formData[ifField]?.trim?.()
|
||||
const unlessHasValue = unlessField ? !!formData[unlessField]?.trim?.() : false
|
||||
const ifHasValue = !!trimValue(formData[ifField])
|
||||
const unlessHasValue = unlessField ? !!trimValue(formData[unlessField]) : false
|
||||
|
||||
if (ifHasValue && !unlessHasValue) {
|
||||
for (const field of thenFields) {
|
||||
if (!formData[field]?.trim?.()) {
|
||||
if (!trimValue(formData[field])) {
|
||||
return rule.message
|
||||
}
|
||||
}
|
||||
@@ -331,7 +339,7 @@ export function formatQuotaFromSchema(
|
||||
*/
|
||||
export function formatBalanceExtraFromSchema(
|
||||
schema: CredentialsSchema,
|
||||
extra: Record<string, any>,
|
||||
extra: Record<string, unknown>,
|
||||
): BalanceExtraItem[] {
|
||||
const formats = schema['x-balance-extra-format']
|
||||
if (!formats) return []
|
||||
@@ -367,31 +375,35 @@ export function formatBalanceExtraFromSchema(
|
||||
}
|
||||
|
||||
function formatWindowLimitItem(
|
||||
extra: Record<string, any>,
|
||||
extra: Record<string, unknown>,
|
||||
fmt: BalanceExtraFormat,
|
||||
): BalanceExtraItem | null {
|
||||
if (!fmt.source) return null
|
||||
const limit = extra[fmt.source]
|
||||
if (!limit || limit.remaining === undefined || limit.limit === undefined || limit.limit === 0) {
|
||||
const rawLimit = extra[fmt.source]
|
||||
if (!rawLimit || typeof rawLimit !== 'object') return null
|
||||
const limit = rawLimit as Record<string, unknown>
|
||||
if (limit.remaining === undefined || limit.limit === undefined || limit.limit === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const percent = Math.round((limit.remaining / limit.limit) * 100)
|
||||
const limitVal = Number(limit.limit)
|
||||
const remainingVal = Number(limit.remaining)
|
||||
const percent = Math.round((remainingVal / limitVal) * 100)
|
||||
const divisor = fmt.unit_divisor || 1
|
||||
const remaining = (limit.remaining / divisor).toFixed(2)
|
||||
const total = (limit.limit / divisor).toFixed(2)
|
||||
const remaining = (remainingVal / divisor).toFixed(2)
|
||||
const total = (limitVal / divisor).toFixed(2)
|
||||
|
||||
return {
|
||||
label: fmt.label,
|
||||
value: `${percent}%`,
|
||||
percent,
|
||||
resetsAt: limit.resets_at,
|
||||
resetsAt: typeof limit.resets_at === 'number' ? limit.resets_at : undefined,
|
||||
tooltip: `$${remaining} / $${total}`,
|
||||
}
|
||||
}
|
||||
|
||||
function formatDailyQuotaItem(
|
||||
extra: Record<string, any>,
|
||||
extra: Record<string, unknown>,
|
||||
fmt: BalanceExtraFormat,
|
||||
): BalanceExtraItem | null {
|
||||
const limitKey = fmt.source_limit || 'daily_quota_limit'
|
||||
@@ -410,7 +422,7 @@ function formatDailyQuotaItem(
|
||||
const startDateKey = fmt.source_start_date
|
||||
if (startDateKey && extra[startDateKey]) {
|
||||
try {
|
||||
const startDate = new Date(extra[startDateKey])
|
||||
const startDate = new Date(String(extra[startDateKey]))
|
||||
const now = new Date()
|
||||
const todayReset = new Date(now)
|
||||
todayReset.setHours(startDate.getHours(), startDate.getMinutes(), startDate.getSeconds(), 0)
|
||||
@@ -432,14 +444,14 @@ function formatDailyQuotaItem(
|
||||
}
|
||||
|
||||
function formatMonthlyExpiryItem(
|
||||
extra: Record<string, any>,
|
||||
extra: Record<string, unknown>,
|
||||
fmt: BalanceExtraFormat,
|
||||
): BalanceExtraItem | null {
|
||||
const endDateKey = fmt.source_end_date || 'effective_end_date'
|
||||
if (!extra[endDateKey]) return null
|
||||
|
||||
try {
|
||||
const endDate = new Date(extra[endDateKey])
|
||||
const endDate = new Date(String(extra[endDateKey]))
|
||||
const now = new Date()
|
||||
const daysLeft = Math.ceil((endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||
const resetsAt = Math.floor(endDate.getTime() / 1000)
|
||||
@@ -457,27 +469,27 @@ function formatMonthlyExpiryItem(
|
||||
}
|
||||
|
||||
function formatWeeklySpentItem(
|
||||
extra: Record<string, any>,
|
||||
extra: Record<string, unknown>,
|
||||
fmt: BalanceExtraFormat,
|
||||
): BalanceExtraItem | null {
|
||||
const limitKey = fmt.source_limit || 'weekly_limit'
|
||||
const spentKey = fmt.source_spent || 'weekly_spent'
|
||||
const resetsAtKey = fmt.source_resets_at || 'weekly_resets_at'
|
||||
|
||||
const limit = extra[limitKey]
|
||||
const spent = extra[spentKey]
|
||||
const limitNum = Number(extra[limitKey])
|
||||
const spentNum = Number(extra[spentKey])
|
||||
|
||||
if (limit === undefined || limit <= 0 || spent === undefined) return null
|
||||
if (extra[limitKey] === undefined || limitNum <= 0 || extra[spentKey] === undefined) return null
|
||||
|
||||
const remaining = Math.max(0, limit - spent)
|
||||
const percent = Math.round((remaining / limit) * 100)
|
||||
const remaining = Math.max(0, limitNum - spentNum)
|
||||
const percent = Math.round((remaining / limitNum) * 100)
|
||||
|
||||
return {
|
||||
label: fmt.label,
|
||||
value: `${percent}%`,
|
||||
percent,
|
||||
resetsAt: extra[resetsAtKey],
|
||||
tooltip: `$${remaining.toFixed(2)} / $${(limit as number).toFixed(2)}`,
|
||||
resetsAt: typeof extra[resetsAtKey] === 'number' ? extra[resetsAtKey] : undefined,
|
||||
tooltip: `$${remaining.toFixed(2)} / $${limitNum.toFixed(2)}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,8 +501,8 @@ function formatWeeklySpentItem(
|
||||
export function handleSchemaFieldChange(
|
||||
schema: CredentialsSchema,
|
||||
fieldKey: string,
|
||||
value: any,
|
||||
formData: Record<string, any>,
|
||||
value: unknown,
|
||||
formData: Record<string, unknown>,
|
||||
): void {
|
||||
const hooks = schema['x-field-hooks']
|
||||
if (!hooks) return
|
||||
@@ -499,9 +511,9 @@ export function handleSchemaFieldChange(
|
||||
if (!hook) return
|
||||
|
||||
// 目标字段为空时才填充
|
||||
if (formData[hook.target]?.trim?.()) return
|
||||
if (trimValue(formData[hook.target])) return
|
||||
|
||||
const result = executeFieldHook(hook.action, value)
|
||||
const result = executeFieldHook(hook.action, typeof value === 'string' ? value : String(value ?? ''))
|
||||
if (result) {
|
||||
formData[hook.target] = result
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ export const PROXY_FIELD_GROUP: AuthTemplateFieldGroup = {
|
||||
* @param formData 表单数据
|
||||
* @returns 代理配置对象,展开到 connector.config 中
|
||||
*/
|
||||
export function buildProxyConfig(formData: Record<string, any>): { proxy_node_id?: string } {
|
||||
export function buildProxyConfig(formData: Record<string, unknown>): { proxy_node_id?: string } {
|
||||
if (!formData.proxy_enabled || !formData.proxy_node_id) {
|
||||
return {}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export function buildProxyConfig(formData: Record<string, any>): { proxy_node_id
|
||||
* @param config connector.config 对象
|
||||
* @returns 表单数据
|
||||
*/
|
||||
export function parseProxyConfig(config: any): Record<string, any> {
|
||||
export function parseProxyConfig(config: Record<string, unknown> | null | undefined): Record<string, unknown> {
|
||||
// 代理节点模式
|
||||
if (config?.proxy_node_id) {
|
||||
return {
|
||||
|
||||
@@ -113,10 +113,11 @@ import {
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import { testModel } from '@/api/endpoints/providers'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
metadata: any
|
||||
metadata: Record<string, unknown> | null
|
||||
keyName: string
|
||||
providerId?: string
|
||||
keyId?: string
|
||||
@@ -138,17 +139,19 @@ const { error: showError, success: showSuccess } = useToast()
|
||||
const testingModel = ref<string | null>(null)
|
||||
|
||||
const items = computed<QuotaItem[]>(() => {
|
||||
const quotaByModel = props.metadata?.antigravity?.quota_by_model
|
||||
const antigravity = props.metadata?.antigravity
|
||||
if (!antigravity || typeof antigravity !== 'object') return []
|
||||
const quotaByModel = (antigravity as Record<string, unknown>).quota_by_model
|
||||
if (!quotaByModel || typeof quotaByModel !== 'object') return []
|
||||
|
||||
const result: QuotaItem[] = []
|
||||
for (const [model, rawInfo] of Object.entries(quotaByModel)) {
|
||||
for (const [model, rawInfo] of Object.entries(quotaByModel as Record<string, unknown>)) {
|
||||
if (!model) continue
|
||||
const info: any = rawInfo || {}
|
||||
const info = (rawInfo || {}) as Record<string, unknown>
|
||||
|
||||
let usedPercent = Number(info.used_percent)
|
||||
let usedPercent = Number(info['used_percent'])
|
||||
if (!Number.isFinite(usedPercent)) {
|
||||
const remainingFraction = Number(info.remaining_fraction)
|
||||
const remainingFraction = Number(info['remaining_fraction'])
|
||||
if (Number.isFinite(remainingFraction)) {
|
||||
usedPercent = (1 - remainingFraction) * 100
|
||||
} else {
|
||||
@@ -162,8 +165,9 @@ const items = computed<QuotaItem[]>(() => {
|
||||
const remainingPercent = Math.max(100 - usedPercent, 0)
|
||||
|
||||
let resetSeconds: number | null = null
|
||||
if (typeof info.reset_time === 'string' && info.reset_time.trim()) {
|
||||
const ts = Date.parse(info.reset_time.trim())
|
||||
const resetTime = info['reset_time']
|
||||
if (typeof resetTime === 'string' && resetTime.trim()) {
|
||||
const ts = Date.parse(resetTime.trim())
|
||||
if (!Number.isNaN(ts)) {
|
||||
const diff = Math.floor((ts - Date.now()) / 1000)
|
||||
resetSeconds = diff > 0 ? diff : 0
|
||||
@@ -203,9 +207,8 @@ async function handleTestModel(modelName: string) {
|
||||
} else {
|
||||
showError(`模型测试失败: ${result.error || '未知错误'}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err?.response?.data?.detail || err?.message || '测试请求失败'
|
||||
showError(`模型测试失败: ${errorMsg}`)
|
||||
} catch (err: unknown) {
|
||||
showError(`模型测试失败: ${parseApiError(err, '测试请求失败')}`)
|
||||
} finally {
|
||||
testingModel.value = null
|
||||
}
|
||||
|
||||
@@ -524,7 +524,7 @@ async function handleSave() {
|
||||
try {
|
||||
await deleteModel(props.providerId, existingModel.id)
|
||||
totalSuccess++
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
allErrors.push(parseApiError(err, '移除失败'))
|
||||
}
|
||||
}
|
||||
@@ -541,7 +541,7 @@ async function handleSave() {
|
||||
try {
|
||||
await deleteModel(props.providerId, existingModel.id)
|
||||
totalSuccess++
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
allErrors.push(parseApiError(err, '移除失败'))
|
||||
}
|
||||
}
|
||||
@@ -556,7 +556,7 @@ async function handleSave() {
|
||||
if (result.errors.length > 0) {
|
||||
allErrors.push(...result.errors.map(e => e.error))
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
allErrors.push(parseApiError(err, '批量添加全局模型失败'))
|
||||
}
|
||||
}
|
||||
@@ -570,7 +570,7 @@ async function handleSave() {
|
||||
if (result.errors.length > 0) {
|
||||
allErrors.push(...result.errors.map(e => e.error))
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
allErrors.push(parseApiError(err, '导入上游模型失败'))
|
||||
}
|
||||
}
|
||||
@@ -585,7 +585,7 @@ async function handleSave() {
|
||||
|
||||
emit('changed')
|
||||
emit('update:open', false)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '保存失败'), '错误')
|
||||
// 即使出错,如果已执行过操作,也通知父组件刷新数据
|
||||
if (hasAnyOperation) {
|
||||
@@ -650,7 +650,7 @@ async function loadGlobalModels() {
|
||||
loadingGlobalModels.value = true
|
||||
const response = await getGlobalModels({ limit: 1000 })
|
||||
allGlobalModels.value = response.models
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载全局模型失败'), '错误')
|
||||
} finally {
|
||||
loadingGlobalModels.value = false
|
||||
@@ -661,7 +661,7 @@ async function loadGlobalModels() {
|
||||
async function loadExistingModels() {
|
||||
try {
|
||||
existingModels.value = await getProviderModels(props.providerId)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载已关联模型失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,6 +783,7 @@ import {
|
||||
} from '@/components/ui'
|
||||
import { Settings, Trash2, Check, X, Power, ChevronRight, Plus, Shuffle, RotateCcw, Radio, CheckCircle, Save, Filter, HelpCircle } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { log } from '@/utils/logger'
|
||||
import AlertDialog from '@/components/common/AlertDialog.vue'
|
||||
import {
|
||||
@@ -1000,8 +1001,7 @@ function prepareValueForJsonParse(raw: string): string {
|
||||
}
|
||||
|
||||
// 递归还原: 将 sentinel 字符串还原为 {{$original}}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function restoreOriginalPlaceholder(value: any): any {
|
||||
function restoreOriginalPlaceholder(value: unknown): unknown {
|
||||
if (typeof value === 'string') {
|
||||
if (value === ORIGINAL_SENTINEL) return ORIGINAL_PLACEHOLDER
|
||||
if (value.includes(ORIGINAL_SENTINEL)) {
|
||||
@@ -1011,8 +1011,8 @@ function restoreOriginalPlaceholder(value: any): any {
|
||||
}
|
||||
if (Array.isArray(value)) return value.map(restoreOriginalPlaceholder)
|
||||
if (value !== null && typeof value === 'object') {
|
||||
const result: Record<string, any> = {}
|
||||
for (const [k, v] of Object.entries(value)) result[k] = restoreOriginalPlaceholder(v)
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) result[k] = restoreOriginalPlaceholder(v)
|
||||
return result
|
||||
}
|
||||
return value
|
||||
@@ -1045,7 +1045,7 @@ function parseBodyRulePathParts(path: string): string[] | null {
|
||||
return parts
|
||||
}
|
||||
|
||||
function initBodyRuleSetValueForEditor(value: any): { value: string } {
|
||||
function initBodyRuleSetValueForEditor(value: unknown): { value: string } {
|
||||
if (value === undefined) return { value: '' }
|
||||
|
||||
// 所有值都用 JSON 格式回显
|
||||
@@ -1110,7 +1110,7 @@ function isCodexUrl(baseUrl: string): boolean {
|
||||
// 读取端点的上游流式策略(endpoint.config.upstream_stream_policy)
|
||||
function getEndpointUpstreamStreamPolicy(endpoint: ProviderEndpoint): string {
|
||||
const cfg = endpoint.config || {}
|
||||
const raw = (cfg.upstream_stream_policy ?? cfg.upstreamStreamPolicy ?? cfg.upstream_stream) as any
|
||||
const raw = (cfg.upstream_stream_policy ?? cfg.upstreamStreamPolicy ?? cfg.upstream_stream) as unknown
|
||||
if (raw === null || raw === undefined) return 'auto'
|
||||
if (typeof raw === 'boolean') return raw ? 'force_stream' : 'force_non_stream'
|
||||
const s = String(raw).trim().toLowerCase()
|
||||
@@ -1509,7 +1509,7 @@ function validateBodySetValue(rule: EditableBodyRule): string | null {
|
||||
if (!raw) return '值不能为空'
|
||||
try {
|
||||
JSON.parse(prepareValueForJsonParse(raw))
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
return `JSON 格式错误:${msg}`
|
||||
}
|
||||
@@ -1559,7 +1559,7 @@ function getRegexPatternValidationTip(rule: EditableBodyRule): string {
|
||||
new RegExp(rule.pattern.trim())
|
||||
// 正则有效但 flags 无效
|
||||
return '无效的 flags(仅允许 i/m/s)'
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
return err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
}
|
||||
@@ -1576,7 +1576,7 @@ function getBodySetValueValidationTip(rule: EditableBodyRule): string {
|
||||
try {
|
||||
JSON.parse(prepareValueForJsonParse(rule.value.trim()))
|
||||
return ''
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
return err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
}
|
||||
@@ -1729,7 +1729,7 @@ function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
|
||||
return { path: rule.conditionPath.trim(), op }
|
||||
}
|
||||
const raw = rule.conditionValue.trim()
|
||||
let val: any = raw
|
||||
let val: unknown = raw
|
||||
try { val = JSON.parse(raw) } catch { /* 保留原字符串 */ }
|
||||
return { path: rule.conditionPath.trim(), op, value: val }
|
||||
}
|
||||
@@ -1737,7 +1737,7 @@ function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
|
||||
for (const rule of rules) {
|
||||
const condition = buildCondition(rule)
|
||||
if (rule.action === 'set' && rule.path.trim()) {
|
||||
let value: any = rule.value
|
||||
let value: unknown = rule.value
|
||||
try { value = restoreOriginalPlaceholder(JSON.parse(prepareValueForJsonParse(rule.value.trim()))) } catch { value = rule.value }
|
||||
result.push({ action: 'set', path: rule.path.trim(), value, ...(condition ? { condition } : {}) })
|
||||
} else if (rule.action === 'drop' && rule.path.trim()) {
|
||||
@@ -1745,7 +1745,7 @@ function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
|
||||
} else if (rule.action === 'rename' && rule.from.trim() && rule.to.trim()) {
|
||||
result.push({ action: 'rename', from: rule.from.trim(), to: rule.to.trim(), ...(condition ? { condition } : {}) })
|
||||
} else if ((rule.action === 'insert' || rule.action === 'append') && rule.path.trim()) {
|
||||
let value: any = rule.value
|
||||
let value: unknown = rule.value
|
||||
try { value = restoreOriginalPlaceholder(JSON.parse(prepareValueForJsonParse(rule.value.trim()))) } catch { value = rule.value }
|
||||
const indexStr = rule.index.trim()
|
||||
if (indexStr === '') {
|
||||
@@ -1804,7 +1804,7 @@ function getBodyValidationErrorForEndpoint(endpointId: string): string | null {
|
||||
if (!rule.pattern.trim()) return `${prefix}正则表达式不能为空`
|
||||
try {
|
||||
new RegExp(rule.pattern.trim())
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
return `${prefix}正则表达式无效:${err instanceof Error ? err.message : String(err)}`
|
||||
}
|
||||
const flags = rule.flags.trim()
|
||||
@@ -1984,7 +1984,7 @@ async function saveEndpoint(endpoint: ProviderEndpoint) {
|
||||
savingEndpointId.value = endpoint.id
|
||||
try {
|
||||
// 仅提交变更字段,避免 fixed provider 因 base_url/custom_path 被锁定而更新失败
|
||||
const payload: Record<string, any> = {}
|
||||
const payload: Record<string, unknown> = {}
|
||||
|
||||
if (!isFixedProvider.value) {
|
||||
if (state.url !== endpoint.base_url) payload.base_url = state.url
|
||||
@@ -2001,8 +2001,8 @@ async function saveEndpoint(endpoint: ProviderEndpoint) {
|
||||
await updateEndpoint(endpoint.id, payload)
|
||||
success('端点已更新')
|
||||
emit('endpointUpdated')
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || '更新失败', '错误')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '更新失败'), '错误')
|
||||
} finally {
|
||||
savingEndpointId.value = null
|
||||
}
|
||||
@@ -2020,8 +2020,8 @@ async function handleToggleFormatConversion(endpoint: ProviderEndpoint) {
|
||||
})
|
||||
success(newEnabled ? '已启用格式转换' : '已关闭格式转换')
|
||||
emit('endpointUpdated')
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '操作失败'), '错误')
|
||||
} finally {
|
||||
togglingFormatEndpointId.value = null
|
||||
}
|
||||
@@ -2070,7 +2070,7 @@ async function handleCycleUpstreamStream(endpoint: ProviderEndpoint) {
|
||||
|
||||
savingEndpointId.value = endpoint.id
|
||||
try {
|
||||
const merged: Record<string, any> = { ...(endpoint.config || {}) }
|
||||
const merged: Record<string, unknown> = { ...(endpoint.config || {}) }
|
||||
// 清理旧的 key
|
||||
delete merged.upstream_stream_policy
|
||||
delete merged.upstreamStreamPolicy
|
||||
@@ -2091,8 +2091,8 @@ async function handleCycleUpstreamStream(endpoint: ProviderEndpoint) {
|
||||
|
||||
success(`已切换为${nextLabel}`)
|
||||
emit('endpointUpdated')
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '操作失败'), '错误')
|
||||
} finally {
|
||||
savingEndpointId.value = null
|
||||
}
|
||||
@@ -2122,8 +2122,8 @@ async function handleAddEndpoint() {
|
||||
// 重置表单,保留 URL
|
||||
newEndpoint.value = { api_format: '', base_url: baseUrl, custom_path: '' }
|
||||
emit('endpointCreated')
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || '添加失败', '错误')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '添加失败'), '错误')
|
||||
} finally {
|
||||
addingEndpoint.value = false
|
||||
}
|
||||
@@ -2137,8 +2137,8 @@ async function handleToggleEndpoint(endpoint: ProviderEndpoint) {
|
||||
await updateEndpoint(endpoint.id, { is_active: newStatus })
|
||||
success(newStatus ? '端点已启用' : '端点已停用')
|
||||
emit('endpointUpdated')
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '操作失败'), '错误')
|
||||
} finally {
|
||||
togglingEndpointId.value = null
|
||||
}
|
||||
@@ -2162,8 +2162,8 @@ async function confirmDeleteEndpoint() {
|
||||
await deleteEndpoint(endpoint.id)
|
||||
success(`已删除 ${formatApiFormat(endpoint.api_format)} 端点`)
|
||||
emit('endpointUpdated')
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || '删除失败', '错误')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '删除失败'), '错误')
|
||||
} finally {
|
||||
deletingEndpointId.value = null
|
||||
endpointToDelete.value = null
|
||||
|
||||
@@ -142,6 +142,7 @@ import EndpointHealthTimeline from './EndpointHealthTimeline.vue'
|
||||
import { getEndpointStatusMonitor, getPublicEndpointStatusMonitor } from '@/api/endpoints/health'
|
||||
import type { EndpointStatusMonitor, PublicEndpointStatusMonitor } from '@/api/endpoints/types'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -176,8 +177,8 @@ async function loadMonitors() {
|
||||
const data = await getPublicEndpointStatusMonitor(params)
|
||||
monitors.value = data.formats || []
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载健康监控数据失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载健康监控数据失败'), '错误')
|
||||
} finally {
|
||||
loadingMonitors.value = false
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import Checkbox from '@/components/ui/checkbox.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseUpstreamModelError } from '@/utils/errorParser'
|
||||
import { parseApiError, parseUpstreamModelError } from '@/utils/errorParser'
|
||||
import {
|
||||
importModelsFromUpstream,
|
||||
getProviderModels,
|
||||
@@ -305,9 +305,8 @@ async function fetchUpstreamModels() {
|
||||
// 上游返回空列表但无错误
|
||||
hasQueried.value = true
|
||||
}
|
||||
} catch (err: any) {
|
||||
const rawError = err.response?.data?.detail || err.message || '获取上游模型失败'
|
||||
errorMessage.value = parseUpstreamModelError(rawError)
|
||||
} catch (err: unknown) {
|
||||
errorMessage.value = parseUpstreamModelError(parseApiError(err, '获取上游模型失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -378,8 +377,8 @@ async function handleImport() {
|
||||
const errorMsg = response.errors?.[0]?.error || '导入失败'
|
||||
showError(errorMsg, '导入失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '导入失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '导入失败'), '错误')
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
|
||||
@@ -796,7 +796,7 @@ async function handleSave() {
|
||||
success('模型权限已更新', '成功')
|
||||
emit('saved')
|
||||
emit('close')
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '保存失败'), '错误')
|
||||
} finally {
|
||||
saving.value = false
|
||||
|
||||
@@ -563,7 +563,7 @@ function parsePatternText(text: string): string[] {
|
||||
}
|
||||
|
||||
// 解析 Service Account JSON 文本
|
||||
function parseAuthConfig(): Record<string, any> | null {
|
||||
function parseAuthConfig(): Record<string, unknown> | null {
|
||||
if (form.value.auth_type !== 'vertex_ai') return null
|
||||
const text = form.value.auth_config_text.trim()
|
||||
if (!text) return null
|
||||
@@ -699,7 +699,7 @@ async function handleSave() {
|
||||
|
||||
emit('saved')
|
||||
emit('close')
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '保存密钥失败')
|
||||
showError(errorMessage, '错误')
|
||||
} finally {
|
||||
|
||||
@@ -153,6 +153,7 @@ import { ref, watch } from 'vue'
|
||||
import { Tag, Plus, X, Loader2, GripVertical, Info } from 'lucide-vue-next'
|
||||
import { Dialog, Button, Input, Label } from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import type { Model, ProviderModelAlias } from '@/api/endpoints'
|
||||
|
||||
@@ -262,7 +263,7 @@ function handleDrop(targetIndex: number) {
|
||||
items.forEach(alias => {
|
||||
// 找到这个映射在原数组中的索引
|
||||
const originalIdx = aliases.value.findIndex(a => a === alias)
|
||||
const originalPriority = originalIdx >= 0 ? originalPriorityMap.get(originalIdx)! : alias.priority
|
||||
const originalPriority = originalIdx >= 0 ? (originalPriorityMap.get(originalIdx) ?? alias.priority) : alias.priority
|
||||
|
||||
if (alias === draggedItem) {
|
||||
// 被拖动的映射是独立的新组,获得当前优先级
|
||||
@@ -271,7 +272,7 @@ function handleDrop(targetIndex: number) {
|
||||
} else {
|
||||
if (groupNewPriority.has(originalPriority)) {
|
||||
// 这个组已经分配过优先级,使用相同的值
|
||||
alias.priority = groupNewPriority.get(originalPriority)!
|
||||
alias.priority = groupNewPriority.get(originalPriority) ?? currentPriority
|
||||
} else {
|
||||
// 这个组第一次出现,分配新优先级
|
||||
groupNewPriority.set(originalPriority, currentPriority)
|
||||
@@ -325,8 +326,8 @@ async function handleSubmit() {
|
||||
showSuccess('映射配置已保存')
|
||||
emit('update:open', false)
|
||||
emit('saved')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '保存失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '保存失败'), '错误')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
@@ -261,6 +261,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import {
|
||||
type Model,
|
||||
type ProviderModelAlias,
|
||||
@@ -461,8 +462,8 @@ async function fetchUpstreamModels() {
|
||||
if (result.error) {
|
||||
showError(result.error, '获取上游模型失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '获取上游模型列表失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '获取上游模型列表失败'), '错误')
|
||||
} finally {
|
||||
loadingModels.value = false
|
||||
fetchingUpstreamModels.value = false
|
||||
@@ -579,8 +580,8 @@ async function handleSubmit() {
|
||||
showSuccess(props.editingGroup ? '映射组已更新' : '映射已添加')
|
||||
emit('update:open', false)
|
||||
emit('saved')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '操作失败'), '错误')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
@@ -92,34 +92,72 @@
|
||||
>
|
||||
<!-- Kiro: 设备授权模式 -->
|
||||
<template v-if="isKiroProvider">
|
||||
<!-- 初始状态:输入 Start URL / Region + 开始 -->
|
||||
<!-- 初始状态:选择授权类型 + 开始 -->
|
||||
<div
|
||||
v-if="!device.session_id && !device.starting"
|
||||
class="space-y-3"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Start URL</label>
|
||||
<input
|
||||
v-model="device.start_url"
|
||||
type="text"
|
||||
placeholder="https://view.awsapps.com/start"
|
||||
class="w-full h-8 px-2 text-xs rounded-md border border-border bg-background font-mono"
|
||||
spellcheck="false"
|
||||
<!-- Builder ID / Identity Center 切换 -->
|
||||
<div class="grid grid-cols-2 gap-1.5">
|
||||
<button
|
||||
v-for="opt in ([
|
||||
{ key: 'builder_id', label: 'Builder ID' },
|
||||
{ key: 'identity_center', label: 'Identity Center' },
|
||||
] as const)"
|
||||
:key="opt.key"
|
||||
class="h-8 text-xs font-medium rounded-md border transition-colors"
|
||||
:class="device.auth_type === opt.key
|
||||
? 'border-primary bg-primary/5 text-foreground'
|
||||
: 'border-border text-muted-foreground hover:text-foreground hover:border-foreground/20'"
|
||||
@click="device.auth_type = opt.key"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Region</label>
|
||||
<input
|
||||
v-model="device.region"
|
||||
type="text"
|
||||
placeholder="us-east-1"
|
||||
class="w-full h-8 px-2 text-xs rounded-md border border-border bg-background font-mono"
|
||||
spellcheck="false"
|
||||
|
||||
<!-- grid 叠放保持高度稳定 -->
|
||||
<div class="grid [&>*]:col-start-1 [&>*]:row-start-1">
|
||||
<!-- Builder ID: 说明文字 -->
|
||||
<div
|
||||
class="flex items-center justify-center transition-opacity duration-150"
|
||||
:class="device.auth_type === 'builder_id' ? 'opacity-100' : 'opacity-0 pointer-events-none'"
|
||||
>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
使用个人 AWS Builder ID 进行设备授权,无需额外配置。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Identity Center: Start URL + Region -->
|
||||
<div
|
||||
class="space-y-3 transition-opacity duration-150"
|
||||
:class="device.auth_type === 'identity_center' ? 'opacity-100' : 'opacity-0 pointer-events-none'"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Start URL</label>
|
||||
<input
|
||||
v-model="device.start_url"
|
||||
type="text"
|
||||
placeholder="https://your-org.awsapps.com/start"
|
||||
class="w-full h-8 px-2 text-xs rounded-md border border-border bg-background font-mono"
|
||||
spellcheck="false"
|
||||
>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Region</label>
|
||||
<input
|
||||
v-model="device.region"
|
||||
type="text"
|
||||
placeholder="us-east-1"
|
||||
class="w-full h-8 px-2 text-xs rounded-md border border-border bg-background font-mono"
|
||||
spellcheck="false"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="w-full"
|
||||
:disabled="!device.start_url.trim()"
|
||||
:disabled="device.auth_type === 'identity_center' && !device.start_url.trim()"
|
||||
@click="startDeviceAuth"
|
||||
>
|
||||
开始授权
|
||||
@@ -480,7 +518,10 @@ function createInitialOAuthState(): OAuthState {
|
||||
const oauth = ref<OAuthState>(createInitialOAuthState())
|
||||
|
||||
// 设备授权状态
|
||||
type DeviceAuthType = 'builder_id' | 'identity_center'
|
||||
|
||||
interface DeviceAuthState {
|
||||
auth_type: DeviceAuthType
|
||||
start_url: string
|
||||
region: string
|
||||
starting: boolean
|
||||
@@ -494,8 +535,12 @@ interface DeviceAuthState {
|
||||
error: string
|
||||
}
|
||||
|
||||
const BUILDER_ID_START_URL = 'https://view.awsapps.com/start'
|
||||
const BUILDER_ID_REGION = 'us-east-1'
|
||||
|
||||
function createInitialDeviceState(): DeviceAuthState {
|
||||
return {
|
||||
auth_type: 'builder_id',
|
||||
start_url: '',
|
||||
region: 'us-east-1',
|
||||
starting: false,
|
||||
@@ -563,8 +608,9 @@ function stopDevicePolling() {
|
||||
|
||||
function resetDevice() {
|
||||
stopDevicePolling()
|
||||
const { start_url, region } = device.value
|
||||
const { auth_type, start_url, region } = device.value
|
||||
device.value = createInitialDeviceState()
|
||||
device.value.auth_type = auth_type
|
||||
device.value.start_url = start_url
|
||||
device.value.region = region
|
||||
}
|
||||
@@ -635,7 +681,7 @@ async function initOAuth() {
|
||||
oauth.value.redirect_uri = resp.redirect_uri
|
||||
oauth.value.instructions = resp.instructions
|
||||
oauth.value.provider_type = resp.provider_type
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '初始化授权失败')
|
||||
showError(errorMessage, '错误')
|
||||
mode.value = 'import'
|
||||
@@ -655,7 +701,7 @@ async function handleCompleteOAuth() {
|
||||
success('授权成功,账号已添加')
|
||||
emit('saved')
|
||||
handleClose()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '完成授权失败')
|
||||
showError(errorMessage, '错误')
|
||||
} finally {
|
||||
@@ -699,13 +745,14 @@ function parseImportText(text: string): { refresh_token: string; name?: string }
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
const parsed: unknown = JSON.parse(trimmed)
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
const refreshToken = (parsed as any).refresh_token
|
||||
const obj = parsed as Record<string, unknown>
|
||||
const refreshToken = obj.refresh_token
|
||||
if (typeof refreshToken === 'string' && refreshToken.trim()) {
|
||||
return {
|
||||
refresh_token: refreshToken.trim(),
|
||||
name: (parsed as any).name || (parsed as any).oauth_email || undefined,
|
||||
name: (typeof obj.name === 'string' ? obj.name : undefined) || (typeof obj.oauth_email === 'string' ? obj.oauth_email : undefined),
|
||||
}
|
||||
}
|
||||
return null
|
||||
@@ -789,7 +836,7 @@ async function handleImport() {
|
||||
emit('saved')
|
||||
handleClose()
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '导入失败')
|
||||
showError(errorMessage, '错误')
|
||||
} finally {
|
||||
@@ -821,9 +868,10 @@ async function startDeviceAuth() {
|
||||
device.value.starting = true
|
||||
device.value.error = ''
|
||||
try {
|
||||
const isBuilderID = device.value.auth_type === 'builder_id'
|
||||
const resp = await startDeviceAuthorize(props.providerId, {
|
||||
start_url: device.value.start_url.trim() || undefined,
|
||||
region: device.value.region.trim() || undefined,
|
||||
start_url: isBuilderID ? BUILDER_ID_START_URL : (device.value.start_url.trim() || undefined),
|
||||
region: isBuilderID ? BUILDER_ID_REGION : (device.value.region.trim() || undefined),
|
||||
proxy_node_id: selectedProxyNodeId.value || undefined,
|
||||
})
|
||||
device.value.session_id = resp.session_id
|
||||
@@ -835,7 +883,7 @@ async function startDeviceAuth() {
|
||||
device.value.status = 'pending'
|
||||
startCountdown()
|
||||
scheduleDevicePoll()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '发起设备授权失败')
|
||||
showError(errorMessage, '错误')
|
||||
device.value.status = 'error'
|
||||
@@ -884,7 +932,7 @@ async function pollDevice() {
|
||||
device.value.error = result.error || '授权失败'
|
||||
return
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch {
|
||||
// 网络错误等,继续轮询
|
||||
scheduleDevicePoll()
|
||||
}
|
||||
|
||||
@@ -361,7 +361,7 @@ async function handleSave() {
|
||||
success('账号已更新', '成功')
|
||||
emit('saved')
|
||||
emit('close')
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '保存失败')
|
||||
showError(errorMessage, '错误')
|
||||
} finally {
|
||||
|
||||
@@ -432,12 +432,14 @@ import { Dialog } from '@/components/ui'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { updateProvider, updateProviderKey } from '@/api/endpoints'
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import { batchQueryBalance, type ActionResultResponse, type BalanceInfo } from '@/api/providerOps'
|
||||
import { API_FORMAT_SHORT } from '@/api/endpoints/types'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
interface KeyWithMeta {
|
||||
id: string
|
||||
@@ -555,7 +557,7 @@ async function loadBalances() {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[loadBalances] 加载余额数据失败:', e)
|
||||
log.warn('[loadBalances] 加载余额数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,7 +634,7 @@ async function loadKeysByFormat() {
|
||||
|
||||
// 每个格式独立管理优先级,使用后端返回的 format_priority
|
||||
const data: Record<string, KeyWithMeta[]> = {}
|
||||
for (const [format, keys] of Object.entries(response.data as Record<string, any[]>)) {
|
||||
for (const [format, keys] of Object.entries(response.data as Record<string, Record<string, unknown>[]>)) {
|
||||
// 计算该格式下的默认优先级
|
||||
let maxPriority = 0
|
||||
for (const key of keys) {
|
||||
@@ -656,8 +658,8 @@ async function loadKeysByFormat() {
|
||||
if (formats.length > 0 && !formats.includes(activeFormatTab.value)) {
|
||||
activeFormatTab.value = formats[0]
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载 Key 列表失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载 Key 列表失败'), '错误')
|
||||
} finally {
|
||||
loadingKeys.value = false
|
||||
}
|
||||
@@ -681,8 +683,8 @@ async function toggleKeyActive(format: string, key: KeyWithMeta) {
|
||||
keysByFormat.value[fmt] = sortKeysByActiveAndPriority(keysByFormat.value[fmt])
|
||||
}
|
||||
success(newStatus ? 'Key 已启用' : 'Key 已停用')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '操作失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,7 +799,7 @@ function handleProviderDrop(dropIndex: number) {
|
||||
let currentPriority = 1
|
||||
|
||||
items.forEach(provider => {
|
||||
const originalPriority = originalPriorityMap.get(provider.id)!
|
||||
const originalPriority = originalPriorityMap.get(provider.id) ?? 0
|
||||
|
||||
if (provider === draggedItem) {
|
||||
// 被拖动的项单独成组
|
||||
@@ -806,7 +808,7 @@ function handleProviderDrop(dropIndex: number) {
|
||||
} else {
|
||||
if (groupNewPriority.has(originalPriority)) {
|
||||
// 同组的其他项使用相同的新优先级
|
||||
provider.provider_priority = groupNewPriority.get(originalPriority)!
|
||||
provider.provider_priority = groupNewPriority.get(originalPriority) ?? currentPriority
|
||||
} else {
|
||||
// 新组,分配新优先级
|
||||
groupNewPriority.set(originalPriority, currentPriority)
|
||||
@@ -881,7 +883,7 @@ function handleKeyDrop(format: string, dropIndex: number) {
|
||||
let currentPriority = 1
|
||||
|
||||
items.forEach(key => {
|
||||
const originalPriority = originalPriorityMap.get(key.id)!
|
||||
const originalPriority = originalPriorityMap.get(key.id) ?? 0
|
||||
|
||||
if (key === draggedItem) {
|
||||
// 被拖动的项单独成组
|
||||
@@ -890,7 +892,7 @@ function handleKeyDrop(format: string, dropIndex: number) {
|
||||
} else {
|
||||
if (groupNewPriority.has(originalPriority)) {
|
||||
// 同组的其他项使用相同的新优先级
|
||||
key.priority = groupNewPriority.get(originalPriority)!
|
||||
key.priority = groupNewPriority.get(originalPriority) ?? currentPriority
|
||||
} else {
|
||||
// 新组,分配新优先级
|
||||
groupNewPriority.set(originalPriority, currentPriority)
|
||||
@@ -959,8 +961,8 @@ async function save() {
|
||||
if (activeMainTab.value === 'provider') {
|
||||
close()
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '保存失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '保存失败'), '错误')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -306,6 +306,7 @@ import {
|
||||
deleteProviderOpsConfig,
|
||||
type ArchitectureInfo,
|
||||
} from '@/api/providerOps'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import type { AuthTemplateFieldGroup } from '../auth-templates/types'
|
||||
@@ -325,7 +326,7 @@ const props = defineProps<{
|
||||
open: boolean
|
||||
providerId: string
|
||||
providerWebsite?: string
|
||||
currentConfig?: any
|
||||
currentConfig?: Record<string, unknown> | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -373,7 +374,7 @@ const architecturesLoaded = ref(false)
|
||||
// 当前选择
|
||||
const selectedArchitectureId = ref('new_api')
|
||||
const selectedAuthType = ref('')
|
||||
const formData = ref<Record<string, any>>({})
|
||||
const formData = ref<Record<string, unknown>>({})
|
||||
|
||||
// 当前架构支持的认证方式
|
||||
const currentAuthTypes = computed(() => {
|
||||
@@ -446,7 +447,7 @@ function handleAuthTypeChange() {
|
||||
formChanged.value = true
|
||||
}
|
||||
|
||||
function handleFieldChange(fieldKey: string, value: any) {
|
||||
function handleFieldChange(fieldKey: string, value: unknown) {
|
||||
formChanged.value = true
|
||||
|
||||
// 执行 schema 定义的字段钩子
|
||||
@@ -475,9 +476,9 @@ function resetFormData() {
|
||||
}
|
||||
|
||||
// 初始化表单数据
|
||||
const data: Record<string, any> = {}
|
||||
const data: Record<string, unknown> = {}
|
||||
for (const [key, prop] of Object.entries(schema.properties)) {
|
||||
data[key] = (prop as any)['x-default-value'] ?? ''
|
||||
data[key] = (prop as Record<string, unknown>)['x-default-value'] ?? ''
|
||||
}
|
||||
// 代理相关默认值
|
||||
data.proxy_enabled = false
|
||||
@@ -581,10 +582,9 @@ async function handleVerify() {
|
||||
|
||||
showError(result.message || '验证失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
verifyStatus.value = 'error'
|
||||
const errMsg = error.response?.data?.detail || error.message || '验证失败'
|
||||
showError(errMsg)
|
||||
showError(parseApiError(error, '验证失败'))
|
||||
} finally {
|
||||
isVerifying.value = false
|
||||
}
|
||||
@@ -632,8 +632,8 @@ async function handleSave() {
|
||||
} else {
|
||||
showError(result.message || '保存失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || error.message, '保存失败')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '保存失败'), '保存失败')
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
@@ -666,14 +666,14 @@ async function handleClear() {
|
||||
} else {
|
||||
showError(result.message || '清除失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || error.message, '清除失败')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '清除失败'), '清除失败')
|
||||
} finally {
|
||||
isClearing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromConfig(config: any) {
|
||||
function loadFromConfig(config: Record<string, unknown>) {
|
||||
if (!config?.connector) return
|
||||
|
||||
hasExistingConfig.value = true
|
||||
|
||||
@@ -1007,6 +1007,7 @@ import {
|
||||
ShieldX,
|
||||
Globe,
|
||||
} from 'lucide-vue-next'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
@@ -1022,7 +1023,8 @@ import {
|
||||
updateProvider,
|
||||
getProviderModels,
|
||||
getProviderMappingPreview,
|
||||
type ProviderMappingPreviewResponse
|
||||
type ProviderMappingPreviewResponse,
|
||||
type ProviderWithEndpointsSummary,
|
||||
} from '@/api/endpoints'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import {
|
||||
@@ -1074,8 +1076,8 @@ interface Props {
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void
|
||||
(e: 'edit', provider: any): void
|
||||
(e: 'toggleStatus', provider: any): void
|
||||
(e: 'edit', provider: ProviderWithEndpointsSummary): void
|
||||
(e: 'toggleStatus', provider: ProviderWithEndpointsSummary): void
|
||||
(e: 'refresh'): void
|
||||
}>()
|
||||
|
||||
@@ -1085,7 +1087,7 @@ const { copyToClipboard } = useClipboard()
|
||||
const { tick: countdownTick, start: startCountdownTimer, stop: stopCountdownTimer } = useCountdownTimer()
|
||||
|
||||
const loading = ref(false)
|
||||
const provider = ref<any>(null)
|
||||
const provider = ref<ProviderWithEndpointsSummary | null>(null)
|
||||
const endpoints = ref<ProviderEndpointWithKeys[]>([])
|
||||
const providerKeys = ref<EndpointAPIKey[]>([]) // Provider 级别的 keys
|
||||
const providerModels = ref<Model[]>([]) // Provider 级别的 models
|
||||
@@ -1364,8 +1366,8 @@ async function copyFullKey(key: EndpointAPIKey) {
|
||||
|
||||
revealedKeys.value.set(key.id, textToCopy)
|
||||
copyToClipboard(textToCopy)
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '获取密钥失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '获取密钥失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1385,8 +1387,8 @@ async function downloadRefreshToken(key: EndpointAPIKey) {
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '导出失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '导出失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1409,8 +1411,8 @@ async function confirmDeleteKey() {
|
||||
// 刷新端点列表及模型数据(删除 Key 触发自动解除模型关联)
|
||||
await loadEndpoints()
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '删除密钥失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '删除密钥失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1420,8 +1422,8 @@ async function handleRecoverKey(key: EndpointAPIKey) {
|
||||
showSuccess(result.message || 'Key已完全恢复')
|
||||
await loadEndpoints()
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || 'Key恢复失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, 'Key恢复失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1446,8 +1448,8 @@ async function handleRefreshOAuth(key: EndpointAPIKey) {
|
||||
// Antigravity:token 刷新后可能完成了账号激活,触发配额获取
|
||||
// (不 emit('refresh'),避免触发全局 provider 余额刷新)
|
||||
void autoRefreshQuotaInBackground()
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || 'Token 刷新失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, 'Token 刷新失败'), '错误')
|
||||
} finally {
|
||||
refreshingOAuthKeyId.value = null
|
||||
}
|
||||
@@ -1483,8 +1485,8 @@ async function handleClearOAuthInvalid(key: EndpointAPIKey) {
|
||||
keyInList.is_active = true
|
||||
}
|
||||
await loadEndpoints()
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '清除失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '清除失败'), '错误')
|
||||
} finally {
|
||||
clearingOAuthInvalidKeyId.value = null
|
||||
}
|
||||
@@ -1703,9 +1705,9 @@ async function autoRefreshQuotaInBackground() {
|
||||
} else if (!hadCachedQuota && providerType === 'antigravity') {
|
||||
showError('没有获取到配额信息(请检查账号是否已授权、project_id 是否存在)', '提示')
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (!hadCachedQuota && providerType === 'antigravity') {
|
||||
showError(err.response?.data?.detail || '后台刷新配额失败', '错误')
|
||||
showError(parseApiError(err, '后台刷新配额失败'), '错误')
|
||||
}
|
||||
} finally {
|
||||
refreshingQuota.value = false
|
||||
@@ -1756,8 +1758,8 @@ async function toggleKeyActive(key: EndpointAPIKey) {
|
||||
key.is_active = newStatus
|
||||
showSuccess(newStatus ? '密钥已启用' : '密钥已停用')
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '操作失败'), '错误')
|
||||
} finally {
|
||||
togglingKeyId.value = null
|
||||
}
|
||||
@@ -1768,7 +1770,7 @@ async function toggleKeyActive(key: EndpointAPIKey) {
|
||||
/** 获取 Key 当前代理节点的名称(用于显示) */
|
||||
function getKeyProxyNodeName(key: EndpointAPIKey): string | null {
|
||||
if (!key.proxy?.node_id) return null
|
||||
const node = proxyNodesStore.nodes.find(n => n.id === key.proxy!.node_id)
|
||||
const node = proxyNodesStore.nodes.find(n => n.id === key.proxy?.node_id)
|
||||
return node ? node.name : `${key.proxy.node_id.slice(0, 8) }...`
|
||||
}
|
||||
|
||||
@@ -1791,8 +1793,8 @@ async function setKeyProxy(key: EndpointAPIKey, nodeId: string) {
|
||||
proxyPopoverOpenKeyId.value = null
|
||||
showSuccess('代理节点已设置')
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '设置代理失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '设置代理失败'), '错误')
|
||||
} finally {
|
||||
savingProxyKeyId.value = null
|
||||
}
|
||||
@@ -1807,8 +1809,8 @@ async function clearKeyProxy(key: EndpointAPIKey) {
|
||||
proxyPopoverOpenKeyId.value = null
|
||||
showSuccess('已清除账号代理,将使用提供商级别代理')
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '清除代理失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '清除代理失败'), '错误')
|
||||
} finally {
|
||||
savingProxyKeyId.value = null
|
||||
}
|
||||
@@ -1911,8 +1913,8 @@ async function savePriority(key: EndpointAPIKey) {
|
||||
// 重新排序
|
||||
providerKeys.value.sort((a, b) => (a.internal_priority ?? 0) - (b.internal_priority ?? 0))
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '更新优先级失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '更新优先级失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1999,8 +2001,8 @@ async function saveMultiplier(key: EndpointAPIKey, format: string) {
|
||||
keyToUpdate.rate_multipliers = rateMultipliers
|
||||
}
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '更新倍率失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '更新倍率失败'), '错误')
|
||||
} finally {
|
||||
multiplierSaving.value = false
|
||||
}
|
||||
@@ -2082,7 +2084,7 @@ async function handleKeyDrop(event: DragEvent, targetIndex: number) {
|
||||
const newPriorityMap = new Map<string, number>()
|
||||
|
||||
items.forEach(key => {
|
||||
const originalPriority = originalPriorityMap.get(key.id)!
|
||||
const originalPriority = originalPriorityMap.get(key.id) ?? 0
|
||||
|
||||
if (key === draggedKey) {
|
||||
// 被拖动的项单独成组
|
||||
@@ -2091,7 +2093,7 @@ async function handleKeyDrop(event: DragEvent, targetIndex: number) {
|
||||
} else {
|
||||
if (groupNewPriority.has(originalPriority)) {
|
||||
// 同组的其他项使用相同的新优先级
|
||||
newPriorityMap.set(key.id, groupNewPriority.get(originalPriority)!)
|
||||
newPriorityMap.set(key.id, groupNewPriority.get(originalPriority) ?? currentPriority)
|
||||
} else {
|
||||
// 新组,分配新优先级
|
||||
groupNewPriority.set(originalPriority, currentPriority)
|
||||
@@ -2115,8 +2117,8 @@ async function handleKeyDrop(event: DragEvent, targetIndex: number) {
|
||||
showSuccess('优先级已更新')
|
||||
await loadEndpoints()
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '更新优先级失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '更新优先级失败'), '错误')
|
||||
await loadEndpoints()
|
||||
}
|
||||
}
|
||||
@@ -2479,8 +2481,8 @@ async function loadProvider() {
|
||||
if (!provider.value) {
|
||||
throw new Error('Provider 不存在')
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || err.message || '加载失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载失败'), '错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -2511,8 +2513,8 @@ async function loadEndpoints() {
|
||||
if (bIdx === -1) return -1
|
||||
return aIdx - bIdx
|
||||
})
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载端点失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载端点失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -500,7 +500,7 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
emit('update:modelValue', false)
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
const action = isEditMode.value ? '更新' : '创建'
|
||||
showError(parseApiError(error, `${action}提供商失败`), `${action}失败`)
|
||||
} finally {
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { Loader2, Layers, SquarePen, Plus, Trash2 } from 'lucide-vue-next'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -298,7 +299,7 @@ const VIDEO_RESOLUTION_PRICE_PRESETS: Record<
|
||||
const form = ref({
|
||||
global_model_id: '',
|
||||
price_per_request: undefined as number | undefined,
|
||||
config: {} as Record<string, any>,
|
||||
config: {} as Record<string, unknown>,
|
||||
// 能力配置
|
||||
supports_vision: undefined as boolean | undefined,
|
||||
supports_function_calling: undefined as boolean | undefined,
|
||||
@@ -392,53 +393,54 @@ function resetForm() {
|
||||
availableGlobalModels.value = []
|
||||
}
|
||||
|
||||
function getNested(obj: any, path: string): any {
|
||||
function getNested(obj: Record<string, unknown>, path: string): unknown {
|
||||
if (!obj || typeof obj !== 'object') return undefined
|
||||
const parts = path.split('.').filter(Boolean)
|
||||
let cur: any = obj
|
||||
let cur: unknown = obj
|
||||
for (const p of parts) {
|
||||
if (!cur || typeof cur !== 'object') return undefined
|
||||
cur = cur[p]
|
||||
cur = (cur as Record<string, unknown>)[p]
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
function setNested(obj: any, path: string, value: any) {
|
||||
function setNested(obj: Record<string, unknown>, path: string, value: unknown) {
|
||||
if (!obj || typeof obj !== 'object') return
|
||||
const parts = path.split('.').filter(Boolean)
|
||||
if (parts.length === 0) return
|
||||
let cur: any = obj
|
||||
let cur: Record<string, unknown> = obj
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const p = parts[i]
|
||||
if (!cur[p] || typeof cur[p] !== 'object') {
|
||||
cur[p] = {}
|
||||
}
|
||||
cur = cur[p]
|
||||
cur = cur[p] as Record<string, unknown>
|
||||
}
|
||||
cur[parts[parts.length - 1]] = value
|
||||
}
|
||||
|
||||
function deleteNested(obj: any, path: string) {
|
||||
function deleteNested(obj: Record<string, unknown>, path: string) {
|
||||
if (!obj || typeof obj !== 'object') return
|
||||
const parts = path.split('.').filter(Boolean)
|
||||
if (parts.length === 0) return
|
||||
let cur: any = obj
|
||||
let cur: Record<string, unknown> = obj
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const p = parts[i]
|
||||
if (!cur[p] || typeof cur[p] !== 'object') return
|
||||
cur = cur[p]
|
||||
cur = cur[p] as Record<string, unknown>
|
||||
}
|
||||
delete cur[parts[parts.length - 1]]
|
||||
}
|
||||
|
||||
function pruneEmptyBillingConfig(cfg: Record<string, any>) {
|
||||
function pruneEmptyBillingConfig(cfg: Record<string, unknown>) {
|
||||
const billing = cfg.billing
|
||||
if (!billing || typeof billing !== 'object') return
|
||||
const video = billing.video
|
||||
const billingObj = billing as Record<string, unknown>
|
||||
const video = billingObj.video
|
||||
if (video && typeof video === 'object' && Object.keys(video).length === 0) {
|
||||
delete billing.video
|
||||
delete billingObj.video
|
||||
}
|
||||
if (Object.keys(billing).length === 0) {
|
||||
if (Object.keys(billingObj).length === 0) {
|
||||
delete cfg.billing
|
||||
}
|
||||
}
|
||||
@@ -461,7 +463,7 @@ function normalizeResolutionKey(raw: string): string {
|
||||
return k
|
||||
}
|
||||
|
||||
function loadVideoPricingFromConfig(cfg: Record<string, any>) {
|
||||
function loadVideoPricingFromConfig(cfg: Record<string, unknown>) {
|
||||
const raw = getNested(cfg, 'billing.video.price_per_second_by_resolution')
|
||||
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
// 按分辨率从低到高排序
|
||||
@@ -475,7 +477,7 @@ function loadVideoPricingFromConfig(cfg: Record<string, any>) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyVideoPricingToConfig(cfg: Record<string, any>) {
|
||||
function applyVideoPricingToConfig(cfg: Record<string, unknown>) {
|
||||
// Clean legacy keys
|
||||
deleteNested(cfg, 'billing.video.price_per_second')
|
||||
deleteNested(cfg, 'billing.video.resolution_multipliers')
|
||||
@@ -546,8 +548,8 @@ async function loadAvailableGlobalModels() {
|
||||
availableGlobalModels.value = allGlobalModels.filter(
|
||||
(gm: GlobalModelResponse) => !existingGlobalModelIds.has(gm.id)
|
||||
)
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载模型列表失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载模型列表失败'), '错误')
|
||||
} finally {
|
||||
loadingGlobalModels.value = false
|
||||
}
|
||||
@@ -612,8 +614,8 @@ async function handleSubmit() {
|
||||
}
|
||||
emit('update:open', false)
|
||||
emit('saved')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || (isEditing.value ? '更新失败' : '添加失败'), '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, isEditing.value ? '更新失败' : '添加失败'), '错误')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
@@ -200,11 +200,12 @@ import {
|
||||
type ProviderModelAlias
|
||||
} from '@/api/endpoints'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { parseTestModelError } from '@/utils/errorParser'
|
||||
import { parseApiError, parseTestModelError } from '@/utils/errorParser'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
|
||||
const props = defineProps<{
|
||||
provider: any
|
||||
provider: ProviderWithEndpointsSummary
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -264,7 +265,7 @@ const aliasGroups = computed<AliasGroup[]>(() => {
|
||||
groupMap.set(groupKey, group)
|
||||
groups.push(group)
|
||||
}
|
||||
groupMap.get(groupKey)!.aliases.push(alias)
|
||||
groupMap.get(groupKey)?.aliases.push(alias)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,8 +286,8 @@ async function loadModels() {
|
||||
try {
|
||||
loading.value = true
|
||||
models.value = await getProviderModels(props.provider.id)
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载失败'), '错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -361,8 +362,8 @@ async function confirmDelete() {
|
||||
deletingGroup.value = null
|
||||
await loadModels()
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '删除失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '删除失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,7 +374,7 @@ async function onDialogSaved() {
|
||||
}
|
||||
|
||||
// 测试模型映射
|
||||
async function testMapping(group: any, mapping: any) {
|
||||
async function testMapping(group: AliasGroup, mapping: ProviderModelAlias) {
|
||||
const testingKey = `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`
|
||||
testingMapping.value = testingKey
|
||||
|
||||
@@ -407,9 +408,8 @@ async function testMapping(group: any, mapping: any) {
|
||||
} else {
|
||||
showError(`映射测试失败: ${parseTestModelError(result)}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || '测试请求失败'
|
||||
showError(`映射测试失败: ${errorMsg}`)
|
||||
} catch (err: unknown) {
|
||||
showError(`映射测试失败: ${parseApiError(err, '测试请求失败')}`)
|
||||
} finally {
|
||||
testingMapping.value = null
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="编辑映射"
|
||||
@click="editGroup(item.group!)"
|
||||
@click="item.group && editGroup(item.group)"
|
||||
>
|
||||
<Edit class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -126,7 +126,7 @@
|
||||
size="icon"
|
||||
class="h-8 w-8 hover:text-destructive"
|
||||
title="删除映射"
|
||||
@click="deleteGroup(item.group!)"
|
||||
@click="item.group && deleteGroup(item.group)"
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -362,7 +362,8 @@ import {
|
||||
import { type EndpointAPIKey } from '@/api/endpoints/keys'
|
||||
import type { ProviderEndpoint } from '@/api/endpoints/types'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { parseTestModelError } from '@/utils/errorParser'
|
||||
import { parseApiError, parseTestModelError } from '@/utils/errorParser'
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
|
||||
interface MappingItem {
|
||||
name: string
|
||||
@@ -389,7 +390,7 @@ interface CombinedMapping {
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
provider: any
|
||||
provider: ProviderWithEndpointsSummary
|
||||
providerKeys?: EndpointAPIKey[]
|
||||
models?: Model[]
|
||||
mappingPreview?: ProviderMappingPreviewResponse | null
|
||||
@@ -456,7 +457,7 @@ const exactMappingGroups = computed<AliasGroup[]>(() => {
|
||||
groupMap.set(groupKey, group)
|
||||
groups.push(group)
|
||||
}
|
||||
groupMap.get(groupKey)!.aliases.push(alias)
|
||||
groupMap.get(groupKey)?.aliases.push(alias)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,10 +487,11 @@ const regexMappings = computed<CombinedMapping[]>(() => {
|
||||
mappings: [],
|
||||
matchedKeys: []
|
||||
})
|
||||
result.push(modelMap.get(gm.global_model_id)!)
|
||||
result.push(modelMap.get(gm.global_model_id) as CombinedMapping)
|
||||
}
|
||||
|
||||
const mapping = modelMap.get(gm.global_model_id)!
|
||||
const mapping = modelMap.get(gm.global_model_id)
|
||||
if (!mapping) continue
|
||||
|
||||
// 添加 Key 信息
|
||||
const keyMatches: MappingItem[] = gm.matched_models.map(m => ({
|
||||
@@ -497,7 +499,7 @@ const regexMappings = computed<CombinedMapping[]>(() => {
|
||||
pattern: m.mapping_pattern
|
||||
}))
|
||||
|
||||
mapping.matchedKeys!.push({
|
||||
mapping.matchedKeys?.push({
|
||||
keyId: keyInfo.key_id,
|
||||
keyName: keyInfo.key_name,
|
||||
maskedKey: keyInfo.masked_key,
|
||||
@@ -635,8 +637,8 @@ async function confirmDelete() {
|
||||
deleteConfirmOpen.value = false
|
||||
deletingGroup.value = null
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '删除失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '删除失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,9 +717,8 @@ async function testMapping(item: CombinedMapping, mapping: MappingItem, apiForma
|
||||
} else {
|
||||
showError(`映射测试失败: ${parseTestModelError(result)}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || '测试请求失败'
|
||||
showError(`映射测试失败: ${errorMsg}`)
|
||||
} catch (err: unknown) {
|
||||
showError(`映射测试失败: ${parseApiError(err, '测试请求失败')}`)
|
||||
} finally {
|
||||
testingMapping.value = null
|
||||
}
|
||||
@@ -741,9 +742,8 @@ async function testRegexMapping(item: CombinedMapping, keyItem: MatchedKeyInfo,
|
||||
} else {
|
||||
showError(`映射测试失败: ${parseTestModelError(result)}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || '测试请求失败'
|
||||
showError(`映射测试失败: ${errorMsg}`)
|
||||
} catch (err: unknown) {
|
||||
showError(`映射测试失败: ${parseApiError(err, '测试请求失败')}`)
|
||||
} finally {
|
||||
testingMapping.value = null
|
||||
}
|
||||
|
||||
@@ -266,7 +266,8 @@ import {
|
||||
type ProviderMappingPreviewResponse
|
||||
} from '@/api/endpoints'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { parseTestModelError } from '@/utils/errorParser'
|
||||
import { parseApiError, parseTestModelError } from '@/utils/errorParser'
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
|
||||
interface Endpoint {
|
||||
id: string
|
||||
@@ -276,7 +277,7 @@ interface Endpoint {
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
provider: any
|
||||
provider: ProviderWithEndpointsSummary
|
||||
endpoints?: Endpoint[]
|
||||
models?: Model[]
|
||||
mappingPreview?: ProviderMappingPreviewResponse | null
|
||||
@@ -464,8 +465,8 @@ async function toggleModelActive(model: Model) {
|
||||
await updateModel(props.provider.id, model.id, { is_active: newStatus })
|
||||
model.is_active = newStatus
|
||||
showSuccess(newStatus ? '模型已启用' : '模型已停用')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '操作失败'), '错误')
|
||||
} finally {
|
||||
togglingModelId.value = null
|
||||
}
|
||||
@@ -529,9 +530,8 @@ async function testModelConnection(model: Model, apiFormat?: string) {
|
||||
} else {
|
||||
showError(`模型测试失败: ${parseTestModelError(result)}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || '测试请求失败'
|
||||
showError(`模型测试失败: ${errorMsg}`)
|
||||
} catch (err: unknown) {
|
||||
showError(`模型测试失败: ${parseApiError(err, '测试请求失败')}`)
|
||||
} finally {
|
||||
testingModelId.value = null
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
import { batchQueryBalance, getArchitectures, type ActionResultResponse, type ArchitectureInfo } from '@/api/providerOps'
|
||||
import { formatBalanceExtraFromSchema, type CredentialsSchema } from '@/features/providers/auth-templates/schema-utils'
|
||||
import type { BalanceExtraItem } from '@/features/providers/auth-templates'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
const MAX_BALANCE_RETRIES = 3
|
||||
|
||||
@@ -96,7 +97,7 @@ export function useProviderBalance() {
|
||||
pendingTimers.add(timerId)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[loadBalances] 加载余额数据失败:', e)
|
||||
log.warn('[loadBalances] 加载余额数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +128,7 @@ export function useProviderBalance() {
|
||||
pendingTimers.add(timerId)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[retryPendingBalances] 重试加载余额失败:', e)
|
||||
log.warn('[retryPendingBalances] 重试加载余额失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +166,7 @@ export function useProviderBalance() {
|
||||
if (!result || (result.status !== 'success' && result.status !== 'auth_expired') || !result.data) {
|
||||
return null
|
||||
}
|
||||
const data = result.data as Record<string, any>
|
||||
const data = result.data as Record<string, unknown>
|
||||
const extra = data.extra
|
||||
if (!extra || extra.balance === undefined || extra.points === undefined) {
|
||||
return null
|
||||
@@ -216,7 +217,7 @@ export function useProviderBalance() {
|
||||
if (!result || result.status !== 'success' || !result.data) {
|
||||
return null
|
||||
}
|
||||
const data = result.data as Record<string, any>
|
||||
const data = result.data as Record<string, unknown>
|
||||
const extra = data.extra
|
||||
if (!extra || extra.checkin_success === undefined) {
|
||||
return null
|
||||
@@ -236,7 +237,7 @@ export function useProviderBalance() {
|
||||
if (result.status !== 'success' && result.status !== 'auth_expired') {
|
||||
return null
|
||||
}
|
||||
const data = result.data as Record<string, any>
|
||||
const data = result.data as Record<string, unknown>
|
||||
const extra = data.extra
|
||||
if (!extra || !extra.cookie_expired) {
|
||||
return null
|
||||
@@ -288,7 +289,7 @@ export function useProviderBalance() {
|
||||
return []
|
||||
}
|
||||
|
||||
const data = result.data as Record<string, any>
|
||||
const data = result.data as Record<string, unknown>
|
||||
const extra = data.extra
|
||||
if (!extra) return []
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* 缓存已移至后端(Redis),前端只保留并发请求去重,避免同时发多个相同请求。
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { isAxiosError } from 'axios'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import { parseUpstreamModelError } from '@/utils/errorParser'
|
||||
import type { UpstreamModel } from '@/api/endpoints/types'
|
||||
@@ -42,6 +43,7 @@ export function useUpstreamModelsCache() {
|
||||
|
||||
// 强制刷新时不复用进行中的请求
|
||||
if (!forceRefresh && pendingRequests.has(requestKey)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return pendingRequests.get(requestKey)!
|
||||
}
|
||||
|
||||
@@ -62,9 +64,9 @@ export function useUpstreamModelsCache() {
|
||||
const rawError = response.data?.error || '获取上游模型失败'
|
||||
return { models: [], error: parseUpstreamModelError(rawError) }
|
||||
}
|
||||
} catch (err: any) {
|
||||
const rawError = err.response?.data?.detail || err.message || '获取上游模型失败'
|
||||
return { models: [], error: parseUpstreamModelError(rawError) }
|
||||
} catch (err: unknown) {
|
||||
const rawError = isAxiosError(err) ? (err.response?.data?.detail ?? err.message) : (err instanceof Error ? err.message : String(err))
|
||||
return { models: [], error: parseUpstreamModelError(rawError || '获取上游模型失败') }
|
||||
} finally {
|
||||
loadingMap.value.set(requestKey, false)
|
||||
pendingRequests.delete(requestKey)
|
||||
|
||||
@@ -371,6 +371,7 @@ import Skeleton from '@/components/ui/skeleton.vue'
|
||||
import { ChevronLeft, ChevronRight, ExternalLink } from 'lucide-vue-next'
|
||||
import { requestTraceApi, type RequestTrace, type CandidateRecord } from '@/api/requestTrace'
|
||||
import { log } from '@/utils/logger'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
|
||||
// 节点组类型
|
||||
@@ -459,8 +460,10 @@ const getFinalStatusLabel = (status: string) => {
|
||||
}
|
||||
|
||||
// 获取最终状态徽章样式
|
||||
const getFinalStatusBadgeVariant = (status: string): any => {
|
||||
const variants: Record<string, string> = {
|
||||
type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'dark'
|
||||
|
||||
const getFinalStatusBadgeVariant = (status: string): BadgeVariant => {
|
||||
const variants: Record<string, BadgeVariant> = {
|
||||
success: 'success',
|
||||
failed: 'destructive',
|
||||
streaming: 'secondary',
|
||||
@@ -493,19 +496,19 @@ const formatSize = (bytes: number): string => {
|
||||
}
|
||||
|
||||
// 代理 timing 分阶段展示
|
||||
const proxyTimingBreakdown = (proxy: Record<string, any>): string => {
|
||||
const t = proxy.timing
|
||||
const proxyTimingBreakdown = (proxy: Record<string, unknown>): string => {
|
||||
const t = proxy.timing as Record<string, number | null | undefined> | undefined
|
||||
if (!t) return ''
|
||||
|
||||
const parts: string[] = []
|
||||
|
||||
// 兼容旧版 timing(含 body_read_ms/decompress_ms)
|
||||
const readDecompress = (t.body_read_ms || 0) + (t.decompress_ms || 0)
|
||||
const readDecompress = ((t.body_read_ms as number) || 0) + ((t.decompress_ms as number) || 0)
|
||||
if (readDecompress > 0) {
|
||||
let label = `读取 ${formatLatency(readDecompress)}`
|
||||
if (t.decompress_ms != null && t.decompress_ms > 0 && t.wire_size != null && t.body_size != null && t.body_size > 0) {
|
||||
const ratio = Math.round((1 - t.wire_size / t.body_size) * 100)
|
||||
label += ` ${formatSize(t.wire_size)}→${formatSize(t.body_size)}`
|
||||
if (t.decompress_ms != null && t.decompress_ms > 0 && t.wire_size != null && t.body_size != null && (t.body_size as number) > 0) {
|
||||
const ratio = Math.round((1 - (t.wire_size as number) / (t.body_size as number)) * 100)
|
||||
label += ` ${formatSize(t.wire_size as number)}→${formatSize(t.body_size as number)}`
|
||||
if (ratio > 0) label += ` -${ratio}%`
|
||||
}
|
||||
parts.push(label)
|
||||
@@ -514,29 +517,29 @@ const proxyTimingBreakdown = (proxy: Record<string, any>): string => {
|
||||
const ttfbMs = t.ttfb_ms ?? t.upstream_ms
|
||||
const processingMs = t.upstream_processing_ms ?? (
|
||||
ttfbMs != null && t.connect_ms != null && t.tls_ms != null
|
||||
? Math.max(0, ttfbMs - t.connect_ms - t.tls_ms)
|
||||
? Math.max(0, (ttfbMs as number) - (t.connect_ms as number) - (t.tls_ms as number))
|
||||
: null
|
||||
)
|
||||
|
||||
if (t.dns_ms != null && t.dns_ms > 0) {
|
||||
parts.push(`DNS ${formatLatency(t.dns_ms)}`)
|
||||
if (t.dns_ms != null && (t.dns_ms as number) > 0) {
|
||||
parts.push(`DNS ${formatLatency(t.dns_ms as number)}`)
|
||||
}
|
||||
if (t.connect_ms != null && t.connect_ms > 0) {
|
||||
parts.push(`连接 ${formatLatency(t.connect_ms)}`)
|
||||
if (t.connect_ms != null && (t.connect_ms as number) > 0) {
|
||||
parts.push(`连接 ${formatLatency(t.connect_ms as number)}`)
|
||||
}
|
||||
if (t.tls_ms != null && t.tls_ms > 0) {
|
||||
parts.push(`TLS ${formatLatency(t.tls_ms)}`)
|
||||
if (t.tls_ms != null && (t.tls_ms as number) > 0) {
|
||||
parts.push(`TLS ${formatLatency(t.tls_ms as number)}`)
|
||||
}
|
||||
if (ttfbMs != null && ttfbMs > 0) {
|
||||
parts.push(`TTFB ${formatLatency(ttfbMs)}`)
|
||||
if (ttfbMs != null && (ttfbMs as number) > 0) {
|
||||
parts.push(`TTFB ${formatLatency(ttfbMs as number)}`)
|
||||
}
|
||||
if (processingMs != null && processingMs > 0) {
|
||||
parts.push(`上游处理 ${formatLatency(Math.round(processingMs))}`)
|
||||
if (processingMs != null && (processingMs as number) > 0) {
|
||||
parts.push(`上游处理 ${formatLatency(Math.round(processingMs as number))}`)
|
||||
}
|
||||
|
||||
// 计算 Aether→代理 之间无法解释的耗时差
|
||||
if (proxy.ttfb_ms != null && t.total_ms != null) {
|
||||
const gap = proxy.ttfb_ms - t.total_ms
|
||||
const gap = (proxy.ttfb_ms as number) - (t.total_ms as number)
|
||||
if (gap > 500) {
|
||||
parts.push(`传输 ${formatLatency(Math.round(gap))}`)
|
||||
}
|
||||
@@ -817,9 +820,9 @@ const loadTrace = async (silent = false) => {
|
||||
|
||||
try {
|
||||
trace.value = await requestTraceApi.getRequestTrace(props.requestId)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (!silent) {
|
||||
error.value = err.response?.data?.detail || err.message || '加载失败'
|
||||
error.value = parseApiError(err, '加载失败')
|
||||
}
|
||||
log.error('加载请求追踪失败:', err)
|
||||
} finally {
|
||||
|
||||
@@ -699,6 +699,7 @@ const autoRefreshing = ref(false)
|
||||
const curlCopying = ref(false)
|
||||
const curlCopied = ref(false)
|
||||
const replayDialogOpen = ref(false)
|
||||
const AUTO_REFRESH_INTERVAL_MS = 1000
|
||||
|
||||
// 监听标签页切换
|
||||
watch(activeTab, (newTab) => {
|
||||
@@ -1010,15 +1011,15 @@ const visibleTabs = computed(() => {
|
||||
return tabs.filter(tab => {
|
||||
switch (tab.name) {
|
||||
case 'request-headers':
|
||||
return hasContent(detail.value!.request_headers)
|
||||
return hasContent(detail.value?.request_headers) || hasContent(detail.value?.provider_request_headers)
|
||||
case 'request-body':
|
||||
return hasContent(detail.value!.request_body) || hasContent(detail.value!.provider_request_body)
|
||||
return hasContent(detail.value?.request_body) || hasContent(detail.value?.provider_request_body)
|
||||
case 'response-headers':
|
||||
return hasContent(detail.value!.response_headers) || hasContent(detail.value!.client_response_headers)
|
||||
return hasContent(detail.value?.response_headers) || hasContent(detail.value?.client_response_headers)
|
||||
case 'response-body':
|
||||
return hasContent(detail.value!.response_body) || hasContent(detail.value!.client_response_body)
|
||||
return hasContent(detail.value?.response_body) || hasContent(detail.value?.client_response_body)
|
||||
case 'metadata':
|
||||
return hasContent(detail.value!.metadata)
|
||||
return hasContent(detail.value?.metadata)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -1078,6 +1079,15 @@ async function loadDetail(id: string, silent = false) {
|
||||
if (silent) {
|
||||
timelineRef.value?.refresh()
|
||||
}
|
||||
|
||||
// 抽屉打开时,对进行中请求自动保持刷新,保证详情实时更新
|
||||
if (props.isOpen) {
|
||||
if (isRequestCompleted()) {
|
||||
stopAutoRefresh()
|
||||
} else {
|
||||
startAutoRefresh()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Failed to load request detail:', err)
|
||||
if (!silent) {
|
||||
@@ -1108,6 +1118,23 @@ function stopAutoRefresh() {
|
||||
autoRefreshing.value = false
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
if (autoRefreshTimer.value || !props.requestId || !props.isOpen) {
|
||||
return
|
||||
}
|
||||
autoRefreshing.value = true
|
||||
autoRefreshTimer.value = setInterval(async () => {
|
||||
if (!props.requestId || !props.isOpen) {
|
||||
stopAutoRefresh()
|
||||
return
|
||||
}
|
||||
await loadDetail(props.requestId, true)
|
||||
if (isRequestCompleted()) {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
}, AUTO_REFRESH_INTERVAL_MS)
|
||||
}
|
||||
|
||||
async function refreshDetail() {
|
||||
if (!props.requestId) return
|
||||
|
||||
@@ -1132,16 +1159,7 @@ async function refreshDetail() {
|
||||
return
|
||||
}
|
||||
|
||||
autoRefreshTimer.value = setInterval(async () => {
|
||||
if (!props.requestId || !props.isOpen) {
|
||||
stopAutoRefresh()
|
||||
return
|
||||
}
|
||||
await loadDetail(props.requestId, true)
|
||||
if (isRequestCompleted()) {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
}, 1000)
|
||||
startAutoRefresh()
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -1300,7 +1318,7 @@ function copyContent(tabName: string) {
|
||||
}
|
||||
} else {
|
||||
// JSON 视图模式:复制原始 JSON
|
||||
let data: any = null
|
||||
let data: unknown = null
|
||||
switch (tabName) {
|
||||
case 'request-headers':
|
||||
data = dataSource.value === 'provider'
|
||||
@@ -1380,8 +1398,8 @@ function openReplayDialog() {
|
||||
interface HeaderEntry {
|
||||
key: string
|
||||
status: 'added' | 'modified' | 'removed' | 'unchanged'
|
||||
originalValue?: any
|
||||
newValue?: any
|
||||
originalValue?: unknown
|
||||
newValue?: unknown
|
||||
}
|
||||
|
||||
const mergedHeaderEntries = computed(() => {
|
||||
|
||||
@@ -17,17 +17,17 @@
|
||||
</Card>
|
||||
<!-- 非 JSON 响应(如 HTML 错误页面) -->
|
||||
<Card
|
||||
v-else-if="data.raw_response && data.metadata?.parse_error"
|
||||
v-else-if="hasParseError"
|
||||
class="bg-muted/30 overflow-hidden"
|
||||
>
|
||||
<div class="p-3 bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-amber-600 dark:text-amber-400 text-sm font-medium">Warning: 响应解析失败</span>
|
||||
<span class="text-xs text-amber-700 dark:text-amber-300">{{ data.metadata.parse_error }}</span>
|
||||
<span class="text-xs text-amber-700 dark:text-amber-300">{{ parseErrorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap text-muted-foreground">{{ data.raw_response }}</pre>
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap text-muted-foreground">{{ rawResponseContent }}</pre>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
@@ -74,12 +74,14 @@
|
||||
:style="{ width: `${line.indent * 16}px` }"
|
||||
/>
|
||||
<!-- 内容 -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
class="line-content"
|
||||
:class="{ 'clickable-collapsed': line.canFold && collapsedBlocks.has(line.blockId) }"
|
||||
@click="line.canFold && collapsedBlocks.has(line.blockId) && toggleFold(line.blockId)"
|
||||
v-html="getDisplayHtml(line)"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -112,14 +114,47 @@ interface DisplayLine extends JsonLine {
|
||||
displayLineNumber: number
|
||||
}
|
||||
|
||||
/** JSON data can be any serializable value: object, array, string, number, boolean, null */
|
||||
type JsonValue = Record<string, unknown> | unknown[] | string | number | boolean | null | undefined
|
||||
|
||||
const props = defineProps<{
|
||||
data: any
|
||||
data: JsonValue
|
||||
viewMode: 'formatted' | 'raw' | 'compare'
|
||||
expandDepth: number
|
||||
isDark: boolean
|
||||
emptyMessage: string
|
||||
}>()
|
||||
|
||||
/** Safely cast data to an object for property access in templates */
|
||||
const dataAsObject = computed(() => {
|
||||
if (props.data && typeof props.data === 'object' && !Array.isArray(props.data)) {
|
||||
return props.data as Record<string, unknown>
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
/** Whether the data contains a raw_response with a parse error (non-JSON response) */
|
||||
const hasParseError = computed(() => {
|
||||
const obj = dataAsObject.value
|
||||
if (!obj) return false
|
||||
const metadata = obj.metadata as Record<string, unknown> | undefined
|
||||
return Boolean(obj.raw_response && metadata?.parse_error)
|
||||
})
|
||||
|
||||
/** Parse error message */
|
||||
const parseErrorMessage = computed(() => {
|
||||
const obj = dataAsObject.value
|
||||
if (!obj) return ''
|
||||
const metadata = obj.metadata as Record<string, unknown> | undefined
|
||||
return String(metadata?.parse_error || '')
|
||||
})
|
||||
|
||||
/** Raw response content */
|
||||
const rawResponseContent = computed(() => {
|
||||
const obj = dataAsObject.value
|
||||
return obj ? String(obj.raw_response || '') : ''
|
||||
})
|
||||
|
||||
const collapsedBlocks = ref<Set<string>>(new Set())
|
||||
const lines = ref<JsonLine[]>([])
|
||||
|
||||
@@ -145,14 +180,14 @@ const escapeHtml = (str: string): string => {
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
const parseJsonToLines = (data: any): JsonLine[] => {
|
||||
const parseJsonToLines = (data: unknown): JsonLine[] => {
|
||||
const result: JsonLine[] = []
|
||||
let lineNumber = 1
|
||||
let blockIdCounter = 0
|
||||
|
||||
const getBlockId = () => `block-${blockIdCounter++}`
|
||||
|
||||
const processValue = (value: any, indent: number, isLast: boolean, keyPrefix: string = ''): void => {
|
||||
const processValue = (value: unknown, indent: number, isLast: boolean, keyPrefix: string = ''): void => {
|
||||
const comma = isLast ? '' : ','
|
||||
|
||||
if (value === null) {
|
||||
@@ -232,7 +267,8 @@ const parseJsonToLines = (data: any): JsonLine[] => {
|
||||
result[startLine].blockEnd = result.length - 1
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
const keys = Object.keys(value)
|
||||
const obj = value as Record<string, unknown>
|
||||
const keys = Object.keys(obj)
|
||||
if (keys.length === 0) {
|
||||
result.push({
|
||||
id: result.length,
|
||||
@@ -259,7 +295,7 @@ const parseJsonToLines = (data: any): JsonLine[] => {
|
||||
|
||||
keys.forEach((key, i) => {
|
||||
const keyHtml = getTokenHtml(`"${escapeHtml(key)}"`, 'key') + getTokenHtml(': ', 'punctuation')
|
||||
processValue(value[key], indent + 1, i === keys.length - 1, keyHtml)
|
||||
processValue(obj[key], indent + 1, i === keys.length - 1, keyHtml)
|
||||
})
|
||||
|
||||
result.push({
|
||||
|
||||
@@ -165,11 +165,11 @@ const props = defineProps<{
|
||||
detail: RequestDetail
|
||||
viewMode: 'compare' | 'formatted' | 'raw'
|
||||
dataSource: 'client' | 'provider'
|
||||
currentHeaderData: any
|
||||
currentHeaderData: Record<string, unknown> | null
|
||||
currentExpandDepth: number
|
||||
hasProviderHeaders: boolean
|
||||
clientHeadersWithDiff: Array<{ key: string; value: any; status: string }>
|
||||
providerHeadersWithDiff: Array<{ key: string; value: any; status: string }>
|
||||
clientHeadersWithDiff: Array<{ key: string; value: unknown; status: string }>
|
||||
providerHeadersWithDiff: Array<{ key: string; value: unknown; status: string }>
|
||||
headerStats: { added: number; modified: number; removed: number; unchanged: number }
|
||||
isDark: boolean
|
||||
}>()
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
} from '../types'
|
||||
import { createDefaultStats } from '../types'
|
||||
import { log } from '@/utils/logger'
|
||||
import { getErrorStatus } from '@/types/api-error'
|
||||
|
||||
export interface UseUsageDataOptions {
|
||||
isAdminPage: Ref<boolean>
|
||||
@@ -81,27 +82,31 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
usageApi.getUsageByApiFormat(dateRange)
|
||||
])
|
||||
|
||||
// statsData may contain additional fields not declared in UsageStats
|
||||
const statsRaw = statsData as Record<string, unknown>
|
||||
stats.value = {
|
||||
total_requests: statsData.total_requests || 0,
|
||||
total_tokens: statsData.total_tokens || 0,
|
||||
total_cost: statsData.total_cost || 0,
|
||||
total_actual_cost: (statsData as any).total_actual_cost,
|
||||
total_actual_cost: statsData.total_actual_cost,
|
||||
avg_response_time: statsData.avg_response_time || 0,
|
||||
error_count: (statsData as any).error_count,
|
||||
error_rate: (statsData as any).error_rate,
|
||||
cache_stats: (statsData as any).cache_stats,
|
||||
error_count: typeof statsRaw.error_count === 'number' ? statsRaw.error_count : undefined,
|
||||
error_rate: typeof statsRaw.error_rate === 'number' ? statsRaw.error_rate : undefined,
|
||||
cache_stats: statsRaw.cache_stats as UsageStatsState['cache_stats'],
|
||||
period_start: '',
|
||||
period_end: '',
|
||||
activity_heatmap: null
|
||||
}
|
||||
|
||||
modelStats.value = modelData.map(item => ({
|
||||
model: item.model,
|
||||
request_count: item.request_count || 0,
|
||||
total_tokens: item.total_tokens || 0,
|
||||
total_cost: item.total_cost || 0,
|
||||
actual_cost: (item as any).actual_cost
|
||||
}))
|
||||
modelStats.value = modelData.map(item => {
|
||||
const raw = item as Record<string, unknown>
|
||||
return {
|
||||
model: item.model,
|
||||
request_count: item.request_count || 0,
|
||||
total_tokens: item.total_tokens || 0,
|
||||
total_cost: item.total_cost || 0,
|
||||
actual_cost: typeof raw.actual_cost === 'number' ? raw.actual_cost : undefined
|
||||
}
|
||||
})
|
||||
|
||||
providerStats.value = providerData.map(item => ({
|
||||
provider: item.provider,
|
||||
@@ -142,10 +147,9 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
avg_response_time: userData.avg_response_time || 0,
|
||||
period_start: '',
|
||||
period_end: '',
|
||||
activity_heatmap: null
|
||||
}
|
||||
|
||||
modelStats.value = (userData.summary_by_model || []).map((item: any) => ({
|
||||
modelStats.value = (userData.summary_by_model || []).map((item) => ({
|
||||
model: item.model,
|
||||
request_count: item.requests || 0,
|
||||
total_tokens: item.total_tokens || 0,
|
||||
@@ -153,13 +157,14 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
actual_cost: item.actual_total_cost_usd
|
||||
}))
|
||||
|
||||
providerStats.value = (userData.summary_by_provider || []).map((item: any) => ({
|
||||
providerStats.value = (userData.summary_by_provider || []).map((item) => ({
|
||||
provider: item.provider,
|
||||
requests: item.requests || 0,
|
||||
totalTokens: 0,
|
||||
totalCost: item.total_cost_usd || 0,
|
||||
successRate: item.success_rate || 0,
|
||||
avgResponseTime: item.avg_response_time_ms > 0
|
||||
? `${(item.avg_response_time_ms / 1000).toFixed(2)}s`
|
||||
avgResponseTime: (item.avg_response_time_ms ?? 0) > 0
|
||||
? `${((item.avg_response_time_ms ?? 0) / 1000).toFixed(2)}s`
|
||||
: '-'
|
||||
}))
|
||||
|
||||
@@ -221,8 +226,8 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
})
|
||||
.sort((a, b) => b.request_count - a.request_count)
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.response?.status !== 403) {
|
||||
} catch (error: unknown) {
|
||||
if (getErrorStatus(error) !== 403) {
|
||||
log.error('加载统计数据失败:', error)
|
||||
}
|
||||
stats.value = createDefaultStats()
|
||||
@@ -244,7 +249,7 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
const offset = (pagination.page - 1) * pagination.pageSize
|
||||
|
||||
// 构建请求参数
|
||||
const params: any = {
|
||||
const params: Record<string, unknown> = {
|
||||
limit: pagination.pageSize,
|
||||
offset,
|
||||
...currentDateRange.value
|
||||
|
||||
@@ -33,6 +33,9 @@ import {
|
||||
createEmptyRenderResult,
|
||||
} from './render'
|
||||
|
||||
/** Raw JSON object from API (loosely typed) */
|
||||
type RawObject = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Claude API 格式解析器
|
||||
*/
|
||||
@@ -43,7 +46,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 检测是否为 Claude 格式
|
||||
*/
|
||||
detect(requestBody: any, responseBody: any, hint?: string): number {
|
||||
detect(requestBody: unknown, responseBody: unknown, hint?: string): number {
|
||||
// 1. 后端提示优先
|
||||
if (hint) {
|
||||
const lowerHint = hint.toLowerCase()
|
||||
@@ -52,37 +55,41 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
if (lowerHint.includes('openai') || lowerHint.includes('gemini')) return 0
|
||||
}
|
||||
|
||||
const req = requestBody as RawObject | null | undefined
|
||||
|
||||
// 2. 检查模型名
|
||||
const model = requestBody?.model?.toLowerCase() || ''
|
||||
const model = (typeof req?.model === 'string' ? req.model : '').toLowerCase()
|
||||
if (model.includes('claude')) return 95
|
||||
|
||||
// 3. 检查请求体结构
|
||||
if (!requestBody?.messages || !Array.isArray(requestBody.messages)) {
|
||||
if (!req?.messages || !Array.isArray(req.messages)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 4. 检查响应体特征
|
||||
const respBody = isStreamResponse(responseBody)
|
||||
? responseBody.chunks?.[0]
|
||||
: responseBody
|
||||
const respBody = (isStreamResponse(responseBody)
|
||||
? (responseBody.chunks?.[0] as RawObject | undefined)
|
||||
: responseBody) as RawObject | null | undefined
|
||||
|
||||
if (respBody) {
|
||||
// Claude 响应特征
|
||||
const respType = typeof respBody.type === 'string' ? respBody.type : ''
|
||||
if (
|
||||
respBody.type === 'message' ||
|
||||
respBody.type?.startsWith('content_block') ||
|
||||
respBody.type?.startsWith('message_')
|
||||
respType === 'message' ||
|
||||
respType.startsWith('content_block') ||
|
||||
respType.startsWith('message_')
|
||||
) {
|
||||
return 90
|
||||
}
|
||||
// 明确是 OpenAI 格式
|
||||
if (respBody.choices || respBody.object?.includes('chat.completion')) {
|
||||
const respObject = typeof respBody.object === 'string' ? respBody.object : ''
|
||||
if (respBody.choices || respObject.includes('chat.completion')) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 检查 Claude 特有的请求字段
|
||||
if (requestBody.system !== undefined) {
|
||||
if (req.system !== undefined) {
|
||||
// system 可以是字符串或数组,这是 Claude 的特征
|
||||
return 70
|
||||
}
|
||||
@@ -94,26 +101,27 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析请求体
|
||||
*/
|
||||
parseRequest(requestBody: any): ParsedConversation {
|
||||
parseRequest(requestBody: unknown): ParsedConversation {
|
||||
if (!requestBody) {
|
||||
return createEmptyConversation('claude', '无请求体')
|
||||
}
|
||||
|
||||
try {
|
||||
const body = requestBody as RawObject
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: requestBody.stream === true,
|
||||
isStream: body.stream === true,
|
||||
apiFormat: 'claude',
|
||||
model: requestBody.model,
|
||||
model: typeof body.model === 'string' ? body.model : undefined,
|
||||
}
|
||||
|
||||
// 提取 system prompt
|
||||
result.system = this.extractSystemPrompt(requestBody.system)
|
||||
result.system = this.extractSystemPrompt(body.system)
|
||||
|
||||
// 提取 messages
|
||||
if (Array.isArray(requestBody.messages)) {
|
||||
for (const msg of requestBody.messages) {
|
||||
const parsedMsg = this.parseMessage(msg)
|
||||
if (Array.isArray(body.messages)) {
|
||||
for (const msg of body.messages) {
|
||||
const parsedMsg = this.parseMessage(msg as RawObject)
|
||||
if (parsedMsg) {
|
||||
result.messages.push(parsedMsg)
|
||||
}
|
||||
@@ -129,22 +137,23 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析响应体
|
||||
*/
|
||||
parseResponse(responseBody: any): ParsedConversation {
|
||||
parseResponse(responseBody: unknown): ParsedConversation {
|
||||
if (!responseBody) {
|
||||
return createEmptyConversation('claude', '无响应体')
|
||||
}
|
||||
|
||||
try {
|
||||
const body = responseBody as RawObject
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: false,
|
||||
apiFormat: 'claude',
|
||||
model: responseBody.model,
|
||||
model: typeof body.model === 'string' ? body.model : undefined,
|
||||
}
|
||||
|
||||
// Claude 响应格式: { type: "message", content: [...] }
|
||||
if (Array.isArray(responseBody.content)) {
|
||||
const contentBlocks = this.parseContentBlocks(responseBody.content, 'assistant')
|
||||
if (Array.isArray(body.content)) {
|
||||
const contentBlocks = this.parseContentBlocks(body.content as RawObject[], 'assistant')
|
||||
if (contentBlocks.length > 0) {
|
||||
result.messages.push(createMessage('assistant', contentBlocks))
|
||||
}
|
||||
@@ -159,7 +168,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析流式响应
|
||||
*/
|
||||
parseStreamResponse(chunks: any[]): ParsedConversation {
|
||||
parseStreamResponse(chunks: unknown[]): ParsedConversation {
|
||||
if (!chunks || chunks.length === 0) {
|
||||
return createEmptyConversation('claude', '无响应数据')
|
||||
}
|
||||
@@ -175,47 +184,49 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
const blocks = new Map<number, {
|
||||
type: ContentBlock['type']
|
||||
parts: string[]
|
||||
metadata?: any
|
||||
metadata?: Record<string, string>
|
||||
}>()
|
||||
|
||||
for (const chunk of chunks) {
|
||||
for (const rawChunk of chunks) {
|
||||
const chunk = rawChunk as RawObject
|
||||
// 提取模型名
|
||||
if (chunk.message?.model && !result.model) {
|
||||
result.model = chunk.message.model
|
||||
const chunkMessage = chunk.message as RawObject | undefined
|
||||
if (typeof chunkMessage?.model === 'string' && !result.model) {
|
||||
result.model = chunkMessage.model
|
||||
}
|
||||
|
||||
if (chunk.type === 'content_block_start') {
|
||||
const index = chunk.index ?? 0
|
||||
const block = chunk.content_block
|
||||
const index = (typeof chunk.index === 'number' ? chunk.index : 0)
|
||||
const block = chunk.content_block as RawObject | undefined
|
||||
if (block?.type === 'text') {
|
||||
blocks.set(index, { type: 'text', parts: [block.text || ''] })
|
||||
blocks.set(index, { type: 'text', parts: [String(block.text || '')] })
|
||||
} else if (block?.type === 'thinking') {
|
||||
blocks.set(index, {
|
||||
type: 'thinking',
|
||||
parts: [block.thinking || ''],
|
||||
metadata: { signature: block.signature },
|
||||
parts: [String(block.thinking || '')],
|
||||
metadata: { signature: String(block.signature || '') },
|
||||
})
|
||||
} else if (block?.type === 'tool_use') {
|
||||
blocks.set(index, {
|
||||
type: 'tool_use',
|
||||
parts: [],
|
||||
metadata: { toolName: block.name, toolId: block.id },
|
||||
metadata: { toolName: String(block.name || ''), toolId: String(block.id || '') },
|
||||
})
|
||||
}
|
||||
} else if (chunk.type === 'content_block_delta') {
|
||||
const index = chunk.index ?? 0
|
||||
const delta = chunk.delta
|
||||
const index = (typeof chunk.index === 'number' ? chunk.index : 0)
|
||||
const delta = chunk.delta as RawObject | undefined
|
||||
const block = blocks.get(index)
|
||||
if (block) {
|
||||
if (delta?.type === 'text_delta') {
|
||||
block.parts.push(delta.text || '')
|
||||
} else if (delta?.type === 'thinking_delta') {
|
||||
block.parts.push(delta.thinking || '')
|
||||
} else if (delta?.type === 'input_json_delta') {
|
||||
block.parts.push(delta.partial_json || '')
|
||||
} else if (delta?.type === 'signature_delta') {
|
||||
if (block && delta) {
|
||||
if (delta.type === 'text_delta') {
|
||||
block.parts.push(String(delta.text || ''))
|
||||
} else if (delta.type === 'thinking_delta') {
|
||||
block.parts.push(String(delta.thinking || ''))
|
||||
} else if (delta.type === 'input_json_delta') {
|
||||
block.parts.push(String(delta.partial_json || ''))
|
||||
} else if (delta.type === 'signature_delta') {
|
||||
block.metadata = block.metadata || {}
|
||||
block.metadata.signature = (block.metadata.signature || '') + (delta.signature || '')
|
||||
block.metadata.signature = (block.metadata.signature || '') + String(delta.signature || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,7 +269,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 提取 system prompt
|
||||
*/
|
||||
private extractSystemPrompt(system: any): string | undefined {
|
||||
private extractSystemPrompt(system: unknown): string | undefined {
|
||||
if (!system) return undefined
|
||||
|
||||
if (typeof system === 'string') {
|
||||
@@ -267,8 +278,8 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
|
||||
if (Array.isArray(system)) {
|
||||
return system
|
||||
.filter((b: any) => b.type === 'text')
|
||||
.map((b: any) => b.text)
|
||||
.filter((b: RawObject) => b.type === 'text')
|
||||
.map((b: RawObject) => String(b.text || ''))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
@@ -278,7 +289,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析单条消息
|
||||
*/
|
||||
private parseMessage(msg: any): ParsedMessage | null {
|
||||
private parseMessage(msg: RawObject): ParsedMessage | null {
|
||||
if (!msg || !msg.role) return null
|
||||
|
||||
const role = msg.role as MessageRole
|
||||
@@ -292,13 +303,13 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析消息内容
|
||||
*/
|
||||
private parseMessageContent(content: any, role: MessageRole): ContentBlock[] {
|
||||
private parseMessageContent(content: unknown, role: MessageRole): ContentBlock[] {
|
||||
if (typeof content === 'string') {
|
||||
return [createTextBlock(content)]
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return this.parseContentBlocks(content, role)
|
||||
return this.parseContentBlocks(content as RawObject[], role)
|
||||
}
|
||||
|
||||
return []
|
||||
@@ -307,7 +318,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析内容块数组
|
||||
*/
|
||||
private parseContentBlocks(blocks: any[], role: MessageRole): ContentBlock[] {
|
||||
private parseContentBlocks(blocks: RawObject[], role: MessageRole): ContentBlock[] {
|
||||
const result: ContentBlock[] = []
|
||||
|
||||
for (const block of blocks) {
|
||||
@@ -323,28 +334,31 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析单个内容块
|
||||
*/
|
||||
private parseContentBlock(block: any, _role: MessageRole): ContentBlock | null {
|
||||
private parseContentBlock(block: RawObject, _role: MessageRole): ContentBlock | null {
|
||||
if (!block || !block.type) return null
|
||||
|
||||
switch (block.type) {
|
||||
case 'text':
|
||||
return createTextBlock(block.text || '')
|
||||
return createTextBlock(String(block.text || ''))
|
||||
|
||||
case 'thinking':
|
||||
return createThinkingBlock(block.thinking || '', block.signature)
|
||||
return createThinkingBlock(
|
||||
String(block.thinking || ''),
|
||||
typeof block.signature === 'string' ? block.signature : undefined
|
||||
)
|
||||
|
||||
case 'tool_use':
|
||||
return createToolUseBlock(
|
||||
block.id || '',
|
||||
block.name || '',
|
||||
block.input || {}
|
||||
String(block.id || ''),
|
||||
String(block.name || ''),
|
||||
(block.input as Record<string, unknown>) || {}
|
||||
)
|
||||
|
||||
case 'tool_result':
|
||||
return createToolResultBlock(
|
||||
block.tool_use_id || '',
|
||||
String(block.tool_use_id || ''),
|
||||
this.parseToolResultContent(block.content),
|
||||
block.is_error
|
||||
block.is_error as boolean | undefined
|
||||
)
|
||||
|
||||
case 'image':
|
||||
@@ -358,23 +372,23 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析图片块
|
||||
*/
|
||||
private parseImageBlock(block: any): ContentBlock | null {
|
||||
const source = block.source
|
||||
private parseImageBlock(block: RawObject): ContentBlock | null {
|
||||
const source = block.source as RawObject | undefined
|
||||
if (!source) {
|
||||
return createImageBlock('base64', { alt: '[图片]' })
|
||||
}
|
||||
|
||||
if (source.type === 'base64') {
|
||||
return createImageBlock('base64', {
|
||||
data: source.data,
|
||||
mimeType: source.media_type,
|
||||
data: typeof source.data === 'string' ? source.data : undefined,
|
||||
mimeType: typeof source.media_type === 'string' ? source.media_type : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
if (source.type === 'url') {
|
||||
return createImageBlock('url', {
|
||||
url: source.url,
|
||||
mimeType: source.media_type,
|
||||
url: typeof source.url === 'string' ? source.url : undefined,
|
||||
mimeType: typeof source.media_type === 'string' ? source.media_type : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -384,16 +398,17 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析工具结果内容
|
||||
*/
|
||||
private parseToolResultContent(content: any): string | ContentBlock[] {
|
||||
private parseToolResultContent(content: unknown): string | ContentBlock[] {
|
||||
if (typeof content === 'string') {
|
||||
return content
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
const blocks: ContentBlock[] = []
|
||||
for (const item of content) {
|
||||
for (const rawItem of content) {
|
||||
const item = rawItem as RawObject
|
||||
if (item.type === 'text') {
|
||||
blocks.push(createTextBlock(item.text || ''))
|
||||
blocks.push(createTextBlock(String(item.text || '')))
|
||||
} else if (item.type === 'image') {
|
||||
const imgBlock = this.parseImageBlock(item)
|
||||
if (imgBlock) blocks.push(imgBlock)
|
||||
@@ -412,17 +427,18 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染请求体
|
||||
*/
|
||||
renderRequest(requestBody: any): RenderResult {
|
||||
renderRequest(requestBody: unknown): RenderResult {
|
||||
if (!requestBody) {
|
||||
return createEmptyRenderResult('无请求体')
|
||||
}
|
||||
|
||||
try {
|
||||
const body = requestBody as RawObject
|
||||
const blocks: RenderBlock[] = []
|
||||
const isStream = requestBody.stream === true
|
||||
const isStream = body.stream === true
|
||||
|
||||
// 渲染 system prompt
|
||||
const system = this.extractSystemPrompt(requestBody.system)
|
||||
const system = this.extractSystemPrompt(body.system)
|
||||
if (system) {
|
||||
blocks.push(createMessageBlock('system', [
|
||||
createTextRenderBlock(system),
|
||||
@@ -430,9 +446,9 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
// 渲染 messages
|
||||
if (Array.isArray(requestBody.messages)) {
|
||||
for (const msg of requestBody.messages) {
|
||||
const msgBlock = this.renderMessage(msg)
|
||||
if (Array.isArray(body.messages)) {
|
||||
for (const msg of body.messages) {
|
||||
const msgBlock = this.renderMessage(msg as RawObject)
|
||||
if (msgBlock) {
|
||||
blocks.push(msgBlock)
|
||||
}
|
||||
@@ -448,7 +464,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染响应体
|
||||
*/
|
||||
renderResponse(responseBody: any): RenderResult {
|
||||
renderResponse(responseBody: unknown): RenderResult {
|
||||
if (!responseBody) {
|
||||
return createEmptyRenderResult('无响应体')
|
||||
}
|
||||
@@ -459,13 +475,15 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
try {
|
||||
const body = responseBody as RawObject
|
||||
const blocks: RenderBlock[] = []
|
||||
|
||||
// Claude 响应格式: { type: "message", content: [...] }
|
||||
if (Array.isArray(responseBody.content)) {
|
||||
const contentBlocks = this.renderContentBlocks(responseBody.content)
|
||||
if (Array.isArray(body.content)) {
|
||||
const rawContent = body.content as RawObject[]
|
||||
const contentBlocks = this.renderContentBlocks(rawContent)
|
||||
if (contentBlocks.length > 0) {
|
||||
const badges = this.getBadgesForContent(responseBody.content)
|
||||
const badges = this.getBadgesForContent(rawContent)
|
||||
blocks.push(createMessageBlock('assistant', contentBlocks, {
|
||||
roleLabel: 'Assistant',
|
||||
badges,
|
||||
@@ -482,7 +500,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染流式响应
|
||||
*/
|
||||
private renderStreamResponse(chunks: any[]): RenderResult {
|
||||
private renderStreamResponse(chunks: unknown[]): RenderResult {
|
||||
if (!chunks || chunks.length === 0) {
|
||||
return createEmptyRenderResult('无响应数据')
|
||||
}
|
||||
@@ -517,7 +535,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染单条消息
|
||||
*/
|
||||
private renderMessage(msg: any): RenderBlock | null {
|
||||
private renderMessage(msg: RawObject): RenderBlock | null {
|
||||
if (!msg || !msg.role) return null
|
||||
|
||||
const role = msg.role as MessageRole
|
||||
@@ -536,13 +554,13 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染消息内容
|
||||
*/
|
||||
private renderMessageContent(content: any): RenderBlock[] {
|
||||
private renderMessageContent(content: unknown): RenderBlock[] {
|
||||
if (typeof content === 'string') {
|
||||
return [createTextRenderBlock(content)]
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return this.renderContentBlocks(content)
|
||||
return this.renderContentBlocks(content as RawObject[])
|
||||
}
|
||||
|
||||
return []
|
||||
@@ -551,7 +569,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染原始内容块数组
|
||||
*/
|
||||
private renderContentBlocks(blocks: any[]): RenderBlock[] {
|
||||
private renderContentBlocks(blocks: RawObject[]): RenderBlock[] {
|
||||
const result: RenderBlock[] = []
|
||||
|
||||
for (const block of blocks) {
|
||||
@@ -567,30 +585,30 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染单个原始内容块
|
||||
*/
|
||||
private renderContentBlock(block: any): RenderBlock | null {
|
||||
private renderContentBlock(block: RawObject): RenderBlock | null {
|
||||
if (!block || !block.type) return null
|
||||
|
||||
switch (block.type) {
|
||||
case 'text':
|
||||
return createTextRenderBlock(block.text || '')
|
||||
return createTextRenderBlock(String(block.text || ''))
|
||||
|
||||
case 'thinking':
|
||||
return createCollapsibleBlock(
|
||||
`思考过程 (${(block.thinking || '').length} 字符)`,
|
||||
[createCodeBlock(block.thinking || '')],
|
||||
`思考过程 (${String(block.thinking || '').length} 字符)`,
|
||||
[createCodeBlock(String(block.thinking || ''))],
|
||||
{ defaultOpen: false, className: 'thinking-block' }
|
||||
)
|
||||
|
||||
case 'tool_use':
|
||||
return createToolUseRenderBlock(
|
||||
block.name || '工具调用',
|
||||
String(block.name || '工具调用'),
|
||||
this.formatJson(block.input),
|
||||
block.id
|
||||
typeof block.id === 'string' ? block.id : undefined
|
||||
)
|
||||
|
||||
case 'tool_result': {
|
||||
const content = this.formatToolResultContent(block.content)
|
||||
return createToolResultRenderBlock(content, block.is_error)
|
||||
return createToolResultRenderBlock(content, block.is_error as boolean | undefined)
|
||||
}
|
||||
|
||||
case 'image':
|
||||
@@ -666,8 +684,8 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染图片块
|
||||
*/
|
||||
private renderImageBlock(block: any): RenderBlock | null {
|
||||
const source = block.source
|
||||
private renderImageBlock(block: RawObject): RenderBlock | null {
|
||||
const source = block.source as RawObject | undefined
|
||||
if (!source) {
|
||||
return createImageRenderBlock({ alt: '[图片]' })
|
||||
}
|
||||
@@ -675,14 +693,14 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
if (source.type === 'base64') {
|
||||
return createImageRenderBlock({
|
||||
src: `data:${source.media_type || 'image/png'};base64,${source.data}`,
|
||||
mimeType: source.media_type,
|
||||
mimeType: typeof source.media_type === 'string' ? source.media_type : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
if (source.type === 'url') {
|
||||
return createImageRenderBlock({
|
||||
src: source.url,
|
||||
mimeType: source.media_type,
|
||||
src: typeof source.url === 'string' ? source.url : undefined,
|
||||
mimeType: typeof source.media_type === 'string' ? source.media_type : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -705,11 +723,11 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 获取原始内容的徽章
|
||||
*/
|
||||
private getBadgesForRawContent(content: any): BadgeRenderBlock[] {
|
||||
private getBadgesForRawContent(content: unknown): BadgeRenderBlock[] {
|
||||
if (!Array.isArray(content)) return []
|
||||
|
||||
const badges: BadgeRenderBlock[] = []
|
||||
const types = new Set(content.map((b: any) => b.type))
|
||||
const types = new Set(content.map((b: RawObject) => b.type))
|
||||
|
||||
if (types.has('thinking')) {
|
||||
badges.push(createBadgeBlock('思考', 'secondary'))
|
||||
@@ -730,7 +748,7 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 获取内容的徽章
|
||||
*/
|
||||
private getBadgesForContent(content: any[]): BadgeRenderBlock[] {
|
||||
private getBadgesForContent(content: RawObject[]): BadgeRenderBlock[] {
|
||||
return this.getBadgesForRawContent(content)
|
||||
}
|
||||
|
||||
@@ -760,10 +778,10 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 格式化 JSON
|
||||
*/
|
||||
private formatJson(input: any): string {
|
||||
private formatJson(input: unknown): string {
|
||||
if (typeof input === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(input)
|
||||
const parsed = JSON.parse(input) as unknown
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return input
|
||||
@@ -775,15 +793,15 @@ export class ClaudeParser implements ApiFormatParser {
|
||||
/**
|
||||
* 格式化工具结果内容
|
||||
*/
|
||||
private formatToolResultContent(content: any): string {
|
||||
private formatToolResultContent(content: unknown): string {
|
||||
if (typeof content === 'string') {
|
||||
return content
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.map((item: any) => {
|
||||
if (item.type === 'text') return item.text
|
||||
.map((item: RawObject) => {
|
||||
if (item.type === 'text') return String(item.text || '')
|
||||
if (item.type === 'image') return '[图片]'
|
||||
return ''
|
||||
})
|
||||
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
createEmptyRenderResult,
|
||||
} from './render'
|
||||
|
||||
/** Raw JSON object from API (loosely typed) */
|
||||
type RawObject = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Gemini API 格式解析器
|
||||
*/
|
||||
@@ -39,7 +42,7 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 检测是否为 Gemini 格式
|
||||
*/
|
||||
detect(requestBody: any, responseBody: any, hint?: string): number {
|
||||
detect(requestBody: unknown, responseBody: unknown, hint?: string): number {
|
||||
// 1. 后端提示优先
|
||||
if (hint) {
|
||||
const lowerHint = hint.toLowerCase()
|
||||
@@ -47,19 +50,21 @@ export class GeminiParser implements ApiFormatParser {
|
||||
if (lowerHint.includes('claude') || lowerHint.includes('openai')) return 0
|
||||
}
|
||||
|
||||
const req = requestBody as RawObject | null | undefined
|
||||
|
||||
// 2. 检查模型名
|
||||
const model = requestBody?.model?.toLowerCase() || ''
|
||||
const model = (typeof req?.model === 'string' ? req.model : '').toLowerCase()
|
||||
if (model.includes('gemini')) return 95
|
||||
|
||||
// 3. Gemini 特有结构: 使用 contents 而非 messages
|
||||
if (requestBody?.contents && Array.isArray(requestBody.contents)) {
|
||||
if (req?.contents && Array.isArray(req.contents)) {
|
||||
return 90
|
||||
}
|
||||
|
||||
// 4. 检查响应体特征
|
||||
const respBody = isStreamResponse(responseBody)
|
||||
? responseBody.chunks?.[0]
|
||||
: responseBody
|
||||
const respBody = (isStreamResponse(responseBody)
|
||||
? (responseBody.chunks?.[0] as RawObject | undefined)
|
||||
: responseBody) as RawObject | null | undefined
|
||||
|
||||
if (respBody?.candidates) {
|
||||
return 85
|
||||
@@ -71,32 +76,33 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析请求体
|
||||
*/
|
||||
parseRequest(requestBody: any): ParsedConversation {
|
||||
parseRequest(requestBody: unknown): ParsedConversation {
|
||||
if (!requestBody) {
|
||||
return createEmptyConversation('gemini', '无请求体')
|
||||
}
|
||||
|
||||
try {
|
||||
const body = requestBody as RawObject
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: false,
|
||||
apiFormat: 'gemini',
|
||||
model: requestBody.model,
|
||||
model: typeof body.model === 'string' ? body.model : undefined,
|
||||
}
|
||||
|
||||
// 提取 system instruction
|
||||
const sysInst = requestBody.system_instruction || requestBody.systemInstruction
|
||||
if (sysInst?.parts) {
|
||||
result.system = sysInst.parts
|
||||
.filter((p: any) => p.text)
|
||||
.map((p: any) => p.text)
|
||||
const sysInst = (body.system_instruction || body.systemInstruction) as RawObject | undefined
|
||||
if (sysInst?.parts && Array.isArray(sysInst.parts)) {
|
||||
result.system = (sysInst.parts as RawObject[])
|
||||
.filter((p: RawObject) => typeof p.text === 'string')
|
||||
.map((p: RawObject) => String(p.text))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
// 提取 contents
|
||||
if (Array.isArray(requestBody.contents)) {
|
||||
for (const content of requestBody.contents) {
|
||||
const parsedMsg = this.parseContent(content)
|
||||
if (Array.isArray(body.contents)) {
|
||||
for (const content of body.contents) {
|
||||
const parsedMsg = this.parseContent(content as RawObject)
|
||||
if (parsedMsg) {
|
||||
result.messages.push(parsedMsg)
|
||||
}
|
||||
@@ -112,12 +118,13 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析响应体
|
||||
*/
|
||||
parseResponse(responseBody: any): ParsedConversation {
|
||||
parseResponse(responseBody: unknown): ParsedConversation {
|
||||
if (!responseBody) {
|
||||
return createEmptyConversation('gemini', '无响应体')
|
||||
}
|
||||
|
||||
try {
|
||||
const body = responseBody as RawObject
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: false,
|
||||
@@ -125,9 +132,11 @@ export class GeminiParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
// Gemini 响应格式: { candidates: [{ content: { parts: [...] } }] }
|
||||
const candidate = responseBody.candidates?.[0]
|
||||
if (candidate?.content?.parts) {
|
||||
const contentBlocks = this.parseParts(candidate.content.parts)
|
||||
const candidates = body.candidates as RawObject[] | undefined
|
||||
const candidate = candidates?.[0] as RawObject | undefined
|
||||
const candidateContent = candidate?.content as RawObject | undefined
|
||||
if (candidateContent?.parts && Array.isArray(candidateContent.parts)) {
|
||||
const contentBlocks = this.parseParts(candidateContent.parts as RawObject[])
|
||||
if (contentBlocks.length > 0) {
|
||||
result.messages.push(createMessage('assistant', contentBlocks))
|
||||
}
|
||||
@@ -142,7 +151,7 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析流式响应
|
||||
*/
|
||||
parseStreamResponse(chunks: any[]): ParsedConversation {
|
||||
parseStreamResponse(chunks: unknown[]): ParsedConversation {
|
||||
if (!chunks || chunks.length === 0) {
|
||||
return createEmptyConversation('gemini', '无响应数据')
|
||||
}
|
||||
@@ -155,16 +164,24 @@ export class GeminiParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
const textParts: string[] = []
|
||||
const toolCalls: { name: string; args: any }[] = []
|
||||
const toolCalls: { name: string; args: Record<string, unknown> }[] = []
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const parts = chunk.candidates?.[0]?.content?.parts
|
||||
for (const rawChunk of chunks) {
|
||||
const chunk = rawChunk as RawObject
|
||||
const candidates = chunk.candidates as RawObject[] | undefined
|
||||
const firstCandidate = candidates?.[0] as RawObject | undefined
|
||||
const candidateContent = firstCandidate?.content as RawObject | undefined
|
||||
const parts = candidateContent?.parts as RawObject[] | undefined
|
||||
if (parts) {
|
||||
for (const part of parts) {
|
||||
if (part.text) {
|
||||
if (typeof part.text === 'string') {
|
||||
textParts.push(part.text)
|
||||
} else if (part.functionCall) {
|
||||
toolCalls.push(part.functionCall)
|
||||
const fc = part.functionCall as RawObject
|
||||
toolCalls.push({
|
||||
name: String(fc.name || ''),
|
||||
args: (fc.args as Record<string, unknown>) || {},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,11 +216,12 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析 content 对象
|
||||
*/
|
||||
private parseContent(content: any): ParsedMessage | null {
|
||||
private parseContent(content: RawObject): ParsedMessage | null {
|
||||
if (!content) return null
|
||||
|
||||
const role = this.mapRole(content.role)
|
||||
const contentBlocks = this.parseParts(content.parts || [])
|
||||
const role = this.mapRole(typeof content.role === 'string' ? content.role : undefined)
|
||||
const parts = Array.isArray(content.parts) ? content.parts as RawObject[] : []
|
||||
const contentBlocks = this.parseParts(parts)
|
||||
|
||||
if (contentBlocks.length === 0) return null
|
||||
|
||||
@@ -213,7 +231,7 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析 parts 数组
|
||||
*/
|
||||
private parseParts(parts: any[]): ContentBlock[] {
|
||||
private parseParts(parts: RawObject[]): ContentBlock[] {
|
||||
const result: ContentBlock[] = []
|
||||
|
||||
for (const part of parts) {
|
||||
@@ -229,36 +247,39 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析单个 part
|
||||
*/
|
||||
private parsePart(part: any): ContentBlock | null {
|
||||
private parsePart(part: RawObject): ContentBlock | null {
|
||||
if (!part) return null
|
||||
|
||||
// 文本
|
||||
if (part.text !== undefined) {
|
||||
return createTextBlock(part.text)
|
||||
return createTextBlock(String(part.text))
|
||||
}
|
||||
|
||||
// 内联数据(图片等)
|
||||
if (part.inlineData) {
|
||||
const inlineData = part.inlineData as RawObject
|
||||
return createImageBlock('base64', {
|
||||
data: part.inlineData.data,
|
||||
mimeType: part.inlineData.mimeType,
|
||||
data: typeof inlineData.data === 'string' ? inlineData.data : undefined,
|
||||
mimeType: typeof inlineData.mimeType === 'string' ? inlineData.mimeType : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// 函数调用
|
||||
if (part.functionCall) {
|
||||
const fc = part.functionCall as RawObject
|
||||
return createToolUseBlock(
|
||||
'',
|
||||
part.functionCall.name || '',
|
||||
part.functionCall.args || {}
|
||||
String(fc.name || ''),
|
||||
(fc.args as Record<string, unknown>) || {}
|
||||
)
|
||||
}
|
||||
|
||||
// 函数响应
|
||||
if (part.functionResponse) {
|
||||
const fr = part.functionResponse as RawObject
|
||||
return createToolResultBlock(
|
||||
'', // Gemini 用 name 关联
|
||||
JSON.stringify(part.functionResponse.response, null, 2)
|
||||
JSON.stringify(fr.response, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -286,20 +307,21 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染请求体
|
||||
*/
|
||||
renderRequest(requestBody: any): RenderResult {
|
||||
renderRequest(requestBody: unknown): RenderResult {
|
||||
if (!requestBody) {
|
||||
return createEmptyRenderResult('无请求体')
|
||||
}
|
||||
|
||||
try {
|
||||
const body = requestBody as RawObject
|
||||
const blocks: RenderBlock[] = []
|
||||
|
||||
// 渲染 system instruction
|
||||
const sysInst = requestBody.system_instruction || requestBody.systemInstruction
|
||||
if (sysInst?.parts) {
|
||||
const systemText = sysInst.parts
|
||||
.filter((p: any) => p.text)
|
||||
.map((p: any) => p.text)
|
||||
const sysInst = (body.system_instruction || body.systemInstruction) as RawObject | undefined
|
||||
if (sysInst?.parts && Array.isArray(sysInst.parts)) {
|
||||
const systemText = (sysInst.parts as RawObject[])
|
||||
.filter((p: RawObject) => typeof p.text === 'string')
|
||||
.map((p: RawObject) => String(p.text))
|
||||
.join('\n')
|
||||
if (systemText) {
|
||||
blocks.push(createMessageBlock('system', [
|
||||
@@ -309,9 +331,9 @@ export class GeminiParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
// 渲染 contents
|
||||
if (Array.isArray(requestBody.contents)) {
|
||||
for (const content of requestBody.contents) {
|
||||
const msgBlock = this.renderContent(content)
|
||||
if (Array.isArray(body.contents)) {
|
||||
for (const content of body.contents) {
|
||||
const msgBlock = this.renderContent(content as RawObject)
|
||||
if (msgBlock) {
|
||||
blocks.push(msgBlock)
|
||||
}
|
||||
@@ -327,7 +349,7 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染响应体
|
||||
*/
|
||||
renderResponse(responseBody: any): RenderResult {
|
||||
renderResponse(responseBody: unknown): RenderResult {
|
||||
if (!responseBody) {
|
||||
return createEmptyRenderResult('无响应体')
|
||||
}
|
||||
@@ -338,14 +360,18 @@ export class GeminiParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
try {
|
||||
const body = responseBody as RawObject
|
||||
const blocks: RenderBlock[] = []
|
||||
|
||||
// Gemini 响应格式: { candidates: [{ content: { parts: [...] } }] }
|
||||
const candidate = responseBody.candidates?.[0]
|
||||
if (candidate?.content?.parts) {
|
||||
const contentBlocks = this.renderParts(candidate.content.parts)
|
||||
const candidates = body.candidates as RawObject[] | undefined
|
||||
const candidate = candidates?.[0] as RawObject | undefined
|
||||
const candidateContent = candidate?.content as RawObject | undefined
|
||||
if (candidateContent?.parts && Array.isArray(candidateContent.parts)) {
|
||||
const parts = candidateContent.parts as RawObject[]
|
||||
const contentBlocks = this.renderParts(parts)
|
||||
if (contentBlocks.length > 0) {
|
||||
const badges = this.getBadgesForParts(candidate.content.parts)
|
||||
const badges = this.getBadgesForParts(parts)
|
||||
blocks.push(createMessageBlock('assistant', contentBlocks, {
|
||||
roleLabel: 'Assistant',
|
||||
badges: badges.length > 0 ? badges : undefined,
|
||||
@@ -362,7 +388,7 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染流式响应
|
||||
*/
|
||||
private renderStreamResponse(chunks: any[]): RenderResult {
|
||||
private renderStreamResponse(chunks: unknown[]): RenderResult {
|
||||
if (!chunks || chunks.length === 0) {
|
||||
return createEmptyRenderResult('无响应数据')
|
||||
}
|
||||
@@ -397,15 +423,16 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染 content 对象
|
||||
*/
|
||||
private renderContent(content: any): RenderBlock | null {
|
||||
private renderContent(content: RawObject): RenderBlock | null {
|
||||
if (!content) return null
|
||||
|
||||
const role = this.mapRole(content.role)
|
||||
const contentBlocks = this.renderParts(content.parts || [])
|
||||
const role = this.mapRole(typeof content.role === 'string' ? content.role : undefined)
|
||||
const parts = Array.isArray(content.parts) ? content.parts as RawObject[] : []
|
||||
const contentBlocks = this.renderParts(parts)
|
||||
|
||||
if (contentBlocks.length === 0) return null
|
||||
|
||||
const badges = this.getBadgesForParts(content.parts || [])
|
||||
const badges = this.getBadgesForParts(parts)
|
||||
|
||||
return createMessageBlock(role, contentBlocks, {
|
||||
roleLabel: this.getRoleLabel(role),
|
||||
@@ -416,7 +443,7 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染 parts 数组
|
||||
*/
|
||||
private renderParts(parts: any[]): RenderBlock[] {
|
||||
private renderParts(parts: RawObject[]): RenderBlock[] {
|
||||
const result: RenderBlock[] = []
|
||||
|
||||
for (const part of parts) {
|
||||
@@ -432,34 +459,37 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染单个 part
|
||||
*/
|
||||
private renderPart(part: any): RenderBlock | null {
|
||||
private renderPart(part: RawObject): RenderBlock | null {
|
||||
if (!part) return null
|
||||
|
||||
// 文本
|
||||
if (part.text !== undefined) {
|
||||
return createTextRenderBlock(part.text)
|
||||
return createTextRenderBlock(String(part.text))
|
||||
}
|
||||
|
||||
// 内联数据(图片等)
|
||||
if (part.inlineData) {
|
||||
const inlineData = part.inlineData as RawObject
|
||||
return createImageRenderBlock({
|
||||
src: `data:${part.inlineData.mimeType || 'image/png'};base64,${part.inlineData.data}`,
|
||||
mimeType: part.inlineData.mimeType,
|
||||
src: `data:${inlineData.mimeType || 'image/png'};base64,${inlineData.data}`,
|
||||
mimeType: typeof inlineData.mimeType === 'string' ? inlineData.mimeType : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// 函数调用
|
||||
if (part.functionCall) {
|
||||
const fc = part.functionCall as RawObject
|
||||
return createToolUseRenderBlock(
|
||||
part.functionCall.name || '函数调用',
|
||||
this.formatJson(part.functionCall.args)
|
||||
String(fc.name || '函数调用'),
|
||||
this.formatJson(fc.args)
|
||||
)
|
||||
}
|
||||
|
||||
// 函数响应
|
||||
if (part.functionResponse) {
|
||||
const fr = part.functionResponse as RawObject
|
||||
return createToolResultRenderBlock(
|
||||
this.formatJson(part.functionResponse.response)
|
||||
this.formatJson(fr.response)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -533,11 +563,11 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 获取 parts 的徽章
|
||||
*/
|
||||
private getBadgesForParts(parts: any[]): BadgeRenderBlock[] {
|
||||
private getBadgesForParts(parts: RawObject[]): BadgeRenderBlock[] {
|
||||
const badges: BadgeRenderBlock[] = []
|
||||
const hasImage = parts.some((p: any) => p.inlineData)
|
||||
const hasToolCall = parts.some((p: any) => p.functionCall)
|
||||
const hasToolResult = parts.some((p: any) => p.functionResponse)
|
||||
const hasImage = parts.some((p: RawObject) => p.inlineData)
|
||||
const hasToolCall = parts.some((p: RawObject) => p.functionCall)
|
||||
const hasToolResult = parts.some((p: RawObject) => p.functionResponse)
|
||||
|
||||
if (hasToolCall) {
|
||||
badges.push(createBadgeBlock('函数调用', 'outline'))
|
||||
@@ -575,10 +605,10 @@ export class GeminiParser implements ApiFormatParser {
|
||||
/**
|
||||
* 格式化 JSON
|
||||
*/
|
||||
private formatJson(input: any): string {
|
||||
private formatJson(input: unknown): string {
|
||||
if (typeof input === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(input)
|
||||
const parsed = JSON.parse(input) as unknown
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return input
|
||||
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
createEmptyRenderResult,
|
||||
} from './render'
|
||||
|
||||
/** Raw JSON object from API (loosely typed) */
|
||||
type RawObject = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* OpenAI API 格式解析器
|
||||
*/
|
||||
@@ -39,7 +42,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 检测是否为 OpenAI 格式(包括 Chat Completions 和 CLI/Responses API)
|
||||
*/
|
||||
detect(requestBody: any, responseBody: any, hint?: string): number {
|
||||
detect(requestBody: unknown, responseBody: unknown, hint?: string): number {
|
||||
// 1. 后端提示优先
|
||||
if (hint) {
|
||||
const lowerHint = hint.toLowerCase()
|
||||
@@ -47,24 +50,26 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
if (lowerHint.includes('claude') || lowerHint.includes('gemini')) return 0
|
||||
}
|
||||
|
||||
const req = requestBody as RawObject | null | undefined
|
||||
|
||||
// 2. 检查模型名
|
||||
const model = requestBody?.model?.toLowerCase() || ''
|
||||
const model = (typeof req?.model === 'string' ? req.model : '').toLowerCase()
|
||||
if (model.includes('gpt') || model.includes('o1') || model.includes('o3')) return 95
|
||||
|
||||
// 3. 检查请求体结构
|
||||
// OpenAI CLI (Responses API) 使用 input 字段
|
||||
const isCliFormat = requestBody?.input !== undefined || requestBody?.instructions !== undefined
|
||||
const isCliFormat = req?.input !== undefined || req?.instructions !== undefined
|
||||
// OpenAI Chat Completions 使用 messages 数组
|
||||
const isChatFormat = requestBody?.messages && Array.isArray(requestBody.messages)
|
||||
const isChatFormat = req?.messages && Array.isArray(req.messages)
|
||||
|
||||
if (!isCliFormat && !isChatFormat) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 4. 检查响应体特征
|
||||
const respBody = isStreamResponse(responseBody)
|
||||
? responseBody.chunks?.[0]
|
||||
: responseBody
|
||||
const respBody = (isStreamResponse(responseBody)
|
||||
? (responseBody.chunks?.[0] as RawObject | undefined)
|
||||
: responseBody) as RawObject | null | undefined
|
||||
|
||||
if (respBody) {
|
||||
// OpenAI CLI 响应特征: type 字段为 response.* 格式
|
||||
@@ -72,11 +77,13 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
return 95
|
||||
}
|
||||
// OpenAI Chat Completions 响应特征: choices 数组
|
||||
if (respBody.choices || respBody.object?.includes('chat.completion')) {
|
||||
const respObject = typeof respBody.object === 'string' ? respBody.object : ''
|
||||
if (respBody.choices || respObject.includes('chat.completion')) {
|
||||
return 90
|
||||
}
|
||||
// 明确是 Claude 格式
|
||||
if (respBody.type === 'message' || respBody.type?.startsWith('content_block')) {
|
||||
const respType = typeof respBody.type === 'string' ? respBody.type : ''
|
||||
if (respType === 'message' || respType.startsWith('content_block')) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -87,8 +94,9 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
// OpenAI 的 system 是在 messages 数组中作为 role: system
|
||||
const hasSystemInMessages = requestBody.messages?.some(
|
||||
(m: any) => m.role === 'system'
|
||||
const messages = req?.messages as RawObject[] | undefined
|
||||
const hasSystemInMessages = messages?.some(
|
||||
(m: RawObject) => m.role === 'system'
|
||||
)
|
||||
if (hasSystemInMessages) {
|
||||
return 60
|
||||
@@ -100,7 +108,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 检查是否为 OpenAI CLI (Responses API) 的响应事件
|
||||
*/
|
||||
private isCliResponseEvent(chunk: any): boolean {
|
||||
private isCliResponseEvent(chunk: RawObject | null | undefined): boolean {
|
||||
const type = chunk?.type
|
||||
if (typeof type !== 'string') return false
|
||||
return type.startsWith('response.') || chunk?.object === 'response'
|
||||
@@ -109,35 +117,38 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析请求体(支持 Chat Completions 和 CLI/Responses API 格式)
|
||||
*/
|
||||
parseRequest(requestBody: any): ParsedConversation {
|
||||
parseRequest(requestBody: unknown): ParsedConversation {
|
||||
if (!requestBody) {
|
||||
return createEmptyConversation('openai', '无请求体')
|
||||
}
|
||||
|
||||
const body = requestBody as RawObject
|
||||
|
||||
// 检测是否为 CLI 格式
|
||||
const isCliFormat = requestBody.input !== undefined || requestBody.instructions !== undefined
|
||||
const isCliFormat = body.input !== undefined || body.instructions !== undefined
|
||||
|
||||
if (isCliFormat) {
|
||||
return this.parseCliRequest(requestBody)
|
||||
return this.parseCliRequest(body)
|
||||
}
|
||||
|
||||
return this.parseChatRequest(requestBody)
|
||||
return this.parseChatRequest(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 OpenAI Chat Completions 请求
|
||||
*/
|
||||
private parseChatRequest(requestBody: any): ParsedConversation {
|
||||
private parseChatRequest(requestBody: RawObject): ParsedConversation {
|
||||
try {
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: requestBody.stream === true,
|
||||
apiFormat: 'openai',
|
||||
model: requestBody.model,
|
||||
model: typeof requestBody.model === 'string' ? requestBody.model : undefined,
|
||||
}
|
||||
|
||||
if (Array.isArray(requestBody.messages)) {
|
||||
for (const msg of requestBody.messages) {
|
||||
for (const rawMsg of requestBody.messages) {
|
||||
const msg = rawMsg as RawObject
|
||||
// OpenAI 的 system 消息在 messages 数组中
|
||||
if (msg.role === 'system') {
|
||||
const systemText = typeof msg.content === 'string'
|
||||
@@ -169,17 +180,17 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
* - 使用 input 字段(可以是字符串、消息数组或对象)
|
||||
* - 使用 instructions 字段作为系统指令
|
||||
*/
|
||||
private parseCliRequest(requestBody: any): ParsedConversation {
|
||||
private parseCliRequest(requestBody: RawObject): ParsedConversation {
|
||||
try {
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: requestBody.stream === true,
|
||||
apiFormat: 'openai',
|
||||
model: requestBody.model,
|
||||
model: typeof requestBody.model === 'string' ? requestBody.model : undefined,
|
||||
}
|
||||
|
||||
// 处理 instructions(系统指令)
|
||||
if (requestBody.instructions) {
|
||||
if (typeof requestBody.instructions === 'string') {
|
||||
result.system = requestBody.instructions
|
||||
}
|
||||
|
||||
@@ -192,17 +203,20 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
} else if (Array.isArray(input)) {
|
||||
// 消息数组
|
||||
for (const item of input) {
|
||||
const parsedMsg = this.parseCliInputItem(item)
|
||||
const parsedMsg = this.parseCliInputItem(item as RawObject)
|
||||
if (parsedMsg) {
|
||||
result.messages.push(parsedMsg)
|
||||
}
|
||||
}
|
||||
} else if (input?.messages && Array.isArray(input.messages)) {
|
||||
} else if (input && typeof input === 'object') {
|
||||
const inputObj = input as RawObject
|
||||
// 包装在对象中的消息数组
|
||||
for (const item of input.messages) {
|
||||
const parsedMsg = this.parseCliInputItem(item)
|
||||
if (parsedMsg) {
|
||||
result.messages.push(parsedMsg)
|
||||
if (Array.isArray(inputObj.messages)) {
|
||||
for (const item of inputObj.messages) {
|
||||
const parsedMsg = this.parseCliInputItem(item as RawObject)
|
||||
if (parsedMsg) {
|
||||
result.messages.push(parsedMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,23 +230,24 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析 CLI 格式的单个输入项
|
||||
*/
|
||||
private parseCliInputItem(item: any): ParsedMessage | null {
|
||||
private parseCliInputItem(item: RawObject): ParsedMessage | null {
|
||||
if (!item) return null
|
||||
|
||||
const itemType = item.type
|
||||
|
||||
// 标准消息(有 role 字段)
|
||||
if (itemType === 'message' || item.role) {
|
||||
const role = this.mapRole(item.role)
|
||||
const role = this.mapRole(String(item.role || ''))
|
||||
const contentBlocks: ContentBlock[] = []
|
||||
|
||||
const content = item.content
|
||||
if (typeof content === 'string') {
|
||||
contentBlocks.push(createTextBlock(content))
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const part of content) {
|
||||
for (const rawPart of content) {
|
||||
const part = rawPart as RawObject
|
||||
if (part.type === 'input_text' || part.type === 'output_text' || part.type === 'text') {
|
||||
contentBlocks.push(createTextBlock(part.text || ''))
|
||||
contentBlocks.push(createTextBlock(String(part.text || '')))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,15 +258,15 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
|
||||
// function_call -> 工具调用
|
||||
if (itemType === 'function_call') {
|
||||
const toolId = item.call_id || item.id || ''
|
||||
const toolName = item.name || ''
|
||||
const args = item.arguments || '{}'
|
||||
const toolId = String(item.call_id || item.id || '')
|
||||
const toolName = String(item.name || '')
|
||||
const args = String(item.arguments || '{}')
|
||||
return createMessage('assistant', [createToolUseBlock(toolId, toolName, args)])
|
||||
}
|
||||
|
||||
// function_call_output -> 工具结果
|
||||
if (itemType === 'function_call_output') {
|
||||
const toolUseId = item.call_id || item.id || ''
|
||||
const toolUseId = String(item.call_id || item.id || '')
|
||||
const output = typeof item.output === 'string'
|
||||
? item.output
|
||||
: JSON.stringify(item.output, null, 2)
|
||||
@@ -264,52 +279,58 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析响应体(支持 Chat Completions 和 CLI/Responses API 格式)
|
||||
*/
|
||||
parseResponse(responseBody: any): ParsedConversation {
|
||||
parseResponse(responseBody: unknown): ParsedConversation {
|
||||
if (!responseBody) {
|
||||
return createEmptyConversation('openai', '无响应体')
|
||||
}
|
||||
|
||||
const body = responseBody as RawObject
|
||||
|
||||
// 检测是否为 CLI 格式
|
||||
const isCliFormat = this.isCliResponseEvent(responseBody) ||
|
||||
responseBody.object === 'response' ||
|
||||
responseBody.output !== undefined
|
||||
const isCliFormat = this.isCliResponseEvent(body) ||
|
||||
body.object === 'response' ||
|
||||
body.output !== undefined
|
||||
|
||||
if (isCliFormat) {
|
||||
return this.parseCliResponse(responseBody)
|
||||
return this.parseCliResponse(body)
|
||||
}
|
||||
|
||||
return this.parseChatResponse(responseBody)
|
||||
return this.parseChatResponse(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 OpenAI Chat Completions 响应
|
||||
*/
|
||||
private parseChatResponse(responseBody: any): ParsedConversation {
|
||||
private parseChatResponse(responseBody: RawObject): ParsedConversation {
|
||||
try {
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: false,
|
||||
apiFormat: 'openai',
|
||||
model: responseBody.model,
|
||||
model: typeof responseBody.model === 'string' ? responseBody.model : undefined,
|
||||
}
|
||||
|
||||
// OpenAI 响应格式: { choices: [{ message: { role, content, tool_calls } }] }
|
||||
const message = responseBody.choices?.[0]?.message
|
||||
const choices = responseBody.choices as RawObject[] | undefined
|
||||
const firstChoice = choices?.[0] as RawObject | undefined
|
||||
const message = firstChoice?.message as RawObject | undefined
|
||||
if (message) {
|
||||
const contentBlocks: ContentBlock[] = []
|
||||
|
||||
// 文本内容
|
||||
if (message.content) {
|
||||
if (typeof message.content === 'string') {
|
||||
contentBlocks.push(createTextBlock(message.content))
|
||||
}
|
||||
|
||||
// 工具调用
|
||||
if (message.tool_calls) {
|
||||
for (const call of message.tool_calls) {
|
||||
if (Array.isArray(message.tool_calls)) {
|
||||
for (const rawCall of message.tool_calls) {
|
||||
const call = rawCall as RawObject
|
||||
const fn = call.function as RawObject | undefined
|
||||
contentBlocks.push(createToolUseBlock(
|
||||
call.id || '',
|
||||
call.function?.name || '',
|
||||
call.function?.arguments || '{}'
|
||||
String(call.id || ''),
|
||||
String(fn?.name || ''),
|
||||
String(fn?.arguments || '{}')
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -330,13 +351,13 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
*
|
||||
* CLI 响应格式: { output: [{ type: "message", content: [...] }] }
|
||||
*/
|
||||
private parseCliResponse(responseBody: any): ParsedConversation {
|
||||
private parseCliResponse(responseBody: RawObject): ParsedConversation {
|
||||
try {
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
isStream: false,
|
||||
apiFormat: 'openai',
|
||||
model: responseBody.model,
|
||||
model: typeof responseBody.model === 'string' ? responseBody.model : undefined,
|
||||
}
|
||||
|
||||
const output = responseBody.output
|
||||
@@ -344,13 +365,15 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
return result
|
||||
}
|
||||
|
||||
for (const item of output) {
|
||||
for (const rawItem of output) {
|
||||
const item = rawItem as RawObject
|
||||
if (item?.type === 'message') {
|
||||
const contentBlocks: ContentBlock[] = []
|
||||
|
||||
if (Array.isArray(item.content)) {
|
||||
for (const content of item.content) {
|
||||
if (content?.type === 'output_text' && content?.text) {
|
||||
for (const rawContent of item.content) {
|
||||
const content = rawContent as RawObject
|
||||
if (content?.type === 'output_text' && typeof content?.text === 'string') {
|
||||
contentBlocks.push(createTextBlock(content.text))
|
||||
}
|
||||
}
|
||||
@@ -371,13 +394,13 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析流式响应(支持 Chat Completions 和 CLI/Responses API 格式)
|
||||
*/
|
||||
parseStreamResponse(chunks: any[]): ParsedConversation {
|
||||
parseStreamResponse(chunks: unknown[]): ParsedConversation {
|
||||
if (!chunks || chunks.length === 0) {
|
||||
return createEmptyConversation('openai', '无响应数据')
|
||||
}
|
||||
|
||||
// 检测是否为 CLI 格式
|
||||
const isCliFormat = chunks.some(chunk => this.isCliResponseEvent(chunk))
|
||||
const isCliFormat = chunks.some(chunk => this.isCliResponseEvent(chunk as RawObject))
|
||||
|
||||
if (isCliFormat) {
|
||||
return this.parseCliStreamResponse(chunks)
|
||||
@@ -389,7 +412,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析 OpenAI Chat Completions 流式响应
|
||||
*/
|
||||
private parseChatStreamResponse(chunks: any[]): ParsedConversation {
|
||||
private parseChatStreamResponse(chunks: unknown[]): ParsedConversation {
|
||||
try {
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
@@ -400,35 +423,42 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
const textParts: string[] = []
|
||||
const toolCalls = new Map<number, { name: string; id: string; args: string[] }>()
|
||||
|
||||
for (const chunk of chunks) {
|
||||
for (const rawChunk of chunks) {
|
||||
const chunk = rawChunk as RawObject
|
||||
// 提取模型名
|
||||
if (chunk.model && !result.model) {
|
||||
if (typeof chunk.model === 'string' && !result.model) {
|
||||
result.model = chunk.model
|
||||
}
|
||||
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
const choices = chunk.choices as RawObject[] | undefined
|
||||
const firstChoice = choices?.[0] as RawObject | undefined
|
||||
const delta = firstChoice?.delta as RawObject | undefined
|
||||
if (typeof delta?.content === 'string') {
|
||||
textParts.push(delta.content)
|
||||
}
|
||||
if (delta?.tool_calls) {
|
||||
for (const call of delta.tool_calls) {
|
||||
const index = call.index ?? 0
|
||||
if (Array.isArray(delta?.tool_calls)) {
|
||||
for (const rawCall of delta.tool_calls as unknown[]) {
|
||||
const call = rawCall as RawObject
|
||||
const fn = call.function as RawObject | undefined
|
||||
const index = (typeof call.index === 'number' ? call.index : 0)
|
||||
if (!toolCalls.has(index)) {
|
||||
toolCalls.set(index, {
|
||||
name: call.function?.name || '',
|
||||
id: call.id || '',
|
||||
name: String(fn?.name || ''),
|
||||
id: String(call.id || ''),
|
||||
args: [],
|
||||
})
|
||||
}
|
||||
const existing = toolCalls.get(index)!
|
||||
if (call.function?.name) {
|
||||
existing.name = call.function.name
|
||||
}
|
||||
if (call.id) {
|
||||
existing.id = call.id
|
||||
}
|
||||
if (call.function?.arguments) {
|
||||
existing.args.push(call.function.arguments)
|
||||
const existing = toolCalls.get(index)
|
||||
if (existing) {
|
||||
if (typeof fn?.name === 'string') {
|
||||
existing.name = fn.name
|
||||
}
|
||||
if (typeof call.id === 'string') {
|
||||
existing.id = call.id
|
||||
}
|
||||
if (typeof fn?.arguments === 'string') {
|
||||
existing.args.push(fn.arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -469,7 +499,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
* - response.completed: 响应完成(包含完整响应和 usage)
|
||||
* - response.function_call_arguments.delta: 函数调用参数增量
|
||||
*/
|
||||
private parseCliStreamResponse(chunks: any[]): ParsedConversation {
|
||||
private parseCliStreamResponse(chunks: unknown[]): ParsedConversation {
|
||||
try {
|
||||
const result: ParsedConversation = {
|
||||
messages: [],
|
||||
@@ -482,13 +512,14 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
let currentToolId = ''
|
||||
let currentToolName = ''
|
||||
|
||||
for (const chunk of chunks) {
|
||||
for (const rawChunk of chunks) {
|
||||
const chunk = rawChunk as RawObject
|
||||
const eventType = chunk.type
|
||||
|
||||
// 从 response.created 或 response.completed 提取模型名
|
||||
if (!result.model) {
|
||||
const response = chunk.response
|
||||
if (response?.model) {
|
||||
const response = chunk.response as RawObject | undefined
|
||||
if (typeof response?.model === 'string') {
|
||||
result.model = response.model
|
||||
}
|
||||
}
|
||||
@@ -498,18 +529,21 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
const delta = chunk.delta
|
||||
if (typeof delta === 'string') {
|
||||
textParts.push(delta)
|
||||
} else if (delta?.text) {
|
||||
textParts.push(delta.text)
|
||||
} else if (delta && typeof delta === 'object') {
|
||||
const deltaObj = delta as RawObject
|
||||
if (typeof deltaObj.text === 'string') {
|
||||
textParts.push(deltaObj.text)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理函数调用输出项添加: response.output_item.added
|
||||
if (eventType === 'response.output_item.added') {
|
||||
const item = chunk.item
|
||||
const item = chunk.item as RawObject | undefined
|
||||
if (item?.type === 'function_call') {
|
||||
currentToolId = item.call_id || item.id || ''
|
||||
currentToolName = item.name || ''
|
||||
currentToolId = String(item.call_id || item.id || '')
|
||||
currentToolName = String(item.name || '')
|
||||
if (currentToolId && !toolCalls.has(currentToolId)) {
|
||||
toolCalls.set(currentToolId, {
|
||||
name: currentToolName,
|
||||
@@ -524,8 +558,8 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
// 处理函数调用参数增量: response.function_call_arguments.delta
|
||||
if (eventType === 'response.function_call_arguments.delta') {
|
||||
const delta = chunk.delta
|
||||
if (delta && currentToolId && toolCalls.has(currentToolId)) {
|
||||
toolCalls.get(currentToolId)!.args.push(delta)
|
||||
if (typeof delta === 'string' && currentToolId && toolCalls.has(currentToolId)) {
|
||||
toolCalls.get(currentToolId)?.args.push(delta)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -533,17 +567,19 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
// 处理完成事件: response.completed
|
||||
// 如果之前没有收集到文本,从完成事件中提取
|
||||
if (eventType === 'response.completed') {
|
||||
const response = chunk.response
|
||||
if (response?.model && !result.model) {
|
||||
const response = chunk.response as RawObject | undefined
|
||||
if (typeof response?.model === 'string' && !result.model) {
|
||||
result.model = response.model
|
||||
}
|
||||
|
||||
// 从 output 中提取文本(备用方案)
|
||||
if (textParts.length === 0 && response?.output) {
|
||||
for (const item of response.output) {
|
||||
if (item?.type === 'message' && item?.content) {
|
||||
for (const content of item.content) {
|
||||
if (content?.type === 'output_text' && content?.text) {
|
||||
if (textParts.length === 0 && Array.isArray(response?.output)) {
|
||||
for (const rawItem of response.output as unknown[]) {
|
||||
const item = rawItem as RawObject
|
||||
if (item?.type === 'message' && Array.isArray(item?.content)) {
|
||||
for (const rawContent of item.content as unknown[]) {
|
||||
const content = rawContent as RawObject
|
||||
if (content?.type === 'output_text' && typeof content?.text === 'string') {
|
||||
textParts.push(content.text)
|
||||
}
|
||||
}
|
||||
@@ -583,10 +619,10 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 解析单条消息
|
||||
*/
|
||||
private parseMessage(msg: any): ParsedMessage | null {
|
||||
private parseMessage(msg: RawObject): ParsedMessage | null {
|
||||
if (!msg || !msg.role) return null
|
||||
|
||||
const role = this.mapRole(msg.role)
|
||||
const role = this.mapRole(String(msg.role))
|
||||
const contentBlocks: ContentBlock[] = []
|
||||
|
||||
// 文本内容
|
||||
@@ -594,12 +630,14 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
contentBlocks.push(createTextBlock(msg.content))
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
// Vision API 格式
|
||||
for (const part of msg.content) {
|
||||
for (const rawPart of msg.content) {
|
||||
const part = rawPart as RawObject
|
||||
if (part.type === 'text') {
|
||||
contentBlocks.push(createTextBlock(part.text || ''))
|
||||
contentBlocks.push(createTextBlock(String(part.text || '')))
|
||||
} else if (part.type === 'image_url') {
|
||||
const imageUrl = part.image_url as RawObject | undefined
|
||||
contentBlocks.push(createImageBlock('url', {
|
||||
url: part.image_url?.url,
|
||||
url: typeof imageUrl?.url === 'string' ? imageUrl.url : undefined,
|
||||
alt: '[图片]',
|
||||
}))
|
||||
}
|
||||
@@ -607,12 +645,14 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
// 工具调用(assistant 消息)
|
||||
if (msg.tool_calls) {
|
||||
for (const call of msg.tool_calls) {
|
||||
if (Array.isArray(msg.tool_calls)) {
|
||||
for (const rawCall of msg.tool_calls) {
|
||||
const call = rawCall as RawObject
|
||||
const fn = call.function as RawObject | undefined
|
||||
contentBlocks.push(createToolUseBlock(
|
||||
call.id || '',
|
||||
call.function?.name || '',
|
||||
call.function?.arguments || '{}'
|
||||
String(call.id || ''),
|
||||
String(fn?.name || ''),
|
||||
String(fn?.arguments || '{}')
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -623,7 +663,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content, null, 2)
|
||||
contentBlocks.push(createToolResultBlock(
|
||||
msg.tool_call_id,
|
||||
String(msg.tool_call_id),
|
||||
content
|
||||
))
|
||||
}
|
||||
@@ -658,31 +698,34 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染请求体(支持 Chat Completions 和 CLI/Responses API 格式)
|
||||
*/
|
||||
renderRequest(requestBody: any): RenderResult {
|
||||
renderRequest(requestBody: unknown): RenderResult {
|
||||
if (!requestBody) {
|
||||
return createEmptyRenderResult('无请求体')
|
||||
}
|
||||
|
||||
const body = requestBody as RawObject
|
||||
|
||||
// 检测是否为 CLI 格式
|
||||
const isCliFormat = requestBody.input !== undefined || requestBody.instructions !== undefined
|
||||
const isCliFormat = body.input !== undefined || body.instructions !== undefined
|
||||
|
||||
if (isCliFormat) {
|
||||
return this.renderCliRequest(requestBody)
|
||||
return this.renderCliRequest(body)
|
||||
}
|
||||
|
||||
return this.renderChatRequest(requestBody)
|
||||
return this.renderChatRequest(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 OpenAI Chat Completions 请求
|
||||
*/
|
||||
private renderChatRequest(requestBody: any): RenderResult {
|
||||
private renderChatRequest(requestBody: RawObject): RenderResult {
|
||||
try {
|
||||
const blocks: RenderBlock[] = []
|
||||
const isStream = requestBody.stream === true
|
||||
|
||||
if (Array.isArray(requestBody.messages)) {
|
||||
for (const msg of requestBody.messages) {
|
||||
for (const rawMsg of requestBody.messages) {
|
||||
const msg = rawMsg as RawObject
|
||||
// system 消息单独处理
|
||||
if (msg.role === 'system') {
|
||||
const systemText = typeof msg.content === 'string' ? msg.content : ''
|
||||
@@ -710,13 +753,13 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染 OpenAI CLI (Responses API) 请求
|
||||
*/
|
||||
private renderCliRequest(requestBody: any): RenderResult {
|
||||
private renderCliRequest(requestBody: RawObject): RenderResult {
|
||||
try {
|
||||
const blocks: RenderBlock[] = []
|
||||
const isStream = requestBody.stream === true
|
||||
|
||||
// 渲染 instructions(系统指令)
|
||||
if (requestBody.instructions) {
|
||||
if (typeof requestBody.instructions === 'string') {
|
||||
blocks.push(createMessageBlock('system', [
|
||||
createTextRenderBlock(requestBody.instructions),
|
||||
], { roleLabel: 'Instructions' }))
|
||||
@@ -733,17 +776,20 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
} else if (Array.isArray(input)) {
|
||||
// 消息数组
|
||||
for (const item of input) {
|
||||
const msgBlock = this.renderCliInputItem(item)
|
||||
const msgBlock = this.renderCliInputItem(item as RawObject)
|
||||
if (msgBlock) {
|
||||
blocks.push(msgBlock)
|
||||
}
|
||||
}
|
||||
} else if (input?.messages && Array.isArray(input.messages)) {
|
||||
} else if (input && typeof input === 'object') {
|
||||
const inputObj = input as RawObject
|
||||
// 包装在对象中的消息数组
|
||||
for (const item of input.messages) {
|
||||
const msgBlock = this.renderCliInputItem(item)
|
||||
if (msgBlock) {
|
||||
blocks.push(msgBlock)
|
||||
if (Array.isArray(inputObj.messages)) {
|
||||
for (const item of inputObj.messages) {
|
||||
const msgBlock = this.renderCliInputItem(item as RawObject)
|
||||
if (msgBlock) {
|
||||
blocks.push(msgBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -757,23 +803,24 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染 CLI 格式的单个输入项
|
||||
*/
|
||||
private renderCliInputItem(item: any): RenderBlock | null {
|
||||
private renderCliInputItem(item: RawObject): RenderBlock | null {
|
||||
if (!item) return null
|
||||
|
||||
const itemType = item.type
|
||||
|
||||
// 标准消息
|
||||
if (itemType === 'message' || item.role) {
|
||||
const role = this.mapRole(item.role)
|
||||
const role = this.mapRole(String(item.role || ''))
|
||||
const contentBlocks: RenderBlock[] = []
|
||||
|
||||
const content = item.content
|
||||
if (typeof content === 'string') {
|
||||
contentBlocks.push(createTextRenderBlock(content))
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const part of content) {
|
||||
for (const rawPart of content) {
|
||||
const part = rawPart as RawObject
|
||||
if (part.type === 'input_text' || part.type === 'output_text' || part.type === 'text') {
|
||||
contentBlocks.push(createTextRenderBlock(part.text || ''))
|
||||
contentBlocks.push(createTextRenderBlock(String(part.text || '')))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -784,10 +831,10 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
|
||||
// function_call -> 工具调用
|
||||
if (itemType === 'function_call') {
|
||||
const toolName = item.name || '工具调用'
|
||||
const toolName = String(item.name || '工具调用')
|
||||
const args = this.formatJson(item.arguments)
|
||||
return createMessageBlock('assistant', [
|
||||
createToolUseRenderBlock(toolName, args, item.call_id || item.id),
|
||||
createToolUseRenderBlock(toolName, args, String(item.call_id || item.id || '')),
|
||||
], { roleLabel: 'Assistant', badges: [createBadgeBlock('工具调用', 'outline')] })
|
||||
}
|
||||
|
||||
@@ -807,7 +854,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染响应体(支持 Chat Completions 和 CLI/Responses API 格式)
|
||||
*/
|
||||
renderResponse(responseBody: any): RenderResult {
|
||||
renderResponse(responseBody: unknown): RenderResult {
|
||||
if (!responseBody) {
|
||||
return createEmptyRenderResult('无响应体')
|
||||
}
|
||||
@@ -817,44 +864,50 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
return this.renderStreamResponse(responseBody.chunks || [])
|
||||
}
|
||||
|
||||
const body = responseBody as RawObject
|
||||
|
||||
// 检测是否为 CLI 格式
|
||||
const isCliFormat = this.isCliResponseEvent(responseBody) ||
|
||||
responseBody.object === 'response' ||
|
||||
responseBody.output !== undefined
|
||||
const isCliFormat = this.isCliResponseEvent(body) ||
|
||||
body.object === 'response' ||
|
||||
body.output !== undefined
|
||||
|
||||
if (isCliFormat) {
|
||||
return this.renderCliResponse(responseBody)
|
||||
return this.renderCliResponse(body)
|
||||
}
|
||||
|
||||
return this.renderChatResponse(responseBody)
|
||||
return this.renderChatResponse(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 OpenAI Chat Completions 响应
|
||||
*/
|
||||
private renderChatResponse(responseBody: any): RenderResult {
|
||||
private renderChatResponse(responseBody: RawObject): RenderResult {
|
||||
try {
|
||||
const blocks: RenderBlock[] = []
|
||||
|
||||
// OpenAI 响应格式: { choices: [{ message: { role, content, tool_calls } }] }
|
||||
const message = responseBody.choices?.[0]?.message
|
||||
const choices = responseBody.choices as RawObject[] | undefined
|
||||
const firstChoice = choices?.[0] as RawObject | undefined
|
||||
const message = firstChoice?.message as RawObject | undefined
|
||||
if (message) {
|
||||
const contentBlocks: RenderBlock[] = []
|
||||
const badges: BadgeRenderBlock[] = []
|
||||
|
||||
// 文本内容
|
||||
if (message.content) {
|
||||
if (typeof message.content === 'string') {
|
||||
contentBlocks.push(createTextRenderBlock(message.content))
|
||||
}
|
||||
|
||||
// 工具调用
|
||||
if (message.tool_calls) {
|
||||
if (Array.isArray(message.tool_calls)) {
|
||||
badges.push(createBadgeBlock('工具调用', 'outline'))
|
||||
for (const call of message.tool_calls) {
|
||||
for (const rawCall of message.tool_calls) {
|
||||
const call = rawCall as RawObject
|
||||
const fn = call.function as RawObject | undefined
|
||||
contentBlocks.push(createToolUseRenderBlock(
|
||||
call.function?.name || '工具调用',
|
||||
this.formatJson(call.function?.arguments),
|
||||
call.id
|
||||
String(fn?.name || '工具调用'),
|
||||
this.formatJson(fn?.arguments),
|
||||
typeof call.id === 'string' ? call.id : undefined
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -876,7 +929,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染 OpenAI CLI (Responses API) 响应
|
||||
*/
|
||||
private renderCliResponse(responseBody: any): RenderResult {
|
||||
private renderCliResponse(responseBody: RawObject): RenderResult {
|
||||
try {
|
||||
const blocks: RenderBlock[] = []
|
||||
|
||||
@@ -885,13 +938,15 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
return { blocks, isStream: false }
|
||||
}
|
||||
|
||||
for (const item of output) {
|
||||
for (const rawItem of output) {
|
||||
const item = rawItem as RawObject
|
||||
if (item?.type === 'message') {
|
||||
const contentBlocks: RenderBlock[] = []
|
||||
|
||||
if (Array.isArray(item.content)) {
|
||||
for (const content of item.content) {
|
||||
if (content?.type === 'output_text' && content?.text) {
|
||||
for (const rawContent of item.content) {
|
||||
const content = rawContent as RawObject
|
||||
if (content?.type === 'output_text' && typeof content?.text === 'string') {
|
||||
contentBlocks.push(createTextRenderBlock(content.text))
|
||||
}
|
||||
}
|
||||
@@ -914,7 +969,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染流式响应
|
||||
*/
|
||||
private renderStreamResponse(chunks: any[]): RenderResult {
|
||||
private renderStreamResponse(chunks: unknown[]): RenderResult {
|
||||
if (!chunks || chunks.length === 0) {
|
||||
return createEmptyRenderResult('无响应数据')
|
||||
}
|
||||
@@ -949,10 +1004,10 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 渲染单条消息
|
||||
*/
|
||||
private renderMessage(msg: any): RenderBlock | null {
|
||||
private renderMessage(msg: RawObject): RenderBlock | null {
|
||||
if (!msg || !msg.role) return null
|
||||
|
||||
const role = this.mapRole(msg.role)
|
||||
const role = this.mapRole(String(msg.role))
|
||||
const contentBlocks: RenderBlock[] = []
|
||||
const badges: BadgeRenderBlock[] = []
|
||||
|
||||
@@ -961,13 +1016,15 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
contentBlocks.push(createTextRenderBlock(msg.content))
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
// Vision API 格式
|
||||
for (const part of msg.content) {
|
||||
for (const rawPart of msg.content) {
|
||||
const part = rawPart as RawObject
|
||||
if (part.type === 'text') {
|
||||
contentBlocks.push(createTextRenderBlock(part.text || ''))
|
||||
contentBlocks.push(createTextRenderBlock(String(part.text || '')))
|
||||
} else if (part.type === 'image_url') {
|
||||
badges.push(createBadgeBlock('图片', 'secondary'))
|
||||
const imageUrl = part.image_url as RawObject | undefined
|
||||
contentBlocks.push(createImageRenderBlock({
|
||||
src: part.image_url?.url,
|
||||
src: typeof imageUrl?.url === 'string' ? imageUrl.url : undefined,
|
||||
alt: '[图片]',
|
||||
}))
|
||||
}
|
||||
@@ -975,13 +1032,15 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
}
|
||||
|
||||
// 工具调用(assistant 消息)
|
||||
if (msg.tool_calls) {
|
||||
if (Array.isArray(msg.tool_calls)) {
|
||||
badges.push(createBadgeBlock('工具调用', 'outline'))
|
||||
for (const call of msg.tool_calls) {
|
||||
for (const rawCall of msg.tool_calls) {
|
||||
const call = rawCall as RawObject
|
||||
const fn = call.function as RawObject | undefined
|
||||
contentBlocks.push(createToolUseRenderBlock(
|
||||
call.function?.name || '工具调用',
|
||||
this.formatJson(call.function?.arguments),
|
||||
call.id
|
||||
String(fn?.name || '工具调用'),
|
||||
this.formatJson(fn?.arguments),
|
||||
typeof call.id === 'string' ? call.id : undefined
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1091,10 +1150,10 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
/**
|
||||
* 格式化 JSON
|
||||
*/
|
||||
private formatJson(input: any): string {
|
||||
private formatJson(input: unknown): string {
|
||||
if (typeof input === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(input)
|
||||
const parsed = JSON.parse(input) as unknown
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return input
|
||||
|
||||
@@ -40,7 +40,7 @@ class ParserRegistry {
|
||||
/**
|
||||
* 检测 API 格式并返回最佳匹配的解析器
|
||||
*/
|
||||
detectParser(requestBody: any, responseBody: any, hint?: string): ApiFormatParser | undefined {
|
||||
detectParser(requestBody: unknown, responseBody: unknown, hint?: string): ApiFormatParser | undefined {
|
||||
let bestParser: ApiFormatParser | undefined
|
||||
let bestScore = 0
|
||||
|
||||
@@ -58,7 +58,7 @@ class ParserRegistry {
|
||||
/**
|
||||
* 检测 API 格式
|
||||
*/
|
||||
detectFormat(requestBody: any, responseBody: any, hint?: string): ApiFormat {
|
||||
detectFormat(requestBody: unknown, responseBody: unknown, hint?: string): ApiFormat {
|
||||
const parser = this.detectParser(requestBody, responseBody, hint)
|
||||
return parser?.format ?? 'unknown'
|
||||
}
|
||||
@@ -76,8 +76,8 @@ parserRegistry.register(geminiParser)
|
||||
* 解析请求体
|
||||
*/
|
||||
export function parseRequest(
|
||||
requestBody: any,
|
||||
responseBody?: any,
|
||||
requestBody: unknown,
|
||||
responseBody?: unknown,
|
||||
formatHint?: string
|
||||
): ParsedConversation {
|
||||
if (!requestBody) {
|
||||
@@ -96,8 +96,8 @@ export function parseRequest(
|
||||
* 解析响应体
|
||||
*/
|
||||
export function parseResponse(
|
||||
responseBody: any,
|
||||
requestBody?: any,
|
||||
responseBody: unknown,
|
||||
requestBody?: unknown,
|
||||
formatHint?: string
|
||||
): ParsedConversation {
|
||||
if (!responseBody) {
|
||||
@@ -121,8 +121,8 @@ export function parseResponse(
|
||||
* 检测 API 格式
|
||||
*/
|
||||
export function detectApiFormat(
|
||||
requestBody: any,
|
||||
responseBody: any,
|
||||
requestBody: unknown,
|
||||
responseBody: unknown,
|
||||
hint?: string
|
||||
): ApiFormat {
|
||||
return parserRegistry.detectFormat(requestBody, responseBody, hint)
|
||||
@@ -132,8 +132,8 @@ export function detectApiFormat(
|
||||
* 渲染请求体
|
||||
*/
|
||||
export function renderRequest(
|
||||
requestBody: any,
|
||||
responseBody?: any,
|
||||
requestBody: unknown,
|
||||
responseBody?: unknown,
|
||||
formatHint?: string
|
||||
): RenderResult {
|
||||
if (!requestBody) {
|
||||
@@ -152,8 +152,8 @@ export function renderRequest(
|
||||
* 渲染响应体
|
||||
*/
|
||||
export function renderResponse(
|
||||
responseBody: any,
|
||||
requestBody?: any,
|
||||
responseBody: unknown,
|
||||
requestBody?: unknown,
|
||||
formatHint?: string
|
||||
): RenderResult {
|
||||
if (!responseBody) {
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface ToolUseContentBlock extends ContentBlockBase {
|
||||
type: 'tool_use'
|
||||
toolId: string
|
||||
toolName: string
|
||||
input: Record<string, any> | string
|
||||
input: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
/** 工具结果内容块 */
|
||||
@@ -151,7 +151,7 @@ export interface FormatDetector {
|
||||
* @param hint 后端提供的格式提示
|
||||
* @returns 匹配置信度 (0-100),0 表示不匹配
|
||||
*/
|
||||
detect(requestBody: any, responseBody: any, hint?: string): number
|
||||
detect(requestBody: unknown, responseBody: unknown, hint?: string): number
|
||||
}
|
||||
|
||||
/** 请求体解析器 */
|
||||
@@ -161,7 +161,7 @@ export interface RequestParser {
|
||||
* @param requestBody 请求体
|
||||
* @returns 解析后的对话
|
||||
*/
|
||||
parseRequest(requestBody: any): ParsedConversation
|
||||
parseRequest(requestBody: unknown): ParsedConversation
|
||||
}
|
||||
|
||||
/** 响应体解析器 */
|
||||
@@ -171,14 +171,14 @@ export interface ResponseParser {
|
||||
* @param responseBody 响应体
|
||||
* @returns 解析后的对话
|
||||
*/
|
||||
parseResponse(responseBody: any): ParsedConversation
|
||||
parseResponse(responseBody: unknown): ParsedConversation
|
||||
|
||||
/**
|
||||
* 解析流式响应
|
||||
* @param chunks 响应块列表
|
||||
* @returns 解析后的对话
|
||||
*/
|
||||
parseStreamResponse(chunks: any[]): ParsedConversation
|
||||
parseStreamResponse(chunks: unknown[]): ParsedConversation
|
||||
}
|
||||
|
||||
/** 完整的 API 格式解析器 */
|
||||
@@ -193,14 +193,14 @@ export interface ApiFormatParser extends FormatDetector, RequestParser, Response
|
||||
* @param requestBody 请求体
|
||||
* @returns 渲染结果
|
||||
*/
|
||||
renderRequest(requestBody: any): import('./render').RenderResult
|
||||
renderRequest(requestBody: unknown): import('./render').RenderResult
|
||||
|
||||
/**
|
||||
* 渲染响应体为渲染块
|
||||
* @param responseBody 响应体
|
||||
* @returns 渲染结果
|
||||
*/
|
||||
renderResponse(responseBody: any): import('./render').RenderResult
|
||||
renderResponse(responseBody: unknown): import('./render').RenderResult
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -215,12 +215,15 @@ export interface StreamMetadata {
|
||||
/** 流式响应体结构 */
|
||||
export interface StreamResponseBody {
|
||||
metadata?: StreamMetadata
|
||||
chunks?: any[]
|
||||
chunks?: unknown[]
|
||||
}
|
||||
|
||||
/** 检查是否为流式响应 */
|
||||
export function isStreamResponse(body: any): body is StreamResponseBody {
|
||||
return body?.metadata?.stream === true && Array.isArray(body?.chunks)
|
||||
export function isStreamResponse(body: unknown): body is StreamResponseBody {
|
||||
if (!body || typeof body !== 'object') return false
|
||||
const obj = body as Record<string, unknown>
|
||||
const metadata = obj.metadata as Record<string, unknown> | undefined
|
||||
return metadata?.stream === true && Array.isArray(obj.chunks)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -241,7 +244,7 @@ export function createThinkingBlock(thinking: string, signature?: string): Think
|
||||
export function createToolUseBlock(
|
||||
toolId: string,
|
||||
toolName: string,
|
||||
input: Record<string, any> | string
|
||||
input: Record<string, unknown> | string
|
||||
): ToolUseContentBlock {
|
||||
return { type: 'tool_use', toolId, toolName, input }
|
||||
}
|
||||
|
||||
@@ -528,7 +528,7 @@ const navigation = computed(() => {
|
||||
.sort((a, b) => a.admin_menu_order - b.admin_menu_order)
|
||||
.map(m => ({
|
||||
name: m.display_name,
|
||||
href: m.admin_route!,
|
||||
href: m.admin_route ?? '',
|
||||
icon: iconMap[m.admin_menu_icon || ''] || Puzzle
|
||||
}))
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ app.use(router)
|
||||
preloadCriticalModules()
|
||||
|
||||
// 全局错误处理器 - 只在开发环境下记录详细日志
|
||||
app.config.errorHandler = (err: any, instance, info) => {
|
||||
app.config.errorHandler = (err: unknown, _instance, info) => {
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('Global error handler:', err, info)
|
||||
}
|
||||
|
||||
@@ -69,8 +69,8 @@ export const MOCK_LOGIN_RESPONSE_USER: LoginResponse = {
|
||||
// ========== Profile 数据 ==========
|
||||
|
||||
export const MOCK_ADMIN_PROFILE: Profile = {
|
||||
id: MOCK_ADMIN_USER.id!,
|
||||
email: MOCK_ADMIN_USER.email!,
|
||||
id: MOCK_ADMIN_USER.id ?? '',
|
||||
email: MOCK_ADMIN_USER.email ?? '',
|
||||
username: MOCK_ADMIN_USER.username,
|
||||
role: 'admin',
|
||||
is_active: true,
|
||||
@@ -87,8 +87,8 @@ export const MOCK_ADMIN_PROFILE: Profile = {
|
||||
}
|
||||
|
||||
export const MOCK_USER_PROFILE: Profile = {
|
||||
id: MOCK_NORMAL_USER.id!,
|
||||
email: MOCK_NORMAL_USER.email!,
|
||||
id: MOCK_NORMAL_USER.id ?? '',
|
||||
email: MOCK_NORMAL_USER.email ?? '',
|
||||
username: MOCK_NORMAL_USER.username,
|
||||
role: 'user',
|
||||
is_active: true,
|
||||
|
||||
@@ -40,7 +40,7 @@ function createMockResponse<T>(data: T, status: number = 200): AxiosResponse<T>
|
||||
status,
|
||||
statusText: status === 200 ? 'OK' : 'Error',
|
||||
headers: {},
|
||||
config: {} as any
|
||||
config: {} as AxiosRequestConfig
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ function normalizeApiFormat(apiFormat: string): string {
|
||||
|
||||
function getMockEndpointExtras(apiFormat: string) {
|
||||
const normalizedFormat = normalizeApiFormat(apiFormat)
|
||||
const extras: Record<string, any> = {}
|
||||
const extras: Record<string, unknown> = {}
|
||||
|
||||
if (normalizedFormat === 'claude:chat') {
|
||||
extras.header_rules = [
|
||||
@@ -481,7 +481,7 @@ const MOCK_CAPABILITIES = [
|
||||
/**
|
||||
* Mock API 路由处理器
|
||||
*/
|
||||
const mockHandlers: Record<string, (config: AxiosRequestConfig) => Promise<AxiosResponse<any>>> = {
|
||||
const mockHandlers: Record<string, (config: AxiosRequestConfig) => Promise<AxiosResponse<unknown>>> = {
|
||||
// ========== 认证相关 ==========
|
||||
'POST /api/auth/login': async (config) => {
|
||||
await delay()
|
||||
@@ -1109,11 +1109,11 @@ const mockHandlers: Record<string, (config: AxiosRequestConfig) => Promise<Axios
|
||||
|
||||
// 动态路由匹配器 - 支持 :id 形式的参数
|
||||
interface RouteMatch {
|
||||
handler: (config: AxiosRequestConfig, params: Record<string, string>) => Promise<AxiosResponse<any>>
|
||||
handler: (config: AxiosRequestConfig, params: Record<string, string>) => Promise<AxiosResponse<unknown>>
|
||||
params: Record<string, string>
|
||||
}
|
||||
|
||||
type DynamicHandler = (config: AxiosRequestConfig, params: Record<string, string>) => Promise<AxiosResponse<any>>
|
||||
type DynamicHandler = (config: AxiosRequestConfig, params: Record<string, string>) => Promise<AxiosResponse<unknown>>
|
||||
|
||||
// 动态路由注册表
|
||||
const dynamicRoutes: Array<{
|
||||
@@ -1169,7 +1169,7 @@ function matchDynamicRoute(method: string, url: string): RouteMatch | null {
|
||||
/**
|
||||
* 匹配请求到 handler
|
||||
*/
|
||||
function matchHandler(method: string, url: string): ((config: AxiosRequestConfig) => Promise<AxiosResponse<any>>) | null {
|
||||
function matchHandler(method: string, url: string): ((config: AxiosRequestConfig) => Promise<AxiosResponse<unknown>>) | null {
|
||||
// 移除查询参数
|
||||
const cleanUrl = url.split('?')[0]
|
||||
const upperMethod = method.toUpperCase()
|
||||
@@ -1205,7 +1205,7 @@ function matchHandler(method: string, url: string): ((config: AxiosRequestConfig
|
||||
/**
|
||||
* 处理 Mock 请求
|
||||
*/
|
||||
export async function handleMockRequest(config: AxiosRequestConfig): Promise<AxiosResponse<any> | null> {
|
||||
export async function handleMockRequest(config: AxiosRequestConfig): Promise<AxiosResponse<unknown> | null> {
|
||||
if (!isDemoMode()) {
|
||||
return null
|
||||
}
|
||||
@@ -1219,16 +1219,18 @@ export async function handleMockRequest(config: AxiosRequestConfig): Promise<Axi
|
||||
if (handler) {
|
||||
try {
|
||||
return await handler(config)
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
} catch (error: unknown) {
|
||||
if ((error as Record<string, unknown>)?.response) {
|
||||
throw error
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[Mock] Handler error:', error)
|
||||
throw { response: createMockResponse({ detail: '模拟请求处理失败' }, 500) }
|
||||
}
|
||||
}
|
||||
|
||||
// 未匹配的请求返回默认响应
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[Mock] Unhandled request: ${method} ${url}`)
|
||||
return createMockResponse({ message: '演示模式:该接口暂未模拟', demo_mode: true })
|
||||
}
|
||||
@@ -1277,7 +1279,7 @@ function generateMockEndpointsForProvider(providerId: string) {
|
||||
}
|
||||
|
||||
// 为 provider 生成 keys(Key 归属 Provider,通过 api_formats 关联)
|
||||
const PROVIDER_KEYS_CACHE: Record<string, any[]> = {}
|
||||
const PROVIDER_KEYS_CACHE: Record<string, Record<string, unknown>[]> = {}
|
||||
function generateMockKeysForProvider(providerId: string, count: number = 2) {
|
||||
const provider = MOCK_PROVIDERS.find(p => p.id === providerId)
|
||||
const formats = provider?.api_formats || []
|
||||
@@ -1331,7 +1333,7 @@ function generateMockModelsForProvider(providerId: string) {
|
||||
const hasOpenAI = provider.api_formats.some(f => f.includes('openai'))
|
||||
const hasGemini = provider.api_formats.some(f => f.includes('gemini'))
|
||||
|
||||
const models: any[] = []
|
||||
const models: Record<string, unknown>[] = []
|
||||
const now = new Date().toISOString()
|
||||
|
||||
if (hasClaude) {
|
||||
@@ -1734,7 +1736,7 @@ mockHandlers['GET /api/admin/endpoints/keys/grouped-by-format'] = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const grouped: Record<string, any[]> = {}
|
||||
const grouped: Record<string, Record<string, unknown>[]> = {}
|
||||
for (const provider of MOCK_PROVIDERS) {
|
||||
const endpoints = generateMockEndpointsForProvider(provider.id)
|
||||
const baseUrlByFormat = Object.fromEntries(endpoints.map(e => [e.api_format, e.base_url]))
|
||||
@@ -1812,7 +1814,7 @@ registerDynamicRoute('POST', '/api/admin/providers/:providerId/models/batch', as
|
||||
await delay()
|
||||
requireAdmin()
|
||||
const body = JSON.parse(config.data || '{}')
|
||||
const models = (body.models || []).map((m: any, i: number) => ({
|
||||
const models = ((body.models || []) as Record<string, unknown>[]).map((m: Record<string, unknown>, i: number) => ({
|
||||
id: `pm-demo-${Date.now()}-${i}`,
|
||||
provider_id: params.providerId,
|
||||
...m,
|
||||
|
||||
@@ -4,8 +4,9 @@ import { log } from '@/utils/logger'
|
||||
/**
|
||||
* 判断错误是否为网络错误
|
||||
*/
|
||||
function isNetworkError(error: any): boolean {
|
||||
return !error.response || error.message?.includes('Network') || error.message?.includes('timeout')
|
||||
function isNetworkError(error: unknown): boolean {
|
||||
const err = error as { response?: unknown; message?: string } | null
|
||||
return !err?.response || err?.message?.includes('Network') || err?.message?.includes('timeout') || false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,17 +19,18 @@ export async function ensureUserLoaded(
|
||||
if (authStore.token && !authStore.user) {
|
||||
try {
|
||||
await authStore.fetchCurrentUser()
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
const err = error as { response?: { status?: number }; message?: string }
|
||||
// 区分网络错误和认证错误
|
||||
if (isNetworkError(error)) {
|
||||
log.warn('Network error while fetching user info, keeping session', {
|
||||
error: error?.message
|
||||
error: err?.message
|
||||
})
|
||||
} else if (error.response?.status === 401) {
|
||||
} else if (err.response?.status === 401) {
|
||||
log.info('Authentication failed, clearing session')
|
||||
authStore.logout()
|
||||
} else {
|
||||
log.warn('Failed to fetch user info, but keeping session', { error: error?.message })
|
||||
log.warn('Failed to fetch user info, but keeping session', { error: err?.message })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { ref, computed } from 'vue'
|
||||
import { authApi, type User } from '@/api/auth'
|
||||
import apiClient from '@/api/client'
|
||||
import { log } from '@/utils/logger'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { getErrorStatus } from '@/types/api-error'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// 初始化时从 localStorage 恢复 token
|
||||
@@ -44,16 +46,17 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
user.value = userInfo
|
||||
|
||||
return true
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
// 不要暴露后端的详细错误信息
|
||||
if (err.response?.status === 401) {
|
||||
const status = getErrorStatus(err)
|
||||
if (status === 401) {
|
||||
error.value = '邮箱或密码错误'
|
||||
} else if (err.response?.status === 422) {
|
||||
} else if (status === 422) {
|
||||
error.value = '请输入有效的邮箱地址'
|
||||
} else if (err.response?.status === 429) {
|
||||
} else if (status === 429) {
|
||||
// 限流错误,显示后端返回的具体信息
|
||||
error.value = err.response?.data?.detail || '请求过于频繁,请稍后重试'
|
||||
} else if (err.response?.status === 500) {
|
||||
error.value = parseApiError(err, '请求过于频繁,请稍后重试')
|
||||
} else if (status === 500) {
|
||||
error.value = '服务器错误,请稍后重试'
|
||||
} else {
|
||||
error.value = '登录失败,请检查网络连接'
|
||||
@@ -75,7 +78,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const userInfo = await authApi.getCurrentUser()
|
||||
user.value = userInfo
|
||||
return userInfo
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('Failed to fetch user info', err)
|
||||
// 根据用户要求,不管什么错误都不清除状态
|
||||
// 保持登录状态,除非用户手动退出
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { modulesApi, type ModuleStatus } from '@/api/modules'
|
||||
import { log } from '@/utils/logger'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
export const useModuleStore = defineStore('modules', () => {
|
||||
const modules = ref<Record<string, ModuleStatus>>({})
|
||||
@@ -21,9 +22,9 @@ export const useModuleStore = defineStore('modules', () => {
|
||||
try {
|
||||
modules.value = await modulesApi.getAllStatus()
|
||||
loaded.value = true
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error('Failed to fetch modules status', err)
|
||||
error.value = err.response?.data?.detail || '获取模块状态失败'
|
||||
error.value = parseApiError(err, '获取模块状态失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -60,9 +61,9 @@ export const useModuleStore = defineStore('modules', () => {
|
||||
// 刷新所有模块状态,确保依赖模块的 active 状态同步更新
|
||||
await fetchModules()
|
||||
return true
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
log.error(`Failed to set module ${moduleName} enabled=${enabled}`, err)
|
||||
error.value = err.response?.data?.detail || '设置模块状态失败'
|
||||
error.value = parseApiError(err, '设置模块状态失败')
|
||||
// 重新抛出错误,让调用方可以获取详细错误信息
|
||||
throw err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { proxyNodesApi, type ProxyNode, type ManualProxyNodeCreateRequest } from '@/api/proxy-nodes'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
export const useProxyNodesStore = defineStore('proxy-nodes', () => {
|
||||
const nodes = ref<ProxyNode[]>([])
|
||||
@@ -24,8 +25,8 @@ export const useProxyNodesStore = defineStore('proxy-nodes', () => {
|
||||
nodes.value = data.items
|
||||
total.value = data.total
|
||||
fetched.value = true
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '获取代理节点列表失败'
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '获取代理节点列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -47,8 +48,8 @@ export const useProxyNodesStore = defineStore('proxy-nodes', () => {
|
||||
// 重新获取列表以保持排序一致
|
||||
await fetchNodes()
|
||||
return result
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '创建手动代理节点失败'
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '创建手动代理节点失败')
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -63,8 +64,8 @@ export const useProxyNodesStore = defineStore('proxy-nodes', () => {
|
||||
await proxyNodesApi.deleteProxyNode(nodeId)
|
||||
nodes.value = nodes.value.filter(n => n.id !== nodeId)
|
||||
total.value = Math.max(0, total.value - 1)
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '删除代理节点失败'
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '删除代理节点失败')
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { usersApi, type User, type CreateUserRequest, type UpdateUserRequest, type ApiKey } from '@/api/users'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
export const useUsersStore = defineStore('users', () => {
|
||||
const users = ref<User[]>([])
|
||||
@@ -13,8 +14,8 @@ export const useUsersStore = defineStore('users', () => {
|
||||
|
||||
try {
|
||||
users.value = await usersApi.getAllUsers()
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '获取用户列表失败'
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '获取用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -28,8 +29,8 @@ export const useUsersStore = defineStore('users', () => {
|
||||
const newUser = await usersApi.createUser(userData)
|
||||
users.value.push(newUser)
|
||||
return newUser
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '创建用户失败'
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '创建用户失败')
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -51,8 +52,8 @@ export const useUsersStore = defineStore('users', () => {
|
||||
}
|
||||
}
|
||||
return updatedUser
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '更新用户失败'
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '更新用户失败')
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -66,8 +67,8 @@ export const useUsersStore = defineStore('users', () => {
|
||||
try {
|
||||
await usersApi.deleteUser(userId)
|
||||
users.value = users.value.filter(u => u.id !== userId)
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '删除用户失败'
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '删除用户失败')
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -77,8 +78,8 @@ export const useUsersStore = defineStore('users', () => {
|
||||
async function getUserApiKeys(userId: string): Promise<ApiKey[]> {
|
||||
try {
|
||||
return await usersApi.getUserApiKeys(userId)
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '获取 API Keys 失败'
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '获取 API Keys 失败')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -86,8 +87,8 @@ export const useUsersStore = defineStore('users', () => {
|
||||
async function createApiKey(userId: string, name?: string): Promise<ApiKey> {
|
||||
try {
|
||||
return await usersApi.createApiKey(userId, name)
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '创建 API Key 失败'
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '创建 API Key 失败')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -95,8 +96,8 @@ export const useUsersStore = defineStore('users', () => {
|
||||
async function deleteApiKey(userId: string, keyId: string) {
|
||||
try {
|
||||
await usersApi.deleteApiKey(userId, keyId)
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '删除 API Key 失败'
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '删除 API Key 失败')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -109,8 +110,8 @@ export const useUsersStore = defineStore('users', () => {
|
||||
await usersApi.resetUserQuota(userId)
|
||||
// 刷新用户列表以获取最新数据
|
||||
await fetchUsers()
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '重置配额失败'
|
||||
} catch (err: unknown) {
|
||||
error.value = parseApiError(err, '重置配额失败')
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -130,4 +131,4 @@ export const useUsersStore = defineStore('users', () => {
|
||||
deleteApiKey,
|
||||
resetUserQuota
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ interface CacheItem<T> {
|
||||
}
|
||||
|
||||
class MemoryCache {
|
||||
private cache: Map<string, CacheItem<any>> = new Map()
|
||||
private cache: Map<string, CacheItem<unknown>> = new Map()
|
||||
private defaultTTL = 60000 // 默认缓存60秒
|
||||
|
||||
/**
|
||||
|
||||
@@ -121,13 +121,13 @@ export function parseNullableNumberInput(
|
||||
* // In template:
|
||||
* <Input @update:model-value="handleRateLimit" />
|
||||
*/
|
||||
export function createNumberInputHandler<T extends Record<string, any>>(
|
||||
export function createNumberInputHandler<T extends Record<string, unknown>>(
|
||||
obj: T,
|
||||
field: keyof T,
|
||||
options: Parameters<typeof parseNumberInput>[1] = {}
|
||||
) {
|
||||
return (value: string | number | null | undefined) => {
|
||||
(obj as any)[field] = parseNumberInput(value, options)
|
||||
(obj as Record<string, unknown>)[field as string] = parseNumberInput(value, options)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const RETRY_DELAY = 1000 // 1秒
|
||||
const CACHE_BUSTER_DELAY = 2000 // 2秒后尝试缓存清除
|
||||
|
||||
// 模块缓存
|
||||
const moduleCache = new Map<string, Promise<any>>()
|
||||
const moduleCache = new Map<string, Promise<unknown>>()
|
||||
|
||||
/**
|
||||
* 清除浏览器缓存的工具函数
|
||||
@@ -28,14 +28,15 @@ function clearBrowserCache() {
|
||||
/**
|
||||
* 检查错误是否是网络/缓存相关
|
||||
*/
|
||||
function isNetworkOrCacheError(error: any): boolean {
|
||||
const errorMessage = error?.message || ''
|
||||
function isNetworkOrCacheError(error: unknown): boolean {
|
||||
const err = error as { message?: string; name?: string } | null
|
||||
const errorMessage = err?.message || ''
|
||||
return (
|
||||
errorMessage.includes('Failed to fetch') ||
|
||||
errorMessage.includes('Loading chunk') ||
|
||||
errorMessage.includes('dynamically imported module') ||
|
||||
errorMessage.includes('NetworkError') ||
|
||||
error?.name === 'ChunkLoadError'
|
||||
err?.name === 'ChunkLoadError'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ function isNetworkOrCacheError(error: any): boolean {
|
||||
* @param cacheKey 缓存键
|
||||
* @returns Promise
|
||||
*/
|
||||
export async function importWithRetry<T = any>(
|
||||
export async function importWithRetry<T = unknown>(
|
||||
importFn: () => Promise<T>,
|
||||
retries: number = MAX_RETRIES,
|
||||
cacheKey?: string
|
||||
@@ -54,7 +55,7 @@ export async function importWithRetry<T = any>(
|
||||
try {
|
||||
// 如果有缓存键且缓存中存在,直接返回
|
||||
if (cacheKey && moduleCache.has(cacheKey)) {
|
||||
return await moduleCache.get(cacheKey)!
|
||||
return await moduleCache.get(cacheKey) as T
|
||||
}
|
||||
|
||||
const importPromise = importFn()
|
||||
|
||||
@@ -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
|
||||
}> = []
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user