feat(ai-config): persist AI settings per provider (#2412)
## 背景说明 AI 设置原本只有一个配置槽,切换模型厂商时会覆盖之前填写的 endpoint、API Key、model 等信息。本次改动让每个模型厂商各自保存一份配置,并在设置页和对话窗口提供已配置状态与快速切换能力。 ## 修改内容 - 新增 ai_provider_configs 存储表,用于按 provider 保存独立 AI 配置 - 新增 Tauri/Web API:保存和加载 provider 级 AI 配置 - 设置页 provider 下拉展示“当前/已配置”状态 - 设置页切换 provider 前会先保存当前表单编辑内容,避免未保存输入丢失 - 对话窗口模型选择器支持查看并切换已配置过的其他 provider - provider 配置保存时强制 config.provider 与 provider key 一致,避免错位配置污染 - 所有语言补齐新增 i18n 文案 ## 修复的问题 - 切换模型厂商时原厂商配置会丢失 - 设置页切换 provider 时本地表单编辑态可能未保存 - provider key 与 config.provider 不一致时可能激活错误厂商 - 对话窗口模型选择器只能看到当前 provider 的模型,不能看到其他已配置厂商 ## 测试 - npx tsc --noEmit - cargo check -p dbx-core - cargo check -p dbx-web - cargo check --manifest-path src-tauri/Cargo.toml
This commit is contained in:
1 parent
486b9ee8ed
commit
a0754206d3
18 files changed
+301
-51
No files matched your search
@@ -480,6 +480,8 @@ async fn main() {
|
||||
.route("/saved-sql/folders/{id}", delete(routes::saved_sql::delete_saved_sql_folder))
|
||||
// AI
|
||||
.route("/ai/config", post(routes::ai::save_ai_config).get(routes::ai::load_ai_config))
|
||||
.route("/ai/provider-config", post(routes::ai::save_ai_provider_config))
|
||||
.route("/ai/provider-configs", get(routes::ai::load_ai_provider_configs))
|
||||
.route("/ai/conversation", post(routes::ai::save_ai_conversation))
|
||||
.route("/ai/conversations", get(routes::ai::load_ai_conversations))
|
||||
.route("/ai/conversation/{id}", delete(routes::ai::delete_ai_conversation))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
@@ -20,6 +21,13 @@ use crate::state::WebState;
|
||||
// Request types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SaveAiProviderConfigRequest {
|
||||
pub provider: String,
|
||||
pub config: AiConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SaveAiConfigRequest {
|
||||
@@ -88,6 +96,11 @@ fn reject_web_unsupported_ai_provider(config: &AiConfig) -> Result<(), AppError>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ai_provider_from_key(provider: &str) -> Result<AiProvider, AppError> {
|
||||
serde_json::from_value(serde_json::Value::String(provider.to_string()))
|
||||
.map_err(|_| AppError::bad_request(format!("Invalid AI provider: {provider}")))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -106,6 +119,25 @@ pub async fn load_ai_config(State(state): State<Arc<WebState>>) -> Result<Json<O
|
||||
Ok(Json(config))
|
||||
}
|
||||
|
||||
pub async fn save_ai_provider_config(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(body): Json<SaveAiProviderConfigRequest>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
let parsed_provider = ai_provider_from_key(&body.provider)?;
|
||||
let mut config = body.config;
|
||||
config.provider = parsed_provider;
|
||||
reject_web_unsupported_ai_provider(&config)?;
|
||||
state.app.storage.save_ai_provider_config(&body.provider, &config).await.map_err(AppError)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn load_ai_provider_configs(
|
||||
State(state): State<Arc<WebState>>,
|
||||
) -> Result<Json<HashMap<String, AiConfig>>, AppError> {
|
||||
let configs = state.app.storage.load_ai_provider_configs().await.map_err(AppError)?;
|
||||
Ok(Json(configs))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Conversations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user