优化: 边界加固(AI loop竞态根治+数据/审批/反馈/并发/错误分类)

AI loop 竞态(P0):per-conv epoch/owner token + 存活心跳治 force_send 双loop + stop 3s兜底误判;旧loop stale 全跳过(guard/emit/save)

agentic 收尾(A2-B8):Fatal 退出落库user消息(镜像Exhausted)+ 入口早退补save + usage is_estimated 打标 + emit_ai_completed_once 单点收敛清审批残留

聊天清理(A2-B9):clearChat 先停loop→DB单事务→内存清(clear_conversation_atomic)+ 前端错误气泡

循环并发(A2-B11):三态 ProviderAcquire(NotConfigured/Acquired/Exhausted)+ 候选循环非阻塞+防抖3次饱和降级+单测

错误分类(A2-B12):stream error帧接入 classify_status_or_class + 关键词保守降级 + 7单测

数据(G1.2/G1.4):purge_with_descendants 级联补全(11表单事务+存在性守卫)+ move_task_queue 单事务收口(两调用方共用)

git只读(G3.1):run_git_status/diff/log success判定(exit_code差异语义,失败结构化{success:false,error})

安全(G5.2/G5.6):create_project 目录Err+name校验 + module.rs 路径遍历DRY(分段匹配修a..b.rs误伤)

幂等(V2/V32):裸ALTER全守卫化 + v1..v40全链重跑幂等测试(16过)

附:remote_bridge await 临时引用修(E0716)+ agentic emit 收敛 E0716 app_state 绑定修
This commit is contained in:
lxy
2026-08-05 22:10:32 +08:00
parent ec9f0bf1ea
commit 5667da6cf4
16 changed files with 1696 additions and 215 deletions
@@ -524,6 +524,43 @@ impl AiConversationRepo {
.map_err(storage_err)?
}
/// 清空对话消息内容(单事务原子:ai_conversations.messages 置 '[]' + ai_messages 表全删)。
///
/// A2-B9(G3.2 clearChat 裁决):原 `clear_messages` + `delete_range` 两条独立 DB 写非原子,
/// DB 失败会致 messages JSON 列与 ai_messages 表不一致(如仅一条成功)。本方法一次 transaction
/// 覆盖两条写(① UPDATE ai_conversations 置空消息 + 清零 token;② DELETE ai_messages 该 conv
/// 全部行),成功全成功 / 失败回滚全失败。供 `ai_chat_clear` 先停 loop 再单事务清空。
///
/// 对话壳保留(侧栏仍可见,可继续在该对话内聊);返回 Ok(())——调用方只关心成功与否
/// (对齐 replace_conversation 语义,不返回受影响行数)。
pub async fn clear_conversation_atomic(&self, id: &str) -> Result<()> {
let conn = self.conn.clone();
let id = id.to_owned();
let now = now_millis_str();
tokio::task::spawn_blocking(move || -> Result<()> {
let mut guard = conn.blocking_lock();
let tx = guard.transaction().map_err(storage_err)?;
{
// ① ai_conversations.messages 置空 + token 清零(对话壳保留)
tx.execute(
"UPDATE ai_conversations SET messages = '[]', prompt_tokens = 0, completion_tokens = 0, updated_at = ?1 WHERE id = ?2",
params![now, id],
)
.map_err(storage_err)?;
// ② ai_messages 表全删(等价 delete_range min_seq=0 max=None:seq 恒 >= 0)
tx.execute(
"DELETE FROM ai_messages WHERE conversation_id = ?1",
params![id],
)
.map_err(storage_err)?;
}
tx.commit().map_err(storage_err)?;
Ok(())
})
.await
.map_err(storage_err)?
}
/// 设置归档标记(仅改 archived,不动 updated_at)
///
/// 区别于 update_field(后者强制 SET updated_at=now,会把归档/取消归档误判为内容更新,
@@ -586,6 +623,37 @@ impl AiConversationRepo {
.await
.map_err(storage_err)?
}
/// G1.3: 删除对话 + 其全部 ai_messages 子行(单事务原子)。
///
/// 背景:原宏生成 `delete` 只删 ai_conversations 主行,而 ai_messages 表无外键级联
/// (conversation_id 仅普通索引),子行孤儿累积。本方法在同一事务内**先删子行
/// (ai_messages)再删主行(ai_conversations)**,要么全删要么全不删。
///
/// 顺序注意:先删数据再摘内存(命令层 per_conv.remove 在其后),防后台在途
/// save_conversation 在删主行后把孤儿消息写回复活。与 save_conversation 共享同一
/// conn(Mutex),事务原子性保证删除期间无中间态(半删半留)。
pub async fn delete_with_messages(&self, id: &str) -> Result<bool> {
let conn = self.conn.clone();
let id = id.to_owned();
tokio::task::spawn_blocking(move || {
let mut guard = conn.blocking_lock();
let tx = guard.transaction().map_err(storage_err)?;
// 先删子行(ai_messages)再删主行(ai_conversations),单事务原子
tx.execute(
"DELETE FROM ai_messages WHERE conversation_id = ?1",
params![id],
)
.map_err(storage_err)?;
let conv_affected = tx
.execute("DELETE FROM ai_conversations WHERE id = ?1", params![id])
.map_err(storage_err)?;
tx.commit().map_err(storage_err)?;
Ok(conv_affected > 0)
})
.await
.map_err(storage_err)?
}
}
// ============================================================