新增: F-01阶段6 AiChat模型下拉+override穿透

This commit is contained in:
2026-06-17 00:50:47 +08:00
parent 63bff8bf96
commit 4c84fbc1b6
7 changed files with 256 additions and 16 deletions

View File

@@ -119,6 +119,7 @@ pub(crate) async fn run_agentic_loop(
max_iterations: usize,
max_retries: usize,
start_iteration: usize,
model_override: Option<String>,
) {
// 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-14stream_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;
});
}

View File

@@ -41,6 +41,7 @@ pub async fn ai_regenerate(
state: State<'_, AppState>,
conversation_id: String,
language: Option<String>,
model_override: Option<String>,
) -> Result<String, String> {
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<String>,
skill: Option<String>,
model_override: Option<String>,
) -> Result<String, String> {
// 获取活跃提供商(只读,失败可直接返回,不影响生成标志)
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<String>,
model_override: Option<String>,
) -> Result<String, String> {
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<String>,
skill: Option<String>,
model_override: Option<String>,
) -> Result<String, String> {
// 原子复位:清 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,