重构: crud.rs按表拆分(SMELL-P1-9) + F-09批1 PerConvState数据结构

- SMELL-P1-9: crud.rs 2212行→crud/6文件(mod/settings/project_repo/task_repo/conversation_repo/idea_repo)
  re-export pub use *_repo::* 零调用方改动,宏 pub(crate) use + 子模块 use super::impl_repo
  基线测试锁12表白名单+13Repo构造
- F-09 批1: PerConvState struct(9字段对齐AiSession::new)+AiSession.per_conv HashMap+conv()/conv_read()访问器+3单测
  纯新增无行为变化,b-1主代自主裁决采纳,批2迁移承接
主代统一兜底: cargo check --workspace 0 + df-storage 35+11 + devflow 96 passed
This commit is contained in:
2026-06-19 01:34:59 +08:00
parent d817386418
commit 2c8764abee
11 changed files with 2592 additions and 2214 deletions

View File

@@ -347,6 +347,13 @@ pub struct AiSession {
/// 二次确认AE-05。换会话new/switch必须清空重审见 ai_conversation_create /
/// ai_conversation_switch 清字段)。
pub session_trust: HashSet<TrustKey>,
/// F-260616-09 B 阶段批1 多会话并发架构:会话级状态按 conversation_id 切分。
///
/// **批1 状态(纯新增无行为变化)**:此 HashMap 初始空,批1 不迁移任何调用方 ——
/// 现有所有路径继续读写顶层单例字段(messages/generating/stop_flag 等),完全无感。
/// 批2 承接迁移:调用方改走 `session.conv(conv_id).*`,迁移完成后顶层单例字段废弃删除。
#[allow(dead_code)] // F-09 B 批1 共存期:批2 迁移承接(0 写入方)
pub per_conv: HashMap<String, PerConvState>,
}
impl AiSession {
@@ -364,6 +371,7 @@ impl AiSession {
iteration_used: 0,
model_override: None,
session_trust: HashSet::new(),
per_conv: HashMap::new(),
}
}
@@ -401,6 +409,194 @@ impl AiSession {
SessionState::Idle
}
}
// ============================================================
// F-260616-09 B 阶段批1 PerConvState 访问器(设计 §2.1.2)
//
// 批1 仅定义访问器,**不迁移调用方**(批2 承接)。当前 0 调用方,行为完全不变。
// 批2 迁移:所有 `session.messages` / `session.generating` 等读写改走
// `session.conv(conv_id).*`(写)/ `session.conv_read(conv_id)`(读)。
// ============================================================
/// 取或惰性创建指定会话的可变状态(设计 §2.1.2)。
///
/// 不存在则 `entry().or_insert_with(PerConvState::new)` 创建一份默认状态,
/// 初值对齐 [`AiSession::new`] / [`PerConvState::new`]。
/// 已存在则返回同一实例引用(同 conv_id 复用)。
///
/// **批1 状态**:0 调用方(批2 迁移承接)。行为不变。
#[allow(dead_code)] // F-09 B 批1 共存期:批2 迁移承接(0 调用方)
pub fn conv(&mut self, conv_id: &str) -> &mut PerConvState {
self.per_conv
.entry(conv_id.to_string())
.or_insert_with(PerConvState::new)
}
/// 只读取指定会话状态(不创建)。
///
/// 未创建的 conv_id 返 `None`,已创建返 `Some(&PerConvState)`。用于读取路径(如
/// 列举/查询)避免误触发惰性创建。
///
/// **批1 状态**:0 调用方(批2 迁移承接)。行为不变。
#[allow(dead_code)] // F-09 B 批1 共存期:批2 迁移承接(0 调用方)
pub fn conv_read(&self, conv_id: &str) -> Option<&PerConvState> {
self.per_conv.get(conv_id)
}
}
#[cfg(test)]
mod tests_f09_per_conv {
use super::*;
/// 验证 PerConvState::new() 各字段初值正确(对齐 AiSession::new 基线)。
#[test]
fn test_per_conv_state_new() {
let s = PerConvState::new();
assert!(!s.generating, "generating 初值应为 false");
assert_eq!(s.iteration_used, 0, "iteration_used 初值应为 0");
assert!(s.agent_language.is_none(), "agent_language 初值应为 None");
assert!(s.model_override.is_none(), "model_override 初值应为 None");
assert!(s.session_trust.is_empty(), "session_trust 初值应为空 HashSet");
assert!(s.created_at.is_none(), "created_at 初值应为 None");
// stop_flag 初值 false(与 AiSession::new 对齐)
assert!(
!s.stop_flag.load(std::sync::atomic::Ordering::SeqCst),
"stop_flag 初值应为 false"
);
// notify 是 Arc<Notify>,无可见状态,仅验证不 panic(构造即视为对齐)
// messages 是 ContextManager,内部状态;仅验证能取到引用(初值对齐由 ContextManager::new 保证)
let _messages_ref: &ContextManager = &s.messages;
}
/// 验证 conv() 首次创建 + 二次复用同一实例。
#[test]
fn test_conv_lazy_create() {
let mut session = AiSession::new();
assert!(session.per_conv.is_empty(), "新建 session 的 per_conv 应为空");
// 首次 conv() 创建
let ptr_first = {
let s = session.conv("conv-a");
assert!(!s.generating);
assert_eq!(s.iteration_used, 0);
s as *const PerConvState
};
assert_eq!(session.per_conv.len(), 1, "首次 conv() 后 per_conv 应有 1 项");
// 二次 conv(同 conv_id) 返回同一实例(指针相等)
let ptr_second = session.conv("conv-a") as *const PerConvState;
assert_eq!(
ptr_first, ptr_second,
"同 conv_id 二次 conv() 应返回同一实例"
);
assert_eq!(session.per_conv.len(), 1, "二次 conv(同 id) 不应新增项");
// 不同 conv_id 创建独立实例
let _s_b = session.conv("conv-b");
assert_eq!(session.per_conv.len(), 2, "不同 conv_id 应新建独立实例");
}
/// 验证 conv_read() 未创建的 conv_id 返 None(只读,不惰性创建)。
#[test]
fn test_conv_read_none() {
let session = AiSession::new();
// 未创建的 conv_id
assert!(
session.conv_read("never-created").is_none(),
"未创建的 conv_id conv_read() 应返 None"
);
// conv_read 不触发创建(per_conv 仍空)
assert!(
session.per_conv.is_empty(),
"conv_read 不应触发惰性创建"
);
// 已创建的 conv_id 返 Some(只读访问)
let mut session = session;
let _ = session.conv("exists");
assert!(
session.conv_read("exists").is_some(),
"已创建的 conv_id conv_read() 应返 Some"
);
}
}
// ============================================================
// F-260616-09 B 阶段 多会话并发架构 — PerConvState(批1 纯新增)
//
// 设计文档: docs/02-架构设计/F-09-多会话并发架构设计-2026-06-19.md §2.1.2
//
// 批1 范围(纯新增无行为变化):
// - 仅新增 PerConvState struct + new() + AiSession.per_conv 字段 + conv()/conv_read() 访问器
// - **不迁移**现有单例字段(messages/generating/stop_flag/notify/iteration_used/
// agent_language/model_override/session_trust 保留原样),不迁移现有调用方(批2 承接)
// - per_conv HashMap 初始空,旧调用路径继续读写 AiSession 顶层单例字段,完全无感
//
// 共存期:批1 后顶层单例字段(唯一真相源)与 per_conv(全空,无写入)并存,行为不变。
// 批2 迁移:把所有调用方从 session.messages / session.generating 等改为 session.conv(conv_id).*,
// 迁移完成后顶层单例字段废弃删除(批3)。
// ============================================================
/// 单会话级状态(F-260616-09 B 阶段批1 纯新增,设计 §2.1.2)。
///
/// 持有当前会话(按 conversation_id 切分)私有的可变状态。批1 仅定义 + 惰性创建访问器,
/// **不迁移** AiSession 顶层单例字段(共存期顶层字段仍是唯一真相源)。
///
/// 字段初值逐字对齐 [`AiSession::new`](#method.new),保证批2 迁移调用方时行为不变。
#[allow(dead_code)] // F-09 B 批1 共存期:0 调用方(批2 迁移承接),字段待迁移后消费
pub struct PerConvState {
/// 对话历史(ContextManager:会话级消息真相源,裁剪仅影响发送视图)
pub messages: ContextManager,
/// 是否正在生成
pub generating: bool,
/// 停止信号(会话级):ai_chat_stop 置位,agentic loop / stream_llm 检测后尽快退出
pub stop_flag: Arc<AtomicBool>,
/// 即时停止唤醒(会话级):阻塞在 stream.next() 时 notify_one() 立即唤醒跳出 select!
pub notify: Arc<tokio::sync::Notify>,
/// agentic loop 累计已跑 iteration 计数(语义同 AiSession.iteration_used)
pub iteration_used: usize,
/// 当前 agent 循环的语言设置(用于审批后恢复循环)
pub agent_language: Option<String>,
/// 用户指定模型 override(主对话专用,语义同 AiSession.model_override)
pub model_override: Option<String>,
/// 会话级信任(Session Trust):随会话销毁,不落库
pub session_trust: HashSet<TrustKey>,
/// 会话创建时间(懒创建:首条消息落库前仅存内存,upsert 时用作 created_at)
pub created_at: Option<String>,
}
impl PerConvState {
/// 新建默认会话级状态。
///
/// 各字段初值**逐字对齐 [`AiSession::new`]** —— 批2 迁移调用方时行为不变。
/// - messages: ContextManager::new(ContextConfig::default())
/// - generating: false
/// - stop_flag: Arc::new(AtomicBool::new(false))
/// - notify: Arc::new(tokio::sync::Notify::new())
/// - iteration_used: 0
/// - agent_language: None
/// - model_override: None
/// - session_trust: HashSet::new()
/// - created_at: None(批1 新增字段,AiSession 现有 active_conv_created_at 同语义)
pub fn new() -> Self {
Self {
messages: ContextManager::new(ContextConfig::default()),
generating: false,
stop_flag: Arc::new(AtomicBool::new(false)),
notify: Arc::new(tokio::sync::Notify::new()),
iteration_used: 0,
agent_language: None,
model_override: None,
session_trust: HashSet::new(),
created_at: None,
}
}
}
impl Default for PerConvState {
fn default() -> Self {
Self::new()
}
}
/// 待审批的工具调用