重构: F-09 batch4 启用多会话上线(决策e真并发·F-09完整)
- IPC签名加conv_id: ai_is_generating/ai_chat_send/ai_chat_force_send/ai_chat_stop + 内部移除active一致性校验(后台conv可操作) - 删switch readonly分支 + 删ai_conversation_create强制结束旧loop(决策e,切走旧loop不退出) - 删双写桥接+顶层字段: messages/generating/stop_flag/notify/iteration_used/agent_language/model_override/session_trust全删,per_conv唯一真相源 + 删双写代码(全模块)+ 删fallback读 + pending_approvals retain目标conv - 前端: api/ai.ts+useAiSend/useAiWindow传conv_id 保留: session_state(SW预留标allow)/readonly(前端不读);零调用方预留按用户指导保留 主代兜底: cargo check --workspace 0 + vue-tsc 0 + test 98 + grep顶层删除/per_conv唯一印证 F-09完整(batch1 PerConvState+batch2迁移+batch5限流调整+batch8恢复+batch4上线);双会话人工验收留用户
This commit is contained in:
@@ -95,8 +95,6 @@ impl GeneratingGuard {
|
|||||||
if !self.done {
|
if !self.done {
|
||||||
let mut session = self.session.lock().await;
|
let mut session = self.session.lock().await;
|
||||||
session.conv(&self.conv_id).generating = false;
|
session.conv(&self.conv_id).generating = false;
|
||||||
// F-09 B 批2 桥接:顶层双写,批4 IPC 迁移后移除
|
|
||||||
session.generating = false;
|
|
||||||
self.done = true;
|
self.done = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,8 +115,6 @@ impl Drop for GeneratingGuard {
|
|||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let mut s = session.lock().await;
|
let mut s = session.lock().await;
|
||||||
s.conv(&conv_id).generating = false;
|
s.conv(&conv_id).generating = false;
|
||||||
// F-09 B 批2 桥接:顶层双写,批4 IPC 迁移后移除
|
|
||||||
s.generating = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,8 +367,6 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
let mut session = session_arc.lock().await;
|
let mut session = session_arc.lock().await;
|
||||||
let conv = session.conv(&conv_id);
|
let conv = session.conv(&conv_id);
|
||||||
conv.generating = true;
|
conv.generating = true;
|
||||||
// 顶层同步(批2 桥接,ai_is_generating 等 IPC 仍读顶层,批4 IPC 迁移后移除)。
|
|
||||||
session.generating = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// F-260614-04 / F-260614-04b: 多 Provider 负载均衡池 — 选主 + fallback 候选列表。
|
// F-260614-04 / F-260614-04b: 多 Provider 负载均衡池 — 选主 + fallback 候选列表。
|
||||||
@@ -559,11 +553,9 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
// F-260616-11 决策 a: 累计 iteration 计数(「下一轮起算值」= 当前轮+1)。
|
// F-260616-11 决策 a: 累计 iteration 计数(「下一轮起算值」= 当前轮+1)。
|
||||||
// 审批等待/达 max 暂停退出时,本字段即「当前轮+1」;审批续跑 ai_approve 读此值作
|
// 审批等待/达 max 暂停退出时,本字段即「当前轮+1」;审批续跑 ai_approve 读此值作
|
||||||
// start_iteration 透传,实现 iteration 累计不重置(防多次审批反复跑满 max 致 token 失控)。
|
// start_iteration 透传,实现 iteration 累计不重置(防多次审批反复跑满 max 致 token 失控)。
|
||||||
// F-09 B 批2:写 per_conv.iteration_used + 双写顶层(批2 桥接,ai_approve 续跑读顶层,
|
// F-09 B 批4:per_conv.iteration_used 唯一真相源,删顶层双写。
|
||||||
// 批4 IPC 迁移后顶层双写移除)。
|
|
||||||
let conv = session.conv(&conv_id);
|
let conv = session.conv(&conv_id);
|
||||||
conv.iteration_used = iteration + 1;
|
conv.iteration_used = iteration + 1;
|
||||||
session.iteration_used = iteration + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新一轮通知前端(第二轮起),前端需新建 assistant 消息
|
// 新一轮通知前端(第二轮起),前端需新建 assistant 消息
|
||||||
@@ -1102,11 +1094,10 @@ pub(crate) async fn try_continue_agent_loop(
|
|||||||
let pending_conv_id = session.pending_approvals.values()
|
let pending_conv_id = session.pending_approvals.values()
|
||||||
.find_map(|a| a.conversation_id.clone());
|
.find_map(|a| a.conversation_id.clone());
|
||||||
let conv = session.conv_read(conv_id);
|
let conv = session.conv_read(conv_id);
|
||||||
let is_generating = conv.map(|c| c.generating).unwrap_or(session.generating);
|
// F-09 B 批4:per_conv 唯一真相源,删顶层 fallback(conv 不存在则各字段默认值)。
|
||||||
let agent_language = conv.and_then(|c| c.agent_language.clone())
|
let is_generating = conv.map(|c| c.generating).unwrap_or(false);
|
||||||
.or_else(|| session.agent_language.clone());
|
let agent_language = conv.and_then(|c| c.agent_language.clone());
|
||||||
let model_override = conv.and_then(|c| c.model_override.clone())
|
let model_override = conv.and_then(|c| c.model_override.clone());
|
||||||
.or_else(|| session.model_override.clone());
|
|
||||||
ContinueSnapshot {
|
ContinueSnapshot {
|
||||||
is_generating,
|
is_generating,
|
||||||
has_pending,
|
has_pending,
|
||||||
@@ -1152,10 +1143,9 @@ pub(crate) async fn try_continue_agent_loop(
|
|||||||
// build_provider_for Err 分支处理(同样 emit AiError),此处与之一致。
|
// build_provider_for Err 分支处理(同样 emit AiError),此处与之一致。
|
||||||
// 不用 GeneratingGuard:try_continue 的 should_continue=false 路径需保 generating=true(审批等待态),
|
// 不用 GeneratingGuard:try_continue 的 should_continue=false 路径需保 generating=true(审批等待态),
|
||||||
// 全函数 guard 会误复位。此点单点 provider-Err 复位,语义独立。
|
// 全函数 guard 会误复位。此点单点 provider-Err 复位,语义独立。
|
||||||
// F-09 B 批2:双写复位 per_conv.generating + 顶层 generating(批2 桥接)。
|
// F-09 B 批4:per_conv 唯一真相源,删顶层双写复位。
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
session.conv(conv_id).generating = false;
|
session.conv(conv_id).generating = false;
|
||||||
session.generating = false;
|
|
||||||
drop(session);
|
drop(session);
|
||||||
tracing::warn!(conv_id = %conv_id, error = %e, "[ai] try_continue 失败:无可用 provider");
|
tracing::warn!(conv_id = %conv_id, error = %e, "[ai] try_continue 失败:无可用 provider");
|
||||||
let _ = app.emit("ai-chat-event", AiChatEvent::AiError {
|
let _ = app.emit("ai-chat-event", AiChatEvent::AiError {
|
||||||
@@ -1188,10 +1178,10 @@ pub(crate) async fn try_continue_agent_loop(
|
|||||||
// BUG-260617-05 续: provider 解析/build_system_prompt 期间用户可能点 stop。
|
// BUG-260617-05 续: provider 解析/build_system_prompt 期间用户可能点 stop。
|
||||||
// spawn 前单次 lock 原子重检 generating——若已被 stop 复位,收敛退出而非覆盖用户的 stop。
|
// spawn 前单次 lock 原子重检 generating——若已被 stop 复位,收敛退出而非覆盖用户的 stop。
|
||||||
// (run_agentic_loop 入口 GeneratingGuard 会再次置 generating=true,若不重检会抹掉 stop。)
|
// (run_agentic_loop 入口 GeneratingGuard 会再次置 generating=true,若不重检会抹掉 stop。)
|
||||||
// F-09 B 批2:重检 per_conv.generating(批2 桥接期与顶层等价)。
|
// F-09 B 批4:重检 per_conv.generating(唯一真相源;conv 不存在则 false)。
|
||||||
let still_generating = {
|
let still_generating = {
|
||||||
let session = state.ai_session.lock().await;
|
let session = state.ai_session.lock().await;
|
||||||
session.conv_read(conv_id).map(|c| c.generating).unwrap_or(session.generating)
|
session.conv_read(conv_id).map(|c| c.generating).unwrap_or(false)
|
||||||
};
|
};
|
||||||
if !still_generating {
|
if !still_generating {
|
||||||
tracing::info!(conv_id = %conv_id, "[ai] try_continue 终止:spawn 前重检 generating 已被 stop 复位,补发 AiCompleted");
|
tracing::info!(conv_id = %conv_id, "[ai] try_continue 终止:spawn 前重检 generating 已被 stop 复位,补发 AiCompleted");
|
||||||
|
|||||||
@@ -36,23 +36,25 @@ use super::AiChatEvent;
|
|||||||
///
|
///
|
||||||
/// 参数 `final_text` 由调用方按清理场景语义选择(如"会话已停止"/"已取消"/"会话已清除")。
|
/// 参数 `final_text` 由调用方按清理场景语义选择(如"会话已停止"/"已取消"/"会话已清除")。
|
||||||
///
|
///
|
||||||
/// F-260616-09 B 批2:`conv_id` 指定要终态化的 per_conv.messages(批2 真相源)。
|
/// F-260616-09 B 批4:per_conv.messages 唯一真相源,删顶层 messages 双写。
|
||||||
/// 顶层 messages 双写(批2 桥接,批4 移除)。conv_id 为空(无活跃对话)时只终态化顶层。
|
/// 终态化策略:每条 pending 审批按其自身 `conversation_id` 终态化到对应 conv 的 per_conv.messages;
|
||||||
|
/// conv_id 入参作 fallback(审批无 conversation_id 的无主审批 R-9 异常数据,终态化到入参 conv,
|
||||||
|
/// 入参也空则跳过——审计仍记,占位不残留内存因 pending 即将被 clear/retain)。
|
||||||
fn finalize_pending_placeholders(session: &mut super::AiSession, conv_id: &str, final_text: &str) {
|
fn finalize_pending_placeholders(session: &mut super::AiSession, conv_id: &str, final_text: &str) {
|
||||||
// SW-260618-02: 先 clone pending 的 tool_call_id(借用在此结束),再可变借 messages。
|
// SW-260618-02: 先 clone pending 的 tool_call_id(借用在此结束),再可变借 messages。
|
||||||
// 必须整体借 &mut session 在函数体内做 disjoint field borrow —— 调用方若分别传
|
// 必须整体借 &mut session 在函数体内做 disjoint field borrow —— 调用方若分别传
|
||||||
// &mut messages + &pending 两个引用,函数参数列表不做 disjoint 推断会触发 E0502(2026-06-18 主代修)。
|
// &mut messages + &pending 两个引用,函数参数列表不做 disjoint 推断会触发 E0502(2026-06-18 主代修)。
|
||||||
let ids: Vec<String> = session
|
let entries: Vec<(String, Option<String>)> = session
|
||||||
.pending_approvals
|
.pending_approvals
|
||||||
.values()
|
.values()
|
||||||
.map(|a| a.tool_call_id.clone())
|
.map(|a| (a.tool_call_id.clone(), a.conversation_id.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
for id in ids {
|
for (id, approval_conv) in entries {
|
||||||
// replace_tool_result_content 反向遍历,找不到对应 tool_call_id 时返回 false,无副作用
|
// replace_tool_result_content 反向遍历,找不到对应 tool_call_id 时返回 false,无副作用
|
||||||
if !conv_id.is_empty() {
|
let target = approval_conv.as_deref().filter(|s| !s.is_empty()).unwrap_or(conv_id);
|
||||||
session.conv(conv_id).messages.replace_tool_result_content(&id, final_text);
|
if !target.is_empty() {
|
||||||
|
session.conv(target).messages.replace_tool_result_content(&id, final_text);
|
||||||
}
|
}
|
||||||
session.messages.replace_tool_result_content(&id, final_text);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,21 +81,15 @@ pub async fn ai_regenerate(
|
|||||||
let provider_config = super::prompt::get_active_provider(&state).await?;
|
let provider_config = super::prompt::get_active_provider(&state).await?;
|
||||||
|
|
||||||
// 原子占用 generating + 弹出末尾 AI 回复(保留 user 消息)
|
// 原子占用 generating + 弹出末尾 AI 回复(保留 user 消息)
|
||||||
// F-260616-09 B 批2:写 per_conv(conv_id 来源:IPC 参数 conversation_id,已与 active 一致性校验)。
|
// F-260616-09 B 批4(决策 e):conv_id 来源 IPC 参数 conversation_id,移除 active 一致性校验
|
||||||
// 顶层字段同步双写(批2 桥接,批4 IPC 迁移后移除)。
|
// (真并发下后台 conv 也应可重新生成);per_conv 唯一真相源,删顶层双写。
|
||||||
{
|
{
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
// generating 拦截:读 per_conv(若已建)否则顶层(批2 桥接兼容)。
|
// generating 拦截:仅查目标 conv 的 per_conv.generating。
|
||||||
let is_gen = session.conv_read(&conversation_id).map(|c| c.generating).unwrap_or(session.generating);
|
let is_gen = session.conv_read(&conversation_id).map(|c| c.generating).unwrap_or(false);
|
||||||
if is_gen {
|
if is_gen {
|
||||||
return Err("AI 正在生成中,请等待完成".to_string());
|
return Err("AI 正在生成中,请等待完成".to_string());
|
||||||
}
|
}
|
||||||
// 一致性:regenerate 限定当前活跃对话(避免历史快照陈旧时弹错对话的消息)。
|
|
||||||
// 必须在 pop_last_assistant_round 之前校验——否则对话不匹配时已弹出 active 对话
|
|
||||||
// 的 AI 回复却报错退出,内存消息被破坏无法恢复(对齐 ai_chat_edit:637-640 先校验后改)。
|
|
||||||
if session.active_conversation_id.as_deref() != Some(conversation_id.as_str()) {
|
|
||||||
return Err("对话已切换,无法重新生成".to_string());
|
|
||||||
}
|
|
||||||
let conv = session.conv(&conversation_id);
|
let conv = session.conv(&conversation_id);
|
||||||
conv.generating = true;
|
conv.generating = true;
|
||||||
conv.stop_flag.store(false, Ordering::SeqCst);
|
conv.stop_flag.store(false, Ordering::SeqCst);
|
||||||
@@ -106,15 +102,8 @@ pub async fn ai_regenerate(
|
|||||||
if !popped {
|
if !popped {
|
||||||
// 历史末尾无 AI 回复可弹(空对话/末尾是 user 错误态等),复位 generating 报错
|
// 历史末尾无 AI 回复可弹(空对话/末尾是 user 错误态等),复位 generating 报错
|
||||||
conv.generating = false;
|
conv.generating = false;
|
||||||
session.generating = false;
|
|
||||||
return Err("没有可重新生成的回复".to_string());
|
return Err("没有可重新生成的回复".to_string());
|
||||||
}
|
}
|
||||||
// 顶层双写(批2 桥接)。
|
|
||||||
session.generating = true;
|
|
||||||
session.stop_flag.store(false, Ordering::SeqCst);
|
|
||||||
session.agent_language = language.clone();
|
|
||||||
session.iteration_used = 0;
|
|
||||||
session.model_override = model_override.clone();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _tool_defs = state.ai_tools.tool_definitions();
|
let _tool_defs = state.ai_tools.tool_definitions();
|
||||||
@@ -122,18 +111,16 @@ pub async fn ai_regenerate(
|
|||||||
let system_prompt = build_system_prompt(&state, &lang).await;
|
let system_prompt = build_system_prompt(&state, &lang).await;
|
||||||
|
|
||||||
// 知识注入:取末尾 user 消息文本做检索(与 send 同款,语义命中刷新上下文)
|
// 知识注入:取末尾 user 消息文本做检索(与 send 同款,语义命中刷新上下文)
|
||||||
// F-260616-09 B 批2:读 per_conv.messages(conv_id 来源:active_conversation_id,regenerate
|
// F-260616-09 B 批4:conv_id 直接用 IPC 参数 conversation_id,读 per_conv.messages。
|
||||||
// 已校验 == conversation_id)。
|
let conv_id = conversation_id.clone();
|
||||||
let (conv_id, last_user_text) = {
|
let last_user_text = {
|
||||||
let session = state.ai_session.lock().await;
|
let session = state.ai_session.lock().await;
|
||||||
let cid = session.active_conversation_id.clone().unwrap_or_default();
|
let msgs = session.conv_read(&conv_id).map(|c| c.messages.all_messages_clone())
|
||||||
let msgs = session.conv_read(&cid).map(|c| c.messages.all_messages_clone())
|
.unwrap_or_default();
|
||||||
.unwrap_or_else(|| session.messages.all_messages_clone());
|
msgs.iter().rev()
|
||||||
let last_user = msgs.iter().rev()
|
|
||||||
.find(|m| matches!(m.role, df_ai::provider::MessageRole::User))
|
.find(|m| matches!(m.role, df_ai::provider::MessageRole::User))
|
||||||
.map(|m| m.content.clone())
|
.map(|m| m.content.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default()
|
||||||
(cid, last_user)
|
|
||||||
};
|
};
|
||||||
let mut system_prompt = system_prompt;
|
let mut system_prompt = system_prompt;
|
||||||
{
|
{
|
||||||
@@ -172,12 +159,22 @@ pub async fn ai_regenerate(
|
|||||||
/// 可能仍为 true(或反之),预检放行后撞后端 `ai_chat_send` 的 generating 拦截。
|
/// 可能仍为 true(或反之),预检放行后撞后端 `ai_chat_send` 的 generating 拦截。
|
||||||
/// 发送前调此命令取后端真值,据之与本地 streaming 对齐,消除状态不同步。
|
/// 发送前调此命令取后端真值,据之与本地 streaming 对齐,消除状态不同步。
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn ai_is_generating(state: State<'_, AppState>) -> Result<bool, String> {
|
pub async fn ai_is_generating(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
// F-260616-09 B 批4:加 conv_id 参数(决策 e 真并发上线),精确查指定 conv 的 generating,
|
||||||
|
// 不依赖 active_conversation_id 单值(否则多会话下查 A 的生成态会读到 B 的值)。
|
||||||
|
// conv_id 为空(前端未传/分离窗口无活跃)时 fallback active conv 兼容旧调用方。
|
||||||
|
conversation_id: Option<String>,
|
||||||
|
) -> Result<bool, String> {
|
||||||
let session = state.ai_session.lock().await;
|
let session = state.ai_session.lock().await;
|
||||||
// F-260616-09 B 批2:批2 单 active 场景,读 active conv 的 per_conv.generating;
|
let target = conversation_id.filter(|s| !s.is_empty())
|
||||||
// per_conv 未建时 fallback 顶层(批2 桥接兼容)。批4 改签名 ai_is_generating(conv_id) 后精确查询。
|
.or_else(|| session.active_conversation_id.clone())
|
||||||
let active = session.active_conversation_id.clone().unwrap_or_default();
|
.unwrap_or_default();
|
||||||
Ok(session.conv_read(&active).map(|c| c.generating).unwrap_or(session.generating))
|
if target.is_empty() {
|
||||||
|
// 无目标 conv 且无 active:无任何 conv 在跑,返 false(批4 顶层 generating 已退役)。
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
Ok(session.conv_read(&target).map(|c| c.generating).unwrap_or(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 发送消息并获取流式 AI 响应
|
/// 发送消息并获取流式 AI 响应
|
||||||
@@ -191,6 +188,10 @@ pub async fn ai_chat_send(
|
|||||||
language: Option<String>,
|
language: Option<String>,
|
||||||
skill: Option<String>,
|
skill: Option<String>,
|
||||||
model_override: Option<String>,
|
model_override: Option<String>,
|
||||||
|
// F-260616-09 B 批4:加 conversation_id 参数(决策 e 真并发上线),操作指定 conv 的 per_conv,
|
||||||
|
// 不依赖 active_conversation_id 单值。前端传 activeConversationId 或目标 conv_id。
|
||||||
|
// None/空 → 沿用旧逻辑(取 active,首次发送懒创建)。
|
||||||
|
conversation_id: Option<String>,
|
||||||
// F-260614-05 Phase 2c: 多模态 parts(前端 invoke 透传,serde 反序列化为 ContentPart 数组)。
|
// F-260614-05 Phase 2c: 多模态 parts(前端 invoke 透传,serde 反序列化为 ContentPart 数组)。
|
||||||
// None/空 → 等价 user()(纯文本,向后兼容);Some(含 Image 片) → user_parts() + provider vision 端点。
|
// None/空 → 等价 user()(纯文本,向后兼容);Some(含 Image 片) → user_parts() + provider vision 端点。
|
||||||
// FR-S1 核验:ContentPart Image base64 是图片数据非 api_key,不入敏感面
|
// FR-S1 核验:ContentPart Image base64 是图片数据非 api_key,不入敏感面
|
||||||
@@ -202,29 +203,41 @@ pub async fn ai_chat_send(
|
|||||||
let provider_config = super::prompt::get_active_provider(&state).await?;
|
let provider_config = super::prompt::get_active_provider(&state).await?;
|
||||||
|
|
||||||
// 原子检查并占用生成标志,防止并发双发;同步追加用户消息,按需自动创建对话
|
// 原子检查并占用生成标志,防止并发双发;同步追加用户消息,按需自动创建对话
|
||||||
// F-260616-09 B 批2:写 per_conv(conv_id 来源:active_conversation_id;首次发送懒创建)。
|
// F-260616-09 B 批4(决策 e 真并发上线):
|
||||||
// 顶层双写(批2 桥接,批4 移除)。
|
// - 目标 conv 取入参 conversation_id(前端传 activeConversationId),None/空时 fallback
|
||||||
{
|
// active(旧调用方兼容);若 active 也为 None(首次发送),懒创建新 conv id。
|
||||||
|
// - per_conv 是唯一真相源,删除顶层双写(批2 桥接已退役)。
|
||||||
|
let conv_id = {
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
// generating 拦截:active conv 的 per_conv.generating(未建 fallback 顶层)。
|
// 目标 conv:入参优先 → active → 懒创建。
|
||||||
let active_for_check = session.active_conversation_id.clone().unwrap_or_default();
|
let target = conversation_id.clone().filter(|s| !s.is_empty())
|
||||||
let is_gen = if active_for_check.is_empty() {
|
.or_else(|| session.active_conversation_id.clone());
|
||||||
session.generating
|
let target = match target {
|
||||||
} else {
|
Some(id) => id,
|
||||||
session.conv_read(&active_for_check).map(|c| c.generating).unwrap_or(session.generating)
|
None => {
|
||||||
|
// 首次发送懒创建新 conv id(不立即落库,避免空对话残留;
|
||||||
|
// 实际记录由 save_conversation 在生成内容后 upsert 写入)。
|
||||||
|
let id = new_id();
|
||||||
|
session.active_conversation_id = Some(id.clone());
|
||||||
|
session.active_conv_created_at = Some(now_millis());
|
||||||
|
id
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
// active_conversation_id 同步到 target(用户当前面板即 target,路由键跟随)。
|
||||||
|
// 决策 e:旧 conv 的后台 loop 不受 active 变更影响(写自己的 per_conv)。
|
||||||
|
if session.active_conversation_id.as_deref() != Some(target.as_str()) {
|
||||||
|
session.active_conversation_id = Some(target.clone());
|
||||||
|
// 切到已有 conv(非本次懒建)时,确保其 active_conv_created_at 已设(switch 路径已设)。
|
||||||
|
if session.active_conv_created_at.is_none() {
|
||||||
|
session.active_conv_created_at = Some(now_millis());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// generating 拦截:仅查目标 conv 的 per_conv.generating(真并发下各 conv 独立)。
|
||||||
|
let is_gen = session.conv_read(&target).map(|c| c.generating).unwrap_or(false);
|
||||||
if is_gen {
|
if is_gen {
|
||||||
return Err("AI 正在生成中,请等待完成".to_string());
|
return Err("AI 正在生成中,请等待完成".to_string());
|
||||||
}
|
}
|
||||||
// 首次发送时生成对话 id(懒创建:不立即落库,避免空对话残留;
|
let conv = session.conv(&target);
|
||||||
// 实际记录由 save_conversation 在生成内容后 upsert 写入)
|
|
||||||
if session.active_conversation_id.is_none() {
|
|
||||||
let conv_id = new_id();
|
|
||||||
session.active_conversation_id = Some(conv_id);
|
|
||||||
session.active_conv_created_at = Some(now_millis());
|
|
||||||
}
|
|
||||||
let active = session.active_conversation_id.clone().unwrap_or_default();
|
|
||||||
let conv = session.conv(&active);
|
|
||||||
conv.generating = true;
|
conv.generating = true;
|
||||||
conv.stop_flag.store(false, Ordering::SeqCst);
|
conv.stop_flag.store(false, Ordering::SeqCst);
|
||||||
conv.agent_language = language.clone();
|
conv.agent_language = language.clone();
|
||||||
@@ -252,18 +265,8 @@ pub async fn ai_chat_send(
|
|||||||
} else {
|
} else {
|
||||||
conv.messages.push(ChatMessage::user(&user_content));
|
conv.messages.push(ChatMessage::user(&user_content));
|
||||||
}
|
}
|
||||||
// 顶层双写(批2 桥接)。
|
target
|
||||||
session.generating = true;
|
};
|
||||||
session.stop_flag.store(false, Ordering::SeqCst);
|
|
||||||
session.agent_language = language.clone();
|
|
||||||
session.iteration_used = 0;
|
|
||||||
session.model_override = model_override.clone();
|
|
||||||
if let Some(ps) = parts.as_ref().filter(|p| !p.is_empty()) {
|
|
||||||
session.messages.push(ChatMessage::user_parts(&user_content, ps.clone()));
|
|
||||||
} else {
|
|
||||||
session.messages.push(ChatMessage::user(&user_content));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取工具定义(预取仅用于触发注册表初始化,实际 tool_defs 在 agentic loop 内部按需获取)
|
// 获取工具定义(预取仅用于触发注册表初始化,实际 tool_defs 在 agentic loop 内部按需获取)
|
||||||
let _tool_defs = state.ai_tools.tool_definitions();
|
let _tool_defs = state.ai_tools.tool_definitions();
|
||||||
@@ -280,11 +283,7 @@ pub async fn ai_chat_send(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 快照当前对话 ID,供知识注入溯源 + spawn 后台 loop(不受切换影响)
|
// conv_id 已在上方状态占用块得出(入参/active/懒创建),供知识注入溯源 + spawn 后台 loop。
|
||||||
let conv_id = {
|
|
||||||
let session = state.ai_session.lock().await;
|
|
||||||
session.active_conversation_id.clone().unwrap_or_default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// 知识注入:检索相关知识拼到 system prompt 前(可配置开关 auto_inject,默认开)
|
// 知识注入:检索相关知识拼到 system prompt 前(可配置开关 auto_inject,默认开)
|
||||||
// 最终顺序:[知识库上下文] --- [技能指令] --- [原始 system prompt]
|
// 最终顺序:[知识库上下文] --- [技能指令] --- [原始 system prompt]
|
||||||
@@ -351,13 +350,12 @@ pub async fn ai_approve(
|
|||||||
|
|
||||||
if !approved {
|
if !approved {
|
||||||
// 替换占位 tool_result 为拒绝结果
|
// 替换占位 tool_result 为拒绝结果
|
||||||
// F-260616-09 B 批2:写 per_conv.messages(conv_id 来源:approval.conversation_id,业务真相源)。
|
// F-260616-09 B 批4:per_conv.messages 唯一真相源(conv_id 来源 approval.conversation_id)。
|
||||||
// per_conv 未建时 fallback 顶层(批2 桥接兼容,无 live loop 时顶层仍是真相源)。
|
// 无 conversation_id 的无主审批(R-9 异常数据)无 per_conv 可写,拒绝结果不入 messages
|
||||||
let _conv_id_for_msg = approval.conversation_id.clone();
|
// (审计仍记 rejected),避免写错 conv。
|
||||||
if let Some(ref cid) = _conv_id_for_msg {
|
if let Some(ref cid) = approval.conversation_id {
|
||||||
session.conv(cid).messages.replace_tool_result_content(&tool_call_id, "用户拒绝了此操作");
|
session.conv(cid).messages.replace_tool_result_content(&tool_call_id, "用户拒绝了此操作");
|
||||||
}
|
}
|
||||||
session.messages.replace_tool_result_content(&tool_call_id, "用户拒绝了此操作");
|
|
||||||
let conv_id = approval.conversation_id.clone();
|
let conv_id = approval.conversation_id.clone();
|
||||||
let _ = app.emit("ai-chat-event", AiChatEvent::AiApprovalResult {
|
let _ = app.emit("ai-chat-event", AiChatEvent::AiApprovalResult {
|
||||||
id: tool_call_id.clone(),
|
id: tool_call_id.clone(),
|
||||||
@@ -372,14 +370,15 @@ pub async fn ai_approve(
|
|||||||
}
|
}
|
||||||
// 审计:拒绝(决策者=human)
|
// 审计:拒绝(决策者=human)
|
||||||
audit_finalize(&state, &tool_call_id, "rejected", None).await;
|
audit_finalize(&state, &tool_call_id, "rejected", None).await;
|
||||||
// F-260616-11 决策 a: 审批续跑 iteration 累计(不重置)——读 session.iteration_used 透传
|
// F-260616-11 决策 a: 审批续跑 iteration 累计(不重置)——读 per_conv.iteration_used 透传
|
||||||
// 作 start_iteration,防多次审批反复跑满 max_iterations 致 token 失控。
|
// 作 start_iteration,防多次审批反复跑满 max_iterations 致 token 失控。
|
||||||
// F-09 B 批2:读 per_conv.iteration_used(conv_id 来源:approval.conversation_id,本分支业务真相源)。
|
// F-09 B 批4:per_conv 唯一真相源(conv_id 来源 approval.conversation_id);无 conv_id 时 0。
|
||||||
let start_iter = {
|
let start_iter = {
|
||||||
let session = state.ai_session.lock().await;
|
let session = state.ai_session.lock().await;
|
||||||
session.conv_read(&conv_id.clone().unwrap_or_default())
|
conv_id.as_deref()
|
||||||
|
.and_then(|c| session.conv_read(c))
|
||||||
.map(|c| c.iteration_used)
|
.map(|c| c.iteration_used)
|
||||||
.unwrap_or(session.iteration_used)
|
.unwrap_or(0)
|
||||||
};
|
};
|
||||||
// 所有待审批处理完毕后恢复 agentic 循环
|
// 所有待审批处理完毕后恢复 agentic 循环
|
||||||
// F-09 B 批2:传 conv_id(approval.conversation_id),try_continue 按 conv 过滤 pending/读 per_conv。
|
// F-09 B 批2:传 conv_id(approval.conversation_id),try_continue 按 conv 过滤 pending/读 per_conv。
|
||||||
@@ -395,18 +394,14 @@ pub async fn ai_approve(
|
|||||||
// AE-2025-04 会话级信任:用户人工批准 write_file / run_command 时,把该工具+目录写入
|
// AE-2025-04 会话级信任:用户人工批准 write_file / run_command 时,把该工具+目录写入
|
||||||
// session_trust —— 下次同会话同类操作(同工具+同目录 TrustKey)自动放行,跳过 pending + 二次确认。
|
// session_trust —— 下次同会话同类操作(同工具+同目录 TrustKey)自动放行,跳过 pending + 二次确认。
|
||||||
// 仅首批工具(write_file/run_command)命中 trust_key_for 的 Some,其余工具 noop(None 不写)。
|
// 仅首批工具(write_file/run_command)命中 trust_key_for 的 Some,其余工具 noop(None 不写)。
|
||||||
// F-260616-09 B 批2:写 per_conv.session_trust(conv_id 来源:approval.conversation_id,业务真相源)。
|
// F-260616-09 B 批4:per_conv.session_trust 唯一真相源(conv_id 来源 approval.conversation_id)。
|
||||||
// 顶层双写(批2 桥接)。无 conv_id(异常数据)时只写顶层。
|
// 无 conv_id(无主审批 R-9 异常数据)时无 per_conv 可写,信任不记(审计仍记 executed)。
|
||||||
if let Some(key) = super::trust_key_for(&approval.tool_name, &approval.arguments) {
|
if let Some(key) = super::trust_key_for(&approval.tool_name, &approval.arguments) {
|
||||||
let mut inserted = false;
|
let inserted = if let Some(ref cid) = conv_id {
|
||||||
if let Some(ref cid) = conv_id {
|
session.conv(cid).session_trust.insert(key.clone())
|
||||||
if session.conv(cid).session_trust.insert(key.clone()) {
|
} else {
|
||||||
inserted = true;
|
false
|
||||||
}
|
};
|
||||||
}
|
|
||||||
if session.session_trust.insert(key.clone()) {
|
|
||||||
inserted = true;
|
|
||||||
}
|
|
||||||
if inserted {
|
if inserted {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
tool = %approval.tool_name,
|
tool = %approval.tool_name,
|
||||||
@@ -450,12 +445,12 @@ pub async fn ai_approve(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 重新获取锁,替换占位 tool_result 为真实结果(失败时为错误信息,LLM 据此决定下一步)
|
// 重新获取锁,替换占位 tool_result 为真实结果(失败时为错误信息,LLM 据此决定下一步)
|
||||||
// F-260616-09 B 批2:写 per_conv.messages + 顶层 messages(双写,conv_id 来源:approval.conversation_id)。
|
// F-260616-09 B 批4:per_conv.messages 唯一真相源(conv_id 来源 approval.conversation_id)。
|
||||||
|
// 无 conv_id 的无主审批不入 messages(审计仍记 executed/failed)。
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
if let Some(ref cid) = conv_id {
|
if let Some(ref cid) = conv_id {
|
||||||
session.conv(cid).messages.replace_tool_result_content(&id, &result_val.to_string());
|
session.conv(cid).messages.replace_tool_result_content(&id, &result_val.to_string());
|
||||||
}
|
}
|
||||||
session.messages.replace_tool_result_content(&id, &result_val.to_string());
|
|
||||||
|
|
||||||
let _ = app.emit("ai-chat-event", AiChatEvent::AiToolCallCompleted {
|
let _ = app.emit("ai-chat-event", AiChatEvent::AiToolCallCompleted {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
@@ -476,14 +471,13 @@ pub async fn ai_approve(
|
|||||||
save_conversation(&state.ai_session, &state.db, cid, None, None).await;
|
save_conversation(&state.ai_session, &state.db, cid, None, None).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// F-260616-11 决策 a: 审批续跑 iteration 累计(不重置)——读 session.iteration_used 透传
|
// F-260616-11 决策 a: 审批续跑 iteration 累计(不重置)——读 per_conv.iteration_used 透传
|
||||||
// 作 start_iteration,防多次审批反复跑满 max_iterations 致 token 失控。
|
// 作 start_iteration,防多次审批反复跑满 max_iterations 致 token 失控。
|
||||||
// F-09 B 批2:读 per_conv.iteration_used + 传 conv_id 给 try_continue(按 conv 过滤 pending)。
|
// F-09 B 批4:per_conv 唯一真相源;无 conv_id 时 start_iter=0。
|
||||||
let cont_conv_id = conv_id.clone().unwrap_or_default();
|
let cont_conv_id = conv_id.clone().unwrap_or_default();
|
||||||
let start_iter = {
|
let start_iter = {
|
||||||
let session = state.ai_session.lock().await;
|
let session = state.ai_session.lock().await;
|
||||||
session.conv_read(&cont_conv_id).map(|c| c.iteration_used)
|
session.conv_read(&cont_conv_id).map(|c| c.iteration_used).unwrap_or(0)
|
||||||
.unwrap_or(session.iteration_used)
|
|
||||||
};
|
};
|
||||||
// 所有待审批处理完毕后恢复 agentic 循环(recovered 无 live loop,try_continue 因 generating=false 自然不续)
|
// 所有待审批处理完毕后恢复 agentic 循环(recovered 无 live loop,try_continue 因 generating=false 自然不续)
|
||||||
try_continue_agent_loop(&app, &state, &cont_conv_id, start_iter).await;
|
try_continue_agent_loop(&app, &state, &cont_conv_id, start_iter).await;
|
||||||
@@ -525,13 +519,13 @@ pub async fn ai_chat_clear(state: State<'_, AppState>) -> Result<(), String> {
|
|||||||
let active_id = session.active_conversation_id.clone();
|
let active_id = session.active_conversation_id.clone();
|
||||||
// SW-260618-02:清 pending 前先把占位 tool_result 终态化(此处 messages 紧随即 clear,
|
// SW-260618-02:清 pending 前先把占位 tool_result 终态化(此处 messages 紧随即 clear,
|
||||||
// 调用为 no-op,但保持清理点统一口径防顺序变更时占位随 messages 残留 DB)。
|
// 调用为 no-op,但保持清理点统一口径防顺序变更时占位随 messages 残留 DB)。
|
||||||
// F-260616-09 B 批2:per_conv.messages + 顶层双清,conv_id 来源 active_id。
|
// F-260616-09 B 批4:per_conv.messages 唯一真相源,删顶层 messages.clear 双写;
|
||||||
|
// pending_approvals 仅清 active conv 的(retain 保留其他 conv 的审批)。
|
||||||
finalize_pending_placeholders(&mut *session, &active_id.clone().unwrap_or_default(), "会话已清除");
|
finalize_pending_placeholders(&mut *session, &active_id.clone().unwrap_or_default(), "会话已清除");
|
||||||
if let Some(ref id) = active_id {
|
if let Some(ref id) = active_id {
|
||||||
session.conv(id).messages.clear();
|
session.conv(id).messages.clear();
|
||||||
}
|
}
|
||||||
session.messages.clear();
|
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != active_id.as_deref());
|
||||||
session.pending_approvals.clear();
|
|
||||||
drop(session);
|
drop(session);
|
||||||
// 真删 DB:清空该对话 messages JSON + 清零 token(保留对话壳),刷新不再恢复(AR-7)
|
// 真删 DB:清空该对话 messages JSON + 清零 token(保留对话壳),刷新不再恢复(AR-7)
|
||||||
if let Some(id) = active_id {
|
if let Some(id) = active_id {
|
||||||
@@ -559,14 +553,10 @@ pub async fn ai_chat_clear_context(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
conversation_id: String,
|
conversation_id: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// 活跃对话一致性:仅对当前活跃对话分段(防陈旧快照标错对话的消息)。
|
// F-260616-09 B 批4(决策 e):conv_id 来源 IPC 参数 conversation_id,移除 active 一致性校验
|
||||||
// 与 ai_chat_send/ai_regenerate 取 session 模式一致(state.ai_session.lock)。
|
// (真并发下后台 conv 也应可分段);messages 操作走 per_conv。
|
||||||
// F-260616-09 B 批2:messages 操作改 per_conv(conv_id 来源:IPC 参数 conversation_id,已与 active 校验)。
|
|
||||||
let conv_id = {
|
let conv_id = {
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
if session.active_conversation_id.as_deref() != Some(conversation_id.as_str()) {
|
|
||||||
return Err("对话已切换,无法分段".to_string());
|
|
||||||
}
|
|
||||||
// 保护区:保留最近 PROTECT_COUNT 条 active。
|
// 保护区:保留最近 PROTECT_COUNT 条 active。
|
||||||
const PROTECT_COUNT: usize = 6;
|
const PROTECT_COUNT: usize = 6;
|
||||||
let conv = session.conv(&conversation_id);
|
let conv = session.conv(&conversation_id);
|
||||||
@@ -621,9 +611,7 @@ pub async fn ai_chat_compress_context(
|
|||||||
// F-260616-09 B 批2:messages 操作改 per_conv(conv_id 来源:IPC 参数 conversation_id,已与 active 校验)。
|
// F-260616-09 B 批2:messages 操作改 per_conv(conv_id 来源:IPC 参数 conversation_id,已与 active 校验)。
|
||||||
let (conv_id, active_msgs, compress_end) = {
|
let (conv_id, active_msgs, compress_end) = {
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
if session.active_conversation_id.as_deref() != Some(conversation_id.as_str()) {
|
// F-260616-09 B 批4(决策 e):移除 active 一致性校验(真并发下后台 conv 也应可压缩)。
|
||||||
return Err("对话已切换,无法压缩".to_string());
|
|
||||||
}
|
|
||||||
let conv = session.conv(&conversation_id);
|
let conv = session.conv(&conversation_id);
|
||||||
if conv.messages.is_compressing() {
|
if conv.messages.is_compressing() {
|
||||||
return Err("压缩正在进行中".to_string());
|
return Err("压缩正在进行中".to_string());
|
||||||
@@ -746,17 +734,14 @@ pub async fn ai_chat_edit(
|
|||||||
let provider_config = super::prompt::get_active_provider(&state).await?;
|
let provider_config = super::prompt::get_active_provider(&state).await?;
|
||||||
|
|
||||||
// 原子占用 generating + 替换末条 user content + truncate 其后
|
// 原子占用 generating + 替换末条 user content + truncate 其后
|
||||||
// F-260616-09 B 批2:messages/generating 等改 per_conv(conv_id 来源:IPC 参数 conversation_id,已与 active 校验)。
|
// F-260616-09 B 批4(决策 e):conv_id 来源 IPC 参数 conversation_id,移除 active 一致性校验
|
||||||
|
// (真并发下后台 conv 也应可编辑);per_conv 唯一真相源,删顶层双写。
|
||||||
{
|
{
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
let is_gen = session.conv_read(&conversation_id).map(|c| c.generating).unwrap_or(session.generating);
|
let is_gen = session.conv_read(&conversation_id).map(|c| c.generating).unwrap_or(false);
|
||||||
if is_gen {
|
if is_gen {
|
||||||
return Err("AI 正在生成中,请等待完成".to_string());
|
return Err("AI 正在生成中,请等待完成".to_string());
|
||||||
}
|
}
|
||||||
// 活跃对话一致性(防切走后编辑老快照)
|
|
||||||
if session.active_conversation_id.as_deref() != Some(conversation_id.as_str()) {
|
|
||||||
return Err("对话已切换,无法编辑".to_string());
|
|
||||||
}
|
|
||||||
let conv = session.conv(&conversation_id);
|
let conv = session.conv(&conversation_id);
|
||||||
// ① 替换末条 active user 消息 content(无 active user → Err)
|
// ① 替换末条 active user 消息 content(无 active user → Err)
|
||||||
if conv.messages.replace_last_active_user_content(&new_message).is_err() {
|
if conv.messages.replace_last_active_user_content(&new_message).is_err() {
|
||||||
@@ -773,12 +758,6 @@ pub async fn ai_chat_edit(
|
|||||||
conv.iteration_used = 0;
|
conv.iteration_used = 0;
|
||||||
// F-01 阶段6: 记录用户指定模型 override(主对话专用)。
|
// F-01 阶段6: 记录用户指定模型 override(主对话专用)。
|
||||||
conv.model_override = model_override.clone();
|
conv.model_override = model_override.clone();
|
||||||
// 顶层双写(批2 桥接)。
|
|
||||||
session.generating = true;
|
|
||||||
session.stop_flag.store(false, Ordering::SeqCst);
|
|
||||||
session.agent_language = language.clone();
|
|
||||||
session.iteration_used = 0;
|
|
||||||
session.model_override = model_override.clone();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _tool_defs = state.ai_tools.tool_definitions();
|
let _tool_defs = state.ai_tools.tool_definitions();
|
||||||
@@ -786,20 +765,18 @@ pub async fn ai_chat_edit(
|
|||||||
let system_prompt = build_system_prompt(&state, &lang).await;
|
let system_prompt = build_system_prompt(&state, &lang).await;
|
||||||
|
|
||||||
// 知识注入:用新 user 文本检索(与 send/regenerate 同款)
|
// 知识注入:用新 user 文本检索(与 send/regenerate 同款)
|
||||||
// F-260616-09 B 批2:读 per_conv.messages(conv_id 来源:active_conversation_id,edit 已校验 == conversation_id)。
|
// F-260616-09 B 批4:conv_id 直接用 IPC 参数 conversation_id,读 per_conv.messages。
|
||||||
let (conv_id, last_user_text) = {
|
let conv_id = conversation_id.clone();
|
||||||
|
let last_user_text = {
|
||||||
let session = state.ai_session.lock().await;
|
let session = state.ai_session.lock().await;
|
||||||
let cid = session.active_conversation_id.clone().unwrap_or_default();
|
let msgs = session.conv_read(&conv_id).map(|c| c.messages.all_messages_clone())
|
||||||
let msgs = session.conv_read(&cid).map(|c| c.messages.all_messages_clone())
|
.unwrap_or_default();
|
||||||
.unwrap_or_else(|| session.messages.all_messages_clone());
|
|
||||||
// 取末条 active user 文本(sanitize 前的全量,但 truncated 已标,这里取 active 的末条)
|
// 取末条 active user 文本(sanitize 前的全量,但 truncated 已标,这里取 active 的末条)
|
||||||
let last_user = msgs
|
msgs.iter()
|
||||||
.iter()
|
|
||||||
.rev()
|
.rev()
|
||||||
.find(|m| matches!(m.role, df_ai::provider::MessageRole::User) && m.is_active())
|
.find(|m| matches!(m.role, df_ai::provider::MessageRole::User) && m.is_active())
|
||||||
.map(|m| m.content.clone())
|
.map(|m| m.content.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default()
|
||||||
(cid, last_user)
|
|
||||||
};
|
};
|
||||||
let mut system_prompt = system_prompt;
|
let mut system_prompt = system_prompt;
|
||||||
{
|
{
|
||||||
@@ -868,48 +845,61 @@ pub async fn ai_chat_force_send(
|
|||||||
// F-260614-05 Phase 2c: 透传 parts(强制发送同款多模态支持)。
|
// F-260614-05 Phase 2c: 透传 parts(强制发送同款多模态支持)。
|
||||||
// FR-S1 核验:ContentPart Image base64 是图片数据非 api_key,不入敏感面,语义同 ai_chat_send。
|
// FR-S1 核验:ContentPart Image base64 是图片数据非 api_key,不入敏感面,语义同 ai_chat_send。
|
||||||
parts: Option<Vec<ContentPart>>,
|
parts: Option<Vec<ContentPart>>,
|
||||||
|
// F-260616-09 B 批4(决策 e 真并发上线):加 conversation_id 参数,强制复位+发送仅作用于目标 conv。
|
||||||
|
// None/空 → fallback active(旧调用方兼容)。
|
||||||
|
conversation_id: Option<String>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
// 获取活跃提供商(只读,失败可直接返回,不占用 generating)
|
// 获取活跃提供商(只读,失败可直接返回,不占用 generating)
|
||||||
let provider_config = super::prompt::get_active_provider(&state).await?;
|
let provider_config = super::prompt::get_active_provider(&state).await?;
|
||||||
|
|
||||||
// 原子"复位 + 占用":同一把锁内先清旧生成态、再占用 generating + 追加用户消息。
|
// 原子"复位 + 占用":同一把锁内先清目标 conv 的旧生成态,再占用 generating + 追加用户消息。
|
||||||
// stop_flag 置 true(清旧)随即 false(新 loop 起跑)在锁内瞬变,无人能观察到中间态;
|
// stop_flag 置 true(清旧)随即 false(新 loop 起跑)在锁内瞬变,无人能观察到中间态;
|
||||||
// 关键是复位与占用之间无锁释放窗口,杜绝并发 send IPC 抢占 generating。
|
// 关键是复位与占用之间无锁释放窗口,杜绝并发 send IPC 抢占 generating。
|
||||||
let old_conv_id = {
|
//
|
||||||
|
// F-260616-09 B 批4(决策 e):force_send 仅复位**目标 conv 自己**的 generating(用户当前面板),
|
||||||
|
// 不再跨 conv 杀(旧实现清全局 generating + 全 clear pending_approvals,在真并发下会误杀其他
|
||||||
|
// 后台 conv 的 loop/审批)。pending_approvals 仅清目标 conv 的(retain),保留其他 conv 的。
|
||||||
|
let (old_conv_id, conv_id) = {
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
// ① 软停止复位(照旧实现,与 ai_chat_stop 审批分支一致)
|
// 目标 conv:入参优先 → active → 懒创建。
|
||||||
let old = session.active_conversation_id.clone();
|
let target = conversation_id.clone().filter(|s| !s.is_empty())
|
||||||
// F-260616-09 B 批2:per_conv + 顶层双写。old 为旧 conv(若存在),置其 per_conv.generating=false。
|
.or_else(|| session.active_conversation_id.clone());
|
||||||
if let Some(ref old_id) = old {
|
let target = match target {
|
||||||
session.conv(old_id).generating = false;
|
Some(id) => id,
|
||||||
|
None => {
|
||||||
|
let id = new_id();
|
||||||
|
session.active_conversation_id = Some(id.clone());
|
||||||
|
session.active_conv_created_at = Some(now_millis());
|
||||||
|
id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if session.active_conversation_id.as_deref() != Some(target.as_str()) {
|
||||||
|
session.active_conversation_id = Some(target.clone());
|
||||||
|
if session.active_conv_created_at.is_none() {
|
||||||
|
session.active_conv_created_at = Some(now_millis());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
session.generating = false;
|
// ① 软停止复位目标 conv(与 ai_chat_stop 审批分支一致),emit AiCompleted 用旧 target 自身。
|
||||||
// SW-260618-02:强制发送丢弃旧审批,先终态化占位 tool_result,防占位随 messages 残留
|
let was_gen = session.conv_read(&target).map(|c| c.generating).unwrap_or(false);
|
||||||
// 下次发送喂给 LLM(此处 messages 不 clear,是真实残留路径)。
|
// SW-260618-02:强制发送丢弃目标 conv 的旧审批,先终态化占位 tool_result,防占位随 messages
|
||||||
// F-09 B 批2:finalize 双写 per_conv(old)+ 顶层;conv_id 来源 old active(若存在)。
|
// 残留下次发送喂给 LLM。仅清目标 conv 的 pending_approvals(retain 保留其他 conv)。
|
||||||
finalize_pending_placeholders(&mut *session, &old.clone().unwrap_or_default(), "已取消");
|
finalize_pending_placeholders(&mut *session, &target, "已取消");
|
||||||
session.pending_approvals.clear();
|
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(target.as_str()));
|
||||||
if let Some(ref old_id) = old {
|
{
|
||||||
session.conv(old_id).stop_flag.store(true, Ordering::SeqCst);
|
let conv = session.conv(&target);
|
||||||
|
conv.generating = false;
|
||||||
|
conv.stop_flag.store(true, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
session.stop_flag.store(true, Ordering::SeqCst);
|
// ② 同锁内立即占用 + 追加用户消息(逻辑照 ai_chat_send,无 generating 拦截——
|
||||||
// ② 同锁内立即占用 + 追加用户消息(逻辑照 ai_chat_send:160-194,无 generating 拦截——
|
|
||||||
// 这是"强制"语义本身,前面已主动复位)
|
// 这是"强制"语义本身,前面已主动复位)
|
||||||
// 首次发送懒创建 conv id(与 ai_chat_send:190-194 一致)
|
let conv = session.conv(&target);
|
||||||
if session.active_conversation_id.is_none() {
|
|
||||||
let conv_id = new_id();
|
|
||||||
session.active_conversation_id = Some(conv_id);
|
|
||||||
session.active_conv_created_at = Some(now_millis());
|
|
||||||
}
|
|
||||||
let active = session.active_conversation_id.clone().unwrap_or_default();
|
|
||||||
let conv = session.conv(&active);
|
|
||||||
conv.generating = true;
|
conv.generating = true;
|
||||||
conv.stop_flag.store(false, Ordering::SeqCst);
|
conv.stop_flag.store(false, Ordering::SeqCst);
|
||||||
conv.agent_language = language.clone();
|
conv.agent_language = language.clone();
|
||||||
// F-260616-11: 强制发送 = 新对话生命周期起点,iteration 从头计数。
|
// F-260616-11: 强制发送 = 新对话生命周期起点,iteration 从头计数。
|
||||||
conv.iteration_used = 0;
|
conv.iteration_used = 0;
|
||||||
conv.model_override = model_override.clone();
|
conv.model_override = model_override.clone();
|
||||||
// user content 处理(技能调用空文本标记 /{skillname}),照 ai_chat_send:171-179
|
// user content 处理(技能调用空文本标记 /{skillname}),照 ai_chat_send
|
||||||
let user_content = if message.trim().is_empty() {
|
let user_content = if message.trim().is_empty() {
|
||||||
if let Some(ref skill_name) = skill {
|
if let Some(ref skill_name) = skill {
|
||||||
format!("/{}", skill_name)
|
format!("/{}", skill_name)
|
||||||
@@ -925,21 +915,10 @@ pub async fn ai_chat_force_send(
|
|||||||
} else {
|
} else {
|
||||||
conv.messages.push(ChatMessage::user(&user_content));
|
conv.messages.push(ChatMessage::user(&user_content));
|
||||||
}
|
}
|
||||||
// 顶层双写(批2 桥接)。
|
(was_gen.then_some(target.clone()), target)
|
||||||
session.generating = true;
|
|
||||||
session.stop_flag.store(false, Ordering::SeqCst);
|
|
||||||
session.agent_language = language.clone();
|
|
||||||
session.iteration_used = 0;
|
|
||||||
session.model_override = model_override.clone();
|
|
||||||
if let Some(ps) = parts.as_ref().filter(|p| !p.is_empty()) {
|
|
||||||
session.messages.push(ChatMessage::user_parts(&user_content, ps.clone()));
|
|
||||||
} else {
|
|
||||||
session.messages.push(ChatMessage::user(&user_content));
|
|
||||||
}
|
|
||||||
old
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 通知前端旧生成已结束(若有残留 conv;emit 在锁外,避免持锁调 runtime emit)
|
// 通知前端目标 conv 旧生成已结束(若先前在生成;emit 在锁外,避免持锁调 runtime emit)
|
||||||
if let Some(ref cid) = old_conv_id {
|
if let Some(ref cid) = old_conv_id {
|
||||||
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
||||||
total_tokens: 0,
|
total_tokens: 0,
|
||||||
@@ -950,7 +929,7 @@ pub async fn ai_chat_force_send(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// system prompt 构建(照 ai_chat_send:199-226):技能注入 + 知识注入顺序一致。
|
// system prompt 构建(照 ai_chat_send):技能注入 + 知识注入顺序一致。
|
||||||
let _tool_defs = state.ai_tools.tool_definitions();
|
let _tool_defs = state.ai_tools.tool_definitions();
|
||||||
let lang = language.unwrap_or_else(|| "zh-CN".to_string());
|
let lang = language.unwrap_or_else(|| "zh-CN".to_string());
|
||||||
let mut system_prompt = build_system_prompt(&state, &lang).await;
|
let mut system_prompt = build_system_prompt(&state, &lang).await;
|
||||||
@@ -962,10 +941,7 @@ pub async fn ai_chat_force_send(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let conv_id = {
|
// conv_id 已在上方状态占用块得出。
|
||||||
let session = state.ai_session.lock().await;
|
|
||||||
session.active_conversation_id.clone().unwrap_or_default()
|
|
||||||
};
|
|
||||||
{
|
{
|
||||||
let config = state.knowledge_config.lock().await.clone();
|
let config = state.knowledge_config.lock().await.clone();
|
||||||
let knowledge_context = build_knowledge_context(&state, &conv_id, &message, &config).await;
|
let knowledge_context = build_knowledge_context(&state, &conv_id, &message, &config).await;
|
||||||
@@ -998,57 +974,62 @@ pub async fn ai_chat_force_send(
|
|||||||
/// - 有挂起审批(loop 已 return 等待中):stop_flag 无人读取,直接清审批 + 复位 generating,
|
/// - 有挂起审批(loop 已 return 等待中):stop_flag 无人读取,直接清审批 + 复位 generating,
|
||||||
/// 否则停止按钮表面无反应、会话卡在 generating=true
|
/// 否则停止按钮表面无反应、会话卡在 generating=true
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn ai_chat_stop(state: State<'_, AppState>, app: AppHandle) -> Result<(), String> {
|
pub async fn ai_chat_stop(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
app: AppHandle,
|
||||||
|
// F-260616-09 B 批4(决策 e 真并发上线):加 conversation_id 参数,停止仅作用于目标 conv。
|
||||||
|
// None/空 → fallback active(旧调用方兼容)。
|
||||||
|
conversation_id: Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
// F-260616-09 B 批2:读 per_conv.generating(active conv)+ 顶层 fallback。
|
// 目标 conv:入参优先 → active。
|
||||||
let active = session.active_conversation_id.clone().unwrap_or_default();
|
let target = conversation_id.filter(|s| !s.is_empty())
|
||||||
let is_gen = if active.is_empty() {
|
.or_else(|| session.active_conversation_id.clone())
|
||||||
session.generating
|
.unwrap_or_default();
|
||||||
} else {
|
if target.is_empty() {
|
||||||
session.conv_read(&active).map(|c| c.generating).unwrap_or(session.generating)
|
// 无目标 conv 且无 active:无任何 conv 在跑(批4 顶层 generating 已退役),直接返回。
|
||||||
};
|
drop(session);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// 仅查目标 conv 的 generating(真并发下各 conv 独立)。
|
||||||
|
let is_gen = session.conv_read(&target).map(|c| c.generating).unwrap_or(false);
|
||||||
if !is_gen {
|
if !is_gen {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if !session.pending_approvals.is_empty() {
|
// 目标 conv 是否有待审批(仅本 conv 的)。
|
||||||
// 审批等待态:loop 已退出,直接清理让会话立即可用
|
let has_pending = session.pending_approvals.values()
|
||||||
|
.any(|a| a.conversation_id.as_deref() == Some(target.as_str()));
|
||||||
|
if has_pending {
|
||||||
|
// 审批等待态:loop 已退出,直接清理目标 conv 让其立即可用
|
||||||
// SW-260618-02:messages 不 clear,占位 tool_result 会残留,下次发送喂给 LLM。
|
// SW-260618-02:messages 不 clear,占位 tool_result 会残留,下次发送喂给 LLM。
|
||||||
// 清 pending 前先终态化为"会话已停止"。
|
// 清 pending 前先终态化为"会话已停止"。仅清目标 conv 的 pending(retain 保留其他 conv)。
|
||||||
// F-09 B 批2:per_conv + 顶层双清,conv_id 来源 active。
|
finalize_pending_placeholders(&mut *session, &target, "会话已停止");
|
||||||
finalize_pending_placeholders(&mut *session, &active, "会话已停止");
|
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(target.as_str()));
|
||||||
session.pending_approvals.clear();
|
let conv = session.conv(&target);
|
||||||
if !active.is_empty() {
|
conv.generating = false;
|
||||||
let conv = session.conv(&active);
|
conv.stop_flag.store(true, Ordering::SeqCst); // 双保险:防 try_continue 误判重启
|
||||||
conv.generating = false;
|
|
||||||
conv.stop_flag.store(true, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
session.generating = false;
|
|
||||||
session.stop_flag.store(true, Ordering::SeqCst); // 双保险:防 try_continue 误判重启
|
|
||||||
let conv_id = session.active_conversation_id.clone();
|
|
||||||
drop(session);
|
drop(session);
|
||||||
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: 0, prompt_tokens: 0, completion_tokens: 0, incomplete: None, conversation_id: conv_id });
|
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
||||||
|
total_tokens: 0, prompt_tokens: 0, completion_tokens: 0, incomplete: None,
|
||||||
|
conversation_id: Some(target),
|
||||||
|
});
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
// 流式生成中:置位让 loop 自行收尾
|
// 流式生成中:置位让目标 conv 的 loop 自行收尾
|
||||||
if !active.is_empty() {
|
let notify = {
|
||||||
let conv = session.conv(&active);
|
let conv = session.conv(&target);
|
||||||
conv.stop_flag.store(true, Ordering::SeqCst);
|
conv.stop_flag.store(true, Ordering::SeqCst);
|
||||||
}
|
conv.notify.clone()
|
||||||
session.stop_flag.store(true, Ordering::SeqCst);
|
};
|
||||||
// B-260615-14:置位后立即 notify_one 唤醒阻塞在 stream.next() 的 select!,
|
// B-260615-14:置位后立即 notify_one 唤醒阻塞在 stream.next() 的 select!,
|
||||||
// 不再等 30s 心跳 tick 或 120s idle timeout 才轮到 stop_flag 检查。
|
// 不再等 30s 心跳 tick 或 120s idle timeout 才轮到 stop_flag 检查。
|
||||||
// Notify 仅承载「即时唤醒」,停止真值仍由 stop_flag 决定(stream_llm 唤醒后再判 flag)。
|
// Notify 仅承载「即时唤醒」,停止真值仍由 stop_flag 决定(stream_llm 唤醒后再判 flag)。
|
||||||
// F-09 B 批2:per_conv.notify 与顶层 notify 批2 桥接期共享同一 Arc(入口桥接搬入),notify_one 互通。
|
notify.notify_one();
|
||||||
if let Some(c) = session.conv_read(&active).map(|c| c.notify.clone()) {
|
let conv_id = Some(target);
|
||||||
c.notify_one();
|
|
||||||
} else {
|
|
||||||
session.stop_notify().notify_one();
|
|
||||||
}
|
|
||||||
let conv_id = session.active_conversation_id.clone();
|
|
||||||
drop(session);
|
drop(session);
|
||||||
|
|
||||||
// B-260615-13 兜底任务:loop 若 panic/异常退出漏发收尾,stop_flag 无人读,
|
// B-260615-13 兜底任务:loop 若 panic/异常退出漏发收尾,stop_flag 无人读,
|
||||||
// 用户点 stop 无反应、generating 卡 true。这里 sleep 短超时后重检 generating,
|
// 用户点 stop 无反应、generating 卡 true。这里 sleep 短超时后重检目标 conv generating,
|
||||||
// 仍 true(loop 没复位)则强制复位 + emit AiCompleted 通知前端收尾。
|
// 仍 true(loop 没复位)则强制复位 + emit AiCompleted 通知前端收尾。
|
||||||
// 正常路径(loop 活自行复位)此时 generating 已 false,无操作退出。
|
// 正常路径(loop 活自行复位)此时 generating 已 false,无操作退出。
|
||||||
let session_arc = state.ai_session.clone();
|
let session_arc = state.ai_session.clone();
|
||||||
@@ -1056,18 +1037,14 @@ pub async fn ai_chat_stop(state: State<'_, AppState>, app: AppHandle) -> Result<
|
|||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
||||||
let mut session = session_arc.lock().await;
|
let mut session = session_arc.lock().await;
|
||||||
// F-09 B 批2:兜底复位读 per_conv(若有 active)+ 顶层。
|
let target_owned = conv_id.clone().unwrap_or_default();
|
||||||
let act = session.active_conversation_id.clone().unwrap_or_default();
|
let still_gen = if target_owned.is_empty() {
|
||||||
let still_gen = if act.is_empty() {
|
false
|
||||||
session.generating
|
|
||||||
} else {
|
} else {
|
||||||
session.conv_read(&act).map(|c| c.generating).unwrap_or(session.generating)
|
session.conv_read(&target_owned).map(|c| c.generating).unwrap_or(false)
|
||||||
};
|
};
|
||||||
if still_gen {
|
if still_gen {
|
||||||
if !act.is_empty() {
|
session.conv(&target_owned).generating = false;
|
||||||
session.conv(&act).generating = false;
|
|
||||||
}
|
|
||||||
session.generating = false;
|
|
||||||
drop(session); // 释放锁后再 emit,避免持锁调 runtime emit
|
drop(session); // 释放锁后再 emit,避免持锁调 runtime emit
|
||||||
let _ = app_handle.emit(
|
let _ = app_handle.emit(
|
||||||
"ai-chat-event",
|
"ai-chat-event",
|
||||||
@@ -1104,23 +1081,18 @@ pub async fn ai_continue_loop(
|
|||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
{
|
{
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
// F-260616-09 B 批2:读 per_conv.generating(conv_id 来源:IPC 参数 conversation_id,已与 active 校验)。
|
// F-260616-09 B 批4(决策 e):conv_id 来源 IPC 参数 conversation_id,可对任意 conv 操作
|
||||||
let is_gen = session.conv_read(&conversation_id).map(|c| c.generating).unwrap_or(session.generating);
|
// (含切走后的后台 conv),移除 active 一致性校验(真并发下后台 conv 也应可续跑)。
|
||||||
|
let is_gen = session.conv_read(&conversation_id).map(|c| c.generating).unwrap_or(false);
|
||||||
if !is_gen {
|
if !is_gen {
|
||||||
return Err("AI 未在暂停态,无需继续".to_string());
|
return Err("AI 未在暂停态,无需继续".to_string());
|
||||||
}
|
}
|
||||||
if session.active_conversation_id.as_deref() != Some(conversation_id.as_str()) {
|
|
||||||
return Err("对话已切换,无法继续".to_string());
|
|
||||||
}
|
|
||||||
let conv = session.conv(&conversation_id);
|
let conv = session.conv(&conversation_id);
|
||||||
// 复位停止信号:暂停态可能因上一轮 stop_flag 残留为 true,续跑 loop 入口会立即退出走完成流程
|
// 复位停止信号:暂停态可能因上一轮 stop_flag 残留为 true,续跑 loop 入口会立即退出走完成流程
|
||||||
conv.stop_flag.store(false, Ordering::SeqCst);
|
conv.stop_flag.store(false, Ordering::SeqCst);
|
||||||
// F-260616-11 决策 a: 达 max 续跑重计 iteration(F-260616-03 决策 a,用户点继续=授权重来)。
|
// F-260616-11 决策 a: 达 max 续跑重计 iteration(F-260616-03 决策 a,用户点继续=授权重来)。
|
||||||
// 审批续跑(ai_approve)累计不重置见另路径;本路径 start_iteration 传 0,loop 从头计数。
|
// 审批续跑(ai_approve)累计不重置见另路径;本路径 start_iteration 传 0,loop 从头计数。
|
||||||
conv.iteration_used = 0;
|
conv.iteration_used = 0;
|
||||||
// 顶层双写(批2 桥接)。
|
|
||||||
session.stop_flag.store(false, Ordering::SeqCst);
|
|
||||||
session.iteration_used = 0;
|
|
||||||
}
|
}
|
||||||
// 复用审批恢复续 loop 入口(不重写 loop),其内部 spawn run_agentic_loop
|
// 复用审批恢复续 loop 入口(不重写 loop),其内部 spawn run_agentic_loop
|
||||||
// F-09 B 批2:传 conv_id(IPC 参数 conversation_id)。
|
// F-09 B 批2:传 conv_id(IPC 参数 conversation_id)。
|
||||||
@@ -1142,32 +1114,26 @@ pub async fn ai_stop_loop(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
conversation_id: String,
|
conversation_id: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let conv_id = {
|
{
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
// F-260616-09 B 批2:读 per_conv.generating(conv_id 来源:IPC 参数 conversation_id,已与 active 校验)。
|
// F-260616-09 B 批4(决策 e):conv_id 来源 IPC 参数 conversation_id,移除 active 一致性校验
|
||||||
let is_gen = session.conv_read(&conversation_id).map(|c| c.generating).unwrap_or(session.generating);
|
// (真并发下后台 conv 也应可停止)。
|
||||||
|
let is_gen = session.conv_read(&conversation_id).map(|c| c.generating).unwrap_or(false);
|
||||||
if !is_gen {
|
if !is_gen {
|
||||||
return Err("AI 未在暂停态,无需停止".to_string());
|
return Err("AI 未在暂停态,无需停止".to_string());
|
||||||
}
|
}
|
||||||
if session.active_conversation_id.as_deref() != Some(conversation_id.as_str()) {
|
|
||||||
return Err("对话已切换,无法停止".to_string());
|
|
||||||
}
|
|
||||||
let conv = session.conv(&conversation_id);
|
let conv = session.conv(&conversation_id);
|
||||||
// 置 stop_flag 双保险:防 try_continue 误判重启(与 ai_chat_stop 审批分支一致)
|
// 置 stop_flag 双保险:防 try_continue 误判重启(与 ai_chat_stop 审批分支一致)
|
||||||
conv.stop_flag.store(true, Ordering::SeqCst);
|
conv.stop_flag.store(true, Ordering::SeqCst);
|
||||||
conv.generating = false;
|
conv.generating = false;
|
||||||
// 顶层双写(批2 桥接)。
|
}
|
||||||
session.stop_flag.store(true, Ordering::SeqCst);
|
|
||||||
session.generating = false;
|
|
||||||
session.active_conversation_id.clone().unwrap_or_default()
|
|
||||||
};
|
|
||||||
// 暂停态进入前已 save_conversation,此处零 token 上报仅作收敛信号(与 try_continue 补发 AiCompleted 一致)
|
// 暂停态进入前已 save_conversation,此处零 token 上报仅作收敛信号(与 try_continue 补发 AiCompleted 一致)
|
||||||
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
||||||
total_tokens: 0,
|
total_tokens: 0,
|
||||||
prompt_tokens: 0,
|
prompt_tokens: 0,
|
||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
incomplete: None,
|
incomplete: None,
|
||||||
conversation_id: Some(conv_id),
|
conversation_id: Some(conversation_id),
|
||||||
});
|
});
|
||||||
Ok("ok".to_string())
|
Ok("ok".to_string())
|
||||||
}
|
}
|
||||||
@@ -1495,7 +1461,7 @@ pub async fn ai_probe_model(
|
|||||||
/// 创建新对话
|
/// 创建新对话
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn ai_conversation_create(
|
pub async fn ai_conversation_create(
|
||||||
app: AppHandle,
|
_app: AppHandle,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<serde_json::Value, String> {
|
) -> Result<serde_json::Value, String> {
|
||||||
// 懒创建:仅生成 id 存内存,不落库;避免新建后不发消息产生空记录。
|
// 懒创建:仅生成 id 存内存,不落库;避免新建后不发消息产生空记录。
|
||||||
@@ -1503,80 +1469,37 @@ pub async fn ai_conversation_create(
|
|||||||
let id = new_id();
|
let id = new_id();
|
||||||
let now = now_millis();
|
let now = now_millis();
|
||||||
|
|
||||||
let mut session = state.ai_session.lock().await;
|
// F-260616-09 B 批4(决策 e 真并发上线):新建会话**不杀旧 loop**。
|
||||||
// B-260615-10: 生成中软复位取代硬拦——强制结束当前生成,让用户能立即新建对话。
|
// 旧实现(B-260615-10)生成中强制结束旧 conv 的 loop + clear 其 per_conv,在真并发下会误杀
|
||||||
// 旧 loop 经 B-260615-11 一致性校验(active_conversation_id 变更)自动退出,不污染新对话;
|
// 后台跑着的 conv。决策 e:旧 conv 的 per_conv 完整保留(后台 loop 继续跑自己的 conv),
|
||||||
// stop_flag 置位作双保险,让 streaming 中的旧 loop 也尽快收尾。
|
// 仅切换 active_conversation_id 到新会话 + 为新会话建独立 per_conv。
|
||||||
// F-260616-09 B 批2:per_conv + 顶层双写。active 旧 conv(若存在)的 per_conv 也复位。
|
//
|
||||||
let old_active = session.active_conversation_id.clone();
|
// B-260618-06 保留:切换 active 前先持久化旧 active conv(防其内存 messages 因后续操作丢失)。
|
||||||
let is_gen = session.conv_read(&old_active.clone().unwrap_or_default())
|
// 注意:旧 conv 若有后台 loop 在跑,loop 自身的 save_conversation 也会持久化,此处 save 幂等。
|
||||||
.map(|c| c.generating).unwrap_or(session.generating);
|
let old_conv = {
|
||||||
if is_gen {
|
let session = state.ai_session.lock().await;
|
||||||
let old_conv = session.active_conversation_id.clone();
|
session.active_conversation_id.clone()
|
||||||
if let Some(ref old_id) = old_conv {
|
};
|
||||||
session.conv(old_id).generating = false;
|
let old_has_msgs = {
|
||||||
}
|
let session = state.ai_session.lock().await;
|
||||||
session.generating = false;
|
old_conv.as_deref()
|
||||||
// SW-260618-02:清 pending 前终态化旧对话占位 tool_result(下方 messages.clear 会清旧消息,
|
.and_then(|oc| session.conv_read(oc))
|
||||||
// 此处调用为 no-op,但保持统一口径防清理顺序变更)。
|
.map(|c| !c.messages.is_empty())
|
||||||
finalize_pending_placeholders(&mut *session, &old_conv.clone().unwrap_or_default(), "已取消");
|
.unwrap_or(false)
|
||||||
session.pending_approvals.clear();
|
};
|
||||||
if let Some(ref old_id) = old_conv {
|
|
||||||
session.conv(old_id).stop_flag.store(true, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
session.stop_flag.store(true, Ordering::SeqCst);
|
|
||||||
drop(session);
|
|
||||||
if let Some(old_conv) = old_conv {
|
|
||||||
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
|
||||||
total_tokens: 0,
|
|
||||||
prompt_tokens: 0,
|
|
||||||
completion_tokens: 0,
|
|
||||||
incomplete: None,
|
|
||||||
conversation_id: Some(old_conv),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
session = state.ai_session.lock().await;
|
|
||||||
}
|
|
||||||
// 修复 B-260618-06: 清内存前先持久化当前活跃会话,防「新建会话致旧对话消息丢失」。
|
|
||||||
// 此处 save 在 clear 前 await 完成,内存仍是旧会话完整消息;幂等(已落库则 update 路径不累加 token/不改 model)。
|
|
||||||
// F-09 B 批2:old_has_msgs 读 per_conv.messages(若 per_conv 已建)。
|
|
||||||
let old_conv = session.active_conversation_id.clone();
|
|
||||||
let old_has_msgs = session.conv_read(&old_conv.clone().unwrap_or_default())
|
|
||||||
.map(|c| !c.messages.is_empty())
|
|
||||||
.unwrap_or_else(|| !session.messages.is_empty());
|
|
||||||
drop(session);
|
|
||||||
if let Some(ref oc) = old_conv {
|
if let Some(ref oc) = old_conv {
|
||||||
if old_has_msgs {
|
if old_has_msgs {
|
||||||
save_conversation(&state.ai_session, &state.db, oc.as_str(), None, None).await;
|
save_conversation(&state.ai_session, &state.db, oc.as_str(), None, None).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
session.active_conversation_id = Some(id.clone());
|
session.active_conversation_id = Some(id.clone());
|
||||||
session.active_conv_created_at = Some(now);
|
session.active_conv_created_at = Some(now);
|
||||||
// SW-260618-02:清 pending 前终态化占位 tool_result(此处 messages 紧即将 clear,调用为 no-op,
|
|
||||||
// 保持清理点统一口径;非生成分支无上方 messages.clear 之外的清场,防占位随新会话残留)。
|
|
||||||
finalize_pending_placeholders(&mut *session, &old_conv.clone().unwrap_or_default(), "会话已清除");
|
|
||||||
// F-09 B 批2:旧 conv 的 per_conv 清空(若存在);新会话 id 建独立 per_conv(全空默认)。
|
|
||||||
if let Some(ref old_id) = old_conv {
|
|
||||||
if session.per_conv.contains_key(old_id) {
|
|
||||||
session.conv(old_id).messages.clear();
|
|
||||||
session.conv(old_id).agent_language = None;
|
|
||||||
session.conv(old_id).model_override = None;
|
|
||||||
session.conv(old_id).session_trust.clear();
|
|
||||||
session.conv(old_id).stop_flag.store(false, Ordering::SeqCst);
|
|
||||||
session.conv(old_id).iteration_used = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 新会话建一份独立 per_conv(全新 state,与旧会话隔离)。
|
// 新会话建一份独立 per_conv(全新 state,与旧会话隔离)。
|
||||||
let new_conv = session.conv(&id);
|
// 旧 conv 的 per_conv **不清除**(决策 e:后台 loop 继续跑),其 pending_approvals 也保留
|
||||||
let _ = new_conv; // conv(&id) 已惰性建立空 PerConvState,字段全为默认值
|
// (switch 路径会 retain 清目标 conv 的,create 不动他人)。
|
||||||
session.messages.clear();
|
let _ = session.conv(&id); // 惰性建立空 PerConvState(字段全默认值)
|
||||||
session.pending_approvals.clear();
|
|
||||||
// F-260616-09(A 路线):补漏清字段维持单例软隔离,解「新建会话上下文残留」(顶层双写批2 桥接)。
|
|
||||||
session.stop_flag.store(false, Ordering::SeqCst);
|
|
||||||
session.agent_language = None;
|
|
||||||
session.model_override = None;
|
|
||||||
session.session_trust.clear();
|
|
||||||
|
|
||||||
Ok(serde_json::json!({ "id": id }))
|
Ok(serde_json::json!({ "id": id }))
|
||||||
}
|
}
|
||||||
@@ -1639,44 +1562,28 @@ pub async fn ai_conversation_switch(
|
|||||||
let need_title_regen = record.title.is_none();
|
let need_title_regen = record.title.is_none();
|
||||||
|
|
||||||
let mut session = state.ai_session.lock().await;
|
let mut session = state.ai_session.lock().await;
|
||||||
// 生成中允许只读切换:返回目标对话的 messages 供前端展示,但不修改 session 状态
|
// F-260616-09 B 批4(决策 e 真并发上线):删除 readonly 分支。
|
||||||
// 后台 loop 持有快照的 conv_id,不受 active_conversation_id 变更影响
|
// 旧实现:active conv 生成中 → 只读切换(不改 session 状态),因单例 messages 会被新 conv 覆盖。
|
||||||
// F-260616-09 B 批2:读 active conv 的 per_conv.generating(批3+ 决策 e 删 readonly 分支)。
|
// 决策 e:per_conv 已隔离,切走直接改 active_conversation_id + 新 conv per_conv 惰性建/从 DB reload。
|
||||||
let active_for_check = session.active_conversation_id.clone().unwrap_or_default();
|
// **后台 conv(目标正在跑 loop)的 per_conv 已存在则跳过 reload**——防覆盖其内存 messages
|
||||||
let is_gen = if active_for_check.is_empty() {
|
// (后台 loop 正在写自己的 per_conv.messages,reload 会用 DB 旧快照覆盖内存新消息)。
|
||||||
session.generating
|
|
||||||
} else {
|
|
||||||
session.conv_read(&active_for_check).map(|c| c.generating).unwrap_or(session.generating)
|
|
||||||
};
|
|
||||||
if is_gen {
|
|
||||||
return Ok(serde_json::json!({
|
|
||||||
"id": record.id,
|
|
||||||
"title": title,
|
|
||||||
"messages": messages_json,
|
|
||||||
"readonly": true,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
session.active_conversation_id = Some(conversation_id.clone());
|
session.active_conversation_id = Some(conversation_id.clone());
|
||||||
// F-09 B 批2:messages restore 到 per_conv(conv_id 来源:IPC 参数 conversation_id)。
|
let already_live = session.conv_read(&conversation_id).map(|c| c.generating).unwrap_or(false);
|
||||||
// 顶层 messages 同步双写(批2 桥接,批4 IPC 迁移后顶层移除)。
|
if !already_live {
|
||||||
let conv = session.conv(&conversation_id);
|
// 目标 conv 未在生成:从 DB reload messages 到其 per_conv(首次切入或上次切走后无后台 loop)。
|
||||||
conv.messages.restore_from_messages(messages);
|
// 已在生成:保留其 per_conv 现状(后台 loop 持有),messages 由 loop 自行维护。
|
||||||
conv.model_override = None;
|
let conv = session.conv(&conversation_id);
|
||||||
conv.session_trust.clear();
|
conv.messages.restore_from_messages(messages);
|
||||||
conv.agent_language = None;
|
conv.model_override = None;
|
||||||
conv.iteration_used = 0;
|
conv.session_trust.clear();
|
||||||
conv.stop_flag.store(false, Ordering::SeqCst);
|
conv.agent_language = None;
|
||||||
// 顶层 messages 双写:先 clone per_conv.messages 再 restore(避免借用冲突)。
|
conv.iteration_used = 0;
|
||||||
let top_restore = session.conv_read(&conversation_id).map(|c| c.messages.all_messages_clone()).unwrap_or_default();
|
conv.stop_flag.store(false, Ordering::SeqCst);
|
||||||
session.messages.restore_from_messages(top_restore);
|
}
|
||||||
// 仅清空目标对话自身的 pending_approvals,保留其他对话的(防 init 重建的内存 HashMap 被清空,
|
// 仅清空目标对话自身的 pending_approvals,保留其他对话的(防 init 重建的内存 HashMap 被清空,
|
||||||
// 重启恢复链路:restore_pending_approvals(init 重建) → switchConversation(此处不清目标对话的)
|
// 重启恢复链路:restore_pending_approvals(init 重建) → switchConversation(此处不清目标对话的)
|
||||||
// → ai_pending_tool_calls 查询 → ai_approve 落库)
|
// → ai_pending_tool_calls 查询 → ai_approve 落库)
|
||||||
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&conversation_id));
|
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&conversation_id));
|
||||||
// F-01 阶段6: 切换对话清旧 override(顶层双写)。
|
|
||||||
session.model_override = None;
|
|
||||||
// AE-2025-04:切换对话清会话信任(顶层双写)。
|
|
||||||
session.session_trust.clear();
|
|
||||||
// 释放 session lock 再做 async provider 查询 + spawn(避免持锁 await DB)
|
// 释放 session lock 再做 async provider 查询 + spawn(避免持锁 await DB)
|
||||||
drop(session);
|
drop(session);
|
||||||
|
|
||||||
@@ -1721,12 +1628,11 @@ pub async fn ai_conversation_delete(
|
|||||||
// 非活跃对话的恢复审批(recovered,conversation_id 指向被删对话)若不 retain 清理,
|
// 非活跃对话的恢复审批(recovered,conversation_id 指向被删对话)若不 retain 清理,
|
||||||
// 会永久残留死审批条目。对齐 ai_conversation_switch 的 retain 口径(仅清目标对话,保留其他)。
|
// 会永久残留死审批条目。对齐 ai_conversation_switch 的 retain 口径(仅清目标对话,保留其他)。
|
||||||
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&conversation_id));
|
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&conversation_id));
|
||||||
// F-260616-09 B 批2:删除 conv 时移除其 per_conv 条目(设计 §4.1 conv 存在性判据依赖此)。
|
// F-260616-09 B 批4:删除 conv 时移除其 per_conv 条目(设计 §4.1 conv 存在性判据依赖此,
|
||||||
// 顶层 messages.clear 仅在删的是 active conv 时执行(批2 桥接双清)。
|
// 旧 loop 检测 conv 不存在即退出)。per_conv 唯一真相源,删顶层 messages.clear 双写。
|
||||||
session.per_conv.remove(&conversation_id);
|
session.per_conv.remove(&conversation_id);
|
||||||
if session.active_conversation_id.as_deref() == Some(&conversation_id) {
|
if session.active_conversation_id.as_deref() == Some(&conversation_id) {
|
||||||
session.active_conversation_id = None;
|
session.active_conversation_id = None;
|
||||||
session.messages.clear();
|
|
||||||
}
|
}
|
||||||
// F-260616-09 B 批5:同时清理 LlmConcurrency 的 per_conv Semaphore 条目(防 HashMap 无限增长)。
|
// F-260616-09 B 批5:同时清理 LlmConcurrency 的 per_conv Semaphore 条目(防 HashMap 无限增长)。
|
||||||
// conv 已删=LlmConcurrency 该条目不再被 acquire(无 conv 则无 loop/标题/提炼/压缩针对它)。
|
// conv 已删=LlmConcurrency 该条目不再被 acquire(无 conv 则无 loop/标题/提炼/压缩针对它)。
|
||||||
|
|||||||
@@ -145,16 +145,13 @@ pub(crate) async fn save_conversation(
|
|||||||
// list_directory 13782 项)落库后每轮重发累积致 token 暴增。仅影响持久化视图,不污染
|
// list_directory 13782 项)落库后每轮重发累积致 token 暴增。仅影响持久化视图,不污染
|
||||||
// 内存真相源(ContextManager)——build_for_request 仍读全量 messages。
|
// 内存真相源(ContextManager)——build_for_request 仍读全量 messages。
|
||||||
//
|
//
|
||||||
// F-260616-09 B 批2:读 per_conv.messages(conv_id 来源:本函数入参,与 save 落库的 conv 一致)。
|
// F-260616-09 B 批4:per_conv.messages 唯一真相源(conv_id 来源:本函数入参,与 save 落库的 conv 一致)。
|
||||||
// loop 内 save 由 run_agentic_loop 入参 conv_id 透传;IPC 路径(commands.rs)save 也传 conv_id。
|
// loop 内 save 由 run_agentic_loop 入参 conv_id 透传;IPC 路径(commands.rs)save 也传 conv_id。
|
||||||
// per_conv 未建时 fallback 顶层 messages(兼容批2 桥接未覆盖的边缘路径,如 ai_chat_clear 后
|
// conv() 惰性建:save 路径 conv 必然已建(send/regenerate/edit/switch 均先 conv());若极端
|
||||||
// 无 per_conv 但顶层有,虽然 clear 已清空,fallback 顶层也是空,行为一致)。
|
// 未建(如启动恢复无 live conv),conv() 建空 PerConvState,save 空 messages(幂等不污染)。
|
||||||
let (messages_json, provider_id, created_at) = {
|
let (messages_json, provider_id, created_at) = {
|
||||||
let session = session_arc.lock().await;
|
let mut session = session_arc.lock().await;
|
||||||
let mut msgs = match session.conv_read(conv_id) {
|
let mut msgs = session.conv(conv_id).messages.all_messages_clone();
|
||||||
Some(c) => c.messages.all_messages_clone(),
|
|
||||||
None => session.messages.all_messages_clone(),
|
|
||||||
};
|
|
||||||
for m in &mut msgs {
|
for m in &mut msgs {
|
||||||
m.content = truncate_for_persist(&m.content);
|
m.content = truncate_for_persist(&m.content);
|
||||||
// F-260614-05 Phase 2a: parts(Image base64) 同样截断(替换占位 Text 片),
|
// F-260614-05 Phase 2a: parts(Image base64) 同样截断(替换占位 Text 片),
|
||||||
|
|||||||
@@ -250,14 +250,11 @@ pub(crate) async fn maybe_spawn_extraction(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
// 消息数守卫(总消息数,含 system/assistant/tool)
|
// 消息数守卫(总消息数,含 system/assistant/tool)
|
||||||
// F-260616-09 B 批2:读 per_conv.messages.len()(conv_id 来源:本函数入参)。
|
// F-260616-09 B 批4:per_conv.messages 唯一真相源(conv_id 来源:本函数入参)。
|
||||||
// per_conv 未建时 fallback 顶层 messages.len()(同锁内取,避免双锁死锁)。
|
// conv_read 未建返 0(< min_messages 自然跳过,语义=空对话不注入知识)。
|
||||||
let msg_count = {
|
let msg_count = {
|
||||||
let session = session_arc.lock().await;
|
let session = session_arc.lock().await;
|
||||||
match session.conv_read(conv_id) {
|
session.conv_read(conv_id).map(|c| c.messages.len()).unwrap_or(0)
|
||||||
Some(c) => c.messages.len(),
|
|
||||||
None => session.messages.len(),
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if (msg_count as u32) < config.min_messages {
|
if (msg_count as u32) < config.min_messages {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@@ -279,12 +279,15 @@ fn workspace_root_str() -> String {
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// AI 会话内状态(Mutex 保护)
|
/// AI 会话内状态(Mutex 保护)
|
||||||
|
///
|
||||||
|
/// F-260616-09 B 批4(决策 e 真并发上线):会话级状态全部迁入 [`per_conv`](Self::per_conv)
|
||||||
|
/// HashMap(按 conversation_id 切分),顶层单例字段(messages/generating/stop_flag/notify/
|
||||||
|
/// iteration_used/agent_language/model_override/session_trust)已退役删除。批2 双写桥接代码
|
||||||
|
/// (IPC 写路径的顶层双写行 + 各文件 fallback 读)在批4 全部清理完毕,per_conv 是唯一真相源。
|
||||||
pub struct AiSession {
|
pub struct AiSession {
|
||||||
/// 对话历史(ContextManager:唯一消息真相源,裁剪仅影响发送视图,不影响持久化)
|
|
||||||
pub messages: ContextManager,
|
|
||||||
/// 当前提供商 ID
|
/// 当前提供商 ID
|
||||||
pub active_provider_id: Option<String>,
|
pub active_provider_id: Option<String>,
|
||||||
/// 当前活跃对话 ID
|
/// 当前活跃对话 ID(应用级路由键:当前展示的 conv;切走即变,不影响后台 loop)
|
||||||
pub active_conversation_id: Option<String>,
|
pub active_conversation_id: Option<String>,
|
||||||
/// 活跃对话创建时间(懒创建:首条消息落库前仅存内存,upsert 时用作 created_at)
|
/// 活跃对话创建时间(懒创建:首条消息落库前仅存内存,upsert 时用作 created_at)
|
||||||
pub active_conv_created_at: Option<String>,
|
pub active_conv_created_at: Option<String>,
|
||||||
@@ -300,111 +303,51 @@ pub struct AiSession {
|
|||||||
/// **未来**:若多会话并发、审批量显著增大,可考虑加二级索引
|
/// **未来**:若多会话并发、审批量显著增大,可考虑加二级索引
|
||||||
/// (`conversation_id → Vec<tool_call_id>`)把按会话过滤降到 O(1)。
|
/// (`conversation_id → Vec<tool_call_id>`)把按会话过滤降到 O(1)。
|
||||||
pub pending_approvals: HashMap<String, PendingApproval>,
|
pub pending_approvals: HashMap<String, PendingApproval>,
|
||||||
/// 是否正在生成
|
/// F-260616-09 B 阶段 多会话并发架构 — 会话级状态按 conversation_id 切分。
|
||||||
pub generating: bool,
|
|
||||||
/// 当前 agent 循环的语言设置(用于审批后恢复循环)
|
|
||||||
pub agent_language: Option<String>,
|
|
||||||
/// 停止信号:ai_chat_stop 置位,agentic loop / stream_llm 检测后尽快退出
|
|
||||||
pub stop_flag: Arc<AtomicBool>,
|
|
||||||
/// 即时停止唤醒(B-260615-14):stream_llm 阻塞在 `stream.next()` 等 chunk 时,
|
|
||||||
/// `notify_one()` 立即唤醒跳出 select!,不再等 30s 心跳 tick 轮询。
|
|
||||||
///
|
///
|
||||||
/// 语义边界:Notify 仅承载「即时唤醒」这一职责——停止的真值仍由 `stop_flag`
|
/// **批4 状态(per_conv 唯一真相源)**:批2-批4 已迁移所有调用方(agentic.rs loop +
|
||||||
/// (AtomicBool)决定。`ai_chat_stop` 置 `stop_flag=true` 后调 `notify_one()`;
|
|
||||||
/// 被唤醒的 select! 分支再判 `stop_flag` 决定退出或继续,避免 Notify 的通知
|
|
||||||
/// 与「真的该停」耦合(防误唤醒继续跑)。
|
|
||||||
///
|
|
||||||
/// `Arc<Notify>` 可 Clone(Notify 内部用 Atomic 计数,Clone 等价于 Arc 引用 +1),
|
|
||||||
/// 不影响 AiSession 其他字段语义。
|
|
||||||
pub notify: Arc<tokio::sync::Notify>,
|
|
||||||
/// agentic loop 累计已跑 iteration 计数(F-260616-11 决策 a)。
|
|
||||||
///
|
|
||||||
/// **语义**:「下一轮起算值」。run_agentic_loop 每轮写 `iteration + 1`,审批等待/
|
|
||||||
/// 达 max 暂停退出时即「当前轮+1」。续跑时由调用方决定如何接:
|
|
||||||
/// - **审批续跑**(ai_approve):传本字段作 start_iteration(**累计**,防多次审批反复
|
|
||||||
/// 跑满 max_iterations 致 token 失控);
|
|
||||||
/// - **达 max 续跑**(ai_continue_loop):重置本字段=0 + 传 0(**重计**,F-260616-03
|
|
||||||
/// 决策 a,用户点继续=授权重来);
|
|
||||||
/// - **新消息首次发送**(ai_chat_send):重置本字段=0(新对话生命周期从头计数)。
|
|
||||||
///
|
|
||||||
/// 单例一份:当前 AiSession 是全局单例(F-09 B 多会话架构落地时改 per-conv)。
|
|
||||||
pub iteration_used: usize,
|
|
||||||
/// F-01 阶段6: 用户指定模型 override(主对话专用)。
|
|
||||||
///
|
|
||||||
/// **语义**:前端顶部下拉「指定」模式选的 model_id;None/空=自动模式(路由器选)。
|
|
||||||
/// 仅主对话(agentic)生效——标题/扫描/灵感等内部调用仍走路由,不读此字段。
|
|
||||||
///
|
|
||||||
/// **兜底原则**(agentic.rs run_agentic_loop 内强制):override 非空且在该 provider
|
|
||||||
/// model_configs 池中 → 用 override;否则用 resolved_model(路由结果)。绝不让
|
|
||||||
/// override 导致无模型。
|
|
||||||
///
|
|
||||||
/// **生命周期**:ai_chat_send/regenerate/edit 写入(随发消息带);新对话/会话切换时清空
|
|
||||||
/// (防上一对话 override 残留污染新对话)。try_continue_agent_loop 读此字段透传给续跑
|
|
||||||
/// loop(审批续跑/达 max 续跑保持 override 一致,语义=同一主对话)。
|
|
||||||
pub model_override: Option<String>,
|
|
||||||
/// AE-2025-04 会话级信任(Session Trust):内存态,随会话销毁,不落库。
|
|
||||||
/// 同会话已批准过同类操作(同工具+同目录 TrustKey 命中)后自动放行,跳过 pending 审批 +
|
|
||||||
/// 二次确认(AE-05)。换会话(new/switch)必须清空重审(见 ai_conversation_create /
|
|
||||||
/// ai_conversation_switch 清字段)。
|
|
||||||
pub session_trust: HashSet<TrustKey>,
|
|
||||||
/// F-260616-09 B 阶段批1 多会话并发架构:会话级状态按 conversation_id 切分。
|
|
||||||
///
|
|
||||||
/// **批2 状态(per-conv 是新真相源)**:批2 已迁移所有调用方(agentic.rs loop +
|
|
||||||
/// GeneratingGuard + try_continue + commands.rs IPC 写路径 + audit.rs process_tool_calls +
|
/// GeneratingGuard + try_continue + commands.rs IPC 写路径 + audit.rs process_tool_calls +
|
||||||
/// conversation.rs save + title.rs + knowledge_inject.rs + lib.rs L0)读写 `session.conv(conv_id).*`
|
/// conversation.rs save + title.rs + knowledge_inject.rs + lib.rs L0)读写
|
||||||
/// / `session.conv_read(conv_id)`。顶层单例字段(messages/generating/stop_flag 等)批2 期间
|
/// `session.conv(conv_id).*` / `session.conv_read(conv_id)`。顶层单例会话级字段已全部删除。
|
||||||
/// 作双写桥接保留(IPC 仍双写顶层保 ai_is_generating 等读路径兼容,批4 IPC 迁移后顶层移除)。
|
|
||||||
pub per_conv: HashMap<String, PerConvState>,
|
pub per_conv: HashMap<String, PerConvState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AiSession {
|
impl AiSession {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
messages: ContextManager::new(ContextConfig::default()),
|
|
||||||
active_provider_id: None,
|
active_provider_id: None,
|
||||||
active_conversation_id: None,
|
active_conversation_id: None,
|
||||||
active_conv_created_at: None,
|
active_conv_created_at: None,
|
||||||
pending_approvals: HashMap::new(),
|
pending_approvals: HashMap::new(),
|
||||||
generating: false,
|
|
||||||
agent_language: None,
|
|
||||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
|
||||||
notify: Arc::new(tokio::sync::Notify::new()),
|
|
||||||
iteration_used: 0,
|
|
||||||
model_override: None,
|
|
||||||
session_trust: HashSet::new(),
|
|
||||||
per_conv: HashMap::new(),
|
per_conv: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 取即时停止唤醒器引用(B-260615-14)。
|
/// 推导指定会话读侧状态视图(只读,不写任何字段)。
|
||||||
///
|
///
|
||||||
/// 供 `stream_llm`(接 `&Notify` 进 select! 监听 `notified()`)与 `ai_chat_stop`
|
/// 优先级:该 conv 有 pending 审批 → [`SessionState::AwaitingApproval`];
|
||||||
/// (置 `stop_flag` 后调 `notify_one()`)等外部持有方获取同一 Notify 实例。
|
/// 否则 per_conv.generating → [`SessionState::Streaming`];否则 → [`SessionState::Idle`]。
|
||||||
/// 返回 `&Notify` 而非 clone,避免不必要的 Arc 引用计数调整。
|
|
||||||
pub fn stop_notify(&self) -> &tokio::sync::Notify {
|
|
||||||
&self.notify
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 推导会话读侧状态视图(只读,不写任何字段)。
|
|
||||||
///
|
///
|
||||||
/// 优先级:`pending_approvals` 非空 → [`SessionState::AwaitingApproval`];
|
/// 收敛点:调用方读「某会话处于什么阶段」时统一走本方法,替代散布的
|
||||||
/// 否则 `generating` → [`SessionState::Streaming`];否则 → [`SessionState::Idle`]。
|
/// `if pending / if generating` 三路组合。
|
||||||
///
|
///
|
||||||
/// 收敛点:调用方读「会话处于什么阶段」时统一走本方法,替代散布的
|
/// F-260616-09 B 批4:per_conv 化后接 conv_id 参数(顶层 generating 已删除);
|
||||||
/// `if !pending_approvals.is_empty() / if generating` 三路组合。
|
/// pending 判断改为按 conv_id 过滤(决策 e 真并发下各 conv 独立)。
|
||||||
///
|
///
|
||||||
/// # 待替换调用点(本次仅新增视图,不替换调用点)
|
/// # 待替换调用点(本次仅新增视图,不替换调用点)
|
||||||
///
|
///
|
||||||
/// - `agentic.rs:283` — agent loop 入口/恢复处对 generating + 审批的组合判断
|
/// - `agentic.rs` — agent loop 入口/恢复处对 generating + 审批的组合判断
|
||||||
/// - `commands.rs:250` — `ai_pending_tool_calls` 等读取前的状态门控
|
/// - `commands.rs` — `ai_pending_tool_calls` 等读取前的状态门控
|
||||||
/// - `commands.rs:451` — 审批提交/会话复位路径的状态判断
|
/// - `commands.rs` — 审批提交/会话复位路径的状态判断
|
||||||
///
|
///
|
||||||
/// 上述三处替换由 P0 任务承接,本方法先就位供其切换。
|
/// 上述三处替换由 P0 任务承接,本方法先就位供其切换。
|
||||||
#[allow(dead_code)] // SW-260618-21 b:预留(P0 终态化任务承接·当前 0 调用·保留扩展点)
|
#[allow(dead_code)] // SW-260618-21 b:预留(P0 终态化任务承接·当前 0 调用·保留扩展点)
|
||||||
pub fn session_state(&self) -> SessionState {
|
pub fn session_state(&self, conv_id: &str) -> SessionState {
|
||||||
if !self.pending_approvals.is_empty() {
|
let has_pending = self.pending_approvals.values()
|
||||||
|
.any(|a| a.conversation_id.as_deref() == Some(conv_id));
|
||||||
|
if has_pending {
|
||||||
SessionState::AwaitingApproval
|
SessionState::AwaitingApproval
|
||||||
} else if self.generating {
|
} else if self.conv_read(conv_id).map(|c| c.generating).unwrap_or(false) {
|
||||||
SessionState::Streaming
|
SessionState::Streaming
|
||||||
} else {
|
} else {
|
||||||
SessionState::Idle
|
SessionState::Idle
|
||||||
|
|||||||
@@ -43,47 +43,28 @@ pub(crate) async fn ensure_conversation_title(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 取对话文本(仅 user/assistant,跳过 tool 噪音),取前 6 条供 LLM 总结
|
// 取对话文本(仅 user/assistant,跳过 tool 噪音),取前 6 条供 LLM 总结
|
||||||
// F-260616-09 B 批2:读 per_conv.messages(conv_id 来源:本函数入参)。
|
// F-260616-09 B 批4:per_conv.messages 唯一真相源(conv_id 来源:本函数入参)。
|
||||||
// per_conv 未建时 fallback 顶层(switch 触发的 spawn_ensure_title 可能无 per_conv)。
|
// conv() 惰性建:switch 触发的 spawn_ensure_title 前序 switch 已 conv();若极端未建,
|
||||||
|
// conv() 建空 PerConvState → summary_msgs 空 → 下方 return(无素材不生成标题)。
|
||||||
let (summary_msgs, all_msgs) = {
|
let (summary_msgs, all_msgs) = {
|
||||||
let session = session_arc.lock().await;
|
let mut session = session_arc.lock().await;
|
||||||
let conv = session.conv_read(conv_id);
|
let conv = session.conv(conv_id);
|
||||||
let summary: Vec<ChatMessage> = match conv {
|
let summary: Vec<ChatMessage> = conv.messages.iter()
|
||||||
Some(c) => c.messages.iter()
|
.filter(|m| matches!(m.role, MessageRole::User | MessageRole::Assistant))
|
||||||
.filter(|m| matches!(m.role, MessageRole::User | MessageRole::Assistant))
|
.take(6)
|
||||||
.take(6)
|
.map(|m| ChatMessage {
|
||||||
.map(|m| ChatMessage {
|
role: m.role.clone(),
|
||||||
role: m.role.clone(),
|
content: m.content.clone(),
|
||||||
content: m.content.clone(),
|
parts: None,
|
||||||
parts: None,
|
tool_call_id: None,
|
||||||
tool_call_id: None,
|
tool_calls: None,
|
||||||
tool_calls: None,
|
model: None,
|
||||||
model: None,
|
status: None,
|
||||||
status: None,
|
reasoning_content: m.reasoning_content.clone(),
|
||||||
reasoning_content: m.reasoning_content.clone(),
|
timestamp: m.timestamp,
|
||||||
timestamp: m.timestamp,
|
})
|
||||||
})
|
.collect();
|
||||||
.collect(),
|
let all_msgs = conv.messages.all_messages_clone();
|
||||||
None => session.messages.iter()
|
|
||||||
.filter(|m| matches!(m.role, MessageRole::User | MessageRole::Assistant))
|
|
||||||
.take(6)
|
|
||||||
.map(|m| ChatMessage {
|
|
||||||
role: m.role.clone(),
|
|
||||||
content: m.content.clone(),
|
|
||||||
parts: None,
|
|
||||||
tool_call_id: None,
|
|
||||||
tool_calls: None,
|
|
||||||
model: None,
|
|
||||||
status: None,
|
|
||||||
reasoning_content: m.reasoning_content.clone(),
|
|
||||||
timestamp: m.timestamp,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
let all_msgs = match conv {
|
|
||||||
Some(c) => c.messages.all_messages_clone(),
|
|
||||||
None => session.messages.all_messages_clone(),
|
|
||||||
};
|
|
||||||
(summary, all_msgs)
|
(summary, all_msgs)
|
||||||
};
|
};
|
||||||
if summary_msgs.is_empty() {
|
if summary_msgs.is_empty() {
|
||||||
|
|||||||
@@ -42,35 +42,24 @@ pub fn run() {
|
|||||||
let mut session = session_arc.lock().await;
|
let mut session = session_arc.lock().await;
|
||||||
// F-260616-09 B 批8(设计 §3 batch8 + §5.2):遍历 per_conv(HashMap)清多 conv
|
// F-260616-09 B 批8(设计 §3 batch8 + §5.2):遍历 per_conv(HashMap)清多 conv
|
||||||
// 残留 generating(HMR/dev 热载场景多 conv 并发跑 loop 致多 conv 卡 generating)。
|
// 残留 generating(HMR/dev 热载场景多 conv 并发跑 loop 致多 conv 卡 generating)。
|
||||||
// 批2 前:仅读 active per_conv.generating 单值;批8 改遍历全部 per_conv 各归各复位。
|
// 批4:per_conv 唯一真相源,删顶层 session.generating 双写复位(顶层字段已退役)。
|
||||||
// 顶层 session.generating(批2 双写桥接)同步复位(批4 IPC 迁移后顶层移除)。
|
let dirty_convs: Vec<String> = session
|
||||||
let mut dirty_convs: Vec<String> = session
|
|
||||||
.per_conv
|
.per_conv
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, c)| c.generating)
|
.filter(|(_, c)| c.generating)
|
||||||
.map(|(id, _)| id.clone())
|
.map(|(id, _)| id.clone())
|
||||||
.collect();
|
.collect();
|
||||||
let was_generating = !dirty_convs.is_empty() || session.generating;
|
let was_generating = !dirty_convs.is_empty();
|
||||||
// 复位每个残留 generating 的 conv(批8 多 conv 全覆盖)。
|
// 复位每个残留 generating 的 conv(批8 多 conv 全覆盖)。
|
||||||
for cid in &dirty_convs {
|
for cid in &dirty_convs {
|
||||||
if let Some(conv) = session.per_conv.get_mut(cid) {
|
if let Some(conv) = session.per_conv.get_mut(cid) {
|
||||||
conv.generating = false;
|
conv.generating = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 顶层双写复位(批2 桥接:IPC ai_is_generating 等仍读顶层,须同步)。
|
|
||||||
session.generating = false;
|
|
||||||
// BUG-260619-06 修复: clear 致冷启动 restore 重建审批丢失(restore 填充后 clear 无条件清空,
|
// BUG-260619-06 修复: clear 致冷启动 restore 重建审批丢失(restore 填充后 clear 无条件清空,
|
||||||
// 重启后待审批工具全丢)。改 retain 仅清非 recovered(本次会话/HMR 死 pending),
|
// 重启后待审批工具全丢)。改 retain 仅清非 recovered(本次会话/HMR 死 pending),
|
||||||
// 保留 restore 重建(recovered=true,audit.rs:331),对齐 switchConversation retain 保护意图。
|
// 保留 restore 重建(recovered=true,audit.rs:331),对齐 switchConversation retain 保护意图。
|
||||||
session.pending_approvals.retain(|_, a| !a.recovered);
|
session.pending_approvals.retain(|_, a| !a.recovered);
|
||||||
// active_conversation_id 兜底入 dirty_convs:批2 双写期顶层 generating=true 但
|
|
||||||
// per_conv 为空(理论上不会发生,防御性保证 active conv 至少收一个 AiCompleted
|
|
||||||
// 复位前端 streaming/generatingConvId)。
|
|
||||||
if let Some(active) = session.active_conversation_id.clone() {
|
|
||||||
if !active.is_empty() && !dirty_convs.contains(&active) {
|
|
||||||
dirty_convs.push(active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drop(session);
|
drop(session);
|
||||||
if was_generating {
|
if was_generating {
|
||||||
// 补偿事件:每个残留 conv 各发一个 AiCompleted(按 conversation_id 路由),
|
// 补偿事件:每个残留 conv 各发一个 AiCompleted(按 conversation_id 路由),
|
||||||
|
|||||||
@@ -10,24 +10,28 @@ export const aiApi = {
|
|||||||
* F-260614-05 Phase 2c: parts 透传给后端 ai_chat_send → user_parts → provider vision 端点。
|
* F-260614-05 Phase 2c: parts 透传给后端 ai_chat_send → user_parts → provider vision 端点。
|
||||||
* parts 为空/undefined → 后端走 user() 纯文本(向后兼容)。
|
* parts 为空/undefined → 后端走 user() 纯文本(向后兼容)。
|
||||||
*/
|
*/
|
||||||
sendMessage(message: string, language?: string, skill?: string, modelOverride?: string | null, parts?: ContentPart[]): Promise<string> {
|
sendMessage(message: string, language?: string, skill?: string, modelOverride?: string | null, parts?: ContentPart[], conversationId?: string | null): Promise<string> {
|
||||||
return invoke('ai_chat_send', {
|
return invoke('ai_chat_send', {
|
||||||
message,
|
message,
|
||||||
language: language || 'zh-CN',
|
language: language || 'zh-CN',
|
||||||
skill: skill || null,
|
skill: skill || null,
|
||||||
modelOverride: modelOverride || null,
|
modelOverride: modelOverride || null,
|
||||||
parts: parts && parts.length > 0 ? parts : null,
|
parts: parts && parts.length > 0 ? parts : null,
|
||||||
|
// F-260616-09 B 批4(决策 e):传 conv_id,操作指定 conv 的 per_conv(不依赖 active 单值)。
|
||||||
|
conversationId: conversationId || null,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 强制发送(B-260616-02: 复位 generating 残留后走 send 同款流程);parts 透传同 sendMessage。 */
|
/** 强制发送(B-260616-02: 复位 generating 残留后走 send 同款流程);parts 透传同 sendMessage。 */
|
||||||
forceSend(message: string, language?: string, skill?: string, modelOverride?: string | null, parts?: ContentPart[]): Promise<string> {
|
forceSend(message: string, language?: string, skill?: string, modelOverride?: string | null, parts?: ContentPart[], conversationId?: string | null): Promise<string> {
|
||||||
return invoke('ai_chat_force_send', {
|
return invoke('ai_chat_force_send', {
|
||||||
message,
|
message,
|
||||||
language: language || 'zh-CN',
|
language: language || 'zh-CN',
|
||||||
skill: skill || null,
|
skill: skill || null,
|
||||||
modelOverride: modelOverride || null,
|
modelOverride: modelOverride || null,
|
||||||
parts: parts && parts.length > 0 ? parts : null,
|
parts: parts && parts.length > 0 ? parts : null,
|
||||||
|
// F-260616-09 B 批4(决策 e):传 conv_id,强制复位+发送仅作用于目标 conv。
|
||||||
|
conversationId: conversationId || null,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -91,9 +95,21 @@ export const aiApi = {
|
|||||||
return invoke('ai_chat_clear')
|
return invoke('ai_chat_clear')
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 停止当前生成 */
|
/**
|
||||||
stopChat(): Promise<void> {
|
* 停止当前生成。
|
||||||
return invoke('ai_chat_stop')
|
* F-260616-09 B 批4(决策 e):传 convId,停止仅作用于目标 conv(不杀其他后台 conv 的 loop)。
|
||||||
|
* convId 省略时后端 fallback active conv。
|
||||||
|
*/
|
||||||
|
stopChat(convId?: string | null): Promise<void> {
|
||||||
|
return invoke('ai_chat_stop', { conversationId: convId || null })
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询后端真实 generating 状态(B-260615-22:发送前 IPC 查后端真值)。
|
||||||
|
* F-260616-09 B 批4(决策 e):传 convId 精确查指定 conv;省略时后端 fallback active conv。
|
||||||
|
*/
|
||||||
|
isGenerating(convId?: string | null): Promise<boolean> {
|
||||||
|
return invoke('ai_is_generating', { conversationId: convId || null })
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── F-15 阶段2: 手动上下文管理(清空 / 压缩) ──
|
// ── F-15 阶段2: 手动上下文管理(清空 / 压缩) ──
|
||||||
|
|||||||
@@ -121,9 +121,10 @@ async function doSend(text: string, skill?: string, force = false, parts?: Conte
|
|||||||
const override = modelOverride.value
|
const override = modelOverride.value
|
||||||
if (force) {
|
if (force) {
|
||||||
// F-05 Phase 2c: 透传 parts(图片)给后端 ai_chat_force_send,多模态全链生效
|
// F-05 Phase 2c: 透传 parts(图片)给后端 ai_chat_force_send,多模态全链生效
|
||||||
await aiApi.forceSend(text.trim(), lang, skill, override, parts)
|
// F-260616-09 B 批4(决策 e):传 activeConversationId,操作指定 conv 的 per_conv。
|
||||||
|
await aiApi.forceSend(text.trim(), lang, skill, override, parts, state.activeConversationId)
|
||||||
} else {
|
} else {
|
||||||
await aiApi.sendMessage(text.trim(), lang, skill, override, parts)
|
await aiApi.sendMessage(text.trim(), lang, skill, override, parts, state.activeConversationId)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// IPC 失败(spawn 前/provider 配置错等):回滚 streaming 防光标卡死 + 移除 user 消息与空气泡占位;
|
// IPC 失败(spawn 前/provider 配置错等):回滚 streaming 防光标卡死 + 移除 user 消息与空气泡占位;
|
||||||
@@ -310,9 +311,10 @@ async function sendMessage(text: string, skill?: string, forceMode = false, part
|
|||||||
}
|
}
|
||||||
|
|
||||||
// B-260615-22(方案 A):发送前 IPC 查后端真实 generating。
|
// B-260615-22(方案 A):发送前 IPC 查后端真实 generating。
|
||||||
|
// F-260616-09 B 批4(决策 e):传 activeConversationId 精确查当前 conv(不读全局单值)。
|
||||||
let backendGenerating = false
|
let backendGenerating = false
|
||||||
try {
|
try {
|
||||||
backendGenerating = await invoke<boolean>('ai_is_generating')
|
backendGenerating = await invoke<boolean>('ai_is_generating', { conversationId: state.activeConversationId || null })
|
||||||
} catch {
|
} catch {
|
||||||
backendGenerating = false
|
backendGenerating = false
|
||||||
}
|
}
|
||||||
@@ -495,7 +497,8 @@ async function stopChat() {
|
|||||||
// 后端 agentic.rs:190 emit AiCompleted 注释明确「保证前端收事件时后端已可接下一条,队列续发不被拒」;
|
// 后端 agentic.rs:190 emit AiCompleted 注释明确「保证前端收事件时后端已可接下一条,队列续发不被拒」;
|
||||||
// 当前未完成回复已入库(agentic.rs:185 save_conversation)保留为截断回复。
|
// 当前未完成回复已入库(agentic.rs:185 save_conversation)保留为截断回复。
|
||||||
try {
|
try {
|
||||||
await aiApi.stopChat()
|
// F-260616-09 B 批4(决策 e):传 activeConversationId,停止仅作用于当前 conv。
|
||||||
|
await aiApi.stopChat(state.activeConversationId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// stop 信号未送达后端:重启看门狗(后端继续生成时活跃事件会重置它,真 hang 则 130s 后兜底收尾),
|
// stop 信号未送达后端:重启看门狗(后端继续生成时活跃事件会重置它,真 hang 则 130s 后兜底收尾),
|
||||||
// 避免后端仍在跑却无任何恢复路径(原 clearStreamWatchdog 已把兜底关掉)。
|
// 避免后端仍在跑却无任何恢复路径(原 clearStreamWatchdog 已把兜底关掉)。
|
||||||
|
|||||||
@@ -116,9 +116,10 @@ export async function restoreGeneratingState(opts?: { fromMainPanel?: boolean })
|
|||||||
const gen = fromMainPanel ? state.activeConversationId : localStorage.getItem('df-ai-gen')
|
const gen = fromMainPanel ? state.activeConversationId : localStorage.getItem('df-ai-gen')
|
||||||
if (!gen) return false
|
if (!gen) return false
|
||||||
// 核对后端 ai_is_generating 真值
|
// 核对后端 ai_is_generating 真值
|
||||||
|
// F-260616-09 B 批4(决策 e):传 gen(目标 conv)精确查,不读全局单值。
|
||||||
let backendGenerating = false
|
let backendGenerating = false
|
||||||
try {
|
try {
|
||||||
backendGenerating = await invoke<boolean>('ai_is_generating')
|
backendGenerating = await invoke<boolean>('ai_is_generating', { conversationId: gen })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[AI] restoreGeneratingState: 核对 ai_is_generating 失败,跳过重建生成态', e)
|
console.error('[AI] restoreGeneratingState: 核对 ai_is_generating 失败,跳过重建生成态', e)
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user