mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 10:40:50 +08:00
feat: add success and failure statistics display to AiProvidersPage, refactor data retrieval methods for better clarity and consistency
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
@use '../styles/variables' as *;
|
||||||
|
@use '../styles/mixins' as *;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -44,3 +47,37 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 成功失败次数统计样式
|
||||||
|
.cardStats {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 0 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statSuccess {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #22c55e;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statFailure {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #ef4444;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,9 +66,37 @@ const parseExcludedModels = (text: string): string[] =>
|
|||||||
|
|
||||||
const excludedModelsToText = (models?: string[]) => (Array.isArray(models) ? models.join('\n') : '');
|
const excludedModelsToText = (models?: string[]) => (Array.isArray(models) ? models.join('\n') : '');
|
||||||
|
|
||||||
// 根据 auth_index 获取统计数据
|
// 根据 source (apiKey) 获取统计数据 - 与旧版逻辑一致
|
||||||
const getStatsByAuthIndex = (authIndex: string, keyStats: KeyStats): KeyStatBucket => {
|
const getStatsBySource = (
|
||||||
return keyStats.byAuthIndex?.[authIndex] ?? { success: 0, failure: 0 };
|
apiKey: string,
|
||||||
|
keyStats: KeyStats,
|
||||||
|
maskFn: (key: string) => string
|
||||||
|
): KeyStatBucket => {
|
||||||
|
const bySource = keyStats.bySource ?? {};
|
||||||
|
const masked = maskFn(apiKey);
|
||||||
|
return bySource[apiKey] || bySource[masked] || { success: 0, failure: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 对于 OpenAI 提供商,汇总所有 apiKeyEntries 的统计 - 与旧版逻辑一致
|
||||||
|
const getOpenAIProviderStats = (
|
||||||
|
apiKeyEntries: ApiKeyEntry[] | undefined,
|
||||||
|
keyStats: KeyStats,
|
||||||
|
maskFn: (key: string) => string
|
||||||
|
): KeyStatBucket => {
|
||||||
|
const bySource = keyStats.bySource ?? {};
|
||||||
|
let totalSuccess = 0;
|
||||||
|
let totalFailure = 0;
|
||||||
|
|
||||||
|
(apiKeyEntries || []).forEach((entry) => {
|
||||||
|
const key = entry?.apiKey || '';
|
||||||
|
if (!key) return;
|
||||||
|
const masked = maskFn(key);
|
||||||
|
const stats = bySource[key] || bySource[masked] || { success: 0, failure: 0 };
|
||||||
|
totalSuccess += stats.success;
|
||||||
|
totalFailure += stats.failure;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: totalSuccess, failure: totalFailure };
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildApiKeyEntry = (input?: Partial<ApiKeyEntry>): ApiKeyEntry => ({
|
const buildApiKeyEntry = (input?: Partial<ApiKeyEntry>): ApiKeyEntry => ({
|
||||||
@@ -505,20 +533,31 @@ export function AiProvidersPage() {
|
|||||||
{renderList<GeminiKeyConfig>(
|
{renderList<GeminiKeyConfig>(
|
||||||
geminiKeys,
|
geminiKeys,
|
||||||
(item) => item.apiKey,
|
(item) => item.apiKey,
|
||||||
(item, index) => (
|
(item, index) => {
|
||||||
<Fragment>
|
const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey);
|
||||||
<div className="item-title">
|
return (
|
||||||
{t('ai_providers.gemini_item_title')} #{index + 1}
|
<Fragment>
|
||||||
</div>
|
<div className="item-title">
|
||||||
<div className="item-subtitle">{maskApiKey(item.apiKey)}</div>
|
{t('ai_providers.gemini_item_title')} #{index + 1}
|
||||||
{item.baseUrl && <div className="pill">{item.baseUrl}</div>}
|
|
||||||
{item.excludedModels?.length ? (
|
|
||||||
<div className="item-subtitle">
|
|
||||||
{t('ai_providers.excluded_models_count', { count: item.excludedModels.length })}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
<div className="item-subtitle">{maskApiKey(item.apiKey)}</div>
|
||||||
</Fragment>
|
{item.baseUrl && <div className="pill">{item.baseUrl}</div>}
|
||||||
),
|
{item.excludedModels?.length ? (
|
||||||
|
<div className="item-subtitle">
|
||||||
|
{t('ai_providers.excluded_models_count', { count: item.excludedModels.length })}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.cardStats}>
|
||||||
|
<span className={styles.statSuccess}>
|
||||||
|
{t('stats.success')}:{stats.success}次
|
||||||
|
</span>
|
||||||
|
<span className={styles.statFailure}>
|
||||||
|
{t('stats.failure')}:{stats.failure}次
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
(index) => openGeminiModal(index),
|
(index) => openGeminiModal(index),
|
||||||
(item) => deleteGemini(item.apiKey),
|
(item) => deleteGemini(item.apiKey),
|
||||||
t('ai_providers.gemini_add_button')
|
t('ai_providers.gemini_add_button')
|
||||||
@@ -536,13 +575,24 @@ export function AiProvidersPage() {
|
|||||||
{renderList<ProviderKeyConfig>(
|
{renderList<ProviderKeyConfig>(
|
||||||
codexConfigs,
|
codexConfigs,
|
||||||
(item) => item.apiKey,
|
(item) => item.apiKey,
|
||||||
(item) => (
|
(item, _index) => {
|
||||||
<Fragment>
|
const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey);
|
||||||
<div className="item-title">{item.baseUrl || t('ai_providers.codex_item_title')}</div>
|
return (
|
||||||
<div className="item-subtitle">{maskApiKey(item.apiKey)}</div>
|
<Fragment>
|
||||||
{item.proxyUrl && <div className="pill">{item.proxyUrl}</div>}
|
<div className="item-title">{item.baseUrl || t('ai_providers.codex_item_title')}</div>
|
||||||
</Fragment>
|
<div className="item-subtitle">{maskApiKey(item.apiKey)}</div>
|
||||||
),
|
{item.proxyUrl && <div className="pill">{item.proxyUrl}</div>}
|
||||||
|
<div className={styles.cardStats}>
|
||||||
|
<span className={styles.statSuccess}>
|
||||||
|
{t('stats.success')}:{stats.success}次
|
||||||
|
</span>
|
||||||
|
<span className={styles.statFailure}>
|
||||||
|
{t('stats.failure')}:{stats.failure}次
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
(index) => openProviderModal('codex', index),
|
(index) => openProviderModal('codex', index),
|
||||||
(item) => deleteProviderEntry('codex', item.apiKey),
|
(item) => deleteProviderEntry('codex', item.apiKey),
|
||||||
t('ai_providers.codex_add_button')
|
t('ai_providers.codex_add_button')
|
||||||
@@ -560,18 +610,29 @@ export function AiProvidersPage() {
|
|||||||
{renderList<ProviderKeyConfig>(
|
{renderList<ProviderKeyConfig>(
|
||||||
claudeConfigs,
|
claudeConfigs,
|
||||||
(item) => item.apiKey,
|
(item) => item.apiKey,
|
||||||
(item) => (
|
(item, _index) => {
|
||||||
<Fragment>
|
const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey);
|
||||||
<div className="item-title">{item.baseUrl || t('ai_providers.claude_item_title')}</div>
|
return (
|
||||||
<div className="item-subtitle">{maskApiKey(item.apiKey)}</div>
|
<Fragment>
|
||||||
{item.proxyUrl && <div className="pill">{item.proxyUrl}</div>}
|
<div className="item-title">{item.baseUrl || t('ai_providers.claude_item_title')}</div>
|
||||||
{item.models?.length ? (
|
<div className="item-subtitle">{maskApiKey(item.apiKey)}</div>
|
||||||
<div className="item-subtitle">
|
{item.proxyUrl && <div className="pill">{item.proxyUrl}</div>}
|
||||||
{t('ai_providers.claude_models_count')}: {item.models.length}
|
{item.models?.length ? (
|
||||||
|
<div className="item-subtitle">
|
||||||
|
{t('ai_providers.claude_models_count')}: {item.models.length}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.cardStats}>
|
||||||
|
<span className={styles.statSuccess}>
|
||||||
|
{t('stats.success')}:{stats.success}次
|
||||||
|
</span>
|
||||||
|
<span className={styles.statFailure}>
|
||||||
|
{t('stats.failure')}:{stats.failure}次
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</Fragment>
|
||||||
</Fragment>
|
);
|
||||||
),
|
},
|
||||||
(index) => openProviderModal('claude', index),
|
(index) => openProviderModal('claude', index),
|
||||||
(item) => deleteProviderEntry('claude', item.apiKey),
|
(item) => deleteProviderEntry('claude', item.apiKey),
|
||||||
t('ai_providers.claude_add_button')
|
t('ai_providers.claude_add_button')
|
||||||
@@ -589,19 +650,30 @@ export function AiProvidersPage() {
|
|||||||
{renderList<OpenAIProviderConfig>(
|
{renderList<OpenAIProviderConfig>(
|
||||||
openaiProviders,
|
openaiProviders,
|
||||||
(item) => item.name,
|
(item) => item.name,
|
||||||
(item) => (
|
(item, _index) => {
|
||||||
<Fragment>
|
const stats = getOpenAIProviderStats(item.apiKeyEntries, keyStats, maskApiKey);
|
||||||
<div className="item-title">{item.name}</div>
|
return (
|
||||||
<div className="item-subtitle">{item.baseUrl}</div>
|
<Fragment>
|
||||||
<div className="pill">
|
<div className="item-title">{item.name}</div>
|
||||||
{t('ai_providers.openai_keys_count')}: {item.apiKeyEntries?.length || 0}
|
<div className="item-subtitle">{item.baseUrl}</div>
|
||||||
</div>
|
<div className="pill">
|
||||||
<div className="pill">
|
{t('ai_providers.openai_keys_count')}: {item.apiKeyEntries?.length || 0}
|
||||||
{t('ai_providers.openai_models_count')}: {item.models?.length || 0}
|
</div>
|
||||||
</div>
|
<div className="pill">
|
||||||
{item.testModel && <div className="pill">{item.testModel}</div>}
|
{t('ai_providers.openai_models_count')}: {item.models?.length || 0}
|
||||||
</Fragment>
|
</div>
|
||||||
),
|
{item.testModel && <div className="pill">{item.testModel}</div>}
|
||||||
|
<div className={styles.cardStats}>
|
||||||
|
<span className={styles.statSuccess}>
|
||||||
|
{t('stats.success')}:{stats.success}次
|
||||||
|
</span>
|
||||||
|
<span className={styles.statFailure}>
|
||||||
|
{t('stats.failure')}:{stats.failure}次
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
(index) => openOpenaiModal(index),
|
(index) => openOpenaiModal(index),
|
||||||
(item) => deleteOpenai(item.name),
|
(item) => deleteOpenai(item.name),
|
||||||
t('ai_providers.openai_add_button')
|
t('ai_providers.openai_add_button')
|
||||||
|
|||||||
Reference in New Issue
Block a user