mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Fix provider list scope and key stability
This commit is contained in:
@@ -35,6 +35,11 @@ interface OpenAISectionProps {
|
||||
onDelete: (index: number) => void;
|
||||
}
|
||||
|
||||
interface IndexedOpenAIProvider {
|
||||
config: OpenAIProviderConfig;
|
||||
originalIndex: number;
|
||||
}
|
||||
|
||||
export function OpenAISection({
|
||||
configs,
|
||||
keyStats,
|
||||
@@ -149,10 +154,11 @@ export function OpenAISection({
|
||||
return cache;
|
||||
}, [configs, usageDetailsBySource]);
|
||||
|
||||
const sortedConfigs = useMemo(() => {
|
||||
const filtered = configs.filter((provider) => {
|
||||
const sortedConfigs = useMemo<IndexedOpenAIProvider[]>(() => {
|
||||
const indexed = configs.map((config, originalIndex) => ({ config, originalIndex }));
|
||||
const filtered = indexed.filter(({ config }) => {
|
||||
if (selectedModels.size === 0) return true;
|
||||
return provider.models?.some((model) => selectedModels.has(model.name));
|
||||
return config.models?.some((model) => selectedModels.has(model.name));
|
||||
});
|
||||
|
||||
const sorted = [...filtered];
|
||||
@@ -160,40 +166,40 @@ export function OpenAISection({
|
||||
const providerStats =
|
||||
sortOption === 'recent-success'
|
||||
? new Map(
|
||||
sorted.map((provider) => [
|
||||
provider,
|
||||
getOpenAIProviderStats(provider.apiKeyEntries, keyStats, provider.prefix),
|
||||
sorted.map(({ config }) => [
|
||||
config,
|
||||
getOpenAIProviderStats(config.apiKeyEntries, keyStats, config.prefix),
|
||||
])
|
||||
)
|
||||
: null;
|
||||
|
||||
switch (sortOption) {
|
||||
case 'name':
|
||||
sorted.sort((a, b) => direction * a.name.localeCompare(b.name));
|
||||
sorted.sort((a, b) => direction * a.config.name.localeCompare(b.config.name));
|
||||
break;
|
||||
case 'priority':
|
||||
sorted.sort((a, b) => {
|
||||
const priorityA = a.priority ?? Number.MAX_SAFE_INTEGER;
|
||||
const priorityB = b.priority ?? Number.MAX_SAFE_INTEGER;
|
||||
const priorityA = a.config.priority ?? Number.MAX_SAFE_INTEGER;
|
||||
const priorityB = b.config.priority ?? Number.MAX_SAFE_INTEGER;
|
||||
const priorityDiff = priorityA - priorityB;
|
||||
|
||||
if (priorityDiff !== 0) {
|
||||
return direction * priorityDiff;
|
||||
}
|
||||
|
||||
return direction * a.name.localeCompare(b.name);
|
||||
return direction * a.config.name.localeCompare(b.config.name);
|
||||
});
|
||||
break;
|
||||
case 'recent-success':
|
||||
sorted.sort((a, b) => {
|
||||
const successDiff =
|
||||
(providerStats?.get(a)?.success ?? 0) - (providerStats?.get(b)?.success ?? 0);
|
||||
(providerStats?.get(a.config)?.success ?? 0) - (providerStats?.get(b.config)?.success ?? 0);
|
||||
|
||||
if (successDiff !== 0) {
|
||||
return direction * successDiff;
|
||||
}
|
||||
|
||||
return direction * a.name.localeCompare(b.name);
|
||||
return direction * a.config.name.localeCompare(b.config.name);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
@@ -203,11 +209,6 @@ export function OpenAISection({
|
||||
return sorted;
|
||||
}, [configs, sortOption, sortDirection, keyStats, selectedModels]);
|
||||
|
||||
const getProviderKey = (item: OpenAIProviderConfig) => `${item.name}-${item.prefix ?? ''}-${item.baseUrl}`;
|
||||
|
||||
const getProviderIndex = (item: OpenAIProviderConfig) =>
|
||||
configs.findIndex((config) => config === item);
|
||||
|
||||
const toggleModelSelection = (modelName: string) => {
|
||||
setSelectedModels((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -351,49 +352,48 @@ export function OpenAISection({
|
||||
}
|
||||
extra={renderToolbar('top')}
|
||||
>
|
||||
<ProviderList<OpenAIProviderConfig>
|
||||
<ProviderList<IndexedOpenAIProvider>
|
||||
items={sortedConfigs}
|
||||
loading={loading}
|
||||
keyField={(item) => getProviderKey(item)}
|
||||
keyField={(item) => `openai-provider-${item.originalIndex}`}
|
||||
emptyTitle={t('ai_providers.openai_empty_title')}
|
||||
emptyDescription={t('ai_providers.openai_empty_desc')}
|
||||
listClassName={styles.providerList}
|
||||
rowClassName={styles.providerCard}
|
||||
metaClassName={styles.providerMeta}
|
||||
actionsClassName={styles.providerActions}
|
||||
onEdit={(item) => {
|
||||
const index = getProviderIndex(item);
|
||||
if (index >= 0) {
|
||||
onEdit(index);
|
||||
}
|
||||
onEdit(item.originalIndex);
|
||||
}}
|
||||
onDelete={(item) => {
|
||||
const index = getProviderIndex(item);
|
||||
if (index >= 0) {
|
||||
onDelete(index);
|
||||
}
|
||||
onDelete(item.originalIndex);
|
||||
}}
|
||||
actionsDisabled={actionsDisabled}
|
||||
renderContent={(item) => {
|
||||
const stats = getOpenAIProviderStats(item.apiKeyEntries, keyStats, item.prefix);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const apiKeyEntries = item.apiKeyEntries || [];
|
||||
const statusData = statusBarCache.get(item.name) || calculateStatusBarData([]);
|
||||
const provider = item.config;
|
||||
const stats = getOpenAIProviderStats(provider.apiKeyEntries, keyStats, provider.prefix);
|
||||
const headerEntries = Object.entries(provider.headers || {});
|
||||
const apiKeyEntries = provider.apiKeyEntries || [];
|
||||
const statusData = statusBarCache.get(provider.name) || calculateStatusBarData([]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="item-title">{item.name}</div>
|
||||
{item.priority !== undefined && (
|
||||
<div className={styles.providerTitle}>{provider.name}</div>
|
||||
{provider.priority !== undefined && (
|
||||
<div className={styles.fieldRow}>
|
||||
<span className={styles.fieldLabel}>{t('common.priority')}:</span>
|
||||
<span className={styles.fieldValue}>{item.priority}</span>
|
||||
<span className={styles.fieldValue}>{provider.priority}</span>
|
||||
</div>
|
||||
)}
|
||||
{item.prefix && (
|
||||
{provider.prefix && (
|
||||
<div className={styles.fieldRow}>
|
||||
<span className={styles.fieldLabel}>{t('common.prefix')}:</span>
|
||||
<span className={styles.fieldValue}>{item.prefix}</span>
|
||||
<span className={styles.fieldValue}>{provider.prefix}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.fieldRow}>
|
||||
<span className={styles.fieldLabel}>{t('common.base_url')}:</span>
|
||||
<span className={styles.fieldValue}>{item.baseUrl}</span>
|
||||
<span className={styles.fieldValue}>{provider.baseUrl}</span>
|
||||
</div>
|
||||
{headerEntries.length > 0 && (
|
||||
<div className={styles.headerBadgeList}>
|
||||
@@ -439,11 +439,11 @@ export function OpenAISection({
|
||||
)}
|
||||
<div className={styles.fieldRow} style={{ marginTop: '8px' }}>
|
||||
<span className={styles.fieldLabel}>{t('ai_providers.openai_models_count')}:</span>
|
||||
<span className={styles.fieldValue}>{item.models?.length || 0}</span>
|
||||
<span className={styles.fieldValue}>{provider.models?.length || 0}</span>
|
||||
</div>
|
||||
{item.models?.length ? (
|
||||
{provider.models?.length ? (
|
||||
<div className={styles.modelTagList}>
|
||||
{item.models.map((model) => (
|
||||
{provider.models.map((model) => (
|
||||
<span key={model.name} className={styles.modelTag}>
|
||||
<span className={styles.modelName}>{model.name}</span>
|
||||
{model.alias && model.alias !== model.name && (
|
||||
@@ -453,10 +453,10 @@ export function OpenAISection({
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{item.testModel && (
|
||||
{provider.testModel && (
|
||||
<div className={styles.fieldRow}>
|
||||
<span className={styles.fieldLabel}>Test Model:</span>
|
||||
<span className={styles.fieldValue}>{item.testModel}</span>
|
||||
<span className={styles.fieldValue}>{provider.testModel}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.cardStats}>
|
||||
|
||||
@@ -16,6 +16,10 @@ interface ProviderListProps<T> {
|
||||
actionsDisabled?: boolean;
|
||||
getRowDisabled?: (item: T, index: number) => boolean;
|
||||
renderExtraActions?: (item: T, index: number) => ReactNode;
|
||||
listClassName?: string;
|
||||
rowClassName?: string;
|
||||
metaClassName?: string;
|
||||
actionsClassName?: string;
|
||||
}
|
||||
|
||||
export function ProviderList<T>({
|
||||
@@ -31,6 +35,10 @@ export function ProviderList<T>({
|
||||
actionsDisabled = false,
|
||||
getRowDisabled,
|
||||
renderExtraActions,
|
||||
listClassName,
|
||||
rowClassName,
|
||||
metaClassName,
|
||||
actionsClassName,
|
||||
}: ProviderListProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -43,17 +51,17 @@ export function ProviderList<T>({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="item-list">
|
||||
<div className={listClassName ?? 'item-list'}>
|
||||
{items.map((item, index) => {
|
||||
const rowDisabled = getRowDisabled ? getRowDisabled(item, index) : false;
|
||||
return (
|
||||
<div
|
||||
key={keyField(item, index)}
|
||||
className="item-row"
|
||||
className={rowClassName ?? 'item-row'}
|
||||
style={rowDisabled ? { opacity: 0.6 } : undefined}
|
||||
>
|
||||
<div className="item-meta">{renderContent(item, index)}</div>
|
||||
<div className="item-actions">
|
||||
<div className={metaClassName ?? 'item-meta'}>{renderContent(item, index)}</div>
|
||||
<div className={actionsClassName ?? 'item-actions'}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
|
||||
@@ -66,6 +66,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
.providerCard {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-md;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: $spacing-sm;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.providerMeta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.providerActions {
|
||||
display: flex;
|
||||
gap: $spacing-sm;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.providerTitle {
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
// 排序控件
|
||||
.sortControls {
|
||||
display: flex;
|
||||
|
||||
@@ -642,35 +642,27 @@ textarea {
|
||||
}
|
||||
|
||||
.item-list {
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-sm;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $spacing-md;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-md;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: $spacing-sm;
|
||||
min-height: 0;
|
||||
|
||||
.item-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
@@ -687,9 +679,9 @@ textarea {
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user