@@ -36,23 +36,25 @@ use super::AiChatEvent;
///
/// 参数 `final_text` 由调用方按清理场景语义选择(如"会话已停止"/"已取消"/"会话已清除")。
///
/// F-260616-09 B 批2:`conv_id` 指定要终态化的 per_conv.messages(批2 真相源) 。
/// 顶层 messages 双写(批2 桥接,批4 移除)。conv_id 为空(无活跃对话)时只终态化顶层。
/// F-260616-09 B 批4:per_conv.messages 唯一真相源,删顶层 messages 双写 。
/// 终态化策略:每条 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 ) {
// SW-260618-02: 先 clone pending 的 tool_call_id(借用在此结束),再可变借 messages。
// 必须整体借 &mut session 在函数体内做 disjoint field borrow —— 调用方若分别传
// &mut messages + &pending 两个引用,函数参数列表不做 disjoint 推断会触发 E0502(2026-06-18 主代修)。
let id s : Vec < String > = session
let entrie s : Vec < ( String , Option < String > ) > = session
. pending_approvals
. values ( )
. map ( | a | a . tool_call_id . clone ( ) )
. map ( | a | ( a . tool_call_id . clone ( ) , a . conversation_id . clone ( ) ) )
. collect ( ) ;
for id in id s {
for ( id , approval_conv ) in entrie s {
// replace_tool_result_content 反向遍历,找不到对应 tool_call_id 时返回 false,无副作用
if ! conv_id . is_empty ( ) {
session . conv ( conv_id ) . messa ges . replace_tool_result_content ( & id , final_text ) ;
let target = approval_conv . as_deref ( ) . filter ( | s | ! s . is_empty ( ) ) . unwrap_or ( conv_id ) ;
if ! tar get . 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 ? ;
// 原子占用 generating + 弹出末尾 AI 回复(保留 user 消息)
// F-260616-09 B 批2:写 per_conv( conv_id 来源: IPC 参数 conversation_id,已与 active 一致性校验)。
// 顶层字段同步双写(批2 桥接,批4 IPC 迁移后移除) 。
// F-260616-09 B 批4(决策 e): conv_id 来源 IPC 参数 conversation_id,移除 active 一致性校验
// (真并发下后台 conv 也应可重新生成);per_conv 唯一真相源,删顶层双写 。
{
let mut session = state . ai_session . lock ( ) . await ;
// generating 拦截:读 per_conv(若已建)否则顶层(批2 桥接兼容) 。
let is_gen = session . conv_read ( & conversation_id ) . map ( | c | c . generating ) . unwrap_or ( session . generating ) ;
// generating 拦截:仅查目标 conv 的 per_conv.generating 。
let is_gen = session . conv_read ( & conversation_id ) . map ( | c | c . generating ) . unwrap_or ( false ) ;
if is_gen {
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 ) ;
conv . generating = true ;
conv . stop_flag . store ( false , Ordering ::SeqCst ) ;
@@ -106,15 +102,8 @@ pub async fn ai_regenerate(
if ! popped {
// 历史末尾无 AI 回复可弹(空对话/末尾是 user 错误态等),复位 generating 报错
conv . generating = false ;
session . generating = false ;
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 ( ) ;
@@ -122,18 +111,16 @@ pub async fn ai_regenerate(
let system_prompt = build_system_prompt ( & state , & lang ) . await ;
// 知识注入:取末尾 user 消息文本做检索(与 send 同款,语义命中刷新上下文)
// F-260616-09 B 批2:读 per_conv.messages(conv_id 来源:active_conversation_id,regenerate
// 已校验 == conversation_id)。
let ( conv_id , last_user_text ) = {
// F-260616-09 B 批4:conv_id 直接用 IPC 参数 conversation_id,读 per_conv.messages。
let conv_id = conversation_id . clone ( ) ;
let last_user_text = {
let session = state . ai_session . lock ( ) . await ;
let cid = session . active_conversati on_id. clone ( ) . unwrap_or_default ( ) ;
let msgs = session . conv_read ( & cid ) . map ( | c | c . messages . all_messages_clone ( ) )
. unwrap_or_else ( | | session . messages . all_messages_clone ( ) ) ;
let last_user = msgs . iter ( ) . rev ( )
let msgs = session . conv_read ( & c onv _id) . map ( | c | c . messages . all_messages_clone ( ) )
. unwrap_or_default ( ) ;
msgs . iter ( ) . rev ( )
. find ( | m | matches! ( m . role , df_ai ::provider ::MessageRole ::User ) )
. map ( | m | m . content . clone ( ) )
. unwrap_or_default ( ) ;
( cid , last_user )
. unwrap_or_default ( )
} ;
let mut system_prompt = system_prompt ;
{
@@ -172,12 +159,22 @@ pub async fn ai_regenerate(
/// 可能仍为 true(或反之),预检放行后撞后端 `ai_chat_send` 的 generating 拦截。
/// 发送前调此命令取后端真值,据之与本地 streaming 对齐,消除状态不同步。
#[ 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 ;
// F-260616-09 B 批2:批2 单 active 场景,读 active conv 的 per_conv.generating;
// per_conv 未建时 fallback 顶层(批2 桥接兼容)。批4 改签名 ai_is_gen erating(c onv _id) 后精确查询。
let active = session . active_conversation_id . clone ( ) . unwrap_or_default ( ) ;
Ok ( session . conv_read ( & active ) . map ( | c | c . generating ) . unwrap_or ( session . generating ) )
let target = conversation_id . filter ( | s | ! s . is_empty ( ) )
. or_else ( | | session . active_conv ers ation_id. clone ( ) )
. unwrap_or_default ( ) ;
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 响应
@@ -191,6 +188,10 @@ pub async fn ai_chat_send(
language : Option < String > ,
skill : 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 数组)。
// None/空 → 等价 user()(纯文本,向后兼容);Some(含 Image 片) → user_parts() + provider vision 端点。
// 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 ? ;
// 原子检查并占用生成标志,防止并发双发;同步追加用户消息,按需自动创建对话
// F-260616-09 B 批2:写 per_conv(conv_id 来源:active_conversation_id;首次发送懒创建)。
// 顶层双写(批2 桥接,批4 移除)。
{
// F-260616-09 B 批4(决策 e 真并发上线):
// - 目标 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 ;
// generating 拦截:active conv 的 per_conv.generating(未建 fallback 顶层) 。
let active_for_check = session . active_conversation_id . clone ( ) . unwrap_or_default ( ) ;
let is_gen = if active_for_check . is_empty ( ) {
session . generating
} else {
session . conv_read ( & active_for_check ) . map ( | c | c . generating ) . unwrap_or ( session . generating )
// 目标 conv:入参优先 → active → 懒创建 。
let target = conversation_id . clone ( ) . filter ( | s | ! s . is_empty ( ) )
. or_else ( | | session . active_conversation_id . clone ( ) ) ;
let target = match target {
Some ( id ) = > id ,
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 {
return Err ( " AI 正在生成中,请等待完成 " . to_string ( ) ) ;
}
// 首次发送时生成对话 id(懒创建:不立即落库,避免空对话残留;
// 实际记录由 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 ) ;
let conv = session . conv ( & target ) ;
conv . generating = true ;
conv . stop_flag . store ( false , Ordering ::SeqCst ) ;
conv . agent_language = language . clone ( ) ;
@@ -252,18 +265,8 @@ pub async fn ai_chat_send(
} else {
conv . messages . push ( ChatMessage ::user ( & user_content ) ) ;
}
// 顶层双写(批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 ( ) ;
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 ) ) ;
}
}
target
} ;
// 获取工具定义(预取仅用于触发注册表初始化,实际 tool_defs 在 agentic loop 内部按需获取)
let _tool_defs = state . ai_tools . tool_definitions ( ) ;
@@ -280,11 +283,7 @@ pub async fn ai_chat_send(
) ;
}
}
// 快照当前对话 ID ,供知识注入溯源 + spawn 后台 loop(不受切换影响)
let conv_id = {
let session = state . ai_session . lock ( ) . await ;
session . active_conversation_id . clone ( ) . unwrap_or_default ( )
} ;
// conv_id 已在上方状态占用块得出(入参/active/懒创建) ,供知识注入溯源 + spawn 后台 loop。
// 知识注入:检索相关知识拼到 system prompt 前(可配置开关 auto_inject,默认开)
// 最终顺序:[知识库上下文] --- [技能指令] --- [原始 system prompt]
@@ -351,13 +350,12 @@ pub async fn ai_approve(
if ! approved {
// 替换占位 tool_result 为拒绝结果
// F-260616-09 B 批2:写 per_conv.messages(conv_id 来源: approval.conversation_id,业务真相源 )。
// per_conv 未建时 fallback 顶层(批2 桥接兼容,无 live loop 时顶层仍是真相源)。
let _conv_id_for_msg = approval . conversation_id . clone ( ) ;
if let Some ( ref cid ) = _conv_id_for_msg {
// F-260616-09 B 批4: per_conv.messages 唯一真相源 (conv_id 来源 approval.conversation_id)。
// 无 conversation_id 的无主审批(R-9 异常数据)无 per_conv 可写,拒绝结果不入 messages
// (审计仍记 rejected),避免写错 conv。
if let Some ( ref cid ) = approval . conversation_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 _ = app . emit ( " ai-chat-event " , AiChatEvent ::AiApprovalResult {
id : tool_call_id . clone ( ) ,
@@ -372,14 +370,15 @@ pub async fn ai_approve(
}
// 审计:拒绝(决策者=human)
audit_finalize ( & state , & tool_call_id , " rejected " , None ) . await ;
// F-260616-11 决策 a: 审批续跑 iteration 累计(不重置)——读 sessi on.iteration_used 透传
// F-260616-11 决策 a: 审批续跑 iteration 累计(不重置)——读 per_c onv .iteration_used 透传
// 作 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 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 )
. unwrap_or ( session . iteration_used )
. unwrap_or ( 0 )
} ;
// 所有待审批处理完毕后恢复 agentic 循环
// 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 时,把该工具+目录写入
// session_trust —— 下次同会话同类操作(同工具+同目录 TrustKey)自动放行,跳过 pending + 二次确认。
// 仅首批工具(write_file/run_command)命中 trust_key_for 的 Some,其余工具 noop(None 不写)。
// F-260616-09 B 批2:写 per_conv.session_trust(conv_id 来源: approval.conversation_id,业务真相源 )。
// 顶层双写(批2 桥接)。无 conv_id(异常数据)时只写顶层 。
// F-260616-09 B 批4: per_conv.session_trust 唯一真相源 (conv_id 来源 approval.conversation_id)。
// 无 conv_id(无主审批 R-9 异常数据)时无 per_conv 可写,信任不记(审计仍记 executed) 。
if let Some ( key ) = super ::trust_key_for ( & approval . tool_name , & approval . arguments ) {
let mut inserted = false ;
if let Some ( ref cid ) = conv_id {
if session . conv ( cid ) . session_trust . insert ( key . clone ( ) ) {
inserted = true ;
}
}
if session . session_trust . insert ( key . clone ( ) ) {
inserted = true ;
}
let inserted = if let Some ( ref cid ) = conv_id {
session . conv ( cid ) . session_trust . insert ( key . cl one ( ) )
} else {
false
};
if inserted {
tracing ::info! (
tool = % approval . tool_name ,
@@ -450,12 +445,12 @@ pub async fn ai_approve(
}
// 重新获取锁,替换占位 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 ;
if let Some ( ref cid ) = conv_id {
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 {
id : id . clone ( ) ,
@@ -476,14 +471,13 @@ pub async fn ai_approve(
save_conversation ( & state . ai_session , & state . db , cid , None , None ) . await ;
}
// F-260616-11 决策 a: 审批续跑 iteration 累计(不重置)——读 sessi on.iteration_used 透传
// F-260616-11 决策 a: 审批续跑 iteration 累计(不重置)——读 per_c onv .iteration_used 透传
// 作 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 start_iter = {
let session = state . ai_session . lock ( ) . await ;
session . conv_read ( & cont_conv_id ) . map ( | c | c . iteration_used )
. unwrap_or ( session . iteration_used )
session . conv_read ( & cont_conv_id ) . map ( | c | c . iteration_used ) . unwrap_or ( 0 )
} ;
// 所有待审批处理完毕后恢复 agentic 循环(recovered 无 live loop,try_continue 因 generating=false 自然不续)
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 ( ) ;
// SW-260618-02:清 pending 前先把占位 tool_result 终态化(此处 messages 紧随即 clear,
// 调用为 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 ( ) , " 会话已清除 " ) ;
if let Some ( ref id ) = active_id {
session . conv ( id ) . messages . clear ( ) ;
}
session . messages . clear ( ) ;
session . pending_approvals . clear ( ) ;
session . pending_approvals . retain ( | _ , a | a . conversation_id . as_deref ( ) ! = active_id . as_deref ( ) ) ;
drop ( session ) ;
// 真删 DB:清空该对话 messages JSON + 清零 token(保留对话壳),刷新不再恢复(AR-7)
if let Some ( id ) = active_id {
@@ -559,14 +553,10 @@ pub async fn ai_chat_clear_context(
state : State < '_ , AppState > ,
conversation_id : String ,
) -> Result < ( ) , String > {
// 活跃对话一致性:仅对当前活跃对话分段(防陈旧快照标错对话的消息)。
// 与 ai_chat_send/ai_regenerate 取 session 模式一致(state.ai_session.lock) 。
// F-260616-09 B 批2:messages 操作改 per_conv(conv_id 来源:IPC 参数 conversation_id,已与 active 校验)。
// F-260616-09 B 批4(决策 e):conv_id 来源 IPC 参数 conversation_id,移除 active 一致性校验
// (真并发下后台 conv 也应可分段);messages 操作走 per_conv 。
let conv_id = {
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。
const PROTECT_COUNT : usize = 6 ;
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 校验)。
let ( conv_id , active_msgs , compress_end ) = {
let mut session = state . ai_session . lock ( ) . await ;
if session . active_conversation_id . as_deref ( ) ! = Some ( conversation_id . as_str ( ) ) {
return Err ( " 对话已切换,无法压缩 " . to_string ( ) ) ;
}
// F-260616-09 B 批4(决策 e):移除 active 一致性校验(真并发下后台 conv 也应可压缩)。
let conv = session . conv ( & conversation_id ) ;
if conv . messages . is_compressing ( ) {
return Err ( " 压缩正在进行中 " . to_string ( ) ) ;
@@ -746,17 +734,14 @@ pub async fn ai_chat_edit(
let provider_config = super ::prompt ::get_active_provider ( & state ) . await ? ;
// 原子占用 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 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 {
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 ) ;
// ① 替换末条 active user 消息 content(无 active user → 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 ;
// F-01 阶段6: 记录用户指定模型 override(主对话专用)。
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 ( ) ;
@@ -786,20 +765,18 @@ pub async fn ai_chat_edit(
let system_prompt = build_system_prompt ( & state , & lang ) . await ;
// 知识注入:用新 user 文本检索(与 send/regenerate 同款)
// F-260616-09 B 批2:读 per_conv.messages(conv_id 来源:active_conversation_id,edit 已校验 == conversation_id) 。
let ( conv_id , last_user_text ) = {
// F-260616-09 B 批4:conv_id 直接用 IPC 参数 conversation_id,读 per_conv.messages 。
let conv_id = conversation_id . clone ( ) ;
let last_user_text = {
let session = state . ai_session . lock ( ) . await ;
let cid = session . active_conversati on_id. clone ( ) . unwrap_or_default ( ) ;
let msgs = session . conv_read ( & cid ) . map ( | c | c . messages . all_messages_clone ( ) )
. unwrap_or_else ( | | session . messages . all_messages_clone ( ) ) ;
let msgs = session . conv_read ( & c onv _id) . map ( | c | c . messages . all_messages_clone ( ) )
. unwrap_or_default ( ) ;
// 取末条 active user 文本(sanitize 前的全量,但 truncated 已标,这里取 active 的末条)
let last_user = msgs
. iter ( )
msgs . iter ( )
. rev ( )
. find ( | m | matches! ( m . role , df_ai ::provider ::MessageRole ::User ) & & m . is_active ( ) )
. map ( | m | m . content . clone ( ) )
. unwrap_or_default ( ) ;
( cid , last_user )
. unwrap_or_default ( )
} ;
let mut system_prompt = system_prompt ;
{
@@ -868,48 +845,61 @@ pub async fn ai_chat_force_send(
// F-260614-05 Phase 2c: 透传 parts(强制发送同款多模态支持)。
// FR-S1 核验:ContentPart Image base64 是图片数据非 api_key,不入敏感面,语义同 ai_chat_send。
parts : Option < Vec < ContentPart > > ,
// F-260616-09 B 批4(决策 e 真并发上线):加 conversation_id 参数,强制复位+发送仅作用于目标 conv。
// None/空 → fallback active(旧调用方兼容)。
conversation_id : Option < String > ,
) -> Result < String , String > {
// 获取活跃提供商(只读,失败可直接返回,不占用 generating)
let provider_config = super ::prompt ::get_active_provider ( & state ) . await ? ;
// 原子"复位 + 占用":同一把锁内先清旧生成态、 再占用 generating + 追加用户消息。
// 原子"复位 + 占用":同一把锁内先清目标 conv 的 旧生成态, 再占用 generating + 追加用户消息。
// stop_flag 置 true(清旧)随即 false(新 loop 起跑)在锁内瞬变,无人能观察到中间态;
// 关键是复位与占用之间无锁释放窗口,杜绝并发 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 ;
// ① 软停止复位(照旧实现,与 ai_chat_stop 审批分支一致)
let old = session . active_ conversation_id. clone ( ) ;
// F-260616-09 B 批2:per_conv + 顶层双写。old 为旧 conv(若存在),置其 per _conv.gen erating=false。
if let Some ( ref old_id ) = old {
session . conv ( old_ id) . generating = false ;
// 目标 conv:入参优先 → active → 懒创建。
let target = conversation_id . clone ( ) . filter ( | s | ! s . is_empty ( ) )
. or_else ( | | session . active _convers ation_id . clone ( ) ) ;
let target = match target {
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 ;
// SW-260618-02:强制发送丢弃旧审批,先终态化占位 tool_result,防占位随 messages 残留
// 下次发送喂给 LLM(此处 messages 不 clear,是真实残留路径)。
// F-09 B 批2:finalize 双写 per_conv(old)+ 顶层;conv_id 来源 old active(若存在 )。
finalize_pending_placeholders ( & mut * session , & old . clone ( ) . unwrap_or_default ( ) , " 已取消 " ) ;
session . pending_approvals . clear ( ) ;
if let Some ( ref old_id ) = old {
session . conv ( old_id ) . stop_flag . store ( true , Ordering ::SeqCs t) ;
// ① 软停止复位目标 conv(与 ai_chat_stop 审批分支一致),emit AiCompleted 用旧 target 自身。
let was_gen = session . conv_read ( & target ) . map ( | c | c . generating ) . unwrap_or ( false ) ;
// SW-260618-02:强制发送丢弃目标 conv 的旧审批,先终态化占位 tool_result,防占位随 messages
// 残留下次发送喂给 LLM。仅清目标 conv 的 pending_approvals(retain 保留其他 conv )。
finalize_pending_placeholders ( & mut * session , & target , " 已取消 " ) ;
session . pending_approvals . retain ( | _ , a | a . conversation_id . as_deref ( ) ! = Some ( target . as_str ( ) ) ) ;
{
let conv = session . conv ( & targe t) ;
conv . generating = false ;
conv . stop_flag . store ( true , Ordering ::SeqCst ) ;
}
session . stop_flag . store ( true , Ordering ::SeqCst ) ;
// ② 同锁内立即占用 + 追加用户消息(逻辑照 ai_chat_send:160-194,无 generating 拦截——
// ② 同锁内立即占用 + 追加用户消息(逻辑照 ai_chat_send,无 generating 拦截——
// 这是"强制"语义本身,前面已主动复位)
// 首次发送懒创建 conv id(与 ai_chat_send:190-194 一致)
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 ) ;
let conv = session . conv ( & target ) ;
conv . generating = true ;
conv . stop_flag . store ( false , Ordering ::SeqCst ) ;
conv . agent_language = language . clone ( ) ;
// F-260616-11: 强制发送 = 新对话生命周期起点,iteration 从头计数。
conv . iteration_used = 0 ;
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 ( ) {
if let Some ( ref skill_name ) = skill {
format! ( " / {} " , skill_name )
@@ -925,21 +915,10 @@ pub async fn ai_chat_force_send(
} else {
conv . messages . push ( ChatMessage ::user ( & user_content ) ) ;
}
// 顶层双写(批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 ( ) ;
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
( was_gen . then_some ( target . clone ( ) ) , target )
} ;
// 通知前端旧生成已结束(若有残留 conv ;emit 在锁外,避免持锁调 runtime emit)
// 通知前端目标 conv 旧生成已结束(若先前在生成 ;emit 在锁外,避免持锁调 runtime emit)
if let Some ( ref cid ) = old_conv_id {
let _ = app . emit ( " ai-chat-event " , AiChatEvent ::AiCompleted {
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 lang = language . unwrap_or_else ( | | " zh-CN " . to_string ( ) ) ;
let mut system_prompt = build_system_prompt ( & state , & lang ) . await ;
@@ -962,10 +941,7 @@ pub async fn ai_chat_force_send(
) ;
}
}
let conv_id = {
let session = state . ai_session . lock ( ) . await ;
session . active_conversation_id . clone ( ) . unwrap_or_default ( )
} ;
// conv_id 已在上方状态占用块得出。
{
let config = state . knowledge_config . lock ( ) . await . clone ( ) ;
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,
/// 否则停止按钮表面无反应、会话卡在 generating=true
#[ 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 ;
// F-260616-09 B 批2:读 per_conv.generating(active conv)+ 顶层 fallback 。
let active = session . active_ conversation_id. clone ( ) . unwrap_or_default ( ) ;
let is_gen = if active . is_empty ( ) {
session . generating
} else {
session . conv_read ( & active ) . map ( | c | c . generating ) . unwrap_or ( session . generating )
} ;
// 目标 conv:入参优先 → active 。
let target = conversation_id . filter ( | s | ! s . is_empty ( ) )
. or_else ( | | session . active_conversation_id . clone ( ) )
. unwrap_or_default ( ) ;
if target . is_empty ( ) {
// 无目标 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 {
return Ok ( ( ) ) ;
}
if ! session . pending_approvals . is_empty ( ) {
// 审批等待态: loop 已退出,直接清理让会话立即可用
// 目标 conv 是否有待审批(仅本 conv 的)。
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。
// 清 pending 前先终态化为"会话已停止"。
// F-09 B 批2:per_conv + 顶层双清,conv_id 来源 active。
finalize_pending_placeholders ( & mut * session , & active , " 会话已停止 " ) ;
session . pending_approvals . clear ( ) ;
if ! active . is_empty ( ) {
let conv = session . conv ( & active ) ;
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 ( ) ;
// 清 pending 前先终态化为"会话已停止"。仅清目标 conv 的 pending(retain 保留其他 conv)。
finalize_pending_placeholders ( & mut * session , & target , " 会话已停止 " ) ;
session . pending_approvals . retain ( | _ , a | a . conversation_id . as_deref ( ) ! = Some ( target . as_str ( ) ) ) ;
let conv = session . conv ( & target ) ;
conv . generating = false ;
conv . stop_flag . store ( true , Ordering ::SeqCst ) ; // 双保险:防 try_continue 误判重启
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 ( ( ) ) ;
}
// 流式生成中:置位让 loop 自行收尾
if ! active . is_empty ( ) {
let conv = session . conv ( & active ) ;
// 流式生成中:置位让目标 conv 的 loop 自行收尾
let notify = {
let conv = session . conv ( & target ) ;
conv . stop_flag . store ( true , Ordering ::SeqCst ) ;
}
session . stop_flag . store ( true , Ordering ::SeqCst ) ;
conv . notify . clone ( )
} ;
// B-260615-14:置位后立即 notify_one 唤醒阻塞在 stream.next() 的 select!,
// 不再等 30s 心跳 tick 或 120s idle timeout 才轮到 stop_flag 检查。
// Notify 仅承载「即时唤醒」,停止真值仍由 stop_flag 决定( stream_llm 唤醒后再判 flag) 。
// F-09 B 批2:per_conv.notify 与顶层 notify 批2 桥接期共享同一 Arc(入口桥接搬入),notify_one 互通。
if let Some ( c ) = session . conv_read ( & active ) . map ( | c | c . notify . clone ( ) ) {
c . notify_one ( ) ;
} else {
session . stop_notify ( ) . notify_one ( ) ;
}
let conv_id = session . active_conversation_id . clone ( ) ;
notify . notify_one ( ) ;
let conv_id = Some ( target ) ;
drop ( session ) ;
// B-260615-13 兜底任务: loop 若 panic/异常退出漏发收尾, stop_flag 无人读,
// 用户点 stop 无反应、generating 卡 true。这里 sleep 短超时后重检 generating,
// 用户点 stop 无反应、generating 卡 true。这里 sleep 短超时后重检目标 conv generating,
// 仍 true(loop 没复位)则强制复位 + emit AiCompleted 通知前端收尾。
// 正常路径(loop 活自行复位)此时 generating 已 false, 无操作退出。
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 {
tokio ::time ::sleep ( std ::time ::Duration ::from_secs ( 3 ) ) . await ;
let mut session = session_arc . lock ( ) . await ;
// F-09 B 批2:兜底复位读 per_conv(若有 active)+ 顶层。
let act = session . active_conversation_id . clone ( ) . unwrap_or_default ( ) ;
let still_gen = if act . is_empty ( ) {
session . generating
let target_owned = conv_id . clone ( ) . unwrap_or_default ( ) ;
let still_gen = if target_owned . is_empty ( ) {
false
} 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 ! act . is_empty ( ) {
session . conv ( & act ) . generating = false ;
}
session . generating = false ;
session . conv ( & target_owned ) . generating = false ;
drop ( session ) ; // 释放锁后再 emit, 避免持锁调 runtime emit
let _ = app_handle . emit (
" ai-chat-event " ,
@@ -1104,23 +1081,18 @@ pub async fn ai_continue_loop(
) -> Result < String , String > {
{
let mut session = state . ai_session . lock ( ) . await ;
// F-260616-09 B 批2:读 per_conv.generating( conv_id 来源: IPC 参数 conversation_id,已与 active 校验)。
let is_gen = session . conv_read ( & conversation_id ) . map ( | c | c . generating ) . unwrap_or ( session . generating ) ;
// F-260616-09 B 批4(决策 e): conv_id 来源 IPC 参数 conversation_id,可对任意 conv 操作
// (含切走后的后台 conv),移除 active 一致性校验(真并发下后台 conv 也应可续跑)。
let is_gen = session . conv_read ( & conversation_id ) . map ( | c | c . generating ) . unwrap_or ( false ) ;
if ! is_gen {
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 ) ;
// 复位停止信号:暂停态可能因上一轮 stop_flag 残留为 true, 续跑 loop 入口会立即退出走完成流程
conv . stop_flag . store ( false , Ordering ::SeqCst ) ;
// F-260616-11 决策 a: 达 max 续跑重计 iteration(F-260616-03 决策 a,用户点继续=授权重来)。
// 审批续跑(ai_approve)累计不重置见另路径;本路径 start_iteration 传 0,loop 从头计数。
conv . iteration_used = 0 ;
// 顶层双写(批2 桥接)。
session . stop_flag . store ( false , Ordering ::SeqCst ) ;
session . iteration_used = 0 ;
}
// 复用审批恢复续 loop 入口(不重写 loop) , 其内部 spawn run_agentic_loop
// F-09 B 批2:传 conv_id(IPC 参数 conversation_id)。
@@ -1142,32 +1114,26 @@ pub async fn ai_stop_loop(
state : State < '_ , AppState > ,
conversation_id : String ,
) -> Result < String , String > {
let conv_id = {
{
let mut session = state . ai_session . lock ( ) . await ;
// F-260616-09 B 批2:读 per_conv.generating( conv_id 来源: IPC 参数 conversation_id,已与 active 校验)。
let is_gen = session . conv_read ( & conversation_id ) . map ( | c | c . generating ) . unwrap_or ( session . generating ) ;
// F-260616-09 B 批4(决策 e): conv_id 来源 IPC 参数 conversation_id,移除 active 一致性 校验
// (真并发下后台 conv 也应可停止)。
let is_gen = session . conv_read ( & conversation_id ) . map ( | c | c . generating ) . unwrap_or ( false ) ;
if ! is_gen {
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 ) ;
// 置 stop_flag 双保险:防 try_continue 误判重启(与 ai_chat_stop 审批分支一致)
conv . stop_flag . store ( true , Ordering ::SeqCst ) ;
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 一致)
let _ = app . emit ( " ai-chat-event " , AiChatEvent ::AiCompleted {
total_tokens : 0 ,
prompt_tokens : 0 ,
completion_tokens : 0 ,
incomplete : None ,
conversation_id : Some ( conv_id ) ,
conversation_id : Some ( conversation _id ) ,
} ) ;
Ok ( " ok " . to_string ( ) )
}
@@ -1495,7 +1461,7 @@ pub async fn ai_probe_model(
/// 创建新对话
#[ tauri::command ]
pub async fn ai_conversation_create (
app : AppHandle ,
_ app : AppHandle ,
state : State < '_ , AppState > ,
) -> Result < serde_json ::Value , String > {
// 懒创建:仅生成 id 存内存,不落库;避免新建后不发消息产生空记录。
@@ -1503,80 +1469,37 @@ pub async fn ai_conversation_create(
let id = new_id ( ) ;
let now = now_millis ( ) ;
let mut session = state . ai_session . lock ( ) . await ;
// B-260615-10: 生成中软复位取代硬拦——强制结束当前生成,让用户能立即新建对话。
// 旧 loop 经 B-260615-11 一致性校验(active_conversation_id 变更)自动退出,不污染新对话;
// stop_flag 置位作双保险,让 streaming 中的旧 loop 也尽快收尾 。
// F-260616-09 B 批2:per_conv + 顶层双写。active 旧 conv(若存在)的 per_conv 也复位。
let old_active = session . active_ conversation_id . clone ( ) ;
let is_gen = session . conv_read ( & old_active . clone ( ) . unwrap_or_default ( ) )
. map ( | c | c . generating ) . unwrap_or ( session . generating ) ;
if is_gen {
let old_conv = session . active_conversation_id . clone ( ) ;
if let Some ( ref old_id ) = old_conv {
session . conv ( old_id ) . generating = false ;
}
sessi on. generating = false ;
// SW-260618-02:清 pending 前终态化旧对话占位 tool_result(下方 messages.clear 会清旧消息,
// 此处调用为 no-op,但保持统一口径防清理顺序变更)。
finalize_pending_placeholders ( & mut * session , & old_conv . clone ( ) . unwrap_or_default ( ) , " 已取消 " ) ;
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 ) ;
// F-260616-09 B 批4(决策 e 真并发上线):新建会话**不杀旧 loop**。
// 旧实现( B-260615-10) 生成中强制结束旧 conv 的 loop + clear 其 per_conv,在真并发下会误杀
// 后台跑着的 conv。决策 e:旧 conv 的 per_conv 完整保留(后台 loop 继续跑自己的 conv),
// 仅切换 active_conversation_id 到新会话 + 为新会话建独立 per_conv 。
//
// B-260618-06 保留:切换 active 前先持久化旧 active conv(防其内存 messages 因后续操作丢失)。
// 注意:旧 conv 若有后台 loop 在跑,loop 自身的 save_conversation 也会持久化,此处 save 幂等。
let old_conv = {
let session = state . ai_session . lock ( ) . await ;
session . active_conversation_id . clone ( )
} ;
let old_has_msgs = {
let session = state . ai_session . lock ( ) . await ;
old_c onv . as_deref ( )
. and_then ( | oc | session . conv_read ( oc ) )
. map ( | c | ! c . messages . is_empty ( ) )
. unwrap_or ( false )
} ;
if let Some ( ref oc ) = old_conv {
if old_has_msgs {
save_conversation ( & state . ai_session , & state . db , oc . as_str ( ) , None , None ) . await ;
}
}
let mut session = state . ai_session . lock ( ) . await ;
session . active_conversation_id = Some ( id . clone ( ) ) ;
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,与旧会话隔离)。
let new_conv = session . conv ( & id ) ;
let _ = new_conv ; // conv(&id) 已惰性建立空 PerConvState,字段全为默认值
session . messages . clear ( ) ;
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 ( ) ;
// 旧 conv 的 per_conv **不清除**(决策 e:后台 loop 继续跑),其 pending_approvals 也保留
// (switch 路径会 retain 清目标 conv 的,create 不动他人)。
let _ = session . conv ( & id ) ; // 惰性建立空 PerConvState(字段全默认值)
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 mut session = state . ai_session . lock ( ) . await ;
// 生成中允许只读切换:返回目标对话的 messages 供前端展示,但不修改 session 状态
// 后台 loop 持有快照的 conv_id, 不受 active_conversation_id 变更影响
// F-260616-09 B 批2:读 active conv 的 per _conv.gen erating(批3+ 决策 e 删 readonly 分支) 。
let active_for_check = session . active_conversation_id . clone ( ) . unwrap_or_default ( ) ;
let is_gen = if active_for_check . is_empty ( ) {
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 ,
} ) ) ;
}
// F-260616-09 B 批4(决策 e 真并发上线):删除 readonly 分支。
// 旧实现:active conv 生成中 → 只读切换(不改 session 状态),因单例 messages 会被新 conv 覆盖。
// 决策 e:per_conv 已隔离,切走直接改 active _convers ation_id + 新 conv per_conv 惰性建/从 DB reload 。
// **后台 conv(目标正在跑 loop)的 per_conv 已存在则跳过 reload**——防覆盖其内存 messages
// (后台 loop 正在写自己的 per_conv.messages,reload 会用 DB 旧快照覆盖内存新消息)。
session . active_conversation_id = Some ( conversation_id . clone ( ) ) ;
// F-09 B 批2:messages restore 到 per_conv(conv_id 来源:IPC 参数 conversation_id)。
// 顶层 messages 同步双写(批2 桥接,批4 IPC 迁移后顶层移除)。
let conv = session . conv ( & conversation_id ) ;
conv . messages . restore_from_messages ( messages ) ;
conv . model_override = None ;
conv . session_trust . clear ( ) ;
conv . agent_languag e = None ;
conv . iteration_used = 0 ;
conv . stop_flag . store ( false , Ordering ::SeqCst ) ;
// 顶层 messages 双写:先 clone per_conv.messages 再 restore(避免借用冲突)。
let top_restore = session . conv_read ( & conversation_id ) . map ( | c | c . messages . all_messages_clone ( ) ) . unwrap_or_default ( ) ;
session . messages . restore_from_messages ( top_restore ) ;
let already_live = session . conv_read ( & conversation_id ) . map ( | c | c . generating ) . unwrap_or ( false ) ;
if ! already_live {
// 目标 conv 未在生成:从 DB reload messages 到其 per_conv(首次切入或上次切走后无后台 loop)。
// 已在生成:保留其 per_conv 现状(后台 loop 持有),messages 由 loop 自行维护。
let conv = session . conv ( & conversation_id ) ;
conv . messages . restore_from_messages ( messages ) ;
conv . model_overrid e = None ;
conv . session_trust . clear ( ) ;
conv . agent_language = None ;
conv . iteration_used = 0 ;
conv . stop_flag . store ( false , Ordering ::SeqCst ) ;
}
// 仅清空目标对话自身的 pending_approvals,保留其他对话的(防 init 重建的内存 HashMap 被清空,
// 重启恢复链路:restore_pending_approvals(init 重建) → switchConversation(此处不清目标对话的)
// → ai_pending_tool_calls 查询 → ai_approve 落库)
session . pending_approvals . retain ( | _ , a | a . conversation_id . as_deref ( ) ! = Some ( & conversation_id ) ) ;
// F-01 阶段6: 切换对话清旧 override(顶层双写)。
session . model_override = None ;
// AE-2025-04: 切换对话清会话信任(顶层双写)。
session . session_trust . clear ( ) ;
// 释放 session lock 再做 async provider 查询 + spawn(避免持锁 await DB)
drop ( session ) ;
@@ -1721,12 +1628,11 @@ pub async fn ai_conversation_delete(
// 非活跃对话的恢复审批(recovered,conversation_id 指向被删对话)若不 retain 清理,
// 会永久残留死审批条目。对齐 ai_conversation_switch 的 retain 口径(仅清目标对话,保留其他)。
session . pending_approvals . retain ( | _ , a | a . conversation_id . as_deref ( ) ! = Some ( & conversation_id ) ) ;
// F-260616-09 B 批2 :删除 conv 时移除其 per_conv 条目(设计 §4.1 conv 存在性判据依赖此)。
// 顶层 messages.clear 仅在删的是 active conv 时执行(批2 桥接双清) 。
// F-260616-09 B 批4 :删除 conv 时移除其 per_conv 条目(设计 §4.1 conv 存在性判据依赖此,
// 旧 loop 检测 conv 不存在即退出)。per_conv 唯一真相源,删顶层 messages.clear 双写 。
session . per_conv . remove ( & conversation_id ) ;
if session . active_conversation_id . as_deref ( ) = = Some ( & conversation_id ) {
session . active_conversation_id = None ;
session . messages . clear ( ) ;
}
// F-260616-09 B 批5:同时清理 LlmConcurrency 的 per_conv Semaphore 条目(防 HashMap 无限增长)。
// conv 已删=LlmConcurrency 该条目不再被 acquire(无 conv 则无 loop/标题/提炼/压缩针对它)。