From 4c84fbc1b656ee209666835122d66386ca93429c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Wed, 17 Jun 2026 00:50:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20F-01=E9=98=B6=E6=AE=B56?= =?UTF-8?q?=20AiChat=E6=A8=A1=E5=9E=8B=E4=B8=8B=E6=8B=89+override=E7=A9=BF?= =?UTF-8?q?=E9=80=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/ai/agentic.rs | 19 +++- src-tauri/src/commands/ai/commands.rs | 23 +++- src/api/ai.ts | 35 ++++-- src/components/AiChat.vue | 150 ++++++++++++++++++++++++++ src/composables/ai/useAiSend.ts | 29 ++++- src/i18n/en/aiChat.ts | 8 ++ src/i18n/zh-CN/aiChat.ts | 8 ++ 7 files changed, 256 insertions(+), 16 deletions(-) diff --git a/src-tauri/src/commands/ai/agentic.rs b/src-tauri/src/commands/ai/agentic.rs index db6479e..7eddeaf 100644 --- a/src-tauri/src/commands/ai/agentic.rs +++ b/src-tauri/src/commands/ai/agentic.rs @@ -119,6 +119,7 @@ pub(crate) async fn run_agentic_loop( max_iterations: usize, max_retries: usize, start_iteration: usize, + model_override: Option, ) { // B-260615-09: generating 状态由 RAII guard 收敛复位(正常 exit 显式 reset;panic/异常 Drop 兜底) let mut guard = GeneratingGuard::new(session_arc.clone()); @@ -174,6 +175,17 @@ pub(crate) async fn run_agentic_loop( }; let resolved_model = select_model_id(&agentic_req, &provider_config.model_configs) .unwrap_or_else(|| provider_config.default_model.clone()); + // F-01 阶段6: 用户指定模型 override 穿透(仅主对话生效,标题/扫描/灵感仍走路由)。 + // 兜底原则:override 非空且在该 provider model_configs 池中 → 用 override;否则用 resolved_model。 + // 绝不让 override 导致无模型(空/不在池 → 落回路由结果,行为不变)。 + let resolved_model = match model_override.as_deref() { + Some(id) if !id.is_empty() + && provider_config.model_configs.iter().any(|m| m.model_id == id) => + { + id.to_string() + } + _ => resolved_model, + }; let tool_defs = tools_arc.tool_definitions(); // 停止信号副本:stream_llm 与每轮迭代共享读取,避免重复加锁 // notify 同取一份 Arc 引用(B-260615-14):stream_llm select! 监听 notified() 即时唤醒 @@ -684,6 +696,11 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState, s let max_iterations = state.agent_max_iterations.load(Ordering::SeqCst); // F-260616-07: 流式失败重试次数快照 let max_retries = state.agent_max_retries.load(Ordering::SeqCst); + // F-01 阶段6: 续跑沿用同一主对话的 model_override(审批续跑/达 max 续跑保持一致)。 + let model_override = { + let session = state.ai_session.lock().await; + session.model_override.clone() + }; // 恢复循环前通知前端新建 assistant 消息:审批(通过/拒绝)后新一轮文本 // 不应追加到发起工具调用的旧消息,用 AiAgentRound 隔开 @@ -693,6 +710,6 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState, s }); tauri::async_runtime::spawn(async move { - run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, start_iteration).await; + run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, start_iteration, model_override).await; }); } diff --git a/src-tauri/src/commands/ai/commands.rs b/src-tauri/src/commands/ai/commands.rs index aec5ded..37f7bd8 100644 --- a/src-tauri/src/commands/ai/commands.rs +++ b/src-tauri/src/commands/ai/commands.rs @@ -41,6 +41,7 @@ pub async fn ai_regenerate( state: State<'_, AppState>, conversation_id: String, language: Option, + model_override: Option, ) -> Result { let provider_config = super::prompt::get_active_provider(&state).await?; @@ -55,6 +56,8 @@ pub async fn ai_regenerate( session.agent_language = language.clone(); // F-260616-11: 重生成 = 新生命周期起点,iteration 从头计数。 session.iteration_used = 0; + // F-01 阶段6: 记录用户指定模型 override(主对话专用,兜底见 run_agentic_loop)。 + session.model_override = model_override.clone(); let popped = session.messages.pop_last_assistant_round(); if !popped { // 历史末尾无 AI 回复可弹(空对话/末尾是 user 错误态等),复位 generating 报错 @@ -109,7 +112,7 @@ pub async fn ai_regenerate( let max_retries = state.agent_max_retries.load(Ordering::SeqCst); tauri::async_runtime::spawn(async move { - run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, 0).await; + run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, 0, model_override).await; }); Ok("ok".to_string()) @@ -137,6 +140,7 @@ pub async fn ai_chat_send( message: String, language: Option, skill: Option, + model_override: Option, ) -> Result { // 获取活跃提供商(只读,失败可直接返回,不影响生成标志) let provider_config = super::prompt::get_active_provider(&state).await?; @@ -152,6 +156,9 @@ pub async fn ai_chat_send( session.agent_language = language.clone(); // F-260616-11: 新对话生命周期 iteration 从头计数(累计计数器复位)。 session.iteration_used = 0; + // F-01 阶段6: 记录用户指定模型 override(主对话专用)。run_agentic_loop 兜底校验 + // (override 非空且在 provider model_configs 池中才用,否则落回路由结果)。 + session.model_override = model_override.clone(); // F-260614-02 §5.2:纯技能调用(用户未填文本)时,落库 user content 改 /{skillname} // 作为技能调用标记(非伪造用户文本),让 title.rs summary_msgs 取到非空素材生成标题。 // 非空 message 原样落库。 @@ -219,7 +226,7 @@ pub async fn ai_chat_send( let max_retries = state.agent_max_retries.load(Ordering::SeqCst); tauri::async_runtime::spawn(async move { - run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, 0).await; + run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, 0, model_override).await; }); Ok("ok".to_string()) @@ -392,6 +399,7 @@ pub async fn ai_chat_edit( conversation_id: String, new_message: String, language: Option, + model_override: Option, ) -> Result { let provider_config = super::prompt::get_active_provider(&state).await?; @@ -424,6 +432,8 @@ pub async fn ai_chat_edit( session.agent_language = language.clone(); // F-260616-11: 编辑重生成 = 新生命周期起点,iteration 从头计数。 session.iteration_used = 0; + // F-01 阶段6: 记录用户指定模型 override(主对话专用)。 + session.model_override = model_override.clone(); } let _tool_defs = state.ai_tools.tool_definitions(); @@ -480,6 +490,7 @@ pub async fn ai_chat_edit( max_iterations, max_retries, 0, + model_override, ) .await; }); @@ -499,6 +510,7 @@ pub async fn ai_chat_force_send( message: String, language: Option, skill: Option, + model_override: Option, ) -> Result { // 原子复位:清 generating + 清积压审批 + 置 stop_flag,与 ai_chat_stop 审批分支一致 let old_conv_id = { @@ -521,7 +533,7 @@ pub async fn ai_chat_force_send( } // 复位完成后走 ai_chat_send 同款流程(内部会重新设 generating=true 并 spawn loop) // 直接内联而非递归调 ai_chat_send,避免 IPC 嵌套 - ai_chat_send(app, state, message, language, skill).await + ai_chat_send(app, state, message, language, skill, model_override).await } /// 停止当前 AI 生成 @@ -946,6 +958,8 @@ pub async fn ai_conversation_create( // 启动即见 stop_flag=true 异常退出;agent_language 清空防新会话沿用旧会话语言设置。 session.stop_flag.store(false, Ordering::SeqCst); session.agent_language = None; + // F-01 阶段6: 清旧对话的 model_override,防新对话沿用上一次的「指定」模型。 + session.model_override = None; Ok(serde_json::json!({ "id": id })) } @@ -1021,6 +1035,9 @@ pub async fn ai_conversation_switch( // 重启恢复链路:restore_pending_approvals(init 重建) → switchConversation(此处不清目标对话的) // → ai_pending_tool_calls 查询 → ai_approve 落库) session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&conversation_id)); + // F-01 阶段6: 切换对话清旧 override,防新对话沿用上一次的「指定」模型 + // (override 是单对话级 UI 选择,不跨对话持久化)。 + session.model_override = None; Ok(serde_json::json!({ "id": record.id, diff --git a/src/api/ai.ts b/src/api/ai.ts index 267af04..fcc90ed 100644 --- a/src/api/ai.ts +++ b/src/api/ai.ts @@ -6,13 +6,23 @@ import type { AiChatEvent, AiConversationDetail, AiConversationSummary, AiProvid export const aiApi = { /** 发送消息(非阻塞,通过 ai-chat-event 流式返回);skill 传技能名则注入其 SKILL.md */ - sendMessage(message: string, language?: string, skill?: string): Promise { - return invoke('ai_chat_send', { message, language: language || 'zh-CN', skill: skill || null }) + sendMessage(message: string, language?: string, skill?: string, modelOverride?: string | null): Promise { + return invoke('ai_chat_send', { + message, + language: language || 'zh-CN', + skill: skill || null, + modelOverride: modelOverride || null, + }) }, /** 强制发送(B-260616-02: 复位 generating 残留后走 send 同款流程) */ - forceSend(message: string, language?: string, skill?: string): Promise { - return invoke('ai_chat_force_send', { message, language: language || 'zh-CN', skill: skill || null }) + forceSend(message: string, language?: string, skill?: string, modelOverride?: string | null): Promise { + return invoke('ai_chat_force_send', { + message, + language: language || 'zh-CN', + skill: skill || null, + modelOverride: modelOverride || null, + }) }, /** @@ -20,8 +30,12 @@ export const aiApi = { * 后端:占用 generating → 弹出末尾 AI 回复(保留触发它的 user 消息)→ 重跑 agentic loop。 * 前端消费:与 sendMessage 一样需本地 push 空气泡占位 + 置 streaming(见 useAiSend.regenerate)。 */ - regenerate(conversationId: string, language?: string): Promise { - return invoke('ai_regenerate', { conversationId, language: language || 'zh-CN' }) + regenerate(conversationId: string, language?: string, modelOverride?: string | null): Promise { + return invoke('ai_regenerate', { + conversationId, + language: language || 'zh-CN', + modelOverride: modelOverride || null, + }) }, /** @@ -30,8 +44,13 @@ export const aiApi = { * 前端消费:调 editMessage 前需移除被编辑消息之后的所有消息(视图截断)+ push 空气泡占位 + 置 streaming。 * 中间 user 消息编辑被后端拒绝(只允许末条)。 */ - editMessage(conversationId: string, newMessage: string, language?: string): Promise { - return invoke('ai_chat_edit', { conversationId, newMessage, language: language || 'zh-CN' }) + editMessage(conversationId: string, newMessage: string, language?: string, modelOverride?: string | null): Promise { + return invoke('ai_chat_edit', { + conversationId, + newMessage, + language: language || 'zh-CN', + modelOverride: modelOverride || null, + }) }, /** 批准/拒绝工具调用 */ diff --git a/src/components/AiChat.vue b/src/components/AiChat.vue index 5afae9f..730528c 100644 --- a/src/components/AiChat.vue +++ b/src/components/AiChat.vue @@ -211,6 +211,42 @@ ⏳ {{ $t('aiChat.pendingApprovalCount', { n: pendingApprovalCount }) }} + +
+
+ + +
+ + {{ $t('aiChat.noModels') }} +