feat(providers): add visual status bar for API key health monitoring

This commit is contained in:
Supra4E8C
2025-12-27 15:02:32 +08:00
parent 0b2e6efe28
commit b3808add0f
3 changed files with 278 additions and 3 deletions

View File

@@ -397,6 +397,79 @@
line-height: 1.5;
}
// 状态监测栏
.statusBar {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
padding: 8px 0;
max-width: 280px;
}
.statusBlocks {
display: flex;
gap: 2px;
flex: 1;
min-width: 180px;
}
.statusBlock {
flex: 1;
height: 8px;
border-radius: 2px;
min-width: 6px;
transition: transform 0.15s ease, opacity 0.15s ease;
&:hover {
transform: scaleY(1.5);
opacity: 0.85;
}
}
.statusBlockSuccess {
background-color: var(--success-color, #22c55e);
}
.statusBlockFailure {
background-color: var(--danger-color, #ef4444);
}
.statusBlockMixed {
background-color: var(--warning-color, #f59e0b);
}
.statusBlockIdle {
background-color: var(--border-secondary, #e5e7eb);
}
.statusRate {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
padding: 4px 8px;
border-radius: 6px;
background: var(--bg-tertiary);
}
.statusRateHigh {
color: var(--success-badge-text, #065f46);
background: var(--success-badge-bg, #d1fae5);
}
.statusRateMedium {
color: var(--warning-text, #92400e);
background: var(--warning-bg, #fef3c7);
}
.statusRateLow {
color: var(--failure-badge-text, #991b1b);
background: var(--failure-badge-bg, #fee2e2);
}
// 暗色主题适配
:global([data-theme='dark']) {
.headerBadge {
@@ -436,4 +509,23 @@
.apiKeyEntryIndex {
background: var(--primary-color);
}
.statusBlockIdle {
background-color: var(--border-primary, #374151);
}
.statusRateHigh {
background: rgba(34, 197, 94, 0.2);
color: #86efac;
}
.statusRateMedium {
background: rgba(251, 191, 36, 0.2);
color: #fde68a;
}
.statusRateLow {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
}
}

View File

@@ -24,7 +24,8 @@ import type {
AmpcodeConfig,
AmpcodeModelMapping,
} from '@/types';
import type { KeyStats, KeyStatBucket } from '@/utils/usage';
import type { KeyStats, KeyStatBucket, UsageDetail } from '@/utils/usage';
import { collectUsageDetails, calculateStatusBarData } from '@/utils/usage';
import type { ModelInfo } from '@/utils/models';
import { headersToEntries, buildHeaderObject, type HeaderEntry } from '@/utils/headers';
import { maskApiKey } from '@/utils/format';
@@ -202,6 +203,7 @@ export function AiProvidersPage() {
const [claudeConfigs, setClaudeConfigs] = useState<ProviderKeyConfig[]>([]);
const [openaiProviders, setOpenaiProviders] = useState<OpenAIProviderConfig[]>([]);
const [keyStats, setKeyStats] = useState<KeyStats>({ bySource: {}, byAuthIndex: {} });
const [usageDetails, setUsageDetails] = useState<UsageDetail[]>([]);
const [modal, setModal] = useState<ProviderModal | null>(null);
@@ -273,11 +275,16 @@ export function AiProvidersPage() {
[openaiForm.modelEntries]
);
// 加载 key 统计
// 加载 key 统计和 usage 明细
const loadKeyStats = useCallback(async () => {
try {
const stats = await usageApi.getKeyStats();
const usageResponse = await usageApi.getUsage();
const usageData = usageResponse?.usage ?? usageResponse;
const stats = await usageApi.getKeyStats(usageData);
setKeyStats(stats);
// 收集 usage 明细用于状态栏
const details = collectUsageDetails(usageData);
setUsageDetails(details);
} catch {
// 静默失败
}
@@ -1090,6 +1097,77 @@ export function AiProvidersPage() {
);
};
// 渲染状态监测栏
const renderStatusBar = (apiKey: string) => {
const statusData = calculateStatusBarData(usageDetails, apiKey);
const hasData = statusData.totalSuccess + statusData.totalFailure > 0;
const rateClass = !hasData
? ''
: statusData.successRate >= 90
? styles.statusRateHigh
: statusData.successRate >= 50
? styles.statusRateMedium
: styles.statusRateLow;
return (
<div className={styles.statusBar}>
<div className={styles.statusBlocks}>
{statusData.blocks.map((state, idx) => {
const blockClass =
state === 'success'
? styles.statusBlockSuccess
: state === 'failure'
? styles.statusBlockFailure
: state === 'mixed'
? styles.statusBlockMixed
: styles.statusBlockIdle;
return <div key={idx} className={`${styles.statusBlock} ${blockClass}`} />;
})}
</div>
<span className={`${styles.statusRate} ${rateClass}`}>
{hasData ? `${statusData.successRate.toFixed(1)}%` : '--'}
</span>
</div>
);
};
// 渲染 OpenAI 提供商的状态栏(汇总多个 apiKey
const renderOpenAIStatusBar = (apiKeyEntries: ApiKeyEntry[] | undefined) => {
// 合并所有 apiKey 的 usage details
const allKeys = (apiKeyEntries || []).map((e) => e.apiKey).filter(Boolean);
const filteredDetails = usageDetails.filter((detail) => allKeys.includes(detail.source));
const statusData = calculateStatusBarData(filteredDetails);
const hasData = statusData.totalSuccess + statusData.totalFailure > 0;
const rateClass = !hasData
? ''
: statusData.successRate >= 90
? styles.statusRateHigh
: statusData.successRate >= 50
? styles.statusRateMedium
: styles.statusRateLow;
return (
<div className={styles.statusBar}>
<div className={styles.statusBlocks}>
{statusData.blocks.map((state, idx) => {
const blockClass =
state === 'success'
? styles.statusBlockSuccess
: state === 'failure'
? styles.statusBlockFailure
: state === 'mixed'
? styles.statusBlockMixed
: styles.statusBlockIdle;
return <div key={idx} className={`${styles.statusBlock} ${blockClass}`} />;
})}
</div>
<span className={`${styles.statusRate} ${rateClass}`}>
{hasData ? `${statusData.successRate.toFixed(1)}%` : '--'}
</span>
</div>
);
};
const renderList = <T,>(
items: T[],
keyField: (item: T) => string,
@@ -1254,6 +1332,8 @@ export function AiProvidersPage() {
{t('stats.failure')}: {stats.failure}
</span>
</div>
{/* 状态监测栏 */}
{renderStatusBar(item.apiKey)}
</Fragment>
);
},
@@ -1370,6 +1450,8 @@ export function AiProvidersPage() {
{t('stats.failure')}: {stats.failure}
</span>
</div>
{/* 状态监测栏 */}
{renderStatusBar(item.apiKey)}
</Fragment>
);
},
@@ -1502,6 +1584,8 @@ export function AiProvidersPage() {
{t('stats.failure')}: {stats.failure}
</span>
</div>
{/* 状态监测栏 */}
{renderStatusBar(item.apiKey)}
</Fragment>
);
},
@@ -1721,6 +1805,8 @@ export function AiProvidersPage() {
{t('stats.failure')}: {stats.failure}
</span>
</div>
{/* 状态监测栏(汇总) */}
{renderOpenAIStatusBar(item.apiKeyEntries)}
</Fragment>
);
},