优化: aichat 打磨收尾(切换信任保留 + save并发串行 + 后台授权守卫 + 分离窗口失效 + 气泡截图复制)
This commit is contained in:
@@ -203,7 +203,7 @@ pub async fn ai_conversation_create(
|
||||
// F-260619-03 Phase B: 切换 active 会话 → 清空上一会话的临时授权目录(session 字段语义为
|
||||
// "当前活跃会话的临时授权",不跨会话继承)。新会话从空白临时授权开始。
|
||||
drop(session);
|
||||
state.clear_session_allowed_dirs().await;
|
||||
state.clear_session_allowed_dirs_if_idle().await;
|
||||
let mut session = state.ai_session.lock().await;
|
||||
// 新会话建一份独立 per_conv(全新 state,与旧会话隔离)。
|
||||
// 旧 conv 的 per_conv **不清除**(决策 e:后台 loop 继续跑),其 pending_approvals 也保留
|
||||
@@ -327,7 +327,7 @@ pub async fn ai_conversation_switch(
|
||||
// 切换 active 会话 → 清空上一会话的临时授权目录(session 字段语义为"当前活跃会话的临时授权",
|
||||
// 不跨会话继承)。
|
||||
drop(session);
|
||||
state.clear_session_allowed_dirs().await;
|
||||
state.clear_session_allowed_dirs_if_idle().await;
|
||||
let mut session = state.ai_session.lock().await;
|
||||
// 读点:改读无锁 conv_states.is_active()(Generating/Compressed),判断目标会话是否已在生成中
|
||||
// (是否需要从 DB reload)。压缩期间后台 loop 仍持有 per_conv.messages,reload 会用 DB 旧快照
|
||||
@@ -352,6 +352,8 @@ pub async fn ai_conversation_switch(
|
||||
}
|
||||
} else {
|
||||
// 目标 conv 未在生成:从 DB reload messages 到其 per_conv(首次切入或上次切走后无后台 loop)。
|
||||
// 首次切入(内存无该 conv 状态)才复位会话级临时态;切走再切回保留信任/语言/模型覆盖,避免信任遗忘重复审批。
|
||||
let conv_was_present = session.conv_read(&conversation_id).is_some();
|
||||
let conv = session.conv(&conversation_id);
|
||||
// reload 仅在内存无「未落库新消息」时才覆盖 per_conv.messages:
|
||||
// 判据 = 内存消息数 > 已持久化计数 或 有全量重写标记(len > persisted_msg_count 或
|
||||
@@ -395,10 +397,12 @@ pub async fn ai_conversation_switch(
|
||||
};
|
||||
conv.pinned_goals = pinned_goals;
|
||||
}
|
||||
// 会话级临时态复位(无论是否 restore messages,重入即复位)
|
||||
conv.model_override = None;
|
||||
conv.session_trust.clear();
|
||||
conv.agent_language = None;
|
||||
// 首次切入才复位会话级临时态(信任/语言/模型覆盖保留,防切回重复审批);停止信号与迭代计数始终复位
|
||||
if !conv_was_present {
|
||||
conv.model_override = None;
|
||||
conv.session_trust.clear();
|
||||
conv.agent_language = None;
|
||||
}
|
||||
conv.iteration_used = 0;
|
||||
conv.stop_flag.store(false, Ordering::SeqCst);
|
||||
}
|
||||
@@ -626,6 +630,8 @@ pub async fn ai_conversation_delete(
|
||||
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&conversation_id));
|
||||
// 删除 conv 时移除其 per_conv 条目(loop 检测 conv 不存在即退出)。
|
||||
session.per_conv.remove(&conversation_id);
|
||||
// 顺带清理该 conv 的 save 串行锁条目(防 HashMap 无限增长)。
|
||||
session.save_locks.remove(&conversation_id);
|
||||
// conv 删除同步清理生成状态条目,防已删 conv 残留活跃态致 id 复用时脏状态。
|
||||
state.conv_states.remove(&conversation_id);
|
||||
let was_active = session.active_conversation_id.as_deref() == Some(&conversation_id);
|
||||
@@ -639,7 +645,7 @@ pub async fn ai_conversation_delete(
|
||||
// F-260619-03 Phase B: 删除的是当前活跃会话 → 清空其临时授权目录(session 字段语义为
|
||||
// "当前活跃会话的临时授权",active 变 None 时不再属于任何会话)。
|
||||
if was_active {
|
||||
state.clear_session_allowed_dirs().await;
|
||||
state.clear_session_allowed_dirs_if_idle().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ pub(crate) async fn save_conversation(
|
||||
// 超时即放弃本次 save,下次 loop 轮次会重新落库,幂等覆盖无副作用)。
|
||||
if let Err(_elapsed) = tokio::time::timeout(
|
||||
Duration::from_secs(SAVE_CONVERSATION_TIMEOUT_SECS),
|
||||
save_conversation_inner(session_arc, db, conv_id, usage, model, touch_updated_at),
|
||||
save_conversation_serialized(session_arc, db, conv_id, usage, model, touch_updated_at),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -208,6 +208,23 @@ pub(crate) async fn save_conversation(
|
||||
}
|
||||
}
|
||||
|
||||
/// 按 conv 串行化 save(取 per-conv 锁后执行 inner;tokio Mutex lock 可取消,超时放弃不持锁)。
|
||||
async fn save_conversation_serialized(
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
db: &Arc<Database>,
|
||||
conv_id: &str,
|
||||
usage: Option<&df_ai::provider::TokenUsage>,
|
||||
model: Option<&str>,
|
||||
touch_updated_at: bool,
|
||||
) {
|
||||
let save_lock = {
|
||||
let mut session = session_arc.lock().await;
|
||||
session.save_lock(conv_id)
|
||||
};
|
||||
let _save_guard = save_lock.lock().await;
|
||||
save_conversation_inner(session_arc, db, conv_id, usage, model, touch_updated_at).await;
|
||||
}
|
||||
|
||||
/// save_conversation 内部实现(无超时,由外层 save_conversation 包 timeout 调用)。
|
||||
async fn save_conversation_inner(
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
|
||||
@@ -617,6 +617,8 @@ pub struct AiSession {
|
||||
/// 删除语义:删了就删了,loop 后续 save 不应复活。conv_id 是 ULID 不重用,条目长期保留
|
||||
/// (单条 ~40B,删除量级下可忽略)。
|
||||
pub deleted_convs: HashSet<String>,
|
||||
/// 每会话 save 串行锁:同会话并发 save(loop 与 IPC)串行化,防全量重写互踩吞新行
|
||||
pub save_locks: HashMap<String, Arc<tokio::sync::Mutex<()>>>,
|
||||
/// T2 工具命名空间存储(运行时缓存,大工具结果不进主队列)
|
||||
pub namespace_store: df_ai::namespace_store::NamespaceStore,
|
||||
}
|
||||
@@ -630,6 +632,7 @@ impl AiSession {
|
||||
pending_approvals: HashMap::new(),
|
||||
per_conv: HashMap::new(),
|
||||
deleted_convs: HashSet::new(),
|
||||
save_locks: HashMap::new(),
|
||||
namespace_store: df_ai::namespace_store::NamespaceStore::default(),
|
||||
}
|
||||
}
|
||||
@@ -688,6 +691,14 @@ impl AiSession {
|
||||
pub fn conv_read(&self, conv_id: &str) -> Option<&PerConvState> {
|
||||
self.per_conv.get(conv_id)
|
||||
}
|
||||
|
||||
/// 取或惰性创建某会话的 save 串行锁(返回 Arc 供 save 全程持有)。
|
||||
pub fn save_lock(&mut self, conv_id: &str) -> Arc<tokio::sync::Mutex<()>> {
|
||||
self.save_locks
|
||||
.entry(conv_id.to_string())
|
||||
.or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -557,6 +557,13 @@ impl AppState {
|
||||
self.allowed_dirs.write().await.session.clear();
|
||||
}
|
||||
|
||||
/// 仅当无任何会话在生成时才清空会话临时授权(后台 loop 的文件工具依赖它,真并发下切走不能撤)。
|
||||
pub async fn clear_session_allowed_dirs_if_idle(&self) {
|
||||
if self.conv_states.active_convs().is_empty() {
|
||||
self.clear_session_allowed_dirs().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// F-260619-03 Phase B: 追加本次单次授权目录(弹窗"本次"选项)。
|
||||
///
|
||||
/// 写入 `allowed_dirs.once`(进程级内存,不落库)。handler 执行工具时 read lock
|
||||
|
||||
Reference in New Issue
Block a user