修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离
- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条) - queue 加 conversationId 字段,按会话精准 drain - regenerate/editMessage 只清本会话排队消息 - newConversation 保留旧会话排队消息 - AiError 只清出错会话的队列项
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user