重构: F-09 batch4 启用多会话上线(决策e真并发·F-09完整)

- IPC签名加conv_id: ai_is_generating/ai_chat_send/ai_chat_force_send/ai_chat_stop + 内部移除active一致性校验(后台conv可操作)
- 删switch readonly分支 + 删ai_conversation_create强制结束旧loop(决策e,切走旧loop不退出)
- 删双写桥接+顶层字段: messages/generating/stop_flag/notify/iteration_used/agent_language/model_override/session_trust全删,per_conv唯一真相源 + 删双写代码(全模块)+ 删fallback读 + pending_approvals retain目标conv
- 前端: api/ai.ts+useAiSend/useAiWindow传conv_id
保留: session_state(SW预留标allow)/readonly(前端不读);零调用方预留按用户指导保留
主代兜底: cargo check --workspace 0 + vue-tsc 0 + test 98 + grep顶层删除/per_conv唯一印证
F-09完整(batch1 PerConvState+batch2迁移+batch5限流调整+batch8恢复+batch4上线);双会话人工验收留用户
This commit is contained in:
2026-06-19 03:03:48 +08:00
parent 2b40f65ef2
commit d899c58682
10 changed files with 345 additions and 522 deletions

View File

@@ -279,12 +279,15 @@ fn workspace_root_str() -> String {
use std::path::PathBuf;
/// AI 会话内状态Mutex 保护)
///
/// F-260616-09 B 批4(决策 e 真并发上线):会话级状态全部迁入 [`per_conv`](Self::per_conv)
/// HashMap(按 conversation_id 切分),顶层单例字段(messages/generating/stop_flag/notify/
/// iteration_used/agent_language/model_override/session_trust)已退役删除。批2 双写桥接代码
/// (IPC 写路径的顶层双写行 + 各文件 fallback 读)在批4 全部清理完毕,per_conv 是唯一真相源。
pub struct AiSession {
/// 对话历史ContextManager唯一消息真相源裁剪仅影响发送视图不影响持久化
pub messages: ContextManager,
/// 当前提供商 ID
pub active_provider_id: Option<String>,
/// 当前活跃对话 ID
/// 当前活跃对话 ID(应用级路由键:当前展示的 conv;切走即变,不影响后台 loop)
pub active_conversation_id: Option<String>,
/// 活跃对话创建时间(懒创建:首条消息落库前仅存内存,upsert 时用作 created_at)
pub active_conv_created_at: Option<String>,
@@ -300,111 +303,51 @@ pub struct AiSession {
/// **未来**:若多会话并发、审批量显著增大,可考虑加二级索引
/// `conversation_id → Vec<tool_call_id>`)把按会话过滤降到 O(1)。
pub pending_approvals: HashMap<String, PendingApproval>,
/// 是否正在生成
pub generating: bool,
/// 当前 agent 循环的语言设置(用于审批后恢复循环)
pub agent_language: Option<String>,
/// 停止信号ai_chat_stop 置位agentic loop / stream_llm 检测后尽快退出
pub stop_flag: Arc<AtomicBool>,
/// 即时停止唤醒B-260615-14stream_llm 阻塞在 `stream.next()` 等 chunk 时,
/// `notify_one()` 立即唤醒跳出 select!,不再等 30s 心跳 tick 轮询。
/// F-260616-09 B 阶段 多会话并发架构 — 会话级状态按 conversation_id 切分。
///
/// 语义边界Notify 仅承载「即时唤醒」这一职责——停止的真值仍由 `stop_flag`
/// AtomicBool决定。`ai_chat_stop` 置 `stop_flag=true` 后调 `notify_one()`
/// 被唤醒的 select! 分支再判 `stop_flag` 决定退出或继续,避免 Notify 的通知
/// 与「真的该停」耦合(防误唤醒继续跑)。
///
/// `Arc<Notify>` 可 CloneNotify 内部用 Atomic 计数Clone 等价于 Arc 引用 +1
/// 不影响 AiSession 其他字段语义。
pub notify: Arc<tokio::sync::Notify>,
/// agentic loop 累计已跑 iteration 计数F-260616-11 决策 a
///
/// **语义**「下一轮起算值」。run_agentic_loop 每轮写 `iteration + 1`,审批等待/
/// 达 max 暂停退出时即「当前轮+1」。续跑时由调用方决定如何接
/// - **审批续跑**ai_approve传本字段作 start_iteration**累计**,防多次审批反复
/// 跑满 max_iterations 致 token 失控);
/// - **达 max 续跑**ai_continue_loop重置本字段=0 + 传 0**重计**F-260616-03
/// 决策 a用户点继续=授权重来);
/// - **新消息首次发送**ai_chat_send重置本字段=0新对话生命周期从头计数
///
/// 单例一份:当前 AiSession 是全局单例F-09 B 多会话架构落地时改 per-conv
pub iteration_used: usize,
/// F-01 阶段6: 用户指定模型 override(主对话专用)。
///
/// **语义**:前端顶部下拉「指定」模式选的 model_id;None/空=自动模式(路由器选)。
/// 仅主对话(agentic)生效——标题/扫描/灵感等内部调用仍走路由,不读此字段。
///
/// **兜底原则**(agentic.rs run_agentic_loop 内强制):override 非空且在该 provider
/// model_configs 池中 → 用 override;否则用 resolved_model(路由结果)。绝不让
/// override 导致无模型。
///
/// **生命周期**:ai_chat_send/regenerate/edit 写入(随发消息带);新对话/会话切换时清空
/// (防上一对话 override 残留污染新对话)。try_continue_agent_loop 读此字段透传给续跑
/// loop(审批续跑/达 max 续跑保持 override 一致,语义=同一主对话)。
pub model_override: Option<String>,
/// AE-2025-04 会话级信任Session Trust内存态随会话销毁不落库。
/// 同会话已批准过同类操作(同工具+同目录 TrustKey 命中)后自动放行,跳过 pending 审批 +
/// 二次确认AE-05。换会话new/switch必须清空重审见 ai_conversation_create /
/// ai_conversation_switch 清字段)。
pub session_trust: HashSet<TrustKey>,
/// F-260616-09 B 阶段批1 多会话并发架构:会话级状态按 conversation_id 切分。
///
/// **批2 状态(per-conv 是新真相源)**:批2 已迁移所有调用方(agentic.rs loop +
/// **批4 状态(per_conv 唯一真相源)**:批2-批4 已迁移所有调用方(agentic.rs loop +
/// GeneratingGuard + try_continue + commands.rs IPC 写路径 + audit.rs process_tool_calls +
/// conversation.rs save + title.rs + knowledge_inject.rs + lib.rs L0)读写 `session.conv(conv_id).*`
/// / `session.conv_read(conv_id)`。顶层单例字段(messages/generating/stop_flag 等)批2 期间
/// 作双写桥接保留(IPC 仍双写顶层保 ai_is_generating 等读路径兼容,批4 IPC 迁移后顶层移除)。
/// conversation.rs save + title.rs + knowledge_inject.rs + lib.rs L0)读写
/// `session.conv(conv_id).*` / `session.conv_read(conv_id)`。顶层单例会话级字段已全部删除。
pub per_conv: HashMap<String, PerConvState>,
}
impl AiSession {
pub fn new() -> Self {
Self {
messages: ContextManager::new(ContextConfig::default()),
active_provider_id: None,
active_conversation_id: None,
active_conv_created_at: None,
pending_approvals: HashMap::new(),
generating: false,
agent_language: None,
stop_flag: Arc::new(AtomicBool::new(false)),
notify: Arc::new(tokio::sync::Notify::new()),
iteration_used: 0,
model_override: None,
session_trust: HashSet::new(),
per_conv: HashMap::new(),
}
}
/// 取即时停止唤醒器引用B-260615-14)。
/// 推导指定会话读侧状态视图(只读,不写任何字段)。
///
/// 供 `stream_llm`(接 `&Notify` 进 select! 监听 `notified()`)与 `ai_chat_stop`
/// (置 `stop_flag` 后调 `notify_one()`)等外部持有方获取同一 Notify 实例
/// 返回 `&Notify` 而非 clone避免不必要的 Arc 引用计数调整。
pub fn stop_notify(&self) -> &tokio::sync::Notify {
&self.notify
}
/// 推导会话读侧状态视图(只读,不写任何字段)。
/// 优先级:该 conv 有 pending 审批 → [`SessionState::AwaitingApproval`]
/// 否则 per_conv.generating → [`SessionState::Streaming`];否则 → [`SessionState::Idle`]
///
/// 优先级:`pending_approvals` 非空 → [`SessionState::AwaitingApproval`]
/// 否则 `generating` → [`SessionState::Streaming`];否则 → [`SessionState::Idle`]
/// 收敛点:调用方读「某会话处于什么阶段」时统一走本方法,替代散布的
/// `if pending / if generating` 三路组合
///
/// 收敛点:调用方读「会话处于什么阶段」时统一走本方法,替代散布的
/// `if !pending_approvals.is_empty() / if generating` 三路组合
/// F-260616-09 B 批4:per_conv 化后接 conv_id 参数(顶层 generating 已删除);
/// pending 判断改为按 conv_id 过滤(决策 e 真并发下各 conv 独立)
///
/// # 待替换调用点(本次仅新增视图,不替换调用点)
///
/// - `agentic.rs:283` — agent loop 入口/恢复处对 generating + 审批的组合判断
/// - `commands.rs:250` — `ai_pending_tool_calls` 等读取前的状态门控
/// - `commands.rs:451` — 审批提交/会话复位路径的状态判断
/// - `agentic.rs` — agent loop 入口/恢复处对 generating + 审批的组合判断
/// - `commands.rs` — `ai_pending_tool_calls` 等读取前的状态门控
/// - `commands.rs` — 审批提交/会话复位路径的状态判断
///
/// 上述三处替换由 P0 任务承接,本方法先就位供其切换。
#[allow(dead_code)] // SW-260618-21 b:预留(P0 终态化任务承接·当前 0 调用·保留扩展点)
pub fn session_state(&self) -> SessionState {
if !self.pending_approvals.is_empty() {
pub fn session_state(&self, conv_id: &str) -> SessionState {
let has_pending = self.pending_approvals.values()
.any(|a| a.conversation_id.as_deref() == Some(conv_id));
if has_pending {
SessionState::AwaitingApproval
} else if self.generating {
} else if self.conv_read(conv_id).map(|c| c.generating).unwrap_or(false) {
SessionState::Streaming
} else {
SessionState::Idle