fix: allow deleting desktop providers
This commit is contained in:
@@ -5,9 +5,12 @@ use std::collections::HashSet;
|
||||
use crate::crypto::{decrypt_key, encrypt_key};
|
||||
use crate::entity::{builtin_model_deletions, models, provider_keys, providers};
|
||||
use crate::error::{BoosApiError, Result};
|
||||
use crate::repo::settings;
|
||||
use crate::types::*;
|
||||
use crate::utils::{gen_id, now_ts};
|
||||
|
||||
const DELETED_BUILTIN_PROVIDERS_SETTING: &str = "deleted_builtin_providers";
|
||||
|
||||
fn parse_provider_type(s: &str) -> ProviderType {
|
||||
match s {
|
||||
"openai" => ProviderType::OpenAI,
|
||||
@@ -286,6 +289,47 @@ fn key_prefix(raw_key: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_deleted_builtin_provider_ids(db: &DatabaseConnection) -> Result<HashSet<String>> {
|
||||
let Some(raw) = settings::get_setting(db, DELETED_BUILTIN_PROVIDERS_SETTING).await? else {
|
||||
return Ok(HashSet::new());
|
||||
};
|
||||
|
||||
Ok(serde_json::from_str::<Vec<String>>(&raw)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|id| !id.trim().is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn save_deleted_builtin_provider_ids(
|
||||
db: &DatabaseConnection,
|
||||
deleted_ids: &HashSet<String>,
|
||||
) -> Result<()> {
|
||||
let mut values: Vec<String> = deleted_ids.iter().cloned().collect();
|
||||
values.sort();
|
||||
let raw = serde_json::to_string(&values).unwrap_or_else(|_| "[]".to_string());
|
||||
settings::set_setting(db, DELETED_BUILTIN_PROVIDERS_SETTING, &raw).await
|
||||
}
|
||||
|
||||
pub async fn mark_builtin_provider_deleted(
|
||||
db: &DatabaseConnection,
|
||||
builtin_id: &str,
|
||||
) -> Result<()> {
|
||||
if !crate::db::get_builtin_providers()
|
||||
.iter()
|
||||
.any(|provider| provider.builtin_id == builtin_id)
|
||||
{
|
||||
return Err(BoosApiError::NotFound(format!(
|
||||
"Built-in provider {}",
|
||||
builtin_id
|
||||
)));
|
||||
}
|
||||
|
||||
let mut deleted_ids = list_deleted_builtin_provider_ids(db).await?;
|
||||
deleted_ids.insert(builtin_id.to_string());
|
||||
save_deleted_builtin_provider_ids(db, &deleted_ids).await
|
||||
}
|
||||
|
||||
pub async fn import_provider_from_deep_link(
|
||||
db: &DatabaseConnection,
|
||||
master_key: &[u8; 32],
|
||||
@@ -354,11 +398,20 @@ pub async fn import_provider_from_deep_link(
|
||||
}
|
||||
|
||||
pub async fn delete_provider(db: &DatabaseConnection, id: &str) -> Result<()> {
|
||||
let builtin_id = providers::Entity::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| BoosApiError::NotFound(format!("Provider {}", id)))?
|
||||
.builtin_id;
|
||||
|
||||
let result = providers::Entity::delete_by_id(id).exec(db).await?;
|
||||
|
||||
if result.rows_affected == 0 {
|
||||
return Err(BoosApiError::NotFound(format!("Provider {}", id)));
|
||||
}
|
||||
if let Some(builtin_id) = builtin_id {
|
||||
mark_builtin_provider_deleted(db, &builtin_id).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -833,6 +886,7 @@ async fn sync_missing_builtin_models(
|
||||
pub async fn list_providers_merged(db: &DatabaseConnection) -> Result<Vec<ProviderConfig>> {
|
||||
let mut db_providers = list_providers(db).await?;
|
||||
let builtins = crate::db::get_builtin_providers();
|
||||
let deleted_builtin_ids = list_deleted_builtin_provider_ids(db).await?;
|
||||
if sync_missing_builtin_models(db, &builtins, &db_providers).await? {
|
||||
db_providers = list_providers(db).await?;
|
||||
}
|
||||
@@ -840,6 +894,10 @@ pub async fn list_providers_merged(db: &DatabaseConnection) -> Result<Vec<Provid
|
||||
let mut result = Vec::new();
|
||||
|
||||
for bp in &builtins {
|
||||
if deleted_builtin_ids.contains(bp.builtin_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(db_prov) = db_providers
|
||||
.iter()
|
||||
.find(|p| p.builtin_id.as_deref() == Some(bp.builtin_id))
|
||||
@@ -1106,6 +1164,27 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleted_builtin_provider_stays_hidden_from_merged_list() {
|
||||
let h = create_test_pool().await.unwrap();
|
||||
let db = &h.conn;
|
||||
|
||||
let providers = list_providers_merged(db).await.unwrap();
|
||||
assert!(providers
|
||||
.iter()
|
||||
.any(|provider| provider.builtin_id.as_deref() == Some("boosapi")));
|
||||
|
||||
mark_builtin_provider_deleted(db, "boosapi").await.unwrap();
|
||||
|
||||
let providers = list_providers_merged(db).await.unwrap();
|
||||
assert!(
|
||||
!providers
|
||||
.iter()
|
||||
.any(|provider| provider.builtin_id.as_deref() == Some("boosapi")),
|
||||
"deleted built-in provider should not reappear after refresh"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_deep_link_import_creates_provider_and_key() {
|
||||
let h = create_test_pool().await.unwrap();
|
||||
|
||||
@@ -77,9 +77,13 @@ pub async fn update_provider(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_provider(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
||||
// Virtual built-in providers have no DB row — deletion is a no-op (they'll reappear)
|
||||
if id.starts_with("builtin_") {
|
||||
return Ok(());
|
||||
if let Some(builtin_id) = id.strip_prefix("builtin_") {
|
||||
return boosapi_core::repo::provider::mark_builtin_provider_deleted(
|
||||
&state.sea_db,
|
||||
builtin_id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string());
|
||||
}
|
||||
boosapi_core::repo::provider::delete_provider(&state.sea_db, &id)
|
||||
.await
|
||||
|
||||
@@ -1107,7 +1107,6 @@ export function ProviderDetail({ providerId }: ProviderDetailProps) {
|
||||
checkedChildren={t('common.enabled')}
|
||||
unCheckedChildren={t('common.disabled')}
|
||||
/>
|
||||
{!provider.builtin_id && (
|
||||
<Popconfirm
|
||||
title={t('settings.deleteProviderConfirm')}
|
||||
onConfirm={async () => {
|
||||
@@ -1118,9 +1117,15 @@ export function ProviderDetail({ providerId }: ProviderDetailProps) {
|
||||
cancelText={t('common.cancel')}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button type="text" size="small" danger icon={<Trash2 size={14} />} />
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
aria-label={t('settings.deleteProvider')}
|
||||
title={t('settings.deleteProvider')}
|
||||
icon={<Trash2 size={14} />}
|
||||
/>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -231,6 +231,23 @@ describe('ProviderDetail', () => {
|
||||
expect(screen.getByText('settings.urlPreviewLabelhttps://open.bigmodel.cn/api/paas/v4/chat/completions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('allows deleting built-in providers from the detail header', async () => {
|
||||
provider.builtin_id = 'boosapi';
|
||||
mocks.deleteProvider.mockResolvedValue(undefined);
|
||||
|
||||
render(
|
||||
<App>
|
||||
<ProviderDetail providerId="provider-1" />
|
||||
</App>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'settings.deleteProvider' }));
|
||||
await userEvent.click(await screen.findByText('common.confirm'));
|
||||
|
||||
await waitFor(() => expect(mocks.deleteProvider).toHaveBeenCalledWith('provider-1'));
|
||||
expect(mocks.setSelectedProviderId).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('adds a model from the card-level action and derives the default group from the model id', async () => {
|
||||
render(
|
||||
<App>
|
||||
|
||||
Reference in New Issue
Block a user