Files
Aether/frontend/src/api/me.ts
T
AAEE86 1353d76e07 feat(frontend): Responses WebSocket 配置与用量展示
provider 表单支持开启 Responses WebSocket;用量列表、状态与详情
区分 WebSocket 请求。
2026-08-17 14:52:25 +08:00

512 lines
16 KiB
TypeScript

import apiClient from './client'
import type { ActivityHeatmap } from '@/types/activity'
import type { TieredPricingConfig } from './endpoints/types'
import { cachedRequest, buildCacheKey } from '@/utils/cache'
import type { BillingSummary } from './auth'
import type { UserSession } from '@/types/session'
import type { FeatureSettingsMap } from '@/utils/featureSettings'
const ACTIVITY_HEATMAP_CACHE_TTL_MS = 30 * 60 * 1000
export type { UserSession }
export interface Profile {
id: string // UUID
email?: string | null
username: string
role: string
is_active: boolean
billing: BillingSummary
created_at: string
updated_at?: string
last_login_at?: string
auth_source: 'local' | 'ldap' | 'oauth'
has_password: boolean
preferences?: UserPreferences
feature_settings?: FeatureSettingsMap | null
}
export interface UserPreferences {
avatar_url?: string
bio?: string
default_provider_id?: string // UUID
default_provider?: Record<string, unknown> | string | null // 仅管理员可见
theme: string
language: string
timezone?: string
notifications?: {
email?: boolean
usage_alerts?: boolean
announcements?: boolean
}
}
// 提供商配置接口
export interface ProviderConfig {
provider_id: string
priority: number // 优先级(越高越优先)
weight: number // 负载均衡权重
enabled: boolean // 是否启用
}
// 使用记录接口
export interface UsageRecordDetail {
id: string
provider?: string // 仅管理员可见
model: string
request_type?: string | null
requested_reasoning_effort?: string | null
reasoning_effort?: string | null
service_tier?: string | null
actual_service_tier?: string | null
input_tokens: number
effective_input_tokens?: number
output_tokens: number
total_tokens: number
cost: number // 官方费率
actual_cost?: number // 倍率消耗(仅管理员可见)
rate_multiplier?: number // 成本倍率(仅管理员可见)
response_time_ms?: number | null
first_byte_time_ms?: number | null
end_to_end_time_ms?: number | null
end_to_end_first_byte_time_ms?: number | null
updated_at?: string | null
response_time_updated_at?: string | null
is_stream: boolean
is_websocket?: boolean
upstream_is_stream?: boolean
client_requested_stream?: boolean
client_is_stream?: boolean
client_family?: string | null
client_ip?: string | null
user_agent?: string | null
request_path?: string | null
request_path_and_query?: string | null
created_at: string
cache_creation_input_tokens?: number
cache_creation_ephemeral_5m_input_tokens?: number
cache_creation_ephemeral_1h_input_tokens?: number
cache_read_input_tokens?: number
status_code: number
error_message?: string
input_price_per_1m: number
output_price_per_1m: number
cache_creation_price_per_1m?: number
cache_read_price_per_1m?: number
price_per_request?: number // 按次计费价格
has_fallback?: boolean
api_key?: {
id: string
name: string
display: string
}
}
// 模型统计接口
export interface ModelSummary {
model: string
requests: number
input_tokens: number
effective_input_tokens?: number
output_tokens: number
total_tokens: number
cache_read_tokens?: number
cache_creation_tokens?: number
total_input_context?: number
cache_hit_rate?: number
total_cost_usd: number
actual_total_cost_usd?: number // 倍率消耗(仅管理员可见)
}
// 提供商统计接口
export interface ProviderSummary {
provider: string
requests: number
effective_input_tokens?: number
total_tokens: number
output_tokens?: number
cache_read_tokens?: number
cache_creation_tokens?: number
total_input_context?: number
cache_hit_rate?: number
total_cost_usd: number
success_rate: number | null
avg_response_time_ms: number | null
}
// API 格式统计接口
export interface ApiFormatSummary {
api_format: string
request_count: number
effective_input_tokens?: number
total_tokens: number
output_tokens?: number
cache_read_tokens: number
cache_creation_tokens?: number
total_input_context?: number
cache_hit_rate: number
total_cost_usd: number
avg_response_time_ms: number
}
// 使用统计响应接口
export interface UsageResponse {
total_requests: number
total_input_tokens: number
total_output_tokens: number
total_tokens: number
total_cost: number // 官方费率
total_actual_cost?: number // 倍率消耗(仅管理员可见)
avg_response_time: number
billing: BillingSummary
summary_by_model: ModelSummary[]
summary_by_provider?: ProviderSummary[]
summary_by_api_format?: ApiFormatSummary[]
pagination?: {
total: number
limit: number
offset: number
has_more: boolean
}
records: UsageRecordDetail[]
activity_heatmap?: ActivityHeatmap | null
}
export interface ApiKey {
id: string // UUID
name: string
key?: string
key_display: string
is_active: boolean
is_locked: boolean // 管理员锁定标志
last_used_at?: string | null
created_at?: string | null
total_requests?: number
total_cost_usd?: number
rate_limit?: number | null
concurrent_limit?: number | null
ip_rules?: string[] | null
allowed_providers?: ProviderConfig[]
force_capabilities?: Record<string, boolean> | null // 强制能力配置
feature_settings?: FeatureSettingsMap | null
}
export type InstallTargetCli = 'claude_code' | 'codex_cli' | 'gemini_cli'
export type InstallTargetSystem = 'macos' | 'linux' | 'windows' | 'auto'
export type InstallSessionTargetSystem = Exclude<InstallTargetSystem, 'auto'>
export interface ApiKeyInstallSession {
install_code: string
expires_at_unix_secs: number
expires_in_seconds: number
target_cli: InstallTargetCli
target_cli_label: string
target_system: InstallTargetSystem
target_system_label: string
unix_command: string
powershell_command: string
}
export interface UserClientConfig {
base_url: string
site_name?: string
}
// 不再需要 ProviderBinding 接口
export interface ChangePasswordRequest {
old_password?: string // 可选:首次设置密码时不需要
new_password: string
}
export const meApi = {
// 获取个人信息
async getProfile(): Promise<Profile> {
const response = await apiClient.get<Profile>('/api/users/me')
return response.data
},
// 更新个人信息
async updateProfile(data: {
email?: string
username?: string
feature_settings?: FeatureSettingsMap | null
}): Promise<{ message: string }> {
const response = await apiClient.put('/api/users/me', data)
return response.data
},
// 修改密码
async changePassword(data: ChangePasswordRequest): Promise<{ message: string }> {
const response = await apiClient.patch('/api/users/me/password', data)
return response.data
},
async listSessions(): Promise<UserSession[]> {
const response = await apiClient.get<UserSession[]>('/api/users/me/sessions')
return response.data
},
async updateSessionLabel(sessionId: string, deviceLabel: string): Promise<UserSession> {
const response = await apiClient.patch<UserSession>(`/api/users/me/sessions/${sessionId}`, {
device_label: deviceLabel,
})
return response.data
},
async revokeSession(sessionId: string): Promise<{ message: string }> {
const response = await apiClient.delete(`/api/users/me/sessions/${sessionId}`)
return response.data
},
async revokeOtherSessions(): Promise<{ message: string; revoked_count: number }> {
const response = await apiClient.delete('/api/users/me/sessions/others')
return response.data
},
// API密钥管理
async getApiKeys(): Promise<ApiKey[]> {
const response = await apiClient.get<ApiKey[]>('/api/users/me/api-keys')
return response.data
},
async createApiKey(data: { name: string; rate_limit?: number | null; concurrent_limit?: number | null; ip_rules?: string[] | null; feature_settings?: FeatureSettingsMap | null }): Promise<ApiKey> {
const response = await apiClient.post<ApiKey>('/api/users/me/api-keys', data)
return response.data
},
async getApiKeyDetail(keyId: string, includeKey: boolean = false): Promise<ApiKey & { key?: string }> {
const response = await apiClient.get<ApiKey & { key?: string }>(
`/api/users/me/api-keys/${keyId}`,
{ params: { include_key: includeKey } }
)
return response.data
},
async getFullApiKey(keyId: string): Promise<{ key: string }> {
const response = await apiClient.get<{ key: string }>(
`/api/users/me/api-keys/${keyId}`,
{ params: { include_key: true } }
)
return response.data
},
async getClientConfig(): Promise<UserClientConfig> {
const response = await apiClient.get<UserClientConfig>('/api/users/me/client-config')
return response.data
},
async deleteApiKey(keyId: string): Promise<{ message: string }> {
const response = await apiClient.delete(`/api/users/me/api-keys/${keyId}`)
return response.data
},
async toggleApiKey(keyId: string, isActive: boolean): Promise<ApiKey> {
const response = await apiClient.patch<ApiKey>(`/api/users/me/api-keys/${keyId}`, {
is_active: isActive,
})
return response.data
},
async updateApiKey(
keyId: string,
data: { name?: string; rate_limit?: number | null; concurrent_limit?: number | null; ip_rules?: string[] | null; feature_settings?: FeatureSettingsMap | null | undefined }
): Promise<ApiKey & { message: string }> {
const response = await apiClient.put<ApiKey & { message: string }>(
`/api/users/me/api-keys/${keyId}`,
data
)
return response.data
},
async createApiKeyInstallSession(
keyId: string,
data: { target_cli: InstallTargetCli; target_system: InstallSessionTargetSystem }
): Promise<ApiKeyInstallSession> {
const response = await apiClient.post<ApiKeyInstallSession>(
`/api/users/me/api-keys/${keyId}/install-sessions`,
data
)
return response.data
},
// 使用统计
async getUsage(params?: {
start_date?: string
end_date?: string
preset?: string
timezone?: string
tz_offset_minutes?: number
search?: string // 通用搜索:密钥名、模型名
limit?: number
offset?: number
}): Promise<UsageResponse> {
const response = await apiClient.get<UsageResponse>('/api/users/me/usage', { params })
return response.data
},
// 获取活跃请求状态(用于轮询更新)
async getActiveRequests(ids?: string): Promise<{
requests: Array<{
id: string
status: 'pending' | 'streaming' | 'completed' | 'failed' | 'cancelled'
input_tokens: number
effective_input_tokens?: number | null
output_tokens: number
cache_creation_input_tokens?: number | null
cache_creation_ephemeral_5m_input_tokens?: number | null
cache_creation_ephemeral_1h_input_tokens?: number | null
cache_read_input_tokens?: number | null
cost: number
actual_cost?: number | null
rate_multiplier?: number | null
response_time_ms: number | null
first_byte_time_ms: number | null
end_to_end_time_ms?: number | null
end_to_end_first_byte_time_ms?: number | null
updated_at?: string | null
response_time_updated_at?: string | null
status_code?: number | null
error_message?: string | null
api_format?: string | null
endpoint_api_format?: string | null
is_stream?: boolean | null
is_websocket?: boolean | null
upstream_is_stream?: boolean | null
client_requested_stream?: boolean | null
client_is_stream?: boolean | null
has_format_conversion?: boolean | null
has_fallback?: boolean | null
target_model?: string | null
request_type?: string | null
requested_reasoning_effort?: string | null
reasoning_effort?: string | null
service_tier?: string | null
actual_service_tier?: string | null
}>
}> {
const params = ids ? { ids } : {}
const response = await apiClient.get('/api/users/me/usage/active', { params })
return response.data
},
// 获取可用的提供商
async getAvailableProviders(): Promise<Array<Record<string, unknown>>> {
const response = await apiClient.get('/api/users/me/providers')
return response.data
},
// 获取用户可用的模型列表
async getAvailableModels(params?: {
skip?: number
limit?: number
search?: string
}): Promise<{
models: Array<{
id: string
name: string
display_name: string | null
is_active: boolean
default_price_per_request: number | null
default_tiered_pricing: TieredPricingConfig | null
supported_capabilities: string[] | null
supports_embedding?: boolean | null
config: Record<string, unknown> | null
usage_count: number
}>
total: number
}> {
const response = await apiClient.get('/api/users/me/available-models', { params })
return response.data
},
// 获取端点状态(不包含敏感信息)
async getEndpointStatus(): Promise<Array<Record<string, unknown>>> {
const response = await apiClient.get('/api/users/me/endpoint-status')
return response.data
},
// 偏好设置
async getPreferences(): Promise<UserPreferences> {
const response = await apiClient.get('/api/users/me/preferences')
return response.data
},
async updatePreferences(data: Partial<UserPreferences>): Promise<{ message: string }> {
const response = await apiClient.put('/api/users/me/preferences', data)
return response.data
},
// 提供商绑定管理相关方法已移除,改为直接从可用提供商中选择
// API密钥提供商关联
async updateApiKeyProviders(keyId: string, data: {
allowed_providers?: ProviderConfig[]
}): Promise<{ message: string }> {
const response = await apiClient.put(`/api/users/me/api-keys/${keyId}/providers`, data)
return response.data
},
// API密钥能力配置
async updateApiKeyCapabilities(keyId: string, data: {
force_capabilities?: Record<string, boolean> | null
}): Promise<{ message: string; force_capabilities?: Record<string, boolean> | null }> {
const response = await apiClient.put(`/api/users/me/api-keys/${keyId}/capabilities`, data)
return response.data
},
// 模型能力配置
async getModelCapabilitySettings(): Promise<{
model_capability_settings: Record<string, Record<string, boolean>>
}> {
const response = await apiClient.get('/api/users/me/model-capabilities')
return response.data
},
async updateModelCapabilitySettings(data: {
model_capability_settings: Record<string, Record<string, boolean>> | null
}): Promise<{
message: string
model_capability_settings: Record<string, Record<string, boolean>> | null
}> {
const response = await apiClient.put('/api/users/me/model-capabilities', data)
return response.data
},
// 获取请求间隔时间线(用于散点图)
async getIntervalTimeline(params?: {
hours?: number
limit?: number
}): Promise<{
analysis_period_hours: number
total_points: number
points: Array<{ x: string; y: number; model?: string }>
models?: string[]
}> {
const cacheKey = buildCacheKey('me:interval-timeline', params as Record<string, unknown> | undefined)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get('/api/users/me/usage/interval-timeline', { params })
return response.data
},
30000
)
},
/**
* 获取活跃度热力图数据(用户)
* 历史热力图变化很慢,前端做长缓存,避免短时间重复请求。
*/
async getActivityHeatmap(): Promise<ActivityHeatmap> {
return cachedRequest(
'me-activity-heatmap',
async () => {
const response = await apiClient.get<ActivityHeatmap>('/api/users/me/usage/heatmap')
return response.data
},
ACTIVITY_HEATMAP_CACHE_TTL_MS
)
}
}