重构: aichat 双轨状态机收口 + AiCompressed 事件拆分
- generating bool + CONV_STATE_ENABLED 开关双轨退役,ConvState enum 单一真相源 - can_accept_request 接入 chat 域入口(覆盖 Stopping 竞态,严谨于 is_active) - AiCompressed 拆 AiManualCompressed/AiAutoCompressed(治自动压缩误触桌面toast+刷新) - convStates/getConvState 下沉 aiShared.ts 破循环依赖
This commit is contained in:
@@ -30,9 +30,9 @@
|
||||
//!
|
||||
//! ## R1 兜底(硬性,不可省,§3.3 + D6)
|
||||
//!
|
||||
//! `send_message` 路由前置 generating 检查(对齐 ai_is_generating 双轨口径:
|
||||
//! CONV_STATE_ENABLED on 时读 conv_state.is_active(),off 回退 generating bool)。
|
||||
//! generating=true 则拒绝,emit `AiError` 事件回 miniapp 提示「正在生成中」,
|
||||
//! `send_message` 路由前置 generating 检查(对齐 ai_is_generating 口径:
|
||||
//! 批3 收口后读 conv_state.is_active() 单一 enum 路径,开关 + generating bool 已退役)。
|
||||
//! 生成中(Generating/Compressed)则拒绝,emit `AiError` 事件回 miniapp 提示「正在生成中」,
|
||||
//! 不调 ai_chat_send。这是 miniapp 与桌面同时给同 conv 发消息的双保险,
|
||||
//! 无论 ai_chat_send 内部是否已有 generating guard 都加(防御性编程)。
|
||||
//!
|
||||
@@ -75,8 +75,6 @@ use crate::commands::ai::skills::SkillInfo;
|
||||
use df_types::augmentation::MentionSpanDto;
|
||||
// F-#95 扩展:历史消息同步 route_load_messages 用(record_to_message 映射 AiMessageRecord→ChatMessage)。
|
||||
use df_ai::provider::ChatMessage;
|
||||
// CONV_STATE_ENABLED 与 ConvState::is_active 双轨口径(对齐 ai_is_generating 实现)。
|
||||
use crate::commands::ai::agentic::conv_state::CONV_STATE_ENABLED;
|
||||
|
||||
// ============================================================
|
||||
// MiniCommand 协议结构(对齐 apps/df-miniapp/src/types/relay.ts:88-93)
|
||||
@@ -637,11 +635,10 @@ async fn route_list_entities(state: &State<'_, AppState>) {
|
||||
/// ai_chat_send 内部虽有 generating guard,但桥接层再加一层前置检查作为双保险
|
||||
/// (防御性编程,对齐设计文档 D6「桥接层 send 路由硬性加,无论 ai_chat_send 内部是否有 guard」)。
|
||||
///
|
||||
/// 检查口径与 `ai_is_generating` 完全一致(双轨):
|
||||
/// - CONV_STATE_ENABLED on → 读 `conv_state.is_active()`(含 Generating / Compressed 派生态)
|
||||
/// - off → 回退 `generating` bool
|
||||
/// 检查口径与 `ai_is_generating` 完全一致:
|
||||
/// - 读 `conv_state.is_active()`(含 Generating / Compressed 派生态,批3 收口后单一 enum 路径)
|
||||
///
|
||||
/// generating=true 则拒绝:**emit `AiError` 事件回 miniapp 提示「正在生成中」**,不调 ai_chat_send。
|
||||
/// 生成中则拒绝:**emit `AiError` 事件回 miniapp 提示「正在生成中」**,不调 ai_chat_send。
|
||||
/// (事件透传阶段2 已建通路,miniapp handleEvent 能渲染 AiError,用户可见反馈)。
|
||||
///
|
||||
/// message 必填,缺失跳过(ai_chat_send 无 message 无意义,日志告警)。
|
||||
@@ -738,9 +735,8 @@ async fn route_send_message(app: &AppHandle, state: &State<'_, AppState>, args:
|
||||
/// 返 `None` 表示放行(可发)。
|
||||
///
|
||||
/// 双轨口径对齐 `ai_is_generating`(commands/chat.rs:287-293):
|
||||
/// - CONV_STATE_ENABLED on → 读 `conv_state.is_active()`(Generating / Compressed 派生态均视为活跃,
|
||||
/// 压缩期间 loop 仍活跃)
|
||||
/// - off → 回退 `generating` bool
|
||||
/// - 读 `conv_state.is_active()`(Generating / Compressed 派生态均视为活跃,压缩期间 loop 仍活跃;
|
||||
/// 批3 收口后单一 enum 路径,开关 + generating bool 已退役)
|
||||
///
|
||||
/// 目标 conv 解析(对齐 ai_is_generating:278-284):
|
||||
/// - 入参 conversation_id 非空优先
|
||||
@@ -762,16 +758,11 @@ async fn check_generating_reject(
|
||||
Some(id) => id,
|
||||
None => return None, // 无目标 conv 且无 active:无任何 conv 在跑,放行
|
||||
};
|
||||
// 双轨读 generating(对齐 ai_is_generating,无 conv_read 视为 false 放行)。
|
||||
// 读侧(双轨收口批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| {
|
||||
if CONV_STATE_ENABLED {
|
||||
c.conv_state.is_active()
|
||||
} else {
|
||||
c.generating
|
||||
}
|
||||
})
|
||||
.map(|c| c.conv_state.is_active())
|
||||
.unwrap_or(false);
|
||||
if is_gen {
|
||||
Some(target)
|
||||
@@ -932,8 +923,9 @@ mod tests {
|
||||
// ── R1 check_generating_reject 路径测试(隔离 AiSession,绕开 Tauri State 依赖) ──
|
||||
//
|
||||
// check_generating_reject 需 `State<AppState>`,单元测试难构造。
|
||||
// 改测 R1 核心判定逻辑:用 AiSession + PerConvState 模拟 generating 状态,
|
||||
// 复核 conv_read + CONV_STATE_ENABLED 口径对齐 ai_is_generating。
|
||||
// 改测 R1 核心判定逻辑:用 AiSession + PerConvState 模拟生成态,
|
||||
// 复核 conv_read + conv_state.is_active() 口径对齐 ai_is_generating。
|
||||
// (批3 收口后单一 enum 路径,开关 + generating bool 已退役。)
|
||||
|
||||
/// R1 判定:无目标 conv 且无 active → 放行(返 None)。
|
||||
#[test]
|
||||
@@ -950,108 +942,89 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// R1 判定:目标 conv 未在跑 → 放行(generating=false)。
|
||||
/// R1 判定:目标 conv 未在跑 → 放行(conv_state=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.generating = false;
|
||||
// 读 generating 口径(对齐 check_generating_reject / ai_is_generating)
|
||||
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| {
|
||||
if CONV_STATE_ENABLED {
|
||||
c.conv_state.is_active()
|
||||
} else {
|
||||
c.generating
|
||||
}
|
||||
})
|
||||
.map(|c| c.conv_state.is_active())
|
||||
.unwrap_or(false);
|
||||
assert!(!is_gen, "conv-idle 未在跑,is_gen 应为 false(放行)");
|
||||
assert!(!is_gen, "conv-idle Idle 未在跑,is_gen 应为 false(放行)");
|
||||
}
|
||||
|
||||
/// R1 判定:目标 conv 在跑 → 拒绝(generating=true)。
|
||||
/// R1 判定:目标 conv 在跑 → 拒绝(conv_state=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");
|
||||
// 双轨一致:CONV_STATE_ENABLED on 时 R1 读 conv_state.is_active()(非 generating bool),
|
||||
// 须同步设 conv_state=Generating(对齐 test_r1_conv_state_active_reject 设态范式)
|
||||
// 批3 收口:generating bool 已退役,生成态经 conv_state=Generating 表达(单一真相源)。
|
||||
conv.conv_state = ConvState::Generating;
|
||||
conv.generating = true;
|
||||
let is_gen = session
|
||||
.conv_read("conv-busy")
|
||||
.map(|c| {
|
||||
if CONV_STATE_ENABLED {
|
||||
c.conv_state.is_active()
|
||||
} else {
|
||||
c.generating
|
||||
}
|
||||
})
|
||||
.map(|c| c.conv_state.is_active())
|
||||
.unwrap_or(false);
|
||||
assert!(is_gen, "conv-busy generating=true 应拒绝");
|
||||
assert!(is_gen, "conv-busy Generating 应拒绝");
|
||||
}
|
||||
|
||||
/// R1 判定:conv_state 派生态(Generating)is_active()=true(双轨 on 路径)。
|
||||
/// R1 判定:conv_state 派生态(Generating)is_active()=true(单一 enum 路径)。
|
||||
///
|
||||
/// 验证 CONV_STATE_ENABLED on 时读 conv_state.is_active() 而非 generating bool ——
|
||||
/// 即便 generating bool 误为 false 但 conv_state=Generating,仍应判定活跃拒绝。
|
||||
/// 验证批3 收口后读 conv_state.is_active() 判定活跃拒绝(enum 唯一真相源)。
|
||||
#[test]
|
||||
fn test_r1_conv_state_active_reject() {
|
||||
use crate::commands::ai::agentic::conv_state::ConvState;
|
||||
// CONV_STATE_ENABLED 是编译期 const(true),本测试假定 on 路径。
|
||||
// 若未来开关改 runtime,此测试需条件跳过 on 时才跑。
|
||||
// 批3 收口:开关已退役,enum 单一真相源,无条件 on 路径。
|
||||
let mut session = AiSession::new();
|
||||
let conv = session.conv("conv-state-busy");
|
||||
// 模拟状态机写收敛路径:conv_state=Generating,generating bool 同步 true(双轨一致)
|
||||
// 模拟状态机写收敛路径:conv_state=Generating(单一真相源,无 generating bool 双轨)。
|
||||
conv.conv_state = ConvState::Generating;
|
||||
conv.generating = true;
|
||||
let is_gen = session
|
||||
.conv_read("conv-state-busy")
|
||||
.map(|c| {
|
||||
if CONV_STATE_ENABLED {
|
||||
c.conv_state.is_active()
|
||||
} else {
|
||||
c.generating
|
||||
}
|
||||
})
|
||||
.map(|c| c.conv_state.is_active())
|
||||
.unwrap_or(false);
|
||||
assert!(
|
||||
is_gen,
|
||||
"conv_state=Generating is_active() 应为 true(双轨 on 路径拒绝)"
|
||||
"conv_state=Generating 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());
|
||||
// active 在跑,但入参 conv-other 未跑 → 入参优先,应放行
|
||||
session.conv("conv-active").generating = true;
|
||||
let _ = session.conv("conv-other"); // 创建但不 generating
|
||||
// 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.generating)
|
||||
.map(|c| c.conv_state.is_active())
|
||||
.unwrap_or(false);
|
||||
let other_gen = session
|
||||
.conv_read("conv-other")
|
||||
.map(|c| c.generating)
|
||||
.map(|c| c.conv_state.is_active())
|
||||
.unwrap_or(false);
|
||||
assert!(active_gen, "conv-active 在跑");
|
||||
assert!(!other_gen, "conv-other 未跑");
|
||||
// 入参 conv-other 优先 → 判定 conv-other.generating=false → 放行
|
||||
// 入参 conv-other 优先 → 判定 conv-other Idle → 放行
|
||||
// (与 check_generating_reject 入参优先逻辑一致)
|
||||
}
|
||||
|
||||
// ── PerConvState 初值对齐(确保 R1 测试基线 generating=false) ──
|
||||
// ── PerConvState 初值对齐(确保 R1 测试基线 conv_state=Idle) ──
|
||||
|
||||
/// PerConvState::new() generating 初值 false(R1 放行基线)。
|
||||
/// PerConvState::new() conv_state 初值 Idle(R1 放行基线)。
|
||||
#[test]
|
||||
fn test_per_conv_state_generating_default_false() {
|
||||
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!(!s.generating, "PerConvState::new generating 初值应为 false");
|
||||
assert_eq!(s.conv_state, ConvState::Idle, "PerConvState::new conv_state 初值应为 Idle");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user