diff --git a/crates/adapters/src/codex/fingerprint.rs b/crates/adapters/src/codex/fingerprint.rs index 656c67536..9f0502bdc 100644 --- a/crates/adapters/src/codex/fingerprint.rs +++ b/crates/adapters/src/codex/fingerprint.rs @@ -250,7 +250,8 @@ impl FingerprintRepository { chromium_version, user_agent_template, default_headers_json, - header_order_json + header_order_json, + updated_at from fingerprints where id = ? "#, @@ -315,6 +316,7 @@ fn fingerprint_from_row(row: &sqlx::sqlite::SqliteRow) -> Result, /// 请求头顺序。 pub header_order: Vec, + /// DB 最后更新时间。 + #[serde(skip_serializing_if = "Option::is_none")] + pub updated_at: Option, } impl Fingerprint { @@ -37,6 +40,7 @@ impl Fingerprint { user_agent_template: "Codex Desktop/{version} ({platform}; {arch})".to_string(), default_headers: Self::default_headers(), header_order: Self::default_header_order(), + updated_at: None, } } diff --git a/crates/runtime/src/bootstrap.rs b/crates/runtime/src/bootstrap.rs index b3573c664..1e2da77e0 100644 --- a/crates/runtime/src/bootstrap.rs +++ b/crates/runtime/src/bootstrap.rs @@ -20,6 +20,7 @@ pub fn fingerprint_from_config(config: &FingerprintConfig) -> Fingerprint { .map(|header| (header.name.clone(), header.value.clone())) .collect(), header_order: config.header_order.clone(), + updated_at: None, } } diff --git a/crates/server/src/openai_api/diagnostics.rs b/crates/server/src/openai_api/diagnostics.rs index c3c5cd861..ca7046b41 100644 --- a/crates/server/src/openai_api/diagnostics.rs +++ b/crates/server/src/openai_api/diagnostics.rs @@ -148,6 +148,9 @@ pub struct FingerprintDiagnostics { pub chromium_version: String, /// Expanded user-agent. pub user_agent: String, + /// 指纹最后更新时间。 + #[serde(skip_serializing_if = "Option::is_none")] + pub updated_at: Option, } /// Upstream probe diagnostics. @@ -305,6 +308,7 @@ fn fingerprint_diagnostics(fingerprint: &Fingerprint) -> FingerprintDiagnostics arch: fingerprint.arch.clone(), chromium_version: fingerprint.chromium_version.clone(), user_agent: fingerprint.user_agent(), + updated_at: fingerprint.updated_at.clone(), } } diff --git a/web/src/api/modules/accounts.ts b/web/src/api/modules/accounts.ts index f236341aa..5ab150dec 100644 --- a/web/src/api/modules/accounts.ts +++ b/web/src/api/modules/accounts.ts @@ -2,19 +2,15 @@ import { requestJson } from '../request' export interface Account { id: string - email: string + email?: string accountId?: string userId?: string planType?: string status: 'active' | 'expired' | 'disabled' | 'banned' | 'quota_exhausted' | 'refreshing' label?: string accessTokenExpiresAt?: string - nextRefreshAt?: string - createdAt: string - lastUsedAt?: string - totalRequests: number - totalInputTokens: number - totalOutputTokens: number + addedAt: string + updatedAt: string } export function getAccounts() { diff --git a/web/src/api/modules/api-keys.ts b/web/src/api/modules/api-keys.ts index 4f1aea37c..9d2e8072b 100644 --- a/web/src/api/modules/api-keys.ts +++ b/web/src/api/modules/api-keys.ts @@ -12,11 +12,6 @@ export interface ClientApiKey { lastUsedAt?: string } -export interface ApiKeysListResponse { - keys: ClientApiKey[] - total: number -} - export interface CreateApiKeyPayload { name: string label?: string @@ -29,7 +24,7 @@ export interface CreateApiKeyResponse { } export function getApiKeys() { - return requestJson('/api/admin/api-keys') + return requestJson('/api/admin/api-keys') } export function createApiKey(payload: CreateApiKeyPayload) { diff --git a/web/src/api/modules/settings.ts b/web/src/api/modules/settings.ts index 4d4199de1..e6b772465 100644 --- a/web/src/api/modules/settings.ts +++ b/web/src/api/modules/settings.ts @@ -2,17 +2,30 @@ import { requestJson } from '../request' // ==================== 设置管理 ==================== +export interface QuotaWarningThresholds { + primary: number[] + secondary: number[] +} + export interface Settings { - logging: { - enabled: boolean - level: string - } - quota: { - warningThresholds: { - requestsRemaining: number - tokensRemaining: number - } - } + defaultModel: string + defaultReasoningEffort?: string | null + serviceTier?: string | null + modelAliases?: Record + refreshEnabled: boolean + refreshMarginSeconds?: number + refreshConcurrency?: number + maxConcurrentPerAccount: number + requestIntervalMs: number + rotationStrategy: string + tierPriority?: string[] + quotaRefreshIntervalMinutes?: number + quotaWarningThresholds: QuotaWarningThresholds + quotaSkipExhausted: boolean + logsEnabled: boolean + logsCapacity: number + logsCaptureBody?: boolean + usageHistoryRetentionDays?: number | null [key: string]: unknown } diff --git a/web/src/api/modules/usage.ts b/web/src/api/modules/usage.ts index f801f41e3..c0e9eab11 100644 --- a/web/src/api/modules/usage.ts +++ b/web/src/api/modules/usage.ts @@ -3,38 +3,43 @@ import { requestJson } from '../request' // ==================== 使用统计 ==================== export interface UsageSummary { - totalRequests: number - totalInputTokens: number - totalOutputTokens: number - todayRequests: number - todayInputTokens: number - todayOutputTokens: number - activeAccounts: number - errorRate: number - avgLatencyMs: number + accountCount: number + requestCount: number + emptyResponseCount: number + inputTokens: number + outputTokens: number + cachedTokens: number + reasoningTokens: number + totalTokens: number + imageInputTokens: number + imageOutputTokens: number + imageRequestCount: number + imageRequestFailedCount: number } export function getUsageSummary() { return requestJson('/api/admin/usage-stats/summary') } -export interface UsageStats { - hourly: Array<{ - hour: string - requests: number - inputTokens: number - outputTokens: number - errors: number - }> - daily: Array<{ - date: string - requests: number - inputTokens: number - outputTokens: number - errors: number - }> +export interface AccountUsageStats { + accountId: string + email?: string + label?: string + planType?: string + requestCount: number + emptyResponseCount: number + inputTokens: number + outputTokens: number + cachedTokens: number + reasoningTokens: number + totalTokens: number + imageInputTokens: number + imageOutputTokens: number + imageRequestCount: number + imageRequestFailedCount: number + lastUsedAt?: string } -export function getUsageStats(period: 'today' | 'week' | 'month' = 'today') { - return requestJson(`/api/admin/usage-stats?period=${period}`) +export function getUsageStats() { + return requestJson('/api/admin/usage-stats') } diff --git a/web/src/views/accounts/AccountsView.vue b/web/src/views/accounts/AccountsView.vue index 165f312a6..f88981839 100644 --- a/web/src/views/accounts/AccountsView.vue +++ b/web/src/views/accounts/AccountsView.vue @@ -359,18 +359,18 @@ onMounted(() => { - - {{ account.totalRequests.toLocaleString() }} + + — - - {{ formatTokens(account.totalInputTokens + account.totalOutputTokens) }} + + — - {{ formatDate(account.lastUsedAt) }} + {{ formatDate(account.updatedAt) }} diff --git a/web/src/views/api-keys/ApiKeysView.vue b/web/src/views/api-keys/ApiKeysView.vue index 72b4340ea..2dca5643d 100644 --- a/web/src/views/api-keys/ApiKeysView.vue +++ b/web/src/views/api-keys/ApiKeysView.vue @@ -62,7 +62,7 @@ async function loadApiKeys() { try { loading.value = true const data = await getApiKeys() - apiKeys.value = data.keys + apiKeys.value = data } catch (error: any) { toast.error(error.message || '加载失败') } finally { diff --git a/web/src/views/dashboard/DashboardView.vue b/web/src/views/dashboard/DashboardView.vue index b7975be07..b7c0685a5 100644 --- a/web/src/views/dashboard/DashboardView.vue +++ b/web/src/views/dashboard/DashboardView.vue @@ -20,6 +20,9 @@ const { accountUsage, serviceStatuses, eventLogs, + poolSummary, + capacityInfo, + rotationStrategy, refresh, } = useDashboard() @@ -63,7 +66,13 @@ const { - + diff --git a/web/src/views/dashboard/components/AccountOverviewCard.vue b/web/src/views/dashboard/components/AccountOverviewCard.vue index c0c78e9d3..32d39a5fd 100644 --- a/web/src/views/dashboard/components/AccountOverviewCard.vue +++ b/web/src/views/dashboard/components/AccountOverviewCard.vue @@ -1,55 +1,111 @@ @@ -24,16 +88,17 @@ const dotToneClasses: Record = {

请求趋势

-

最近 24 小时 · 14:00 峰值 15.2K

+

最近 24 小时

@@ -42,52 +107,24 @@ const dotToneClasses: Record = {
- - - - - - - - - {{ label }} - - +
diff --git a/web/src/views/dashboard/components/ServiceStatusCard.vue b/web/src/views/dashboard/components/ServiceStatusCard.vue index 6af22cd14..033ac2e89 100644 --- a/web/src/views/dashboard/components/ServiceStatusCard.vue +++ b/web/src/views/dashboard/components/ServiceStatusCard.vue @@ -31,14 +31,20 @@ const valueToneClasses: Record = {
- {{ item.label }} - {{ item.value }} - {{ item.detail }} + +
diff --git a/web/src/views/dashboard/composables/useDashboard.ts b/web/src/views/dashboard/composables/useDashboard.ts index bd9e1c92e..8d87b2471 100644 --- a/web/src/views/dashboard/composables/useDashboard.ts +++ b/web/src/views/dashboard/composables/useDashboard.ts @@ -3,18 +3,19 @@ import type { Ref } from 'vue' import { Activity, - Boxes, CloudCheck, FileText, + Gauge, MonitorCheck, RefreshCw, - ScrollText, Timer, Users, } from '@lucide/vue' -import { getDiagnostics, getLogs, getUsageSummary, getAccounts } from '@/api' +import { getDiagnostics, getLogs, getUsageSummary, getUsageStats, getAccounts } from '@/api' import type { + AccountCapacityInfo, + AccountPoolSummary, AccountUsageItem, EventLogItem, MetricCardItem, @@ -31,6 +32,9 @@ export function useDashboard(): { accountUsage: Ref serviceStatuses: Ref eventLogs: Ref + poolSummary: Ref + capacityInfo: Ref + rotationStrategy: Ref refresh: () => Promise } { const metrics = ref([]) @@ -39,6 +43,9 @@ export function useDashboard(): { const accountUsage = ref([]) const serviceStatuses = ref([]) const eventLogs = ref([]) + const poolSummary = ref(null) + const capacityInfo = ref(null) + const rotationStrategy = ref(null) const loading = ref(true) const autoRefreshTimer = ref | null>(null) @@ -46,36 +53,26 @@ export function useDashboard(): { try { loading.value = true - // 并行加载所有数据 - const [summary, accounts, logs, diagnostics] = await Promise.all([ + // 并行加载所有数据;日志拉 200 条用于趋势聚合 + const [summary, usageStats, accounts, logs, diagnostics] = await Promise.all([ getUsageSummary().catch(() => null), + getUsageStats().catch(() => null), getAccounts().catch(() => null), - getLogs({ limit: 10 }).catch(() => null), + getLogs({ limit: 200 }).catch(() => null), getDiagnostics().catch(() => null), ]) - // 更新指标卡片 - if (summary) { - updateMetrics(summary, accounts?.length || 0) - } + const totalAccounts = diagnostics?.accounts?.pool?.total ?? accounts?.length ?? 0 - // 更新账号使用情况 - if (accounts) { - updateAccountUsage(accounts.slice(0, 4)) - } + poolSummary.value = diagnostics?.accounts?.pool ?? null + capacityInfo.value = diagnostics?.accounts?.capacity ?? null + rotationStrategy.value = diagnostics?.settings?.rotationStrategy ?? null - // 更新事件日志 - if (logs) { - updateEventLogs(logs) - } - - // 更新服务状态 - if (diagnostics) { - updateServiceStatus(diagnostics) - } - - // 更新趋势数据(使用模拟数据,实际应从后端获取) - updateTrendData() + updateMetrics(summary, totalAccounts, diagnostics) + updateAccountUsage(accounts ?? [], usageStats ?? []) + updateEventLogs((logs ?? []).slice(0, 10)) + updateServiceStatus(diagnostics) + updateTrendData(logs ?? [], summary) } catch (error) { console.error('Failed to load dashboard data:', error) } finally { @@ -83,9 +80,17 @@ export function useDashboard(): { } } - function updateMetrics(summary: any, totalAccounts: number) { - const activeAccounts = summary.activeAccounts || 0 - const errorAccounts = totalAccounts - activeAccounts + function updateMetrics(summary: any, totalAccounts: number, diagnostics: any) { + const pool = diagnostics?.accounts?.pool ?? {} + const activeAccounts = pool.active ?? summary?.accountCount ?? 0 + const errorAccounts = (pool.expired ?? 0) + (pool.disabled ?? 0) + (pool.banned ?? 0) + + const requestCount = summary?.requestCount ?? 0 + const inputTokens = summary?.inputTokens ?? 0 + const outputTokens = summary?.outputTokens ?? 0 + const cachedTokens = summary?.cachedTokens ?? 0 + const reasoningTokens = summary?.reasoningTokens ?? 0 + const emptyResponseCount = summary?.emptyResponseCount ?? 0 metrics.value = [ { @@ -95,60 +100,85 @@ export function useDashboard(): { tone: 'normal', details: [ { label: '启用', value: String(activeAccounts), tone: activeAccounts > 0 ? 'success' : 'normal' }, - { label: '错误', value: String(errorAccounts), tone: errorAccounts > 0 ? 'danger' : 'normal' }, + { label: '异常', value: String(errorAccounts), tone: errorAccounts > 0 ? 'danger' : 'normal' }, ], }, { - title: '今日请求', - value: formatNumber(summary.todayRequests || 0), + title: '总请求', + value: formatNumber(requestCount), icon: Activity, tone: 'info', details: [ - { label: '今日', value: formatNumber(summary.todayRequests || 0), tone: 'info' }, - { label: '总计', value: formatNumber(summary.totalRequests || 0), tone: 'info' }, + { label: '成功', value: formatNumber(requestCount - emptyResponseCount), tone: 'success' }, + { label: '空响应', value: formatNumber(emptyResponseCount), tone: emptyResponseCount > 0 ? 'warning' : 'info' }, ], }, { title: '总 Token', - value: formatTokens(summary.totalInputTokens + summary.totalOutputTokens), + value: formatTokens(inputTokens + outputTokens), icon: FileText, tone: 'success', details: [ - { label: '今日', value: formatTokens(summary.todayInputTokens + summary.todayOutputTokens), tone: 'success' }, - { label: '总计', value: formatTokens(summary.totalInputTokens + summary.totalOutputTokens), tone: 'success' }, + { label: '输入', value: formatTokens(inputTokens), tone: 'info' }, + { label: '输出', value: formatTokens(outputTokens), tone: 'success' }, ], }, { - title: '平均响应', - value: formatLatency(summary.avgLatencyMs), + title: '缓存命中率', + value: inputTokens > 0 ? `${((cachedTokens / inputTokens) * 100).toFixed(1)}%` : '—', icon: Timer, - tone: summary.avgLatencyMs > 3000 ? 'warning' : 'success', + tone: cachedTokens > 0 ? 'success' : 'normal', details: [ - { label: '平均', value: formatLatency(summary.avgLatencyMs), tone: summary.avgLatencyMs > 3000 ? 'warning' : 'success' }, - { label: '错误率', value: `${(summary.errorRate * 100).toFixed(2)}%`, tone: summary.errorRate > 0.01 ? 'warning' : 'success' }, + { label: '缓存 Token', value: formatTokens(cachedTokens), tone: cachedTokens > 0 ? 'success' : 'normal' }, + { label: '输入总量', value: formatTokens(inputTokens), tone: 'info' }, ], }, ] } - function updateAccountUsage(accounts: any[]) { - accountUsage.value = accounts.map((acc) => { - const totalTokens = acc.totalInputTokens + acc.totalOutputTokens - const maxTokens = Math.max(...accounts.map((a: any) => a.totalInputTokens + a.totalOutputTokens)) - const loadWidth = maxTokens > 0 ? Math.floor((totalTokens / maxTokens) * 100) : 0 + function updateAccountUsage(accounts: any[], usageStats: any[]) { + // 构建 accountId → usage 的映射 + const usageByAccount: Record = {} + for (const u of usageStats) { + usageByAccount[u.accountId] = u + } + + // 计算用量数据:优先用 usage-stats,回退到 account 自带字段 + const merged = accounts.map((acc) => { + const usage = usageByAccount[acc.id] ?? {} + return { + ...acc, + _requestCount: usage.requestCount ?? acc.totalRequests ?? 0, + _inputTokens: usage.inputTokens ?? acc.totalInputTokens ?? 0, + _outputTokens: usage.outputTokens ?? acc.totalOutputTokens ?? 0, + _lastUsedAt: usage.lastUsedAt ?? acc.lastUsedAt ?? acc.updatedAt ?? acc.addedAt ?? null, + } + }) + + // 按请求数降序排列,取前 4 + const sorted = merged + .sort((a: any, b: any) => b._requestCount - a._requestCount) + .slice(0, 4) + + const maxTokens = Math.max(...sorted.map((a: any) => a._inputTokens + a._outputTokens), 1) + + accountUsage.value = sorted.map((acc: any) => { + const totalTokens = acc._inputTokens + acc._outputTokens + const loadWidth = Math.floor((totalTokens / maxTokens) * 84) // 84px = max bar width let tone: AccountUsageItem['tone'] = 'normal' if (acc.status === 'active') tone = 'success' else if (acc.status === 'quota_exhausted' || acc.status === 'expired') tone = 'warning' else if (acc.status === 'banned' || acc.status === 'disabled') tone = 'danger' + else if (acc.status === 'refreshing') tone = 'info' return { - name: acc.label || acc.email.split('@')[0], - email: acc.email, + name: acc.label || (acc.email ? acc.email.split('@')[0] : acc.id.slice(0, 8)), + email: acc.email || '—', plan: acc.planType || 'free', - requests: formatNumber(acc.totalRequests), + requests: formatNumber(acc._requestCount), tokens: formatTokens(totalTokens), - lastUsed: formatRelativeTime(acc.lastUsedAt), + lastUsed: formatRelativeTime(acc._lastUsedAt), tone, loadWidth, } @@ -156,6 +186,11 @@ export function useDashboard(): { } function updateEventLogs(logs: any[]) { + if (!logs.length) { + eventLogs.value = [] + return + } + eventLogs.value = logs.map((log) => { let tone: EventLogItem['tone'] = 'info' if (log.level === 'warning') tone = 'warning' @@ -176,63 +211,110 @@ export function useDashboard(): { } function updateServiceStatus(diagnostics: any) { + if (!diagnostics) { + serviceStatuses.value = [] + return + } + + const pool = diagnostics.accounts?.pool ?? {} + const capacity = diagnostics.accounts?.capacity ?? {} + const transport = diagnostics.transport ?? {} + const fingerprint = transport.fingerprint ?? {} + const settings = diagnostics.settings ?? {} + const backendUrl = (transport.backendBaseUrl || '').replace(/^https?:\/\//, '') + serviceStatuses.value = [ { - label: '上游连接', - value: diagnostics.database.connected ? '正常' : '断开', - detail: '—', - tone: diagnostics.database.connected ? 'success' : 'danger', + label: '客户端版本', + value: fingerprint.appVersion || '—', + detail: `Build ${fingerprint.buildNumber || '—'}`, + tone: 'info' as const, + icon: MonitorCheck, + }, + { + label: '平台架构', + value: fingerprint.platform || '—', + detail: fingerprint.arch || '—', + tone: 'info' as const, + icon: MonitorCheck, + }, + { + label: 'Chromium', + value: fingerprint.chromiumVersion ? `v${fingerprint.chromiumVersion}` : '—', + detail: fingerprint.originator || '—', + tone: 'normal' as const, icon: CloudCheck, }, { - label: '模型目录', - value: '已同步', - detail: '—', - tone: 'info', - icon: Boxes, - }, - { - label: '自动刷新', - value: '开启', - detail: `${diagnostics.accounts.active} 活跃`, - tone: 'success', + label: '更新时间', + value: fingerprint.updatedAt ? new Date(fingerprint.updatedAt).toLocaleString('zh-CN') : '', + detail: '', + tone: 'normal' as const, icon: RefreshCw, }, { - label: '事件记录', - value: '开启', - detail: `已存 ${formatNumber(diagnostics.requests.total)}`, - tone: 'success', - icon: ScrollText, - }, - { - label: '系统版本', - value: diagnostics.version, - detail: diagnostics.environment, - tone: 'normal', - icon: MonitorCheck, + label: 'User Agent', + value: fingerprint.userAgent || `${fingerprint.originator || 'Codex Desktop'}/${fingerprint.appVersion || '?'} (${fingerprint.platform || '?'}; ${fingerprint.arch || '?'})`, + detail: '', + tone: 'normal' as const, + icon: Gauge, }, ] } - function updateTrendData() { - // 模拟趋势数据,实际应从后端 API 获取 - const hours = ['00', '04', '08', '12', '16', '20', '24'] - trendPoints.value = hours.map(time => ({ + function updateTrendData(logs: any[], summary: any) { + if (!logs.length) { + trendPoints.value = [] + trendSummary.value = [ + { label: '输入', value: '—', tone: 'info' as const }, + { label: '输出', value: '—', tone: 'success' as const }, + { label: '缓存', value: '—', tone: 'normal' as const }, + ] + return + } + + // 按小时分桶,取最近 24 个桶 + const now = Date.now() + const buckets: Record = {} + for (let h = 23; h >= 0; h--) { + const key = new Date(now - h * 3600000).toISOString().slice(11, 13) + buckets[key] = { requests: 0, inputTokens: 0, outputTokens: 0, errors: 0, latencySum: 0, latencyCount: 0 } + } + + for (const log of logs) { + const hour = log.createdAt?.slice(11, 13) + if (!hour || !buckets[hour]) continue + buckets[hour].requests++ + const usage = log.metadata?.usage + if (usage) { + buckets[hour].inputTokens += usage.inputTokens || 0 + buckets[hour].outputTokens += usage.outputTokens || 0 + } + const code = log.statusCode + if (code && (code >= 400 || code < 0)) buckets[hour].errors++ + if (log.latencyMs) { + buckets[hour].latencySum += log.latencyMs + buckets[hour].latencyCount++ + } + } + + const entries = Object.entries(buckets) + trendPoints.value = entries.map(([time, b]) => ({ time, - requests: Math.floor(Math.random() * 1000), - tokens: Math.floor(Math.random() * 50000), - errors: Math.floor(Math.random() * 10), + requests: b.requests, + tokens: b.inputTokens + b.outputTokens, + errors: b.errors, + latency: b.latencyCount > 0 ? Math.round(b.latencySum / b.latencyCount) : 0, })) - const successRate = 99.5 + Math.random() * 0.5 - const peak = Math.max(...trendPoints.value.map(p => p.requests)) - const slowRequests = Math.floor(Math.random() * 100) + const totalInput = entries.reduce((s, [, b]) => s + b.inputTokens, 0) + const totalOutput = entries.reduce((s, [, b]) => s + b.outputTokens, 0) + const totalErrors = entries.reduce((s, [, b]) => s + b.errors, 0) trendSummary.value = [ - { label: '成功率', value: `${successRate.toFixed(2)}%`, tone: 'success' }, - { label: '峰值', value: formatNumber(peak), tone: 'info' }, - { label: '慢请求', value: String(slowRequests), tone: slowRequests > 50 ? 'warning' : 'info' }, + { label: '输入', value: totalInput > 0 ? formatTokens(totalInput) : '—', tone: 'info' as const }, + { label: '输出', value: totalOutput > 0 ? formatTokens(totalOutput) : '—', tone: 'success' as const }, + { label: '缓存', value: formatTokens(summary?.cachedTokens ?? 0), tone: (summary?.cachedTokens ?? 0) > 0 ? 'success' as const : 'normal' as const }, ] } @@ -307,6 +389,9 @@ export function useDashboard(): { accountUsage, serviceStatuses, eventLogs, + poolSummary, + capacityInfo, + rotationStrategy, refresh: loadDashboardData, } } diff --git a/web/src/views/dashboard/types.ts b/web/src/views/dashboard/types.ts index 1a1cac52a..2f744d9e0 100644 --- a/web/src/views/dashboard/types.ts +++ b/web/src/views/dashboard/types.ts @@ -21,6 +21,7 @@ export interface TrendPoint { requests: number tokens: number errors: number + latency: number } export interface TrendSummaryItem { @@ -59,3 +60,33 @@ export interface EventLogItem { latency: string tone: SemanticTone } + +export interface AccountPoolSummary { + total: number + active: number + expired: number + quotaExhausted: number + refreshing: number + disabled: number + banned: number +} + +export interface AccountCapacityInfo { + maxConcurrentPerAccount: number + totalSlots: number + usedSlots: number + availableSlots: number +} + +export interface AccountStatusRow { + label: string + description: string + value: string + tone: SemanticTone + icon: Component +} + +export interface ScheduleStat { + label: string + value: string +} diff --git a/web/src/views/settings/SettingsView.vue b/web/src/views/settings/SettingsView.vue index c81c77821..c783f7773 100644 --- a/web/src/views/settings/SettingsView.vue +++ b/web/src/views/settings/SettingsView.vue @@ -19,26 +19,21 @@ const loading = ref(true) const saving = ref(false) const form = ref({ - logging: { - enabled: true, - level: 'info', - }, - quota: { - warningThresholds: { - requestsRemaining: 1000, - tokensRemaining: 100000, - }, - }, + defaultModel: '', + refreshEnabled: true, + maxConcurrentPerAccount: 3, + requestIntervalMs: 0, + rotationStrategy: 'least_used', + quotaWarningThresholds: { primary: [], secondary: [] }, + quotaSkipExhausted: false, + logsEnabled: true, + logsCapacity: 5000, }) -const requestsThreshold = ref('1000') -const tokensThreshold = ref('100000') - -const logLevelOptions = [ - { label: 'Debug', value: 'debug' }, - { label: 'Info', value: 'info' }, - { label: 'Warning', value: 'warning' }, - { label: 'Error', value: 'error' }, +const rotationOptions = [ + { label: '最少使用优先', value: 'least_used' }, + { label: '轮询', value: 'round_robin' }, + { label: '随机', value: 'random' }, ] async function loadSettings() { @@ -46,8 +41,6 @@ async function loadSettings() { loading.value = true const data = await getSettings() form.value = data - requestsThreshold.value = String(data.quota.warningThresholds.requestsRemaining) - tokensThreshold.value = String(data.quota.warningThresholds.tokensRemaining) } catch (error: any) { toast.error(error.message || '加载失败') } finally { @@ -58,12 +51,18 @@ async function loadSettings() { async function handleSave() { try { saving.value = true - - // 转换字符串为数字 - form.value.quota.warningThresholds.requestsRemaining = parseInt(requestsThreshold.value) || 0 - form.value.quota.warningThresholds.tokensRemaining = parseInt(tokensThreshold.value) || 0 - - await updateSettings(form.value) + const patch: Partial = { + defaultModel: form.value.defaultModel, + refreshEnabled: form.value.refreshEnabled, + maxConcurrentPerAccount: form.value.maxConcurrentPerAccount, + requestIntervalMs: form.value.requestIntervalMs, + rotationStrategy: form.value.rotationStrategy, + quotaWarningThresholds: form.value.quotaWarningThresholds, + quotaSkipExhausted: form.value.quotaSkipExhausted, + logsEnabled: form.value.logsEnabled, + logsCapacity: form.value.logsCapacity, + } + await updateSettings(patch) toast.success('设置已保存') } catch (error: any) { toast.error(error.message || '保存失败') @@ -95,114 +94,87 @@ onMounted(() => {
- + -
-
-

- 日志配置 -

-

- 控制系统日志记录行为 -

-
+

模型配置

+
+ +
+ + + +

调度配置

+
+ + +
+
+ + +
+
+ + +
- -

- 记录系统运行事件和错误 -

+ +

调度时自动跳过已触发配额的账号

- + -
+

日志配置

+
+
+
+ +

记录系统运行事件和错误

+
+
+ -
+ + +

自动刷新

+
- - -

- 当账号剩余请求数低于此值时发出警告 -

-
- -
- - -

- 当账号剩余 Tokens 低于此值时发出警告 -

+ +

定时刷新账号令牌和配额信息

+