修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离

- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条)
- queue 加 conversationId 字段,按会话精准 drain
- regenerate/editMessage 只清本会话排队消息
- newConversation 保留旧会话排队消息
- AiError 只清出错会话的队列项
This commit is contained in:
lxy
2026-07-20 00:19:50 +08:00
parent 42efb31bbf
commit e9e3578d26
59 changed files with 2875 additions and 1330 deletions
@@ -191,6 +191,112 @@ impl std::fmt::Display for InvalidTransition {
impl std::error::Error for InvalidTransition {}
// ============================================================
// ConvStateStore — 无锁并发 ConvState 存储(session 锁重构方案 B-Phase0)
//
// 背景:AiSession 全局 Mutex 把 ConvState(高频读 + 敏感脏读)与 messages/pending_approvals
// (长持锁源)同锁串行化,致 guard.reset 等 lock 竞争 800ms fallback(AiCompleted 延迟 / 工具后
// 中断 / 第二条进队列同源根因)。本 Store 把 ConvState 提到独立 DashMap,guard.reset/new/drop
// 直接 transition(同步无 await,不竞争 session lock),ai_is_generating 直接读(零锁竞争)。
//
// 设计:
// - 基于 dashmap::DashMap<String, ConvState>(行级锁,不同 conv 不互斥,无 tokio runtime 阻塞)
// - 全方法同步无 await(transition 内仅 copy + write enum,纳秒级)
// - transition 经 ConvState::transition_to 守卫(复用状态机语义,非法转换拒绝)
// - get 对不存在的 conv_id 返 Idle(惰性默认,对齐 PerConvState 新建语义)
//
// 方案 B 分阶段迁移完成。
// - Phase0~1: ConvStateStore 骨架 + AppState 接入
// - Phase2~3: 写/读侧迁移至无锁 ConvStateStore,PerConvState.conv_state 字段已删
// - Phase4: conversation_delete 同步清理 conv_states 条目
// ============================================================
use dashmap::DashMap;
/// ConvState 的无锁并发存储(方案 B 核心)。
///
/// 经 `Arc<ConvStateStore>` 共享(app_state.conv_states)。所有方法同步无 await,可在任意
/// async 上下文直接调(不竞争 session lock,不阻塞 tokio runtime)。
///
/// 注:`transition` 用 DashMap entry 原子(get + transition_to + write 一致,无 TOCTOU 窗口)。
pub struct ConvStateStore {
inner: DashMap<String, ConvState>,
}
impl ConvStateStore {
/// 创建空 Store。
pub fn new() -> Self {
Self { inner: DashMap::new() }
}
/// 读 conv_id 的 ConvState(不存在返 Idle 默认,对齐 PerConvState 新建语义)。
pub fn get(&self, conv_id: &str) -> ConvState {
self.inner.get(conv_id).map(|r| *r.value()).unwrap_or(ConvState::Idle)
}
/// 读 conv_id 是否活跃生成态(Generating/Compressed)—— ai_is_generating 零锁读。
pub fn is_active(&self, conv_id: &str) -> bool {
self.get(conv_id).is_active()
}
/// 读 conv_id 是否可接受新请求(Idle/Error)—— can_accept_request 零锁读。
pub fn can_accept_request(&self, conv_id: &str) -> bool {
self.get(conv_id).can_accept_request()
}
/// 原子迁移 conv_id 的 ConvState 到 target(经 transition_to 守卫)。
///
/// DashMap entry 原子(get + transition + write 一致,无 TOCTOU)。不存在的 conv_id 视为
/// Idle(对齐新建语义),Idle→target 经守卫。返回 Ok(新态) 或 Err(InvalidTransition)。
pub fn transition(
&self,
conv_id: &str,
target: ConvState,
) -> Result<ConvState, InvalidTransition> {
// get_mut 持写锁原子迁移(Occupied);Vacant 时 insert(Idle 起步)。
// 注:transition 调用点(guard.new/reset/drop)同 conv 单 loop 不并发,TOCTOU 风险低;
// 跨 conv 各自条目行级锁不互斥(对齐 DashMap 设计)。
if let Some(mut r) = self.inner.get_mut(conv_id) {
let cur = *r.value();
match cur.transition_to(target) {
Ok(ns) => {
*r.value_mut() = ns;
Ok(ns)
}
Err(e) => Err(e),
}
} else {
match ConvState::Idle.transition_to(target) {
Ok(ns) => {
self.inner.insert(conv_id.to_string(), ns);
Ok(ns)
}
Err(e) => Err(e),
}
}
}
/// 删除 conv_id 的条目(会话删除时同步清,防已删 conv 残留 Generating 致 id 复用脏状态)。
pub fn remove(&self, conv_id: &str) {
self.inner.remove(conv_id);
}
/// 所有活跃生成态的 conv_id 快照(L0 握手批量 stop / 恢复生成态用)。
pub fn active_convs(&self) -> Vec<String> {
self.inner
.iter()
.filter(|r| r.value().is_active())
.map(|r| r.key().clone())
.collect()
}
}
impl Default for ConvStateStore {
fn default() -> Self {
Self::new()
}
}
// ============================================================
// 单元测试(纯逻辑无 IO)
// ============================================================
@@ -458,4 +564,60 @@ mod tests {
assert!(msg.contains("Idle"), "Display 应含 from: {}", msg);
assert!(msg.contains("Stopping"), "Display 应含 to: {}", msg);
}
// ---- ConvStateStore(方案 B-Phase0,无锁并发存储)----
#[test]
fn test_store_get_default_idle() {
let s = ConvStateStore::new();
assert_eq!(s.get("conv-1"), ConvState::Idle, "不存在 conv 应返 Idle 默认");
}
#[test]
fn test_store_transition_occupied() {
let s = ConvStateStore::new();
s.transition("conv-1", ConvState::Generating).unwrap();
assert_eq!(s.get("conv-1"), ConvState::Generating);
s.transition("conv-1", ConvState::Idle).unwrap();
assert_eq!(s.get("conv-1"), ConvState::Idle);
}
#[test]
fn test_store_transition_guard_rejects() {
let s = ConvStateStore::new();
s.transition("conv-1", ConvState::Generating).unwrap();
// Generating → Stopping 合法
s.transition("conv-1", ConvState::Stopping).unwrap();
// Stopping → Generating 非法(须先回 Idle 再起)
assert!(s.transition("conv-1", ConvState::Generating).is_err());
}
#[test]
fn test_store_remove() {
let s = ConvStateStore::new();
s.transition("conv-1", ConvState::Generating).unwrap();
s.remove("conv-1");
assert_eq!(s.get("conv-1"), ConvState::Idle, "remove 后应返 Idle 默认");
}
#[test]
fn test_store_is_active_and_can_accept() {
let s = ConvStateStore::new();
assert!(!s.is_active("conv-1"), "Idle 不活跃");
assert!(s.can_accept_request("conv-1"), "Idle 可接");
s.transition("conv-1", ConvState::Generating).unwrap();
assert!(s.is_active("conv-1"), "Generating 活跃");
assert!(!s.can_accept_request("conv-1"), "Generating 不可接");
}
#[test]
fn test_store_active_convs() {
let s = ConvStateStore::new();
s.transition("a", ConvState::Generating).unwrap();
s.transition("b", ConvState::Idle).unwrap();
s.transition("c", ConvState::Generating).unwrap();
let mut active = s.active_convs();
active.sort();
assert_eq!(active, vec!["a".to_string(), "c".to_string()], "仅活跃 conv");
}
}
+53 -183
View File
@@ -1,218 +1,88 @@
//! B-260615-09: generating 状态 RAII guard —— 从 agentic/mod.rs 抽离(重构第一批,纯结构搬迁)
//! generating 状态 RAII guard。
//!
//! 行为零变更:仅文件位置移动,逻辑/字段/语义完全保留
//! 调用方(agentic/mod.rs run_agentic_loop)经 `use super::guard::GeneratingGuard;` 复用
use std::sync::Arc;
//! B-Phase2: ConvState 切无锁 ConvStateStore(不竞争 session lock,删 800ms fallback)
//! 治卡死连环:AiCompleted 延迟 / 工具后中断 / 第二条进队列同源根因
use tauri::{AppHandle, Emitter, Manager};
use tokio::sync::Mutex;
use crate::commands::ai::{AiChatEvent, AiSession};
use crate::commands::ai::AiChatEvent;
// 双轨收口批2:CONV_STATE_ENABLED 常量删除后,模块路径(self)不再使用,仅导入 ConvState 类型。
use super::conv_state::ConvState;
/// generating 复位 RAII guard,取代散布的手动 `session.generating = false`
/// generating 复位 RAII guard。
///
/// 两路复位:
/// - 正常路径:exit 点显式 `reset().await` 即时复位(emit 前调,保证"复位→emit"顺序,
/// 前端收事件时后端已可接下一条)。
/// - 异常路径(panic/未走正常 return):Drop 兜底 spawn 复位,防 generating 永真卡死前端。
///
/// 注:try_continue_agent_loop 不用 guard——其 should_continue=false 路径需保持
/// generating=true(审批等待态),全函数 guard 会误复位;该函数单点 provider-Err 复位保持手动。
///
/// F-260616-09 B 批4:guard 持 `conv_id`,复位改写 `session.conv(&conv_id).generating = false`
/// (per-conv 唯一真相源)。顶层 `session.generating` 字段已在批4 删除,reset/Drop 仅写 per_conv;
/// IPC(ai_is_generating/ai_chat_send 等)亦改读 per_conv,无需双写桥接。
///
/// L2 统一状态机收尾(2026-06-22):本地 `state: Option<ConvState>` 视图层冗余字段已删除。
///
/// 背景:批1 曾在 guard 内嵌 `state` 作「写收敛视图层」(双轨过渡),批2 已把 `ConvState` 落到
/// `PerConvState.conv_state` 字段持久化(读侧真相源)。批2 同时在 `reset`/`drop` 内读
/// `conv.conv_state` 持久化字段做迁移 + emit AiConvStateChanged——故本地 `state` 成纯冗余
/// (双源,且本地 state 在 Drop 无 await 上下文无法持久化,语义本就不完整)。
///
/// 收尾删除后:
/// - `new`:读 `conv.conv_state` 持久化字段,若 Idle→Generating 迁移成功则 emit 通知前端
/// (入口 `run_agentic_loop` 已独立写持久化迁移但**不 emit**,故此处补 emit 防前端漏收生成态)。
/// - `reset`/`drop`:迁移 + emit 均读 `conv.conv_state` 持久化字段(批2 已落地,本批仅删冗余本地块)。
/// - `disarm`:不动 ConvState(审批等待是 Generating 内的暂停点)。
///
/// 批3 双轨收口:`CONV_STATE_ENABLED` 开关与 `generating` bool 已退役,enum 迁移 + emit
/// 无条件执行(单一真相源,无 off 降级分支)。
/// ConvState 经无锁 [`ConvStateStore`](super::conv_state::ConvStateStore) 迁移(不竞争 session
/// lock)。正常退出显式 `reset()`;异常退出 `Drop` 兜底迁移 Idle。`disarm` 解除 Drop 兜底
/// (审批等待保持 Generating,续跑新 guard.new 重新迁移)。
pub(super) struct GeneratingGuard {
session: Arc<Mutex<AiSession>>,
/// guard 所属会话(loop 启动时快照的 conv_id,来自 run_agentic_loop 入参)。
conv_id: String,
done: bool,
/// L2 批2 1b:emit AiConvStateChanged 用的 AppHandle(Clone 廉价,run_agentic_loop 入参传入)。
/// 批3 收口:emit 无条件执行(开关已退役),此字段恒被消费。
app_handle: AppHandle,
}
impl GeneratingGuard {
pub(super) fn new(session: Arc<Mutex<AiSession>>, conv_id: String, app_handle: AppHandle) -> Self {
// L2 收尾:入口 `run_agentic_loop` 已独立完成 Idle→Generating 的持久化迁移(写 conv.conv_state),
// 但**不 emit** AiConvStateChanged。此处补 emit 通知前端进入生成态——读 conv.conv_state
// 持久化字段(真相源)做迁移校验,迁移成功则 emit,非法记 warn 不阻断。
//
// 注:new 是同步函数(无 await),无法持 session 锁读 conv.conv_state;此处经
// tauri::async_runtime::spawn 异步补 emit(非阻塞 guard 创建,入口持久化已发生故 emit 有据)。
// spawn 失败(运行时不可用,理论场景)仅漏一次 emit,前端 watchdog 兜底(对齐失败不阻断核心生成)。
// 批3 双轨收口:删除 CONV_STATE_ENABLED 门控,迁移 + emit 无条件执行(enum 单一真相源)。
// 注:clone 给闭包 move,原始 session/app_handle 留给 Self 字段(对齐原 if 块外 Self 构造)。
let session_for_spawn = session.clone();
let app_handle_for_spawn = app_handle.clone();
let cid = conv_id.clone();
tauri::async_runtime::spawn(async move {
let new_state = {
let mut session = session_for_spawn.lock().await;
let conv = session.conv(&cid);
match conv.conv_state.transition_to(ConvState::Generating) {
Ok(ns) => {
conv.conv_state = ns;
ns
}
Err(e) => {
tracing::warn!(
conv_id = %cid,
error = %e,
"[ai] guard.new ConvState→Generating 非法(持久化层,不阻断核心生成)"
);
return;
}
}
};
// 持久化层迁移成功后 emit 前端(同步,失败忽略)。锁已随作用域 drop,可安全 emit。
// L3 emit 双写:提变量避免构造两次,publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内)。
let ev = AiChatEvent::AiConvStateChanged {
conv_state: new_state,
conversation_id: Some(cid),
};
let _ = app_handle_for_spawn.emit("ai-chat-event", ev.clone());
let _ = app_handle_for_spawn.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
});
Self { session, conv_id, done: false, app_handle }
pub(super) fn new(conv_id: String, app_handle: AppHandle) -> Self {
let guard = Self {
conv_id,
done: false,
app_handle: app_handle.clone(),
};
guard.transition(ConvState::Generating);
guard
}
/// 显式复位生成态。emit 前调保证顺序。幂等。
///
/// 批3 双轨收口:generating bool 已退役,复位经 ConvState 迁移(Generating→Idle)单一表达。
/// L2:同步迁移 `conv.conv_state → Idle`(持久化层,迁移 + emit 无条件执行)。
/// 复位 Idle(emit 前调,保证前端收事件时后端已 Idle)。幂等。
pub(super) async fn reset(&mut self) {
if !self.done {
let mut session = self.session.lock().await;
let conv = session.conv(&self.conv_id);
// L2 批2:ConvState 持久化(Generating→Idle 写收敛)。批3 删除开关门控,迁移无条件执行。
match conv.conv_state.transition_to(ConvState::Idle) {
Ok(ns) => {
conv.conv_state = ns;
// L2 批2 1b:持久化层 Idle 收敛成功后 emit 前端(同步,失败忽略)。
// L3 emit 双写:提变量避免构造两次,publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内)。
let ev = AiChatEvent::AiConvStateChanged {
conv_state: ns,
conversation_id: Some(self.conv_id.clone()),
};
let _ = self.app_handle.emit("ai-chat-event", ev.clone());
let _ = self.app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
}
Err(e) => tracing::warn!(
conv_id = %self.conv_id,
error = %e,
"[ai] guard.reset ConvState→Idle 非法(持久化层,不阻断核心复位)"
),
}
self.done = true;
self.transition(ConvState::Idle);
}
}
/// 解除 Drop 兜底复位但不复位 generating。审批等待 return 路径调用:
/// 保持 generating=true 留 try_continue 续生成,同时 Drop 因 done=true 跳过复位 spawn。
/// (B-260615-26: 修复审批执行后对话不续生成回归)
///
/// L2:disarm 不迁移 ConvState(审批等待是 Generating 内的暂停点,ConvState 仍 Generating,
/// 与「generating 保持 true」语义一致)。续跑时新 guard.new 重新迁移(Generating 自环幂等)。
/// 解除 Drop 兜底(审批等待保持 Generating)。
pub(super) fn disarm(&mut self) {
self.done = true;
}
/// 迁移 ConvState + emit AiConvStateChanged(经无锁 ConvStateStore,同步无 await)。
fn transition(&self, target: ConvState) {
let conv_states = self
.app_handle
.state::<crate::state::AppState>()
.conv_states
.clone();
let ev = match conv_states.transition(&self.conv_id, target) {
Ok(ns) => Some(AiChatEvent::AiConvStateChanged {
conv_state: ns,
conversation_id: Some(self.conv_id.clone()),
}),
Err(e) => {
tracing::warn!(
conv_id = %self.conv_id,
error = %e,
"[ai] guard ConvState 迁移非法"
);
None
}
};
if let Some(ev) = ev {
let _ = self.app_handle.emit("ai-chat-event", ev.clone());
let _ = self
.app_handle
.state::<crate::state::AppState>()
.ai_event_bus
.publish_event(ev);
}
}
}
impl Drop for GeneratingGuard {
fn drop(&mut self) {
// 未 done 即异常退出(panic/未走正常 return):核心生成态经 spawn 复位,
// ConvState 持久化迁移 + emit 也在同一 spawn 内异步完成(无 await 上下文,对齐核心复位路径)。
// done=true(正常 reset/disarm 已走) → 跳过。批3 收口后无开关 off 分支。
if !self.done {
let session = self.session.clone();
let conv_id = self.conv_id.clone();
// L2 批2 1b:Drop 无 await 上下文,持久化迁移 + emit 在 spawn 内异步完成(对齐核心复位 spawn 路径)。
// app_handle Clone 廉价,随 session/conv_id 一起 move 进 async 块。
let app_handle = self.app_handle.clone();
tauri::async_runtime::spawn(async move {
let mut s = session.lock().await;
let conv = s.conv(&conv_id);
// L2 批2:ConvState 持久化(异常退出兜底→Idle)。批3 删除开关门控 + generating bool 写,迁移无条件执行。
if let Ok(ns) = conv.conv_state.transition_to(ConvState::Idle) {
conv.conv_state = ns;
// 1b:持久化 Idle 收敛后 emit 前端(异常退出兜底也需通知前端退出生成态)。
// L3 emit 双写:提变量避免构造两次,publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内)。
let ev = AiChatEvent::AiConvStateChanged {
conv_state: ns,
conversation_id: Some(conv_id.clone()),
};
let _ = app_handle.emit("ai-chat-event", ev.clone());
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
}
});
self.transition(ConvState::Idle);
}
}
}
// ============================================================
// 单元测试(guard ConvState 迁移语义,纯逻辑路径验证)
// ============================================================
#[cfg(test)]
mod tests {
use super::*;
/// guard.new 路径:持久化层 Idle→Generating 迁移(读 conv.conv_state)。
///
/// 注:不构造真实 AiSession(lock/Mutex 建会话状态成本高),仅校验迁移语义——
// guard 内部 transition_to 是纯函数,直接断言逻辑路径。
#[test]
fn test_guard_new_migrates_to_generating() {
// 批3 收口后无开关断言:Idle → Generating(读侧持久化字段迁移路径,无条件执行)。
let mut s = ConvState::Idle;
s = s.transition_to(ConvState::Generating).unwrap();
assert_eq!(s, ConvState::Generating);
}
/// reset 迁移 Generating→Idle(正常收敛),验证迁移合法。
#[test]
fn test_guard_reset_migrates_to_idle() {
let mut s = ConvState::Generating;
s = s.transition_to(ConvState::Idle).unwrap();
assert_eq!(s, ConvState::Idle);
}
/// 异常 Drop 路径同样 Generating→Idle(兜底),验证迁移合法。
#[test]
fn test_guard_drop_path_migrates_to_idle() {
// guard.drop 异常路径(!done)迁移 Generating→Idle(读持久化字段)。
let mut s = ConvState::Generating;
s = s.transition_to(ConvState::Idle).unwrap();
assert_eq!(s, ConvState::Idle);
}
/// disarm 不迁移状态(审批等待保持 Generating)。
/// 验证:disarm 后 enum 仍 Generating(下次 new 时 Generating 自环幂等通过)。
#[test]
fn test_guard_disarm_keeps_generating_then_self_loop_on_new() {
let s = ConvState::Generating;
// disarm 不动 enum;续跑新 guard.new 时 Generating→Generating 自环通过。
assert_eq!(s.transition_to(ConvState::Generating), Ok(ConvState::Generating));
}
}
// ConvState 迁移语义单测见 conv_state.rs(ConvStateStore + ConvState::transition_to)。
// guard struct 委托 ConvStateStore,无独立逻辑,不重复单测。
+8 -11
View File
@@ -30,11 +30,12 @@ pub async fn try_continue_agent_loop(
let snap = {
let session = state.ai_session.lock().await;
let has_pending = session.session_state(conv_id) == SessionState::AwaitingApproval;
let has_pending = session.session_state(conv_id, &state.conv_states) == SessionState::AwaitingApproval;
let pending_conv_id = session.pending_approvals.values()
.find_map(|a| a.conversation_id.clone());
let conv = session.conv_read(conv_id);
let is_generating = conv.map(|c| c.conv_state.is_active()).unwrap_or(false);
// B-Phase2:ConvState 读侧切无锁 conv_states(is_active),不再占 session lock 读 conv_state。
let is_generating = state.conv_states.is_active(conv_id);
let agent_language = conv.and_then(|c| c.agent_language.clone());
let model_override = conv.and_then(|c| c.model_override.clone());
let pinned_goals_snapshot = conv.map(|c| c.pinned_goals.clone()).unwrap_or_default();
@@ -73,12 +74,10 @@ pub async fn try_continue_agent_loop(
let provider_config = match get_active_provider(state).await {
Ok(p) => p,
Err(e) => {
let mut session = state.ai_session.lock().await;
match session.conv(conv_id).conv_state.transition_to(ConvState::Idle) {
Ok(ns) => session.conv(conv_id).conv_state = ns,
Err(e2) => tracing::warn!(conv_id = %conv_id, error = %e2, "ConvState→Idle 非法"),
// B-Phase3:不再持锁操作 conv_state(ConvStateStore 无锁单源)。
if let Err(e2) = state.conv_states.transition(conv_id, ConvState::Idle) {
tracing::warn!(conv_id = %conv_id, error = %e2, "conv_states→Idle 非法");
}
drop(session);
let _ = app.emit("ai-chat-event", AiChatEvent::AiError {
error: e.clone(), error_type: Some(ErrorType::ProviderConfig),
conversation_id: Some(conv_id.to_string()),
@@ -107,10 +106,8 @@ pub async fn try_continue_agent_loop(
let max_retries = state.agent_max_retries.load(std::sync::atomic::Ordering::SeqCst);
let model_override = snap.model_override.clone();
let still_generating = {
let session = state.ai_session.lock().await;
session.conv_read(conv_id).map(|c| c.conv_state.is_active()).unwrap_or(false)
};
// B-Phase2:ConvState 读侧切无锁 conv_states(零锁竞争,无需 session lock)。
let still_generating = state.conv_states.is_active(conv_id);
if !still_generating {
let ev = AiChatEvent::AiCompleted {
total_tokens: 0, prompt_tokens: 0, completion_tokens: 0,
@@ -1,32 +1,2 @@
//! Agentic 循环知识生命周期(文档占位)。
//!
//! ## 为什么没有抽取
//!
//! `run_agentic_loop` 中知识相关的逻辑**已经是模块化的**:所有调用点都是对已抽取函数的
//! 单行调用,不存在可内联抽取的知识逻辑。
//!
//! ### 已存在的知识模块
//!
//! 知识注入/提炼实现位于 [`super::super::knowledge_inject`] 模块,`mod.rs` 顶部已通过
//! `use super::knowledge_inject::{inject_knowledge_into_prompt, maybe_spawn_extraction};` 引入:
//!
//! - `inject_knowledge_into_prompt` —— 构建请求前把知识库上下文注入 system_prompt。
//! - `maybe_spawn_extraction` —— 后台知识提炼(需读已落库的对话消息)。
//!
//! ### mod.rs 中的调用点(均为单行)
//!
//! | 行 | 所在函数 | 调用 |
//! |----|----------|------|
//! | ~1853 | `run_agentic_loop`(正常完成后台 spawn) | `maybe_spawn_extraction(...)` |
//! | ~2092 | `try_continue_agent_loop` | `inject_knowledge_into_prompt(...)` |
//!
//! 注意:`run_agentic_loop` 入口的 system_prompt 构建**不**在 mod.rs 内联调用
//! `inject_knowledge_into_prompt` —— 知识注入发生在调用方(IPC commands.rs /
//! try_continue_agent_loop),loop 入参拿到的 `system_prompt` 已是注入后的最终值。
//! 故 mod.rs 内知识相关代码仅有上述 2 处单行调用。
//!
//! ### 结论
//!
//! 知识生命周期的抽取工作**早已完成**(knowledge_inject 模块独立)。本文件仅作占位与说明,
//! 无代码可移。`inject_knowledge_into_prompt` 的入口收敛(DRY B 改进)已在调用方完成,
//! 非 mod.rs 职责。
//! 知识提炼实现位于 `super::super::knowledge_inject` 模块。
+34 -35
View File
@@ -46,16 +46,9 @@ use super::audit::process_tool_calls;
// compress_via_llm 已随压缩逻辑迁至 context_lifecycle.rs(maybe_auto_compress 内调用)。
use super::conversation::{save_conversation, TokenAccumulator};
use super::knowledge_inject::{maybe_spawn_extraction};
#[allow(unused_imports)]
use super::knowledge_inject::inject_knowledge_into_prompt;
#[allow(unused_imports)]
use super::prompt::{build_system_prompt, get_active_provider};
use super::stream_recv::{stream_llm, StreamResult};
use super::title::{ensure_conversation_title, spawn_ensure_title};
#[allow(unused_imports)]
use super::{AiChatEvent, AiSession, ErrorType, SessionState};
#[allow(unused_imports)]
use super::ToolCallDraft;
use super::{AiChatEvent, AiSession, ErrorType};
// ConvState 经本文件内 `pub mod conv_state;` 同 crate 直接访问(conv_state::ConvState)。
/// L1 补丁:run_agentic_loop 入口 provider 解析超时保护的内部错误类型。
@@ -356,14 +349,12 @@ async fn stream_one_provider(
// 注:primary 候选的启动 Auth 早失败已在外层 run_agentic_loop 顶部处理(emit + return),
// 本函数到达时 primary 的 key 已验证过;此处 Err 多见于 secondary 配置不一致,
// 保守归 Fatal 立即放弃(不浪费预算试下一 provider,因 key 错非瞬态)。
let provider: Box<dyn LlmProvider> = match super::secret::build_provider_for(candidate) {
let provider: Arc<dyn LlmProvider> = Arc::from(match super::secret::build_provider_for(candidate) {
Ok(p) => p,
Err(msg) => {
// UX-260618-15: Fatal 分支 emit 移到外层(stream_one_provider 调用方 Fatal 分支统一 emit)。
// 此处仅返回 error 文本,避免 stream_one_provider 内 emit 与外层 emit 重复(Fatal 终态单 emit)。
return StreamOutcome::Fatal { error: msg };
}
};
});
// resolved_model 在本 candidate.model_configs 上重算(F-260614-04b 核心:provider 切换后
// 模型池不同,必须重选;否则拿主 provider 的 model_id 去打次 provider 会吃 400/404)。
@@ -394,7 +385,7 @@ async fn stream_one_provider(
reasoning_content: last_reasoning_content.clone(),
};
match stream_llm(&*provider, retry_request, app_handle, stop_flag, notify, conv_id).await {
match stream_llm(Arc::clone(&provider), retry_request, app_handle, stop_flag, notify, conv_id).await {
StreamResult::Complete { text, tool_calls, usage, reasoning_content } => {
return StreamOutcome::Success {
text, tool_calls, usage,
@@ -514,7 +505,34 @@ pub(crate) async fn run_agentic_loop(
// B-260615-09: generating 状态由 RAII guard 收敛复位(正常 exit 显式 reset;panic/异常 Drop 兜底)
// F-260616-09 B 批2:guard 持 conv_id,复位改 per-conv.generating(设计 §4.3)。
// L2 批2 1b:guard 持 app_handle,ConvState 迁移后 emit AiConvStateChanged 推前端。
let mut guard = GeneratingGuard::new(session_arc.clone(), conv_id.clone(), app_handle.clone());
let mut guard = GeneratingGuard::new(conv_id.clone(), app_handle.clone());
// 治本:LLM 生成间隙向前端发 AiHeartbeat,防前端 watchdog 误断流。
// 后端 emit AiHeartbeat → useAiEvents.ts resetStreamWatchdog(convId) → per-conv timer 重置。
// 解决:LLM 重试循环(~60s)期间无事件到达前端 → watchdog 45s/90s 到期误杀。
// shutdown_tx 在函数作用域结束(Drop)时通知 heartbeat 任务退出。
let (_heartbeat_tx, mut heartbeat_rx) = tokio::sync::watch::channel(());
let hb_app = app_handle.clone();
let hb_conv_id = conv_id.clone();
tokio::spawn(async move {
loop {
tokio::select! {
_ = heartbeat_rx.changed() => {
// watch 通道关闭(发送端 Drop)→ 退出
break;
}
_ = tokio::time::sleep(std::time::Duration::from_secs(20)) => {
// 每 20s 发一次 AiHeartbeat,前端重置 watchdog timer。
let _ = hb_app.emit(
"ai-chat-event",
AiChatEvent::AiHeartbeat {
conversation_id: Some(hb_conv_id.clone()),
},
);
}
}
}
});
// F-260616-09 B 批2 入口桥接:loop 启动前确保 per_conv 存在(已存在则保留累积,不存在则建)。
//
@@ -533,23 +551,7 @@ pub(crate) async fn run_agentic_loop(
// 单 loop 安全性:批2 阶段是单 active_conversation_id(决策 e 真并发批3+ 落地),无并发 loop 抢
// per_conv 覆盖。批3+ 多 loop 并发时,每 conv 各自 per_conv 条目,互不干扰(本桥接无需改)。
//
// L2 状态机写收敛:ConvState 的写收敛已收敛到 `guard`(GeneratingGuard::new 已在上方 L399 创建,
// new 内部 Idle→Generating 迁移 + reset/drop Generating→Idle 迁移)。
// 批3 双轨收口:generating bool 已退役,此处仅做持久化层 Idle→Generating 迁移(单一真相源),
// emit 由 guard.new 异步补发(入口迁移但不 emit,guard 补 emit 防前端漏收生成态)。
{
let mut session = session_arc.lock().await;
let conv = session.conv(&conv_id);
// L2 批2:ConvState 持久化(Idle→Generating 写收敛)。批3 删除开关门控,迁移无条件执行。
match conv.conv_state.transition_to(conv_state::ConvState::Generating) {
Ok(ns) => conv.conv_state = ns,
Err(e) => tracing::warn!(
conv_id = %conv_id,
error = %e,
"[ai] 入口 ConvState→Generating 非法(状态机持久化层,不阻断核心生成)"
),
}
}
// 入口:ConvState 迁移由 guard.new 在无锁 ConvStateStore 上完成。
// F-260614-04 / F-260614-04b: 多 Provider 负载均衡池 — 选主 + fallback 候选列表。
//
@@ -1544,10 +1546,7 @@ pub(crate) async fn run_agentic_loop(
let _ = converged;
// 处理工具调用(Low 自动执行 / Medium+High 待审批)
let pending_count = {
let mut session = session_arc.lock().await;
process_tool_calls(&mut session, tool_calls_acc, &tools_arc, &db, &app_handle, &conv_id).await
};
let pending_count = process_tool_calls(&session_arc, tool_calls_acc, &tools_arc, &db, &app_handle, &conv_id).await;
// F-260620 卡死已根治(DIRAUTH 审批链已闭环)。原 eprintln 诊断降级为 tracing::debug,
// 避免污染 stderr(用户可见),保留排障能力(RUST_LOG=debug 可见)。
tracing::debug!(
@@ -1,34 +1,2 @@
//! Agentic 循环标题生命周期(文档占位)。
//!
//! ## 为什么没有抽取
//!
//! `run_agentic_loop` 中标题相关的逻辑**已经是模块化的**:所有调用点都是对已抽取函数的
//! 单行调用,不存在可内联抽取的标题逻辑。
//!
//! ### 已存在的标题模块
//!
//! 标题生成实现位于 [`super::super::title`] 模块,`mod.rs` 顶部已通过
//! `use super::title::{ensure_conversation_title, spawn_ensure_title};` 引入:
//!
//! - `spawn_ensure_title` —— 后台 fire-and-forget 标题生成(不阻塞 Completed emit)。
//! - `ensure_conversation_title` —— 标题生成(可 await,正常完成路径后台 spawn 内调用)。
//!
//! ### mod.rs 中的调用点(均为单行)
//!
//! `run_agentic_loop` 内 4 处调用:
//!
//! | 行 | 场景 | 调用 |
//! |----|------|------|
//! | ~893 | 入口 stop(首轮即停) | `spawn_ensure_title(...)` |
//! | ~1469 | MidStream 保文后 | `spawn_ensure_title(...)` |
//! | ~1543 | 流后 stop | `spawn_ensure_title(...)` |
//! | ~1856 | 正常完成后台 spawn | `ensure_conversation_title(...)` |
//!
//! 这些调用点散落在不同的控制流分支(stop/保文/正常完成),每处仅一行,且各自需要
//! 不同的上下文(`resolved_model` 是否已知、是否在 guard.reset 前/后)。强行抽取为
//! 单一函数会引入等量的参数透传 + 控制流分支,无净收益且增加间接层。
//!
//! ### 结论
//!
//! 标题生命周期的抽取工作**早已完成**(title 模块独立)。本文件仅作占位与说明,
//! 无代码可移。若未来需要统一标题调用上下文,应在 title 模块内封装,而非在此处。
//! 标题生成实现位于 `super::super::title` 模块。