Fix provider list scope and key stability

This commit is contained in:
liucong2013
2026-04-17 21:34:00 +00:00
parent e801844f3b
commit feff3f376c
4 changed files with 95 additions and 63 deletions
@@ -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}>
+12 -4
View File
@@ -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"
+32
View File
@@ -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;
+9 -17
View File
@@ -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;
}
}