重构: 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:
lxy
2026-06-25 03:20:42 +08:00
parent fc705443bd
commit c011f864fd
22 changed files with 526 additions and 428 deletions
+43 -70
View File
@@ -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");
}
}