修复: 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
+53 -84
View File
@@ -738,33 +738,30 @@ async fn route_send_message(app: &AppHandle, state: &State<'_, AppState>, args:
/// - 读 `conv_state.is_active()`(Generating / Compressed 派生态均视为活跃,压缩期间 loop 仍活跃;
/// 批3 收口后单一 enum 路径,开关 + generating bool 已退役)
///
/// 目标 conv 解析(对齐 ai_is_generating:278-284):
/// 目标 conv 解析(对齐 ai_is_generating):
/// - 入参 conversation_id 非空优先
/// - 否则 fallback `active_conversation_id`
/// - 都无 → 返 None(放行,无 conv 在跑)
///
/// 读 conv 状态用 `conv_read`(只读不创建),对齐 ai_is_generating 实现
/// 读 ConvState 用无锁 `conv_states`(B-Phase2,零锁竞争)
async fn check_generating_reject(
state: &State<'_, AppState>,
conversation_id: Option<&str>,
) -> Option<String> {
let session = state.ai_session.lock().await;
// 目标 conv:入参优先 → active 兜底。
let target = conversation_id
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.or_else(|| session.active_conversation_id.clone());
// 目标 conv:入参优先 → active 兜底(轻量 lock 取 active 单值)。
let target = {
let session = state.ai_session.lock().await;
conversation_id
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.or_else(|| session.active_conversation_id.clone())
};
let target = match target {
Some(id) => id,
None => return None, // 无目标 conv 且无 active:无任何 conv 在跑,放行
};
// 读侧(双轨收口批2):统一读 conv_state.is_active()(Generating+Compressed),删除
// CONV_STATE_ENABLED off 回退分支(enum 单路径)。对齐 ai_is_generating,无 conv_read 视为 false 放行。
let is_gen = session
.conv_read(&target)
.map(|c| c.conv_state.is_active())
.unwrap_or(false);
if is_gen {
// B-Phase2:读侧切无锁 conv_states.is_active()(Generating/Compressed),对齐 ai_is_generating。
if state.conv_states.is_active(&target) {
Some(target)
} else {
None
@@ -833,7 +830,7 @@ fn args_get_mention_spans(args: &Value, key: &str) -> Option<Vec<MentionSpanDto>
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::ai::{AiSession, PerConvState};
use crate::commands::ai::PerConvState;
use df_ai::context::ContextConfig;
// ── MiniCommand 反序列化测试 ──
@@ -920,97 +917,69 @@ mod tests {
assert_eq!(args_get_bool(&args, "missing"), None);
}
// ── R1 check_generating_reject 路径测试(隔离 AiSession,绕开 Tauri State 依赖) ──
// ── R1 check_generating_reject 路径测试 ──
//
// check_generating_reject 需 `State<AppState>`,单元测试难构造。
// 改测 R1 核心判定逻辑:用 AiSession + PerConvState 模拟生成态,
// 复核 conv_read + conv_state.is_active() 口径对齐 ai_is_generating。
// (批3 收口后单一 enum 路径,开关 + generating bool 已退役。)
// check_generating_reject 生产实现经 State<AppState> 读 conv_states(B-Phase2),
// 单元测试难构造 Tauri State。改测 R1 核心判定口径:用 ConvStateStore(无锁,独立可构造)
// 模拟生成态,复核 is_active() 拒绝/放行判定对齐 check_generating_reject / ai_is_generating。
/// R1 判定:无目标 conv 且无 active → 放行(返 None)。
#[test]
fn test_r1_no_target_pass() {
let session = AiSession::new();
// 无 active,无入参 conv_id → 无任何 conv 在跑
assert!(
session.active_conversation_id.is_none(),
"新 session 无 active conv"
);
assert!(
session.per_conv.is_empty(),
"新 session per_conv 为空"
);
// 无入参 conv_id 且 active_conversation_id=None 在生产侧由 check_generating_reject 早 return。
// 此处只验空 store 的默认语义:任何 conv_id 读返 Idle 不活跃。
let store = crate::commands::ai::agentic::conv_state::ConvStateStore::new();
assert!(!store.is_active("any"), "空 store 读任意 conv_id 不活跃");
}
/// R1 判定:目标 conv 未在跑 → 放行(conv_state=Idle,is_active()=false)。
/// R1 判定:目标 conv 未在跑 → 放行(Idle,is_active()=false)。
#[test]
fn test_r1_target_idle_pass() {
use crate::commands::ai::agentic::conv_state::ConvState;
let mut session = AiSession::new();
let conv = session.conv("conv-idle");
conv.conv_state = ConvState::Idle;
// 批3 收口:读 conv_state.is_active() 口径(对齐 check_generating_reject / ai_is_generating)。
// 开关已退役,直接读 enum 单一路径。
let is_gen = session
.conv_read("conv-idle")
.map(|c| c.conv_state.is_active())
.unwrap_or(false);
use crate::commands::ai::agentic::conv_state::{ConvState, ConvStateStore};
let store = ConvStateStore::new();
store.transition("conv-idle", ConvState::Idle).unwrap();
let is_gen = store.is_active("conv-idle");
assert!(!is_gen, "conv-idle Idle 未在跑,is_gen 应为 false(放行)");
}
/// R1 判定:目标 conv 在跑 → 拒绝(conv_state=Generating,is_active()=true)。
/// R1 判定:目标 conv 在跑 → 拒绝(Generating,is_active()=true)。
#[test]
fn test_r1_target_generating_reject() {
use crate::commands::ai::agentic::conv_state::ConvState;
let mut session = AiSession::new();
let conv = session.conv("conv-busy");
// 批3 收口:generating bool 已退役,生成态经 conv_state=Generating 表达(单一真相源)。
conv.conv_state = ConvState::Generating;
let is_gen = session
.conv_read("conv-busy")
.map(|c| c.conv_state.is_active())
.unwrap_or(false);
use crate::commands::ai::agentic::conv_state::{ConvState, ConvStateStore};
let store = ConvStateStore::new();
// 生成态经 transition(Generating) 写入(单一真相源,无 generating bool 双轨)。
store.transition("conv-busy", ConvState::Generating).unwrap();
let is_gen = store.is_active("conv-busy");
assert!(is_gen, "conv-busy Generating 应拒绝");
}
/// R1 判定:conv_state 派生态(Generating)is_active()=true(单一 enum 路径)。
/// R1 判定:conv_state 派生态(Compressed)is_active()=true(单一 enum 路径)。
///
/// 验证批3 收口后读 conv_state.is_active() 判定活跃拒绝(enum 唯一真相源)。
/// 验证is_active() 判定活跃拒绝(enum 唯一真相源),含 Compressed 派生
#[test]
fn test_r1_conv_state_active_reject() {
use crate::commands::ai::agentic::conv_state::ConvState;
// 批3 收口:开关已退役,enum 单一真相源,无条件 on 路径。
let mut session = AiSession::new();
let conv = session.conv("conv-state-busy");
// 模拟状态机写收敛路径:conv_state=Generating(单一真相源,无 generating bool 双轨)。
conv.conv_state = ConvState::Generating;
let is_gen = session
.conv_read("conv-state-busy")
.map(|c| c.conv_state.is_active())
.unwrap_or(false);
use crate::commands::ai::agentic::conv_state::{ConvState, ConvStateStore};
let store = ConvStateStore::new();
// Compressed 派生态:loop 仍活跃,is_active()=true。
store.transition("conv-state-busy", ConvState::Generating).unwrap();
store.transition("conv-state-busy", ConvState::Compressed).unwrap();
let is_gen = store.is_active("conv-state-busy");
assert!(
is_gen,
"conv_state=Generating is_active() 应为 true(单一 enum 路径拒绝)"
"conv_state=Compressed is_active() 应为 true(单一 enum 路径拒绝)"
);
}
/// R1 目标 conv 解析:入参 conversation_id 优先于 active_conversation_id。
#[test]
fn test_r1_target_input_priority() {
use crate::commands::ai::agentic::conv_state::ConvState;
let mut session = AiSession::new();
session.active_conversation_id = Some("conv-active".to_string());
use crate::commands::ai::agentic::conv_state::{ConvState, ConvStateStore};
let store = ConvStateStore::new();
// active 在跑(Generating),但入参 conv-other 未跑(Idle) → 入参优先,应放行
session.conv("conv-active").conv_state = ConvState::Generating;
let _ = session.conv("conv-other"); // 创建但 Idle(默认)
let active_gen = session
.conv_read("conv-active")
.map(|c| c.conv_state.is_active())
.unwrap_or(false);
let other_gen = session
.conv_read("conv-other")
.map(|c| c.conv_state.is_active())
.unwrap_or(false);
store.transition("conv-active", ConvState::Generating).unwrap();
// conv-other 不写入(默认 Idle)
let active_gen = store.is_active("conv-active");
let other_gen = store.is_active("conv-other");
assert!(active_gen, "conv-active 在跑");
assert!(!other_gen, "conv-other 未跑");
// 入参 conv-other 优先 → 判定 conv-other Idle → 放行
@@ -1019,12 +988,12 @@ mod tests {
// ── PerConvState 初值对齐(确保 R1 测试基线 conv_state=Idle) ──
/// PerConvState::new() conv_state 初值 Idle(R1 放行基线)
/// B-Phase3:PerConvState.conv_state 字段已迁 ConvStateStore
#[test]
fn test_per_conv_state_conv_state_default_idle() {
use crate::commands::ai::agentic::conv_state::ConvState;
let _ = ContextConfig::default(); // 触发 ContextConfig default 可用(避免 unused)
let s = PerConvState::new();
assert_eq!(s.conv_state, ConvState::Idle, "PerConvState::new conv_state 初值应为 Idle");
fn test_conv_state_store_default_idle() {
use crate::commands::ai::agentic::conv_state::ConvStateStore;
let store = ConvStateStore::new();
assert_eq!(store.get("nonexistent"), crate::commands::ai::agentic::conv_state::ConvState::Idle,
"ConvStateStore 不存在的 conv_id 返 Idle");
}
}