feat(settings): protect model provider deletion and cascade credential updates
- Block deletion of a model provider when it is referenced by any agent, returning an error that lists the agent names so the user knows what to unlink first - When a provider's api_url or api_key is updated, automatically propagate the new credentials to all dependent agents: updates env_json in the database and patches on-disk config files (Claude Code settings.json, Gemini settings.json, Codex auth.json + config.toml, OpenCode auth.json) using the same field names and structure as the agent settings UI - Fix error message display in provider dialogs for both Tauri and web transports, which throw plain objects rather than Error instances Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1496,14 +1496,28 @@ fn trim_non_empty(value: Option<String>) -> Option<String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn important_env_targets(agent_type: AgentType) -> (&'static str, &'static str, &'static str) {
|
/// Primary env var keys for each agent type: (api_base_url, api_key, model).
|
||||||
|
/// Shared by runtime env resolution, model-provider cascade, and config patching.
|
||||||
|
fn agent_env_keys(agent_type: AgentType) -> (&'static str, &'static str, &'static str) {
|
||||||
match agent_type {
|
match agent_type {
|
||||||
AgentType::ClaudeCode => ("ANTHROPIC_BASE_URL", "ANTHROPIC_API_KEY", "ANTHROPIC_MODEL"),
|
AgentType::ClaudeCode => ("ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_MODEL"),
|
||||||
AgentType::Gemini => ("GOOGLE_GEMINI_BASE_URL", "GEMINI_API_KEY", "GEMINI_MODEL"),
|
AgentType::Gemini => ("GOOGLE_GEMINI_BASE_URL", "GEMINI_API_KEY", "GEMINI_MODEL"),
|
||||||
_ => ("OPENAI_BASE_URL", "OPENAI_API_KEY", "OPENAI_MODEL"),
|
_ => ("OPENAI_BASE_URL", "OPENAI_API_KEY", "OPENAI_MODEL"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialize a BTreeMap into env_json for database storage.
|
||||||
|
/// Returns `None` when the map is empty.
|
||||||
|
fn serialize_env_map(env: &BTreeMap<String, String>) -> Result<Option<String>, AcpError> {
|
||||||
|
if env.is_empty() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
serde_json::to_string(env)
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|e| AcpError::protocol(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn build_runtime_env_from_setting(
|
pub(crate) fn build_runtime_env_from_setting(
|
||||||
agent_type: AgentType,
|
agent_type: AgentType,
|
||||||
setting: Option<&crate::db::entities::agent_setting::Model>,
|
setting: Option<&crate::db::entities::agent_setting::Model>,
|
||||||
@@ -1529,7 +1543,7 @@ pub(crate) fn build_runtime_env_from_setting(
|
|||||||
merged.insert(key, trimmed.to_string());
|
merged.insert(key, trimmed.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let (api_base_url_key, api_key_key, model_key) = important_env_targets(agent_type);
|
let (api_base_url_key, api_key_key, model_key) = agent_env_keys(agent_type);
|
||||||
if let Some(value) = trim_non_empty(config.api_base_url) {
|
if let Some(value) = trim_non_empty(config.api_base_url) {
|
||||||
merged.insert(api_base_url_key.to_string(), value);
|
merged.insert(api_base_url_key.to_string(), value);
|
||||||
}
|
}
|
||||||
@@ -1560,7 +1574,7 @@ pub(crate) async fn apply_model_provider_env(
|
|||||||
Ok(Some(p)) => p,
|
Ok(Some(p)) => p,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
let (url_key, key_key, _) = important_env_targets(agent_type);
|
let (url_key, key_key, _) = agent_env_keys(agent_type);
|
||||||
if !provider.api_url.trim().is_empty() {
|
if !provider.api_url.trim().is_empty() {
|
||||||
runtime_env.insert(url_key.to_string(), provider.api_url.clone());
|
runtime_env.insert(url_key.to_string(), provider.api_url.clone());
|
||||||
}
|
}
|
||||||
@@ -1569,6 +1583,149 @@ pub(crate) async fn apply_model_provider_env(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update on-disk config files for a single agent when model provider credentials change.
|
||||||
|
/// Uses `agent_env_keys` to determine the correct env var names per agent type.
|
||||||
|
fn cascade_update_agent_config(
|
||||||
|
agent_type: AgentType,
|
||||||
|
api_url: &str,
|
||||||
|
api_key: &str,
|
||||||
|
) -> Result<(), AcpError> {
|
||||||
|
let (url_key, key_key, _) = agent_env_keys(agent_type);
|
||||||
|
match agent_type {
|
||||||
|
AgentType::ClaudeCode | AgentType::Gemini => {
|
||||||
|
// Write into config.env (not root-level)
|
||||||
|
let mut env = serde_json::Map::new();
|
||||||
|
env.insert(url_key.to_string(), serde_json::Value::String(api_url.to_string()));
|
||||||
|
env.insert(key_key.to_string(), serde_json::Value::String(api_key.to_string()));
|
||||||
|
let patch = serde_json::json!({ "env": env });
|
||||||
|
let patch_str = serde_json::to_string(&patch)
|
||||||
|
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||||
|
persist_agent_local_config_json(agent_type, Some(&patch_str))?;
|
||||||
|
}
|
||||||
|
AgentType::OpenClaw => {
|
||||||
|
// agent_local_config_path returns None for OpenClaw — no-op
|
||||||
|
}
|
||||||
|
AgentType::Codex => {
|
||||||
|
let auth_path = codex_auth_json_path();
|
||||||
|
let mut auth_obj = if auth_path.exists() {
|
||||||
|
fs::read_to_string(&auth_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
|
||||||
|
.filter(|v| v.is_object())
|
||||||
|
.unwrap_or_else(|| serde_json::json!({}))
|
||||||
|
} else {
|
||||||
|
serde_json::json!({})
|
||||||
|
};
|
||||||
|
if !api_key.trim().is_empty() {
|
||||||
|
auth_obj[key_key] = serde_json::Value::String(api_key.to_string());
|
||||||
|
}
|
||||||
|
let auth_str = serde_json::to_string_pretty(&auth_obj)
|
||||||
|
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||||
|
|
||||||
|
let config_path = codex_config_toml_path();
|
||||||
|
let mut toml_value = if config_path.exists() {
|
||||||
|
fs::read_to_string(&config_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|raw| raw.parse::<toml::Value>().ok())
|
||||||
|
.filter(|v| v.is_table())
|
||||||
|
.unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()))
|
||||||
|
} else {
|
||||||
|
toml::Value::Table(toml::map::Map::new())
|
||||||
|
};
|
||||||
|
if let Some(table) = toml_value.as_table_mut() {
|
||||||
|
if api_url.trim().is_empty() {
|
||||||
|
table.remove("api_base_url");
|
||||||
|
} else {
|
||||||
|
table.insert("api_base_url".to_string(), toml::Value::String(api_url.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let toml_str = toml::to_string_pretty(&toml_value)
|
||||||
|
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||||
|
|
||||||
|
persist_codex_native_config_files(Some(&auth_str), Some(&toml_str))?;
|
||||||
|
}
|
||||||
|
AgentType::OpenCode => {
|
||||||
|
let auth_path = opencode_auth_json_path();
|
||||||
|
let mut auth_obj = if auth_path.exists() {
|
||||||
|
fs::read_to_string(&auth_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
|
||||||
|
.filter(|v| v.is_object())
|
||||||
|
.unwrap_or_else(|| serde_json::json!({}))
|
||||||
|
} else {
|
||||||
|
serde_json::json!({})
|
||||||
|
};
|
||||||
|
if !api_key.trim().is_empty() {
|
||||||
|
auth_obj["api_key"] = serde_json::Value::String(api_key.to_string());
|
||||||
|
}
|
||||||
|
let auth_str = serde_json::to_string_pretty(&auth_obj)
|
||||||
|
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||||
|
persist_opencode_auth_json(&auth_str)?;
|
||||||
|
|
||||||
|
let patch = serde_json::json!({ "apiBaseUrl": api_url });
|
||||||
|
let patch_str = serde_json::to_string(&patch)
|
||||||
|
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||||
|
persist_agent_local_config_json(agent_type, Some(&patch_str))?;
|
||||||
|
}
|
||||||
|
AgentType::Cline => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cascade model provider credential changes to all dependent agent settings and config files.
|
||||||
|
pub(crate) async fn cascade_update_model_provider(
|
||||||
|
db: &AppDatabase,
|
||||||
|
provider_id: i32,
|
||||||
|
new_api_url: &str,
|
||||||
|
new_api_key: &str,
|
||||||
|
emitter: &EventEmitter,
|
||||||
|
) -> Result<(), AcpError> {
|
||||||
|
let dependents = agent_setting_service::find_by_model_provider_id(&db.conn, provider_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||||
|
|
||||||
|
for setting in &dependents {
|
||||||
|
let agent_type: AgentType = match serde_json::from_str(&setting.agent_type) {
|
||||||
|
Ok(at) => at,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Update env_json in database (uses agent_env_keys for consistent key names)
|
||||||
|
let (url_key, key_key, _) = agent_env_keys(agent_type);
|
||||||
|
let mut env_map: BTreeMap<String, String> = setting
|
||||||
|
.env_json
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|raw| serde_json::from_str(raw).ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if !new_api_url.trim().is_empty() {
|
||||||
|
env_map.insert(url_key.to_string(), new_api_url.to_string());
|
||||||
|
}
|
||||||
|
if !new_api_key.trim().is_empty() {
|
||||||
|
env_map.insert(key_key.to_string(), new_api_key.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let patch = agent_setting_service::AgentSettingsUpdate {
|
||||||
|
enabled: setting.enabled,
|
||||||
|
env_json: serialize_env_map(&env_map)?,
|
||||||
|
model_provider_id: setting.model_provider_id,
|
||||||
|
};
|
||||||
|
agent_setting_service::update(&db.conn, agent_type, patch)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||||
|
|
||||||
|
// 2. Update on-disk config files
|
||||||
|
if let Err(e) = cascade_update_agent_config(agent_type, new_api_url, new_api_key) {
|
||||||
|
eprintln!(
|
||||||
|
"[ModelProvider] cascade_update_agent_config({agent_type}) failed: {e}, skipping config update"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit_acp_agents_updated(emitter, "env_updated", Some(agent_type));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||||
pub async fn acp_preflight(
|
pub async fn acp_preflight(
|
||||||
agent_type: AgentType,
|
agent_type: AgentType,
|
||||||
@@ -1828,7 +1985,7 @@ pub(crate) async fn acp_list_agents_core(
|
|||||||
}
|
}
|
||||||
env.insert(key, trimmed.to_string());
|
env.insert(key, trimmed.to_string());
|
||||||
}
|
}
|
||||||
let (api_base_url_key, api_key_key, model_key) = important_env_targets(agent_type);
|
let (api_base_url_key, api_key_key, model_key) = agent_env_keys(agent_type);
|
||||||
if let Some(value) = trim_non_empty(local_cfg.api_base_url) {
|
if let Some(value) = trim_non_empty(local_cfg.api_base_url) {
|
||||||
env.insert(api_base_url_key.to_string(), value);
|
env.insert(api_base_url_key.to_string(), value);
|
||||||
}
|
}
|
||||||
@@ -1946,11 +2103,7 @@ pub(crate) async fn acp_update_agent_preferences_core(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||||
|
|
||||||
let env_json = if env.is_empty() {
|
let env_json = serialize_env_map(&env)?;
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(serde_json::to_string(&env).map_err(|e| AcpError::protocol(e.to_string()))?)
|
|
||||||
};
|
|
||||||
let config_json = config_json.and_then(|raw| {
|
let config_json = config_json.and_then(|raw| {
|
||||||
let trimmed = raw.trim();
|
let trimmed = raw.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@@ -2074,15 +2227,9 @@ pub(crate) async fn acp_update_agent_env_core(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||||
|
|
||||||
let env_json = if env.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(serde_json::to_string(&env).map_err(|e| AcpError::protocol(e.to_string()))?)
|
|
||||||
};
|
|
||||||
|
|
||||||
let patch = agent_setting_service::AgentSettingsUpdate {
|
let patch = agent_setting_service::AgentSettingsUpdate {
|
||||||
enabled,
|
enabled,
|
||||||
env_json,
|
env_json: serialize_env_map(&env)?,
|
||||||
model_provider_id,
|
model_provider_id,
|
||||||
};
|
};
|
||||||
agent_setting_service::update(&db.conn, agent_type, patch)
|
agent_setting_service::update(&db.conn, agent_type, patch)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use crate::app_error::AppCommandError;
|
use crate::app_error::AppCommandError;
|
||||||
|
use crate::commands::acp;
|
||||||
use crate::db::service::{agent_setting_service, model_provider_service};
|
use crate::db::service::{agent_setting_service, model_provider_service};
|
||||||
use crate::db::AppDatabase;
|
use crate::db::AppDatabase;
|
||||||
use crate::models::agent::AgentType;
|
use crate::models::agent::AgentType;
|
||||||
use crate::models::model_provider::ModelProviderInfo;
|
use crate::models::model_provider::ModelProviderInfo;
|
||||||
|
use crate::web::event_bridge::EventEmitter;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Shared core functions (used by both Tauri commands and web handlers)
|
// Shared core functions (used by both Tauri commands and web handlers)
|
||||||
@@ -89,6 +91,7 @@ pub async fn update_model_provider_core(
|
|||||||
api_url: Option<String>,
|
api_url: Option<String>,
|
||||||
api_key: Option<String>,
|
api_key: Option<String>,
|
||||||
agent_types: Option<Vec<String>>,
|
agent_types: Option<Vec<String>>,
|
||||||
|
emitter: &EventEmitter,
|
||||||
) -> Result<ModelProviderInfo, AppCommandError> {
|
) -> Result<ModelProviderInfo, AppCommandError> {
|
||||||
validate_fields(name.as_deref(), api_url.as_deref(), api_key.as_deref())?;
|
validate_fields(name.as_deref(), api_url.as_deref(), api_key.as_deref())?;
|
||||||
let agent_types_json = if let Some(ref ats) = agent_types {
|
let agent_types_json = if let Some(ref ats) = agent_types {
|
||||||
@@ -101,16 +104,34 @@ pub async fn update_model_provider_core(
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch old provider to detect credential changes.
|
||||||
|
let old_provider = model_provider_service::get_by_id(&db.conn, id)
|
||||||
|
.await
|
||||||
|
.map_err(AppCommandError::from)?
|
||||||
|
.ok_or_else(|| AppCommandError::not_found(format!("model provider not found: {id}")))?;
|
||||||
|
|
||||||
let model = model_provider_service::update(
|
let model = model_provider_service::update(
|
||||||
&db.conn,
|
&db.conn,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
api_url,
|
api_url.clone(),
|
||||||
api_key,
|
api_key.clone(),
|
||||||
agent_types_json,
|
agent_types_json,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(AppCommandError::from)?;
|
.map_err(AppCommandError::from)?;
|
||||||
|
|
||||||
|
// Cascade credential changes to all dependent agent settings and config files.
|
||||||
|
let url_changed = api_url.as_deref().is_some_and(|u| u != old_provider.api_url);
|
||||||
|
let key_changed = api_key.as_deref().is_some_and(|k| k != old_provider.api_key);
|
||||||
|
if url_changed || key_changed {
|
||||||
|
let final_url = api_url.as_deref().unwrap_or(&old_provider.api_url);
|
||||||
|
let final_key = api_key.as_deref().unwrap_or(&old_provider.api_key);
|
||||||
|
acp::cascade_update_model_provider(db, id, final_url, final_key, emitter)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppCommandError::invalid_input(e.to_string()))?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(ModelProviderInfo::from(model))
|
Ok(ModelProviderInfo::from(model))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,10 +139,26 @@ pub async fn delete_model_provider_core(
|
|||||||
db: &AppDatabase,
|
db: &AppDatabase,
|
||||||
id: i32,
|
id: i32,
|
||||||
) -> Result<(), AppCommandError> {
|
) -> Result<(), AppCommandError> {
|
||||||
// Clear any agent settings that reference this provider before deleting.
|
// Check if any agent settings reference this provider.
|
||||||
agent_setting_service::clear_model_provider_id(&db.conn, id)
|
let dependents = agent_setting_service::find_by_model_provider_id(&db.conn, id)
|
||||||
.await
|
.await
|
||||||
.map_err(AppCommandError::from)?;
|
.map_err(AppCommandError::from)?;
|
||||||
|
|
||||||
|
if !dependents.is_empty() {
|
||||||
|
let agent_names: Vec<String> = dependents
|
||||||
|
.iter()
|
||||||
|
.filter_map(|row| {
|
||||||
|
serde_json::from_str::<AgentType>(&row.agent_type)
|
||||||
|
.ok()
|
||||||
|
.map(|at| at.to_string())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let names_joined = agent_names.join(", ");
|
||||||
|
return Err(AppCommandError::invalid_input(format!(
|
||||||
|
"PROVIDER_IN_USE:{names_joined}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
model_provider_service::delete(&db.conn, id)
|
model_provider_service::delete(&db.conn, id)
|
||||||
.await
|
.await
|
||||||
.map_err(AppCommandError::from)?;
|
.map_err(AppCommandError::from)?;
|
||||||
@@ -161,8 +198,10 @@ pub async fn update_model_provider(
|
|||||||
api_url: Option<String>,
|
api_url: Option<String>,
|
||||||
api_key: Option<String>,
|
api_key: Option<String>,
|
||||||
agent_types: Option<Vec<String>>,
|
agent_types: Option<Vec<String>>,
|
||||||
|
app: tauri::AppHandle,
|
||||||
) -> Result<ModelProviderInfo, AppCommandError> {
|
) -> Result<ModelProviderInfo, AppCommandError> {
|
||||||
update_model_provider_core(&db, id, name, api_url, api_key, agent_types).await
|
let emitter = EventEmitter::Tauri(app);
|
||||||
|
update_model_provider_core(&db, id, name, api_url, api_key, agent_types, &emitter).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tauri-runtime")]
|
#[cfg(feature = "tauri-runtime")]
|
||||||
|
|||||||
@@ -205,23 +205,15 @@ async fn reorder_once(conn: &DatabaseConnection, agent_types: &[AgentType]) -> R
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clear_model_provider_id(
|
pub async fn find_by_model_provider_id(
|
||||||
conn: &DatabaseConnection,
|
conn: &DatabaseConnection,
|
||||||
model_provider_id: i32,
|
model_provider_id: i32,
|
||||||
) -> Result<(), DbError> {
|
) -> Result<Vec<agent_setting::Model>, DbError> {
|
||||||
let rows = agent_setting::Entity::find()
|
let rows = agent_setting::Entity::find()
|
||||||
|
.filter(agent_setting::Column::ModelProviderId.eq(Some(model_provider_id)))
|
||||||
.all(conn)
|
.all(conn)
|
||||||
.await?;
|
.await?;
|
||||||
let now = Utc::now();
|
Ok(rows)
|
||||||
for row in rows {
|
|
||||||
if row.model_provider_id == Some(model_provider_id) {
|
|
||||||
let mut active = row.into_active_model();
|
|
||||||
active.model_provider_id = Set(None);
|
|
||||||
active.updated_at = Set(now);
|
|
||||||
active.update(conn).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_sqlite_full_error(err: &DbError) -> bool {
|
fn is_sqlite_full_error(err: &DbError) -> bool {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ pub async fn update_model_provider(
|
|||||||
params.api_url,
|
params.api_url,
|
||||||
params.api_key,
|
params.api_key,
|
||||||
params.agent_types,
|
params.agent_types,
|
||||||
|
&state.emitter,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
|
|||||||
@@ -89,8 +89,14 @@ export function AddModelProviderDialog({
|
|||||||
toast.success(t("createSuccess"))
|
toast.success(t("createSuccess"))
|
||||||
handleOpenChange(false)
|
handleOpenChange(false)
|
||||||
onProviderAdded()
|
onProviderAdded()
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
const msg = err instanceof Error ? err.message : String(err)
|
const raw = err as Record<string, unknown>
|
||||||
|
const msg =
|
||||||
|
typeof raw?.message === "string"
|
||||||
|
? raw.message
|
||||||
|
: err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: String(err)
|
||||||
setError(msg)
|
setError(msg)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|||||||
@@ -97,8 +97,14 @@ export function EditModelProviderDialog({
|
|||||||
toast.success(t("editSuccess"))
|
toast.success(t("editSuccess"))
|
||||||
handleOpenChange(false)
|
handleOpenChange(false)
|
||||||
onProviderUpdated()
|
onProviderUpdated()
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
const msg = err instanceof Error ? err.message : String(err)
|
const raw = err as Record<string, unknown>
|
||||||
|
const msg =
|
||||||
|
typeof raw?.message === "string"
|
||||||
|
? raw.message
|
||||||
|
: err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: String(err)
|
||||||
setError(msg)
|
setError(msg)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|||||||
@@ -72,9 +72,21 @@ export function ModelProviderSettings() {
|
|||||||
toast.success(t("deleteSuccess"))
|
toast.success(t("deleteSuccess"))
|
||||||
setDeleteTarget(null)
|
setDeleteTarget(null)
|
||||||
await loadProviders()
|
await loadProviders()
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
const msg = err instanceof Error ? err.message : String(err)
|
const raw = err as Record<string, unknown>
|
||||||
toast.error(msg)
|
const msg =
|
||||||
|
typeof raw?.message === "string"
|
||||||
|
? raw.message
|
||||||
|
: err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: String(err)
|
||||||
|
const prefix = "PROVIDER_IN_USE:"
|
||||||
|
if (msg.includes(prefix)) {
|
||||||
|
const agentNames = msg.substring(msg.indexOf(prefix) + prefix.length)
|
||||||
|
toast.error(t("deleteBlockedByAgent", { agents: agentNames }))
|
||||||
|
} else {
|
||||||
|
toast.error(msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [deleteTarget, loadProviders, t])
|
}, [deleteTarget, loadProviders, t])
|
||||||
|
|
||||||
|
|||||||
@@ -1849,6 +1849,7 @@
|
|||||||
"deleteSuccess": "تم حذف المزود.",
|
"deleteSuccess": "تم حذف المزود.",
|
||||||
"deleteConfirmTitle": "حذف المزود",
|
"deleteConfirmTitle": "حذف المزود",
|
||||||
"deleteConfirmMessage": "سيتم حذف المزود \"{name}\" نهائياً. هل أنت متأكد؟",
|
"deleteConfirmMessage": "سيتم حذف المزود \"{name}\" نهائياً. هل أنت متأكد؟",
|
||||||
|
"deleteBlockedByAgent": "{agents} يستخدم هذا المزود. يرجى إلغاء الربط قبل الحذف.",
|
||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"delete": "حذف",
|
"delete": "حذف",
|
||||||
"create": "إنشاء",
|
"create": "إنشاء",
|
||||||
|
|||||||
@@ -1849,6 +1849,7 @@
|
|||||||
"deleteSuccess": "Anbieter gelöscht.",
|
"deleteSuccess": "Anbieter gelöscht.",
|
||||||
"deleteConfirmTitle": "Anbieter löschen",
|
"deleteConfirmTitle": "Anbieter löschen",
|
||||||
"deleteConfirmMessage": "Der Anbieter \"{name}\" wird dauerhaft gelöscht. Sind Sie sicher?",
|
"deleteConfirmMessage": "Der Anbieter \"{name}\" wird dauerhaft gelöscht. Sind Sie sicher?",
|
||||||
|
"deleteBlockedByAgent": "{agents} verwendet diesen Anbieter. Bitte trennen Sie die Verbindung vor dem Löschen.",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"create": "Erstellen",
|
"create": "Erstellen",
|
||||||
|
|||||||
@@ -1849,6 +1849,7 @@
|
|||||||
"deleteSuccess": "Provider deleted.",
|
"deleteSuccess": "Provider deleted.",
|
||||||
"deleteConfirmTitle": "Delete Provider",
|
"deleteConfirmTitle": "Delete Provider",
|
||||||
"deleteConfirmMessage": "This will permanently delete the provider \"{name}\". Are you sure?",
|
"deleteConfirmMessage": "This will permanently delete the provider \"{name}\". Are you sure?",
|
||||||
|
"deleteBlockedByAgent": "{agents} is currently using this provider. Please unlink before deleting.",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
|
|||||||
@@ -1849,6 +1849,7 @@
|
|||||||
"deleteSuccess": "Proveedor eliminado.",
|
"deleteSuccess": "Proveedor eliminado.",
|
||||||
"deleteConfirmTitle": "Eliminar Proveedor",
|
"deleteConfirmTitle": "Eliminar Proveedor",
|
||||||
"deleteConfirmMessage": "Esto eliminará permanentemente el proveedor \"{name}\". ¿Está seguro?",
|
"deleteConfirmMessage": "Esto eliminará permanentemente el proveedor \"{name}\". ¿Está seguro?",
|
||||||
|
"deleteBlockedByAgent": "{agents} está usando este proveedor. Desvincúlelo antes de eliminarlo.",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"create": "Crear",
|
"create": "Crear",
|
||||||
|
|||||||
@@ -1849,6 +1849,7 @@
|
|||||||
"deleteSuccess": "Fournisseur supprimé.",
|
"deleteSuccess": "Fournisseur supprimé.",
|
||||||
"deleteConfirmTitle": "Supprimer le Fournisseur",
|
"deleteConfirmTitle": "Supprimer le Fournisseur",
|
||||||
"deleteConfirmMessage": "Le fournisseur \"{name}\" sera définitivement supprimé. Êtes-vous sûr ?",
|
"deleteConfirmMessage": "Le fournisseur \"{name}\" sera définitivement supprimé. Êtes-vous sûr ?",
|
||||||
|
"deleteBlockedByAgent": "{agents} utilise ce fournisseur. Veuillez le dissocier avant de supprimer.",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"create": "Créer",
|
"create": "Créer",
|
||||||
|
|||||||
@@ -1849,6 +1849,7 @@
|
|||||||
"deleteSuccess": "プロバイダーを削除しました。",
|
"deleteSuccess": "プロバイダーを削除しました。",
|
||||||
"deleteConfirmTitle": "プロバイダーを削除",
|
"deleteConfirmTitle": "プロバイダーを削除",
|
||||||
"deleteConfirmMessage": "プロバイダー「{name}」を完全に削除しますか?",
|
"deleteConfirmMessage": "プロバイダー「{name}」を完全に削除しますか?",
|
||||||
|
"deleteBlockedByAgent": "{agents} がこのプロバイダーを使用中です。削除する前にリンクを解除してください。",
|
||||||
"cancel": "キャンセル",
|
"cancel": "キャンセル",
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
"create": "作成",
|
"create": "作成",
|
||||||
|
|||||||
@@ -1849,6 +1849,7 @@
|
|||||||
"deleteSuccess": "제공업체가 삭제되었습니다.",
|
"deleteSuccess": "제공업체가 삭제되었습니다.",
|
||||||
"deleteConfirmTitle": "제공업체 삭제",
|
"deleteConfirmTitle": "제공업체 삭제",
|
||||||
"deleteConfirmMessage": "제공업체 \"{name}\"을(를) 영구적으로 삭제하시겠습니까?",
|
"deleteConfirmMessage": "제공업체 \"{name}\"을(를) 영구적으로 삭제하시겠습니까?",
|
||||||
|
"deleteBlockedByAgent": "{agents}이(가) 이 공급자를 사용 중입니다. 삭제하기 전에 연결을 해제해 주세요.",
|
||||||
"cancel": "취소",
|
"cancel": "취소",
|
||||||
"delete": "삭제",
|
"delete": "삭제",
|
||||||
"create": "생성",
|
"create": "생성",
|
||||||
|
|||||||
@@ -1849,6 +1849,7 @@
|
|||||||
"deleteSuccess": "Provedor excluído.",
|
"deleteSuccess": "Provedor excluído.",
|
||||||
"deleteConfirmTitle": "Excluir Provedor",
|
"deleteConfirmTitle": "Excluir Provedor",
|
||||||
"deleteConfirmMessage": "O provedor \"{name}\" será excluído permanentemente. Tem certeza?",
|
"deleteConfirmMessage": "O provedor \"{name}\" será excluído permanentemente. Tem certeza?",
|
||||||
|
"deleteBlockedByAgent": "{agents} está usando este provedor. Desvincule-o antes de excluir.",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"delete": "Excluir",
|
"delete": "Excluir",
|
||||||
"create": "Criar",
|
"create": "Criar",
|
||||||
|
|||||||
@@ -1849,6 +1849,7 @@
|
|||||||
"deleteSuccess": "供应商已删除。",
|
"deleteSuccess": "供应商已删除。",
|
||||||
"deleteConfirmTitle": "删除供应商",
|
"deleteConfirmTitle": "删除供应商",
|
||||||
"deleteConfirmMessage": "确定要永久删除供应商「{name}」吗?",
|
"deleteConfirmMessage": "确定要永久删除供应商「{name}」吗?",
|
||||||
|
"deleteBlockedByAgent": "{agents} 正在使用该配置,请先解除关联后再删除。",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"create": "创建",
|
"create": "创建",
|
||||||
|
|||||||
@@ -1849,6 +1849,7 @@
|
|||||||
"deleteSuccess": "供應商已刪除。",
|
"deleteSuccess": "供應商已刪除。",
|
||||||
"deleteConfirmTitle": "刪除供應商",
|
"deleteConfirmTitle": "刪除供應商",
|
||||||
"deleteConfirmMessage": "確定要永久刪除供應商「{name}」嗎?",
|
"deleteConfirmMessage": "確定要永久刪除供應商「{name}」嗎?",
|
||||||
|
"deleteBlockedByAgent": "{agents} 正在使用此設定,請先解除關聯後再刪除。",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"delete": "刪除",
|
"delete": "刪除",
|
||||||
"create": "建立",
|
"create": "建立",
|
||||||
|
|||||||
Reference in New Issue
Block a user