优化: aichat L2状态机双轨清理+读侧迁移+L3事件总线emit双写
L2 双轨清理:删 guard 本地 state 冗余字段(ConvState 已落 PerConvState 持久化作读侧真相源)。读侧迁移:ai_is_generating/ai_chat_stop 加 CONV_STATE_ENABLED 门控读 conv_state.is_active()。L3 emit 双写:mod.rs 关键事件 AiCompleted/AiError/AiAgentRound 经 ai_event_bus.publish 双写 (EVENT_BUS_ENABLED 门控,与 app.emit 并存非替换,高频 delta 不双写)。 双轨过渡收尾,可一键回退(CONV_STATE_ENABLED off 回退 bool 真相源)。
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use df_ai::ai_tools::AiToolRegistry;
|
||||
@@ -39,6 +39,8 @@ use df_storage::models::AiProviderRecord;
|
||||
|
||||
use crate::state::{AppState, LlmConcurrency};
|
||||
|
||||
use crate::commands::ai::event_bus::AiBusEvent;
|
||||
|
||||
use super::audit::process_tool_calls;
|
||||
use super::compress::compress_via_llm;
|
||||
use super::conversation::{save_conversation, TokenAccumulator};
|
||||
@@ -514,11 +516,17 @@ pub(crate) async fn run_agentic_loop(
|
||||
Ok(Err(ProviderResolveError::EnsureKeyFailed(msg))) => {
|
||||
guard.reset().await;
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiError {
|
||||
error: msg,
|
||||
error: msg.clone(),
|
||||
// ensure_resolved_key 失败 = key 缺失/钥匙串损坏,归 Auth
|
||||
error_type: Some(ErrorType::Auth),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
// L3 emit 双写(2026-06-22):关键 AiError publish 到事件总线,供跨模块订阅。
|
||||
// EVENT_BUS_ENABLED 门控在 publish 内部(false 静默丢弃),无消费者时空转不报错。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish(AiBusEvent::Error {
|
||||
error: msg,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
Err(_elapsed) => {
|
||||
@@ -526,11 +534,17 @@ pub(crate) async fn run_agentic_loop(
|
||||
// 走 AiError 分支复位 generating,对齐 ensure_resolved_key 失败处理口径。
|
||||
guard.reset().await;
|
||||
tracing::error!(conv_id = %conv_id, "[ai] provider 解析超时(30s),可能 DB/keyring 卡死");
|
||||
let err_msg = "Provider 解析超时(30s),请检查数据库/钥匙串状态后重试".to_string();
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiError {
|
||||
error: "Provider 解析超时(30s),请检查数据库/钥匙串状态后重试".to_string(),
|
||||
error: err_msg.clone(),
|
||||
error_type: Some(ErrorType::Unknown),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
// L3 emit 双写:超时 AiError publish 到事件总线(同上,门控在 publish 内)。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish(AiBusEvent::Error {
|
||||
error: err_msg,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -698,6 +712,13 @@ pub(crate) async fn run_agentic_loop(
|
||||
guard.reset().await;
|
||||
// generating 复位后再 emit Completed:保证前端收事件时后端已可接下一条(发送队列续发不被"正在生成中"拒绝)
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: usage.total_tokens, prompt_tokens: tokens.prompt(), completion_tokens: tokens.completion(), incomplete: None, conversation_id: Some(conv_id.clone()) });
|
||||
// L3 emit 双写:入口 stop 的 AiCompleted publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内)。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish(AiBusEvent::Completed {
|
||||
total_tokens: usage.total_tokens,
|
||||
prompt_tokens: tokens.prompt(),
|
||||
completion_tokens: tokens.completion(),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -779,8 +800,14 @@ pub(crate) async fn run_agentic_loop(
|
||||
|
||||
// 新一轮通知前端(第二轮起),前端需新建 assistant 消息
|
||||
if iteration > 0 {
|
||||
let round_n = (iteration + 1) as u32;
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiAgentRound {
|
||||
round: (iteration + 1) as u32,
|
||||
round: round_n,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
// L3 emit 双写:AiAgentRound publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内)。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish(AiBusEvent::Round {
|
||||
round: round_n,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
}
|
||||
@@ -1142,10 +1169,15 @@ pub(crate) async fn run_agentic_loop(
|
||||
// UX-260618-15: stream_llm 不再 emit AiError,此处统一 emit 最终错误气泡(单气泡聚合)。
|
||||
guard.reset().await;
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiError {
|
||||
error,
|
||||
error: error.clone(),
|
||||
error_type: Some(ErrorType::Network),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
// L3 emit 双写:Fatal AiError publish 到事件总线(门控在 publish 内)。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish(AiBusEvent::Error {
|
||||
error,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1163,12 +1195,18 @@ pub(crate) async fn run_agentic_loop(
|
||||
"[ai] 全 provider 候选流前失败重试耗尽,放弃本轮",
|
||||
);
|
||||
guard.reset().await;
|
||||
let err_msg = last_exhausted_error
|
||||
.unwrap_or_else(|| "AI 调用失败:所有候选 provider 重试耗尽".to_string());
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiError {
|
||||
error: last_exhausted_error
|
||||
.unwrap_or_else(|| "AI 调用失败:所有候选 provider 重试耗尽".to_string()),
|
||||
error: err_msg.clone(),
|
||||
error_type: Some(ErrorType::Network),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
// L3 emit 双写:全 candidate 耗尽 AiError publish 到事件总线(门控在 publish 内)。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish(AiBusEvent::Error {
|
||||
error: err_msg,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1223,6 +1261,13 @@ pub(crate) async fn run_agentic_loop(
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
incomplete: Some(true),
|
||||
});
|
||||
// L3 emit 双写:MidStream 保文 AiCompleted publish 到事件总线(门控在 publish 内)。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish(AiBusEvent::Completed {
|
||||
total_tokens: usage.total_tokens,
|
||||
prompt_tokens: tokens.prompt(),
|
||||
completion_tokens: tokens.completion(),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// F-260616-09 B 批5: global/per_conv permit 已上移 loop 入口(L520-521)整 loop 持有,
|
||||
@@ -1283,6 +1328,13 @@ pub(crate) async fn run_agentic_loop(
|
||||
guard.reset().await;
|
||||
// generating 复位后再 emit Completed:保证前端收事件时后端已可接下一条(发送队列续发不被"正在生成中"拒绝)
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: usage.total_tokens, prompt_tokens: tokens.prompt(), completion_tokens: tokens.completion(), incomplete: None, conversation_id: Some(conv_id.clone()) });
|
||||
// L3 emit 双写:stream 后 stop 的 AiCompleted publish 到事件总线(门控在 publish 内)。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish(AiBusEvent::Completed {
|
||||
total_tokens: usage.total_tokens,
|
||||
prompt_tokens: tokens.prompt(),
|
||||
completion_tokens: tokens.completion(),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1470,6 +1522,14 @@ pub(crate) async fn run_agentic_loop(
|
||||
guard.reset().await;
|
||||
// generating 复位后再 emit Completed:落库/标题/提炼已在后台,前端立即感知完成
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: usage_total, prompt_tokens: tokens.prompt(), completion_tokens: tokens.completion(), incomplete: None, conversation_id: Some(conv_id.clone()) });
|
||||
// L3 emit 双写:正常完成 AiCompleted publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内,
|
||||
// 无消费者空转留批3 真实消费者接入)。AiTextDelta/AiToolCall* 高频事件不双写(无消费者空转)。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish(AiBusEvent::Completed {
|
||||
total_tokens: usage_total,
|
||||
prompt_tokens: tokens.prompt(),
|
||||
completion_tokens: tokens.completion(),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
/// 检查是否所有待审批已处理,如果是则恢复 agentic 循环
|
||||
@@ -1526,7 +1586,15 @@ pub(crate) async fn try_continue_agent_loop(
|
||||
.find_map(|a| a.conversation_id.clone());
|
||||
let conv = session.conv_read(conv_id);
|
||||
// F-09 B 批4:per_conv 唯一真相源,删顶层 fallback(conv 不存在则各字段默认值)。
|
||||
let is_generating = conv.map(|c| c.generating).unwrap_or(false);
|
||||
// L2 读侧迁移(2026-06-22):CONV_STATE_ENABLED on 时读 conv_state.is_active()(Generating/Compressed),
|
||||
// off 回退 generating bool。双轨过渡:off 等价旧行为。
|
||||
let is_generating = conv.map(|c| {
|
||||
if conv_state::CONV_STATE_ENABLED {
|
||||
c.conv_state.is_active()
|
||||
} else {
|
||||
c.generating
|
||||
}
|
||||
}).unwrap_or(false);
|
||||
let agent_language = conv.and_then(|c| c.agent_language.clone());
|
||||
let model_override = conv.and_then(|c| c.model_override.clone());
|
||||
ContinueSnapshot {
|
||||
@@ -1580,11 +1648,16 @@ pub(crate) async fn try_continue_agent_loop(
|
||||
drop(session);
|
||||
tracing::warn!(conv_id = %conv_id, error = %e, "[ai] try_continue 失败:无可用 provider");
|
||||
let _ = app.emit("ai-chat-event", AiChatEvent::AiError {
|
||||
error: e,
|
||||
error: e.clone(),
|
||||
// 无可用 provider(配置丢失/全删):用户需在 Settings 设 provider,归 ProviderConfig
|
||||
error_type: Some(ErrorType::ProviderConfig),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
});
|
||||
// L3 emit 双写:try_continue provider-Err AiError publish 到事件总线(门控在 publish 内)。
|
||||
let _ = app.state::<AppState>().ai_event_bus.publish(AiBusEvent::Error {
|
||||
error: e,
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -1620,9 +1693,16 @@ pub(crate) async fn try_continue_agent_loop(
|
||||
// spawn 前单次 lock 原子重检 generating——若已被 stop 复位,收敛退出而非覆盖用户的 stop。
|
||||
// (run_agentic_loop 入口 GeneratingGuard 会再次置 generating=true,若不重检会抹掉 stop。)
|
||||
// F-09 B 批4:重检 per_conv.generating(唯一真相源;conv 不存在则 false)。
|
||||
// L2 读侧迁移(2026-06-22):CONV_STATE_ENABLED on 时读 conv_state.is_active(),off 回退 generating bool。
|
||||
let still_generating = {
|
||||
let session = state.ai_session.lock().await;
|
||||
session.conv_read(conv_id).map(|c| c.generating).unwrap_or(false)
|
||||
session.conv_read(conv_id).map(|c| {
|
||||
if conv_state::CONV_STATE_ENABLED {
|
||||
c.conv_state.is_active()
|
||||
} else {
|
||||
c.generating
|
||||
}
|
||||
}).unwrap_or(false)
|
||||
};
|
||||
if !still_generating {
|
||||
tracing::info!(conv_id = %conv_id, "[ai] try_continue 终止:spawn 前重检 generating 已被 stop 复位,补发 AiCompleted");
|
||||
@@ -1642,6 +1722,11 @@ pub(crate) async fn try_continue_agent_loop(
|
||||
round: 0,
|
||||
conversation_id: Some(conv_id_owned.clone()),
|
||||
});
|
||||
// L3 emit 双写:try_continue 续跑 AiAgentRound publish 到事件总线(门控在 publish 内)。
|
||||
let _ = app.state::<AppState>().ai_event_bus.publish(AiBusEvent::Round {
|
||||
round: 0,
|
||||
conversation_id: Some(conv_id_owned.clone()),
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id_owned, knowledge_config, llm_concurrency, max_iterations, max_retries, start_iteration, model_override).await;
|
||||
|
||||
Reference in New Issue
Block a user