feat: Added disabling features for some of the AI providers

This commit is contained in:
Supra4E8C
2025-12-14 12:21:54 +08:00
parent 20a69a25bc
commit aea1ceb6be
2 changed files with 248 additions and 43 deletions

View File

@@ -148,6 +148,8 @@
"excluded_models_placeholder": "Comma or newline separated, e.g. gemini-1.5-pro, gemini-1.5-flash",
"excluded_models_hint": "Leave empty to allow all models; values are trimmed and deduplicated automatically.",
"excluded_models_count": "Excluding {count} models",
"config_toggle_label": "Enabled",
"config_disabled_badge": "Disabled",
"codex_title": "Codex API Configuration",
"codex_add_button": "Add Configuration",
"codex_empty_title": "No Codex Configuration",
@@ -636,6 +638,8 @@
"claude_config_added": "Claude configuration added successfully",
"claude_config_updated": "Claude configuration updated successfully",
"claude_config_deleted": "Claude configuration deleted successfully",
"config_enabled": "Configuration enabled",
"config_disabled": "Configuration disabled",
"field_required": "Required fields cannot be empty",
"openai_provider_required": "Please fill in provider name and Base URL",
"openai_provider_added": "OpenAI provider added successfully",

View File

@@ -54,6 +54,26 @@ interface AmpcodeFormState {
mappingEntries: ModelEntry[];
}
const DISABLE_ALL_MODELS_RULE = '*';
const hasDisableAllModelsRule = (models?: string[]) =>
Array.isArray(models) && models.some((model) => String(model ?? '').trim() === DISABLE_ALL_MODELS_RULE);
const stripDisableAllModelsRule = (models?: string[]) =>
Array.isArray(models)
? models.filter((model) => String(model ?? '').trim() !== DISABLE_ALL_MODELS_RULE)
: [];
const withDisableAllModelsRule = (models?: string[]) => {
const base = stripDisableAllModelsRule(models);
return [...base, DISABLE_ALL_MODELS_RULE];
};
const withoutDisableAllModelsRule = (models?: string[]) => {
const base = stripDisableAllModelsRule(models);
return base;
};
const parseExcludedModels = (text: string): string[] =>
text
.split(/[\n,]+/)
@@ -213,6 +233,7 @@ export function AiProvidersPage() {
const [openaiTestStatus, setOpenaiTestStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [openaiTestMessage, setOpenaiTestMessage] = useState('');
const [saving, setSaving] = useState(false);
const [configSwitchingKey, setConfigSwitchingKey] = useState<string | null>(null);
const disableControls = useMemo(() => connectionStatus !== 'connected', [connectionStatus]);
const filteredOpenaiDiscoveryModels = useMemo(() => {
@@ -726,6 +747,90 @@ export function AiProvidersPage() {
}
};
const setConfigEnabled = async (
provider: 'gemini' | 'codex' | 'claude',
index: number,
enabled: boolean
) => {
if (provider === 'gemini') {
const current = geminiKeys[index];
if (!current) return;
const switchingKey = `${provider}:${current.apiKey}`;
setConfigSwitchingKey(switchingKey);
const previousList = geminiKeys;
const nextExcluded = enabled
? withoutDisableAllModelsRule(current.excludedModels)
: withDisableAllModelsRule(current.excludedModels);
const nextItem: GeminiKeyConfig = { ...current, excludedModels: nextExcluded };
const nextList = previousList.map((item, idx) => (idx === index ? nextItem : item));
setGeminiKeys(nextList);
updateConfigValue('gemini-api-key', nextList);
clearCache('gemini-api-key');
try {
await providersApi.saveGeminiKeys(nextList);
showNotification(enabled ? t('notification.config_enabled') : t('notification.config_disabled'), 'success');
} catch (err: any) {
setGeminiKeys(previousList);
updateConfigValue('gemini-api-key', previousList);
clearCache('gemini-api-key');
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
} finally {
setConfigSwitchingKey(null);
}
return;
}
const source = provider === 'codex' ? codexConfigs : claudeConfigs;
const current = source[index];
if (!current) return;
const switchingKey = `${provider}:${current.apiKey}`;
setConfigSwitchingKey(switchingKey);
const previousList = source;
const nextExcluded = enabled
? withoutDisableAllModelsRule(current.excludedModels)
: withDisableAllModelsRule(current.excludedModels);
const nextItem: ProviderKeyConfig = { ...current, excludedModels: nextExcluded };
const nextList = previousList.map((item, idx) => (idx === index ? nextItem : item));
if (provider === 'codex') {
setCodexConfigs(nextList);
updateConfigValue('codex-api-key', nextList);
clearCache('codex-api-key');
} else {
setClaudeConfigs(nextList);
updateConfigValue('claude-api-key', nextList);
clearCache('claude-api-key');
}
try {
if (provider === 'codex') {
await providersApi.saveCodexConfigs(nextList);
} else {
await providersApi.saveClaudeConfigs(nextList);
}
showNotification(enabled ? t('notification.config_enabled') : t('notification.config_disabled'), 'success');
} catch (err: any) {
if (provider === 'codex') {
setCodexConfigs(previousList);
updateConfigValue('codex-api-key', previousList);
clearCache('codex-api-key');
} else {
setClaudeConfigs(previousList);
updateConfigValue('claude-api-key', previousList);
clearCache('claude-api-key');
}
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
} finally {
setConfigSwitchingKey(null);
}
};
const saveProvider = async (type: 'codex' | 'claude') => {
const baseUrl = (providerForm.baseUrl ?? '').trim();
if (!baseUrl) {
@@ -735,6 +840,8 @@ export function AiProvidersPage() {
setSaving(true);
try {
const source = type === 'codex' ? codexConfigs : claudeConfigs;
const payload: ProviderKeyConfig = {
apiKey: providerForm.apiKey.trim(),
baseUrl,
@@ -744,7 +851,6 @@ export function AiProvidersPage() {
excludedModels: parseExcludedModels(providerForm.excludedText)
};
const source = type === 'codex' ? codexConfigs : claudeConfigs;
const nextList =
modal?.type === type && modal.index !== null
? source.map((item, idx) => (idx === modal.index ? payload : item))
@@ -908,7 +1014,11 @@ export function AiProvidersPage() {
onEdit: (index: number) => void,
onDelete: (item: T) => void,
addLabel: string,
deleteLabel?: string
deleteLabel?: string,
options?: {
getRowDisabled?: (item: T, index: number) => boolean;
renderExtraActions?: (item: T, index: number) => ReactNode;
}
) => {
if (loading) {
return <div className="hint">{t('common.loading')}</div>;
@@ -930,19 +1040,33 @@ export function AiProvidersPage() {
return (
<div className="item-list">
{items.map((item, index) => (
<div key={keyField(item)} className="item-row">
{items.map((item, index) => {
const rowDisabled = options?.getRowDisabled ? options.getRowDisabled(item, index) : false;
return (
<div key={keyField(item)} className="item-row" style={rowDisabled ? { opacity: 0.6 } : undefined}>
<div className="item-meta">{renderContent(item, index)}</div>
<div className="item-actions">
<Button variant="secondary" size="sm" onClick={() => onEdit(index)} disabled={disableControls}>
{options?.renderExtraActions ? options.renderExtraActions(item, index) : null}
<Button
variant="secondary"
size="sm"
onClick={() => onEdit(index)}
disabled={disableControls || saving || Boolean(configSwitchingKey)}
>
{t('common.edit')}
</Button>
<Button variant="danger" size="sm" onClick={() => onDelete(item)} disabled={disableControls}>
<Button
variant="danger"
size="sm"
onClick={() => onDelete(item)}
disabled={disableControls || saving || Boolean(configSwitchingKey)}
>
{deleteLabel || t('common.delete')}
</Button>
</div>
</div>
))}
);
})}
</div>
);
};
@@ -954,7 +1078,11 @@ export function AiProvidersPage() {
<Card
title={t('ai_providers.gemini_title')}
extra={
<Button size="sm" onClick={() => openGeminiModal(null)} disabled={disableControls}>
<Button
size="sm"
onClick={() => openGeminiModal(null)}
disabled={disableControls || saving || Boolean(configSwitchingKey)}
>
{t('ai_providers.gemini_add_button')}
</Button>
}
@@ -965,6 +1093,8 @@ export function AiProvidersPage() {
(item, index) => {
const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey);
const headerEntries = Object.entries(item.headers || {});
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
const excludedModels = item.excludedModels ?? [];
return (
<Fragment>
<div className="item-title">
@@ -992,14 +1122,19 @@ export function AiProvidersPage() {
))}
</div>
)}
{configDisabled && (
<div className="status-badge warning" style={{ marginTop: 8, marginBottom: 0 }}>
{t('ai_providers.config_disabled_badge')}
</div>
)}
{/* 排除模型徽章 */}
{item.excludedModels?.length ? (
{excludedModels.length ? (
<div className={styles.excludedModelsSection}>
<div className={styles.excludedModelsLabel}>
{t('ai_providers.excluded_models_count', { count: item.excludedModels.length })}
{t('ai_providers.excluded_models_count', { count: excludedModels.length })}
</div>
<div className={styles.modelTagList}>
{item.excludedModels.map((model) => (
{excludedModels.map((model) => (
<span key={model} className={`${styles.modelTag} ${styles.excludedModelTag}`}>
<span className={styles.modelName}>{model}</span>
</span>
@@ -1021,14 +1156,30 @@ export function AiProvidersPage() {
},
(index) => openGeminiModal(index),
(item) => deleteGemini(item.apiKey),
t('ai_providers.gemini_add_button')
t('ai_providers.gemini_add_button'),
undefined,
{
getRowDisabled: (item) => hasDisableAllModelsRule(item.excludedModels),
renderExtraActions: (item, index) => (
<ToggleSwitch
label={t('ai_providers.config_toggle_label')}
checked={!hasDisableAllModelsRule(item.excludedModels)}
disabled={disableControls || loading || saving || Boolean(configSwitchingKey)}
onChange={(value) => void setConfigEnabled('gemini', index, value)}
/>
)
}
)}
</Card>
<Card
title={t('ai_providers.codex_title')}
extra={
<Button size="sm" onClick={() => openProviderModal('codex', null)} disabled={disableControls}>
<Button
size="sm"
onClick={() => openProviderModal('codex', null)}
disabled={disableControls || saving || Boolean(configSwitchingKey)}
>
{t('ai_providers.codex_add_button')}
</Button>
}
@@ -1039,6 +1190,8 @@ export function AiProvidersPage() {
(item, _index) => {
const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey);
const headerEntries = Object.entries(item.headers || {});
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
const excludedModels = item.excludedModels ?? [];
return (
<Fragment>
<div className="item-title">{t('ai_providers.codex_item_title')}</div>
@@ -1071,14 +1224,19 @@ export function AiProvidersPage() {
))}
</div>
)}
{configDisabled && (
<div className="status-badge warning" style={{ marginTop: 8, marginBottom: 0 }}>
{t('ai_providers.config_disabled_badge')}
</div>
)}
{/* 排除模型徽章 */}
{item.excludedModels?.length ? (
{excludedModels.length ? (
<div className={styles.excludedModelsSection}>
<div className={styles.excludedModelsLabel}>
{t('ai_providers.excluded_models_count', { count: item.excludedModels.length })}
{t('ai_providers.excluded_models_count', { count: excludedModels.length })}
</div>
<div className={styles.modelTagList}>
{item.excludedModels.map((model) => (
{excludedModels.map((model) => (
<span key={model} className={`${styles.modelTag} ${styles.excludedModelTag}`}>
<span className={styles.modelName}>{model}</span>
</span>
@@ -1100,14 +1258,30 @@ export function AiProvidersPage() {
},
(index) => openProviderModal('codex', index),
(item) => deleteProviderEntry('codex', item.apiKey),
t('ai_providers.codex_add_button')
t('ai_providers.codex_add_button'),
undefined,
{
getRowDisabled: (item) => hasDisableAllModelsRule(item.excludedModels),
renderExtraActions: (item, index) => (
<ToggleSwitch
label={t('ai_providers.config_toggle_label')}
checked={!hasDisableAllModelsRule(item.excludedModels)}
disabled={disableControls || loading || saving || Boolean(configSwitchingKey)}
onChange={(value) => void setConfigEnabled('codex', index, value)}
/>
)
}
)}
</Card>
<Card
title={t('ai_providers.claude_title')}
extra={
<Button size="sm" onClick={() => openProviderModal('claude', null)} disabled={disableControls}>
<Button
size="sm"
onClick={() => openProviderModal('claude', null)}
disabled={disableControls || saving || Boolean(configSwitchingKey)}
>
{t('ai_providers.claude_add_button')}
</Button>
}
@@ -1118,6 +1292,8 @@ export function AiProvidersPage() {
(item, _index) => {
const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey);
const headerEntries = Object.entries(item.headers || {});
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
const excludedModels = item.excludedModels ?? [];
return (
<Fragment>
<div className="item-title">{t('ai_providers.claude_item_title')}</div>
@@ -1150,6 +1326,11 @@ export function AiProvidersPage() {
))}
</div>
)}
{configDisabled && (
<div className="status-badge warning" style={{ marginTop: 8, marginBottom: 0 }}>
{t('ai_providers.config_disabled_badge')}
</div>
)}
{/* 模型列表 */}
{item.models?.length ? (
<div className={styles.modelTagList}>
@@ -1167,13 +1348,13 @@ export function AiProvidersPage() {
</div>
) : null}
{/* 排除模型徽章 */}
{item.excludedModels?.length ? (
{excludedModels.length ? (
<div className={styles.excludedModelsSection}>
<div className={styles.excludedModelsLabel}>
{t('ai_providers.excluded_models_count', { count: item.excludedModels.length })}
{t('ai_providers.excluded_models_count', { count: excludedModels.length })}
</div>
<div className={styles.modelTagList}>
{item.excludedModels.map((model) => (
{excludedModels.map((model) => (
<span key={model} className={`${styles.modelTag} ${styles.excludedModelTag}`}>
<span className={styles.modelName}>{model}</span>
</span>
@@ -1195,14 +1376,30 @@ export function AiProvidersPage() {
},
(index) => openProviderModal('claude', index),
(item) => deleteProviderEntry('claude', item.apiKey),
t('ai_providers.claude_add_button')
t('ai_providers.claude_add_button'),
undefined,
{
getRowDisabled: (item) => hasDisableAllModelsRule(item.excludedModels),
renderExtraActions: (item, index) => (
<ToggleSwitch
label={t('ai_providers.config_toggle_label')}
checked={!hasDisableAllModelsRule(item.excludedModels)}
disabled={disableControls || loading || saving || Boolean(configSwitchingKey)}
onChange={(value) => void setConfigEnabled('claude', index, value)}
/>
)
}
)}
</Card>
<Card
title={t('ai_providers.ampcode_title')}
extra={
<Button size="sm" onClick={openAmpcodeModal} disabled={disableControls}>
<Button
size="sm"
onClick={openAmpcodeModal}
disabled={disableControls || saving || ampcodeSaving || Boolean(configSwitchingKey)}
>
{t('common.edit')}
</Button>
}
@@ -1259,7 +1456,11 @@ export function AiProvidersPage() {
<Card
title={t('ai_providers.openai_title')}
extra={
<Button size="sm" onClick={() => openOpenaiModal(null)} disabled={disableControls}>
<Button
size="sm"
onClick={() => openOpenaiModal(null)}
disabled={disableControls || saving || Boolean(configSwitchingKey)}
>
{t('ai_providers.openai_add_button')}
</Button>
}