diff --git a/src-tauri/src/commands/ai/commands/provider.rs b/src-tauri/src/commands/ai/commands/provider.rs index 2180cac..9a06de1 100644 --- a/src-tauri/src/commands/ai/commands/provider.rs +++ b/src-tauri/src/commands/ai/commands/provider.rs @@ -365,6 +365,53 @@ pub async fn ai_fetch_models( Ok(merged) } +/// 一次性测试连接(P0-1):用**表单当前值**而非 DB 值测试,不写库。 +/// +/// 解决:`ai_fetch_models` 从 DB 读 provider,用户改了表单未保存就点「测试连接」时 +/// 测的是旧值(base_url/api_key 已改但 DB 未落),给出误导性「成功/失败」。 +/// +/// 行为: +/// - api_key 非空 → 直接用表单值(base_url + api_key)测,完全不读 DB。 +/// - api_key 空 + provider_id 给出 → 编辑态用户没重填密钥(留空=不改), +/// 从 DB 取该 provider 记录经 FR-S1 内存解析出真实 key,base_url 仍用表单值 +/// (用户可能改了 URL,密钥沿用旧)。 +/// - api_key 空 + provider_id 空 → 新建态没填 key,直接报错(无来源可解析)。 +/// +/// 一次性:不写回 model_configs(测试≠落库);成功后用户可继续编辑或保存。 +/// 保留 `ai_fetch_models`(从 DB 读 + 写回,用于「已保存配置重测」场景),本 IPC 不破坏它。 +#[tauri::command] +pub async fn ai_test_connection( + state: State<'_, AppState>, + provider_type: String, + base_url: String, + api_key: String, + provider_id: Option, +) -> Result, String> { + // 解析测试用 api_key:表单非空用表单;空则从 DB provider 沿用旧密钥(编辑不改 key 场景) + let api_key = if !api_key.is_empty() { + api_key + } else { + let pid = provider_id.ok_or_else(|| { + "测试连接失败:api_key 为空且无已保存配置可沿用,请填写 api_key".to_string() + })?; + let provider = state + .ai_providers + .get_by_id(&pid) + .await + .map_err(err_str)? + .ok_or_else(|| format!("提供商不存在: {}", pid))?; + // FR-S1:内存解析,绝不外泄(不入日志/返回值/错误信息) + df_storage::secret::resolve_provider_secret(&provider) + }; + + let fetch_type = normalize_provider_type_for_fetch(&provider_type); + let probed = df_ai::model_fetch::fetch_and_probe(&fetch_type, &base_url, &api_key) + .await + .map_err(err_str)?; + + Ok(probed) +} + /// 单模型探测(F-01 阶段5):纯 CPU 启发式 + 预设表,无网络。 /// /// 用途:拉取后用户手动补一个模型名、或想重探某模型的能力维度。 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ff9556c..455716f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -442,6 +442,8 @@ pub fn run() { // F-01 阶段5:测试连接拉取模型列表 + 单模型探测 commands::ai::ai_fetch_models, commands::ai::ai_probe_model, + // P0-1:用表单当前值一次性测试连接(不写库,新建/编辑未保存均可测) + commands::ai::ai_test_connection, // AI 对话管理 commands::ai::ai_conversation_create, commands::ai::ai_conversation_list, diff --git a/src/api/ai.ts b/src/api/ai.ts index c25fb6d..f6a85d1 100644 --- a/src/api/ai.ts +++ b/src/api/ai.ts @@ -204,6 +204,27 @@ export const aiApi = { return invoke('ai_fetch_models', { providerId }) }, + /** + * 一次性测试连接(P0-1):用**表单当前值**测试,不写库。 + * - 新建态:传表单填的 providerType/baseUrl/apiKey,providerId=null。 + * - 编辑态(apiKey 留空=不改):传 providerId 让后端从 DB 沿用旧密钥; + * apiKey 非空则直接用表单新值。 + * 成功返回模型列表(ModelConfig,不含 api_key),失败抛错。 + */ + testConnection(input: { + providerType: string + baseUrl: string + apiKey: string + providerId?: string | null + }): Promise { + return invoke('ai_test_connection', { + providerType: input.providerType, + baseUrl: input.baseUrl, + apiKey: input.apiKey, + providerId: input.providerId ?? null, + }) + }, + /** * 单模型探测:纯 CPU 启发式 + 预设表,无网络。 * 返回填充了 probe_source 的 ModelConfig。用途:手动补模型名后探测能力维度。 diff --git a/src/components/AiChat.vue b/src/components/AiChat.vue index e95dec5..9140810 100644 --- a/src/components/AiChat.vue +++ b/src/components/AiChat.vue @@ -790,6 +790,8 @@ async function initContextEventListeners(): Promise { .ai-toast-error { background: var(--df-danger-bg); color: var(--df-danger); border: 0.5px solid var(--df-danger); } .ai-toast-warning { background: var(--df-warning-bg); color: var(--df-warning); border: 0.5px solid var(--df-warning); } .ai-toast-info { background: var(--df-accent-bg); color: var(--df-accent); border: 0.5px solid var(--df-accent); } +/* P0-2: ToastType 新增 'success' 兜底样式 */ +.ai-toast-success { background: var(--df-success-bg); color: var(--df-success); border: 0.5px solid var(--df-success); } .ai-toast-enter-active, .ai-toast-leave-active { transition: opacity 0.2s, transform 0.2s; } .ai-toast-enter-from, .ai-toast-leave-to { opacity: 0; transform: translate(-50%, -8px); } diff --git a/src/components/settings/AllowedDirsPanel.vue b/src/components/settings/AllowedDirsPanel.vue index c1e15b6..2013f03 100644 --- a/src/components/settings/AllowedDirsPanel.vue +++ b/src/components/settings/AllowedDirsPanel.vue @@ -87,7 +87,7 @@ import SettingRow from './SettingRow.vue' const { t } = useI18n() const appSettings = useAppSettingsStore() // F-260620: 接共享 toast 通道(Settings.vue showToast),替代 console.error/warn 用户零反馈 -const emit = defineEmits<{ (e: 'toast', msg: string, type: 'error' | 'warning' | 'info'): void }>() +const emit = defineEmits<{ (e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void }>() // F-260619-03 Phase A: AI 工具文件访问授权目录白名单 UI // 即时持久化:添加/删除立即写后端 KV + 内存白名单,无需显式"保存"(符合"操作即生效"心智, diff --git a/src/components/settings/ConnectionPanel.vue b/src/components/settings/ConnectionPanel.vue index 742831d..e942533 100644 --- a/src/components/settings/ConnectionPanel.vue +++ b/src/components/settings/ConnectionPanel.vue @@ -81,7 +81,8 @@ const { t } = useI18n() const appSettings = useAppSettingsStore() const emit = defineEmits<{ - (e: 'toast', msg: string, type: 'error' | 'warning' | 'info'): void + // P0-2: 对齐 ToastType 加 'success'(本面板暂未用,保类型一致) + (e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void }>() const props = defineProps<{ diff --git a/src/components/settings/ProviderPanel.vue b/src/components/settings/ProviderPanel.vue index 9e4c221..365c378 100644 --- a/src/components/settings/ProviderPanel.vue +++ b/src/components/settings/ProviderPanel.vue @@ -87,14 +87,19 @@ + +
+
{{ providerForm.saveError }}
+
- - {{ $t('settings.fetchHintSaveFirst') }}
@@ -127,7 +132,7 @@