添加qiming-rcoder模块
This commit is contained in:
191
qiming-rcoder/crates/agent_abstraction/src/acp/connection.rs
Normal file
191
qiming-rcoder/crates/agent_abstraction/src/acp/connection.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
//! Agent connection wrapper for ACP protocol.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol::schema::{CancelNotification, PromptRequest, SessionId};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
// 重新导出 shared_types 中的统一类型
|
||||
pub use shared_types::{CancelNotificationRequestWrapper, CancelResult};
|
||||
|
||||
/// Agent connection wrapper
|
||||
#[derive(Debug)]
|
||||
pub struct AgentConnection {
|
||||
/// Project ID (业务主键)
|
||||
/// project_id 是业务的唯一标识符,用于关联项目和对应的 agent 实例
|
||||
/// 可以根据 project_id 找到对应的 session_id
|
||||
pub project_id: String,
|
||||
|
||||
/// Service Type (服务类型)
|
||||
/// 用于区分不同类型的服务,对应不同的 Docker 镜像和运行环境
|
||||
/// 当前所有业务都使用 ServiceType::RCoder
|
||||
pub service_type: shared_types::ServiceType,
|
||||
|
||||
/// Session ID (可空)
|
||||
/// session_id 是 ACP 协议中的会话标识符,在 agent 的 newSession 成功后由 agent 返回
|
||||
/// 创建 agent 时可能没有 session_id,需要在会话建立后更新
|
||||
pub session_id: Option<SessionId>,
|
||||
|
||||
/// Prompt sender channel
|
||||
pub prompt_tx: Arc<mpsc::UnboundedSender<PromptRequest>>,
|
||||
/// Cancel sender channel - wrapped in Arc to avoid Debug requirement
|
||||
pub cancel_tx: Arc<mpsc::UnboundedSender<CancelNotificationRequestWrapper>>,
|
||||
}
|
||||
|
||||
impl AgentConnection {
|
||||
/// Create a new agent connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `project_id` - 业务主键,唯一标识项目
|
||||
/// * `service_type` - 服务类型,对应不同的 Docker 镜像和运行环境
|
||||
/// * `session_id` - ACP 会话 ID(可空,创建时可能没有)
|
||||
/// * `prompt_tx` - 提示消息发送通道
|
||||
/// * `cancel_tx` - 取消消息发送通道
|
||||
pub fn new(
|
||||
project_id: String,
|
||||
service_type: shared_types::ServiceType,
|
||||
session_id: Option<SessionId>,
|
||||
prompt_tx: Arc<mpsc::UnboundedSender<PromptRequest>>,
|
||||
cancel_tx: Arc<mpsc::UnboundedSender<CancelNotificationRequestWrapper>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
service_type,
|
||||
session_id,
|
||||
prompt_tx,
|
||||
cancel_tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get project ID (业务主键)
|
||||
pub fn project_id(&self) -> &str {
|
||||
&self.project_id
|
||||
}
|
||||
|
||||
/// Get session ID (可空)
|
||||
pub fn session_id(&self) -> Option<&SessionId> {
|
||||
self.session_id.as_ref()
|
||||
}
|
||||
|
||||
/// Update session ID after successful newSession
|
||||
/// 在 agent 的 newSession 成功后调用,更新 session_id
|
||||
pub fn set_session_id(&mut self, session_id: SessionId) {
|
||||
self.session_id = Some(session_id);
|
||||
}
|
||||
|
||||
/// Check if session is ready (has session_id)
|
||||
pub fn has_session(&self) -> bool {
|
||||
self.session_id.is_some()
|
||||
}
|
||||
|
||||
/// Send prompt
|
||||
///
|
||||
/// 通过 channel 发送 prompt 到 LocalSet 中运行的 agent
|
||||
/// session_id 由服务端自动管理,调用方无需关心
|
||||
///
|
||||
/// 设计说明:
|
||||
/// - SACP 版本支持 Send trait,可以在 tokio::spawn 中运行
|
||||
/// - 使用 MPSC channel 解耦调用方和 agent 运行环境
|
||||
/// - newSession 在 agent 启动后自动执行
|
||||
/// - prompt handler 会自动处理 session_id 的覆盖
|
||||
pub async fn send_prompt(
|
||||
&self,
|
||||
prompt: PromptRequest,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.prompt_tx.send(prompt).map_err(|e| {
|
||||
Box::new(std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
as Box<dyn std::error::Error + Send + Sync>
|
||||
})
|
||||
}
|
||||
|
||||
/// Send cancel
|
||||
///
|
||||
/// 通过 channel 发送取消请求到 LocalSet 中运行的 agent
|
||||
/// 返回一个 receiver,调用方可以通过它等待取消结果
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(receiver)`: 请求已发送,通过 receiver.await 获取取消结果
|
||||
/// - `Err`: 发送请求失败(channel 已关闭)
|
||||
pub async fn send_cancel(
|
||||
&self,
|
||||
cancel_notification: CancelNotification,
|
||||
) -> Result<oneshot::Receiver<CancelResult>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let (result_tx, result_rx) = oneshot::channel();
|
||||
|
||||
self.cancel_tx
|
||||
.send(CancelNotificationRequestWrapper {
|
||||
cancel_notification,
|
||||
result_tx,
|
||||
})
|
||||
.map_err(|e| {
|
||||
Box::new(std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
as Box<dyn std::error::Error + Send + Sync>
|
||||
})?;
|
||||
|
||||
Ok(result_rx)
|
||||
}
|
||||
|
||||
/// Send cancel and wait for result
|
||||
///
|
||||
/// 发送取消请求并等待结果
|
||||
pub async fn send_cancel_and_wait(
|
||||
&self,
|
||||
cancel_notification: CancelNotification,
|
||||
) -> Result<CancelResult, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let result_rx = self.send_cancel(cancel_notification).await?;
|
||||
|
||||
result_rx.await.map_err(|e| {
|
||||
Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to receive cancel result: {}", e),
|
||||
)) as Box<dyn std::error::Error + Send + Sync>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Agent status type (re-exported from shared_types)
|
||||
pub type AgentStatus = shared_types::AgentStatus;
|
||||
|
||||
/// Connection status enum
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ConnectionStatus {
|
||||
/// Connection is being established
|
||||
Connecting = 1,
|
||||
/// Connection is active
|
||||
Connected = 2,
|
||||
/// Connection is idle
|
||||
Idle = 3,
|
||||
/// Connection has an error
|
||||
Error = 4,
|
||||
/// Connection is closed
|
||||
Closed = 5,
|
||||
}
|
||||
|
||||
impl ConnectionStatus {
|
||||
/// Convert from u8
|
||||
pub fn from_u8(value: u8) -> Self {
|
||||
match value {
|
||||
1 => ConnectionStatus::Connecting,
|
||||
2 => ConnectionStatus::Connected,
|
||||
3 => ConnectionStatus::Idle,
|
||||
4 => ConnectionStatus::Error,
|
||||
5 => ConnectionStatus::Closed,
|
||||
_ => ConnectionStatus::Closed,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to u8
|
||||
pub fn to_u8(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection statistics
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConnectionStats {
|
||||
pub total_connections: u64,
|
||||
pub active_connections: u64,
|
||||
pub idle_connections: u64,
|
||||
pub error_connections: u64,
|
||||
}
|
||||
20
qiming-rcoder/crates/agent_abstraction/src/acp/mod.rs
Normal file
20
qiming-rcoder/crates/agent_abstraction/src/acp/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
//! ACP connection management module.
|
||||
|
||||
mod connection;
|
||||
|
||||
pub use connection::{
|
||||
AgentConnection, AgentStatus, CancelNotificationRequestWrapper, CancelResult, ConnectionStats,
|
||||
ConnectionStatus,
|
||||
};
|
||||
|
||||
/// Legacy type alias for backward compatibility
|
||||
pub type Connection = AgentConnection;
|
||||
|
||||
/// Placeholder error type
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AcpError {
|
||||
#[error("Connection error: {0}")]
|
||||
Connection(String),
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
31
qiming-rcoder/crates/agent_abstraction/src/error.rs
Normal file
31
qiming-rcoder/crates/agent_abstraction/src/error.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
//! Agent abstraction layer error types
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Agent abstraction layer error
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AgentAbstractionError {
|
||||
#[error("connection error: {0}")]
|
||||
Connection(String),
|
||||
|
||||
#[error("registry error: {0}")]
|
||||
Registry(String),
|
||||
|
||||
#[error("process error: {0}")]
|
||||
Process(String),
|
||||
|
||||
#[error("other error: {0}")]
|
||||
Other(String),
|
||||
|
||||
#[error("serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("type cast error")]
|
||||
Cast,
|
||||
|
||||
#[error("not found: {0}")]
|
||||
NotFound(String),
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
618
qiming-rcoder/crates/agent_abstraction/src/launcher/lifecycle.rs
Normal file
618
qiming-rcoder/crates/agent_abstraction/src/launcher/lifecycle.rs
Normal file
@@ -0,0 +1,618 @@
|
||||
//! Agent生命周期管理
|
||||
//!
|
||||
//! 基于RAII原则的简洁生命周期管理设计
|
||||
//!
|
||||
//! ## 僵尸进程问题解决方案
|
||||
//!
|
||||
//! 核心问题:Drop trait 是同步的,无法 await child.wait()
|
||||
//!
|
||||
//! 解决方案:
|
||||
//! 1. **后台回收任务**:立即启动后台任务 wait() 子进程
|
||||
//! 2. **进程组终止**:使用 nix::kill 发送信号到进程组
|
||||
//! 3. **三重保障**:PID 1 的 process_reaper 模块兜底
|
||||
//!
|
||||
//! ## 进程组说明
|
||||
//!
|
||||
//! 使用 `process-wrap` crate 创建真正的进程组:
|
||||
//! - 启动时使用 `ProcessGroup::leader()` 创建进程组
|
||||
//! - 终止时发送 `kill(-pgid, SIGKILL)` 到整个进程组
|
||||
//! - 能够正确清理子进程及其所有孙进程
|
||||
|
||||
use anyhow::Result;
|
||||
use dashmap::DashMap;
|
||||
use process_wrap::tokio::ChildWrapper;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use agent_client_protocol::schema::SessionId;
|
||||
use shared_types::{AgentLifecycle, ModelProviderConfig};
|
||||
|
||||
/// Agent生命周期守卫
|
||||
///
|
||||
/// 遵循RAII原则,当守卫被drop时自动清理agent资源
|
||||
///
|
||||
/// ## 僵尸进程避免机制
|
||||
///
|
||||
/// 1. **后台回收任务**:构造时立即启动 tokio::spawn 等待子进程
|
||||
/// 2. **进程组终止**:Drop 时发送信号到进程组(使用 nix::kill)
|
||||
/// 3. **PID 1 兜底**:process_reaper 模块自动回收所有孤儿进程
|
||||
///
|
||||
/// ## 进程组信号
|
||||
///
|
||||
/// 在 Unix 上,使用负的进程组 ID 发送信号:
|
||||
/// - `kill(-pgid, SIGKILL)` 杀死整个进程组
|
||||
/// - 这会终止子进程及其所有后代(如果子进程创建了真正的进程组)
|
||||
pub struct AgentLifecycleGuard {
|
||||
inner: Arc<AgentLifecycleInner>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AgentLifecycleGuard {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AgentLifecycleGuard")
|
||||
.field("project_id", &self.inner.project_id)
|
||||
.field("session_id", &self.inner.session_id)
|
||||
.field("pgid", &self.inner.pgid)
|
||||
.field("stopped", &self.inner.stopped.load(Ordering::SeqCst))
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
struct AgentLifecycleInner {
|
||||
project_id: String,
|
||||
session_id: SessionId,
|
||||
/// 🔥 进程组 ID(当前实现:使用 child.pid 作为伪进程组)
|
||||
///
|
||||
/// 注意:当前实现使用子进程的 PID 作为 PGID。
|
||||
/// - 如果子进程通过 setsid() 创建了真正的进程组,kill(-pgid) 会杀死整个进程树
|
||||
/// - 如果子进程没有创建进程组,kill(-pgid) 只会杀死子进程本身
|
||||
/// - 未来可以使用 process-wrap 库创建真正的进程组
|
||||
pgid: u32,
|
||||
cancel_token: CancellationToken,
|
||||
resources: AgentResources,
|
||||
stopped: AtomicBool,
|
||||
/// 🔥 共享的 API 密钥管理器引用(用于自动清理)
|
||||
shared_api_key_manager: Option<Arc<DashMap<String, ModelProviderConfig>>>,
|
||||
/// 🔥 project_id -> service_uuid 映射(用于清理时查找 UUID)
|
||||
project_uuid_map: Option<Arc<DashMap<String, String>>>,
|
||||
/// 🔥 关联的 service_uuid(用于清理时定位配置)
|
||||
service_uuid: Option<String>,
|
||||
}
|
||||
|
||||
/// Agent资源管理枚举
|
||||
///
|
||||
/// ## 后台回收版本
|
||||
///
|
||||
/// 存储后台任务句柄,确保子进程被 wait() 回收
|
||||
enum AgentResources {
|
||||
Claude {
|
||||
/// stderr 任务句柄
|
||||
stderr_task: Arc<Mutex<Option<JoinHandle<()>>>>,
|
||||
/// 后台回收任务(已启动,会 wait() 子进程)
|
||||
_reaper_task: JoinHandle<()>,
|
||||
},
|
||||
}
|
||||
|
||||
impl AgentLifecycleGuard {
|
||||
/// 为Claude Agent创建生命周期守卫(兼容旧代码,默认无密钥管理器)
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `child_process` - 已启动的子进程(必须是进程组组长)
|
||||
/// * `stderr_task` - stderr 读取任务
|
||||
/// * `cancel_token` - 取消令牌
|
||||
///
|
||||
/// # 僵尸进程避免
|
||||
///
|
||||
/// 此函数会立即启动后台任务等待子进程,确保子进程退出时被回收。
|
||||
pub fn new_claude(
|
||||
project_id: String,
|
||||
session_id: SessionId,
|
||||
child_process: Box<dyn ChildWrapper>,
|
||||
stderr_task: JoinHandle<()>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Self {
|
||||
Self::new_claude_with_key_manager(
|
||||
project_id,
|
||||
session_id,
|
||||
child_process,
|
||||
stderr_task,
|
||||
cancel_token,
|
||||
None, // 默认无密钥管理器
|
||||
None, // 默认无 project_uuid_map
|
||||
None, // 默认无 service_uuid
|
||||
)
|
||||
}
|
||||
|
||||
/// 🔥 新增:带异常退出标志的构造函数
|
||||
///
|
||||
/// 创建生命周期守卫时传入共享的 `abnormal_exit_flag`,当子进程异常退出时设置此标志。
|
||||
/// 这使得 SACP 连接层可以检测到异常退出并发送相应的通知。
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `abnormal_exit_flag` - 共享的原子布尔标志,子进程异常退出时设置为 true
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// 如果子进程 PID 无效(为 0 或 None),此函数会 panic,因为这意味着进程启动失败。
|
||||
pub fn new_claude_with_abnormal_flag(
|
||||
project_id: String,
|
||||
session_id: SessionId,
|
||||
child_process: Box<dyn ChildWrapper>,
|
||||
stderr_task: JoinHandle<()>,
|
||||
cancel_token: CancellationToken,
|
||||
abnormal_exit_flag: Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
Self::new_claude_with_key_manager_and_abnormal_flag(
|
||||
project_id,
|
||||
session_id,
|
||||
child_process,
|
||||
stderr_task,
|
||||
cancel_token,
|
||||
None, // 默认无密钥管理器
|
||||
None, // 默认无 project_uuid_map
|
||||
None, // 默认无 service_uuid
|
||||
Some(abnormal_exit_flag),
|
||||
)
|
||||
}
|
||||
|
||||
/// 🔥 新增:带密钥管理器和异常退出标志的构造函数
|
||||
///
|
||||
/// 创建生命周期守卫时传入共享的 API 密钥管理器和 service_uuid,
|
||||
/// 当 Agent 停止时(Drop)会自动清理对应的 API 密钥配置。
|
||||
///
|
||||
/// # 进程组管理
|
||||
///
|
||||
/// 当前实现使用子进程的 PID 作为 PGID:
|
||||
/// - `pgid = child_pid`(使用子进程 PID 作为进程组 ID)
|
||||
/// - 终止时发送 `kill(-pgid, SIGKILL)` 到进程组
|
||||
/// - 使用 `process-wrap` 创建真正的进程组,能正确清理所有孙进程
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `shared_api_key_manager` - 共享的 DashMap,用于清理 API 密钥配置
|
||||
/// * `project_uuid_map` - project_id -> service_uuid 映射,用于查找 UUID
|
||||
/// * `service_uuid` - 与此 Agent 关联的 service UUID
|
||||
/// * `abnormal_exit_flag` - 共享的原子布尔标志,子进程异常退出时设置为 true
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// 如果子进程 PID 无效(为 0 或 None),此函数会 panic,因为这意味着进程启动失败。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_claude_with_key_manager(
|
||||
project_id: String,
|
||||
session_id: SessionId,
|
||||
child_process: Box<dyn ChildWrapper>,
|
||||
stderr_task: JoinHandle<()>,
|
||||
cancel_token: CancellationToken,
|
||||
shared_api_key_manager: Option<Arc<DashMap<String, ModelProviderConfig>>>,
|
||||
project_uuid_map: Option<Arc<DashMap<String, String>>>,
|
||||
service_uuid: Option<String>,
|
||||
) -> Self {
|
||||
Self::new_claude_with_key_manager_and_abnormal_flag(
|
||||
project_id,
|
||||
session_id,
|
||||
child_process,
|
||||
stderr_task,
|
||||
cancel_token,
|
||||
shared_api_key_manager,
|
||||
project_uuid_map,
|
||||
service_uuid,
|
||||
None, // 默认无异常退出标志
|
||||
)
|
||||
}
|
||||
|
||||
/// 🔥 完整构造函数:带密钥管理器和异常退出标志
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new_claude_with_key_manager_and_abnormal_flag(
|
||||
project_id: String,
|
||||
session_id: SessionId,
|
||||
mut child_process: Box<dyn ChildWrapper>,
|
||||
stderr_task: JoinHandle<()>,
|
||||
cancel_token: CancellationToken,
|
||||
shared_api_key_manager: Option<Arc<DashMap<String, ModelProviderConfig>>>,
|
||||
project_uuid_map: Option<Arc<DashMap<String, String>>>,
|
||||
service_uuid: Option<String>,
|
||||
abnormal_exit_flag: Option<Arc<AtomicBool>>,
|
||||
) -> Self {
|
||||
// 🔥 关键:PID 有效性检查
|
||||
// ChildWrapper 的 id() 返回 Option<u32>,当进程已终止或无效时返回 None
|
||||
// 如果 PID 无效,这是一个严重的初始化错误,应该 panic
|
||||
let pid = child_process.id().unwrap_or_else(|| {
|
||||
panic!(
|
||||
"[LifecycleGuard] 子进程 PID 无效(None),进程可能已终止: project_id={}",
|
||||
project_id
|
||||
)
|
||||
});
|
||||
|
||||
// 🔥 额外检查:PID 不应该为 0
|
||||
// 虽然 id() 返回 Some(0) 理论上可能,但实际上 PID 0 是内核保留的
|
||||
if pid == 0 {
|
||||
panic!(
|
||||
"[LifecycleGuard] 子进程 PID 为 0,这是无效的 PID: project_id={}",
|
||||
project_id
|
||||
);
|
||||
}
|
||||
|
||||
// 🔥 进程组 ID 等于组长进程的 PID
|
||||
// process-wrap 的 ProcessGroup 使用 setpgid(0, 0) 创建新进程组,使进程成为组长
|
||||
let pgid = pid;
|
||||
let project_id_clone = project_id.clone();
|
||||
let session_id_str = session_id.0.to_string();
|
||||
|
||||
// 🔥 关键:立即启动后台回收任务
|
||||
// 这个任务会等待子进程退出,确保不会产生僵尸进程
|
||||
// 当子进程退出时,设置 abnormal_exit_flag 并触发 cancel_token
|
||||
// 让 SACP 连接层检测到并发送 SSE 通知
|
||||
let cancel_token_for_reaper = cancel_token.clone();
|
||||
let abnormal_exit_flag_clone = abnormal_exit_flag.clone();
|
||||
let project_id_for_reaper = project_id.clone();
|
||||
let reaper_task = tokio::spawn(async move {
|
||||
info!(
|
||||
"[ProcessReaper] 开始监控 Agent 进程: project_id={}, pid={}, pgid={}",
|
||||
project_id_for_reaper, pid, pgid
|
||||
);
|
||||
|
||||
// 🔥 优先等待子进程退出,而不是响应取消信号
|
||||
// 这确保了即使收到取消信号,也能正确检测进程是否已退出
|
||||
let wait_result = child_process.wait().await;
|
||||
|
||||
// 检查是否是外部取消(用户主动 stop)
|
||||
let was_cancelled = cancel_token_for_reaper.is_cancelled();
|
||||
|
||||
match wait_result {
|
||||
Ok(status) => {
|
||||
// 获取详细的退出信息
|
||||
let exit_code = status.code();
|
||||
#[cfg(unix)]
|
||||
let signal = {
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
status.signal()
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let signal: Option<i32> = None;
|
||||
|
||||
if !status.success() {
|
||||
// 🔥 非零退出码或被信号杀死 = 异常退出
|
||||
if let Some(ref flag) = abnormal_exit_flag_clone {
|
||||
// 只有非用户主动取消时才标记为异常
|
||||
if !was_cancelled {
|
||||
flag.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
warn!(
|
||||
"[ProcessReaper] Agent 进程异常退出: project_id={}, pid={}, pgid={}, exit_code={:?}, signal={:?}, was_cancelled={}",
|
||||
project_id_for_reaper, pid, pgid, exit_code, signal, was_cancelled
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"[ProcessReaper] Agent 进程正常退出: project_id={}, pid={}, pgid={}, exit_code={:?}",
|
||||
project_id_for_reaper, pid, pgid, exit_code
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// wait 失败,可能是进程已被其他方式回收
|
||||
if let Some(ref flag) = abnormal_exit_flag_clone {
|
||||
if !was_cancelled {
|
||||
flag.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
warn!(
|
||||
"[ProcessReaper] Agent 进程 wait() 失败: project_id={}, pid={}, pgid={}, error={}, was_cancelled={}",
|
||||
project_id_for_reaper, pid, pgid, e, was_cancelled
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 关键:触发 cancel_token,通知 SACP 连接层进程已退出
|
||||
// 这会让 SACP 连接检测到并发送 SSE 错误通知,然后断开连接
|
||||
if !was_cancelled {
|
||||
info!(
|
||||
"[ProcessReaper] 触发 cancel_token,通知 SACP 连接断开: project_id={}, pid={}",
|
||||
project_id_for_reaper, pid
|
||||
);
|
||||
cancel_token_for_reaper.cancel();
|
||||
} else {
|
||||
debug!(
|
||||
"[ProcessReaper] cancel_token 已被外部取消,跳过: project_id={}, pid={}",
|
||||
project_id_for_reaper, pid
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let resources = AgentResources::Claude {
|
||||
stderr_task: Arc::new(Mutex::new(Some(stderr_task))),
|
||||
_reaper_task: reaper_task,
|
||||
};
|
||||
|
||||
let inner = Arc::new(AgentLifecycleInner {
|
||||
project_id: project_id_clone,
|
||||
session_id,
|
||||
pgid,
|
||||
cancel_token,
|
||||
resources,
|
||||
stopped: AtomicBool::new(false),
|
||||
shared_api_key_manager,
|
||||
project_uuid_map,
|
||||
service_uuid,
|
||||
});
|
||||
|
||||
info!(
|
||||
"[LifecycleGuard] 创建 Claude Agent 守卫: project_id={}, pgid={}, session_id={}",
|
||||
project_id, pgid, session_id_str
|
||||
);
|
||||
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
/// 优雅停止agent
|
||||
///
|
||||
/// 带超时机制(5秒),超时后强制 kill 进程组
|
||||
///
|
||||
/// ## 进程组终止
|
||||
///
|
||||
/// 使用 `process-wrap` 创建真正的进程组,发送信号到 `-pgid` 会终止:
|
||||
/// - 子进程(进程组组长)
|
||||
/// - 所有孙进程(同一进程组中的进程)
|
||||
pub async fn graceful_stop(&self) -> Result<()> {
|
||||
// 🔥 使用原子 CAS 操作确保只执行一次清理
|
||||
// compare_exchange 返回 Ok 表示成功将 false 改为 true,即当前线程获得清理权
|
||||
// 返回 Err 表示已经被其他地方清理(Drop 或其他 graceful_stop 调用)
|
||||
let should_cleanup = self
|
||||
.inner
|
||||
.stopped
|
||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_ok();
|
||||
|
||||
if !should_cleanup {
|
||||
debug!("Agent already stopped, skipping graceful stop");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
"Gracefully stopping Claude agent for project: {}, pgid={}",
|
||||
self.inner.project_id, self.inner.pgid
|
||||
);
|
||||
|
||||
// 1. 发送取消信号
|
||||
self.inner.cancel_token.cancel();
|
||||
|
||||
// 2. 终止进程组
|
||||
self.kill_process_group(false).await?;
|
||||
|
||||
info!(
|
||||
"Gracefully stopped Claude agent for project: {}",
|
||||
self.inner.project_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 发送取消信号(非阻塞)
|
||||
pub fn cancel(&self) {
|
||||
debug!("Sending cancel signal to agent: {}", self.inner.project_id);
|
||||
self.inner.cancel_token.cancel();
|
||||
}
|
||||
|
||||
/// 检查是否已停止
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
self.inner.stopped.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// 获取取消令牌
|
||||
pub fn cancellation_token(&self) -> &CancellationToken {
|
||||
&self.inner.cancel_token
|
||||
}
|
||||
|
||||
/// 🔥 终止进程组
|
||||
///
|
||||
/// 向 `-pgid` 发送信号,杀死整个进程组
|
||||
///
|
||||
/// # Unix 信号语义
|
||||
///
|
||||
/// - `kill(pgid, SIGTERM)` - 发送给单个进程
|
||||
/// - `kill(-pgid, SIGTERM)` - 发送给整个进程组
|
||||
/// - `kill(0, SIGTERM)` - 发送给调用者自己的进程组(危险!)
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `force` - 是否强制使用 SIGKILL(否则使用 SIGTERM)
|
||||
async fn kill_process_group(&self, force: bool) -> Result<()> {
|
||||
let pgid = self.inner.pgid;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use nix::errno::Errno;
|
||||
use nix::sys::signal::{Signal, kill};
|
||||
use nix::unistd::Pid;
|
||||
|
||||
// 🔥 关键防御性检查:pgid 不能为 0
|
||||
// kill(0, SIGKILL) 会杀死调用者自己的进程组,这是危险的
|
||||
if pgid == 0 {
|
||||
warn!(
|
||||
"[LifecycleGuard] 进程组 ID 为 0,跳过进程组终止(可能是初始化失败): project_id={}",
|
||||
self.inner.project_id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 🔥 关键:使用负的进程组 ID(真实的进程组 ID)
|
||||
// -pgid 表示发送信号到整个进程组,而不仅仅是进程组组长
|
||||
let target = Pid::from_raw(-(pgid as i32));
|
||||
|
||||
let signal = if force {
|
||||
Signal::SIGKILL
|
||||
} else {
|
||||
Signal::SIGTERM
|
||||
};
|
||||
|
||||
match kill(target, signal) {
|
||||
Ok(_) => {
|
||||
debug!("already sent signal: pgid={}, signal={:?}", pgid, signal);
|
||||
|
||||
// 如果是 SIGTERM,等待一段时间让进程优雅退出
|
||||
if !force {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
// 强制杀死进程组
|
||||
let _ = kill(target, Signal::SIGKILL);
|
||||
debug!("already force killed: pgid={}", pgid);
|
||||
}
|
||||
}
|
||||
Err(Errno::ESRCH) => {
|
||||
// 进程组已退出,这是正常的
|
||||
debug!("process group already exited: pgid={}", pgid);
|
||||
}
|
||||
Err(Errno::EPERM) => {
|
||||
// 权限不足,无法终止进程组
|
||||
warn!(
|
||||
"[LifecycleGuard] 权限不足,无法终止进程组: pgid={}, project_id={}",
|
||||
pgid, self.inner.project_id
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
// 其他错误(如 EINVAL、EFAULT 等)
|
||||
debug!(" kill failed: pgid={}, error={:?}", pgid, e);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Claude process group stopped: pgid={}", pgid);
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
debug!("Unix platform, skipping process group stop");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for AgentLifecycleGuard {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: Arc::clone(&self.inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AgentLifecycleGuard {
|
||||
fn drop(&mut self) {
|
||||
let strong_count = Arc::strong_count(&self.inner);
|
||||
|
||||
debug!(
|
||||
"[Claude] AgentLifecycleGuard::drop 开始: project_id={}, pgid={}, strong_count={}",
|
||||
self.inner.project_id, self.inner.pgid, strong_count
|
||||
);
|
||||
|
||||
// 🔥 使用原子 CAS 操作确保只执行一次清理
|
||||
// 不再依赖引用计数,因为引用计数可能因为多处 clone 而不准确
|
||||
// compare_exchange 返回 Ok 表示成功将 false 改为 true,即当前线程获得清理权
|
||||
// 返回 Err 表示已经被其他线程清理,当前线程无需操作
|
||||
let should_cleanup = self
|
||||
.inner
|
||||
.stopped
|
||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_ok();
|
||||
|
||||
if should_cleanup {
|
||||
debug!(
|
||||
"[Claude] AgentLifecycleGuard 获得清理权,开始清理资源: {}",
|
||||
self.inner.project_id
|
||||
);
|
||||
|
||||
// 发送取消信号
|
||||
self.inner.cancel_token.cancel();
|
||||
|
||||
// 注意:API 密钥配置的清理由 agent_runner 层的 stop_agent 方法统一负责
|
||||
// 包括:
|
||||
// - shared_api_key_manager 中的配置
|
||||
// - project_uuid_map 中的映射
|
||||
//
|
||||
// 这样避免双重清理,确保资源只被清理一次
|
||||
|
||||
// 🔥 同步终止进程组
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use nix::sys::signal::{Signal, kill};
|
||||
use nix::unistd::Pid;
|
||||
|
||||
let pgid = self.inner.pgid;
|
||||
|
||||
// 🔥 关键防御性检查:pgid 不能为 0
|
||||
// kill(0, SIGKILL) 会杀死调用者自己的进程组,这是危险的
|
||||
if pgid == 0 {
|
||||
debug!(
|
||||
"[Claude] 进程组 ID 为 0,跳过进程组终止: project_id={}",
|
||||
self.inner.project_id
|
||||
);
|
||||
} else {
|
||||
let target = Pid::from_raw(-(pgid as i32));
|
||||
|
||||
if let Err(e) = kill(target, Signal::SIGKILL) {
|
||||
// 进程可能已经退出,这是正常的
|
||||
debug!(
|
||||
"[Claude] 终止进程组失败(可能已退出): pgid={}, error={}",
|
||||
pgid, e
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"[Claude] 进程组已终止: pgid={}, project_id={}",
|
||||
pgid, self.inner.project_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
debug!("[Claude] Unix platform, skipping process group stop");
|
||||
}
|
||||
|
||||
// 注意:后台回收任务 (reaper_task) 会自动完成
|
||||
// 不需要在这里等待或取消
|
||||
|
||||
info!(
|
||||
"[Claude] AgentLifecycleGuard 清理完成: project_id={}",
|
||||
self.inner.project_id
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"[Claude] AgentLifecycleGuard 跳过清理(已被其他引用清理): project_id={}",
|
||||
self.inner.project_id
|
||||
);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"[Claude] AgentLifecycleGuard::drop 完成: project_id={}",
|
||||
self.inner.project_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 为AgentLifecycleGuard实现AgentLifecycle trait
|
||||
impl AgentLifecycle for AgentLifecycleGuard {
|
||||
fn graceful_stop(
|
||||
&self,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>> {
|
||||
Box::pin(async move { AgentLifecycleGuard::graceful_stop(self).await })
|
||||
}
|
||||
|
||||
fn cancel(&self) {
|
||||
AgentLifecycleGuard::cancel(self);
|
||||
}
|
||||
|
||||
fn is_stopped(&self) -> bool {
|
||||
AgentLifecycleGuard::is_stopped(self)
|
||||
}
|
||||
|
||||
fn cancellation_token(&self) -> &CancellationToken {
|
||||
AgentLifecycleGuard::cancellation_token(self)
|
||||
}
|
||||
}
|
||||
91
qiming-rcoder/crates/agent_abstraction/src/launcher/mod.rs
Normal file
91
qiming-rcoder/crates/agent_abstraction/src/launcher/mod.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! # Agent 启动器模块
|
||||
//!
|
||||
//! 提供 Agent 的启动、生命周期管理和通道处理功能。
|
||||
//!
|
||||
//! ## SACP 迁移说明
|
||||
//!
|
||||
//! 本模块已迁移至 SACP (Symposium ACP) 实现,主要优势:
|
||||
//! - **支持 Send trait**: 可使用标准 `tokio::spawn`,无需 `LocalSet` + `spawn_local`
|
||||
//! - **Builder 模式**: 更清晰的连接构建和配置
|
||||
//! - **回调式消息处理**: 通过 `on_receive_notification` / `on_receive_request` 宏
|
||||
//!
|
||||
//! ## 子模块职责
|
||||
//!
|
||||
//! | 子模块 | 职责 | 关键类型 |
|
||||
//! |--------|------|---------|
|
||||
//! | [`lifecycle`] | Agent 生命周期守卫(RAII 资源管理)| `AgentLifecycleGuard` |
|
||||
//! | [`claude_code_sacp`] | SACP 版本的 Claude Code Agent 启动器 | `SacpClaudeCodeLauncher` |
|
||||
//!
|
||||
//! ## Agent 启动流程 (SACP)
|
||||
//!
|
||||
//! ```text
|
||||
//! AcpSessionManager.create_session()
|
||||
//! │
|
||||
//! │ 1. 创建 SacpClaudeCodeLauncher
|
||||
//! ▼
|
||||
//! SacpClaudeCodeLauncher.launch()
|
||||
//! │
|
||||
//! │ 2. 启动 Claude Code 子进程
|
||||
//! │ 3. 使用 SACP Builder 建立连接
|
||||
//! │ 4. 创建 AgentLifecycleGuard
|
||||
//! ▼
|
||||
//! SacpLauncherConnectionInfo
|
||||
//! │
|
||||
//! │ 5. 返回 session_id, prompt_tx, cancel_tx, lifecycle_guard
|
||||
//! ▼
|
||||
//! 存入 SessionRegistry
|
||||
//! ```
|
||||
//!
|
||||
//! ## 与 shared_types::AgentLifecycleGuard 的关系
|
||||
//!
|
||||
//! `AgentLifecycleGuard` 的核心实现定义在 `shared_types::model::agent_model`。
|
||||
//! 本模块的 `lifecycle.rs` 提供 Re-export,
|
||||
//! 方便从 `agent_abstraction::launcher` 统一导入。
|
||||
//!
|
||||
//! ## 生命周期管理 (RAII)
|
||||
//!
|
||||
//! 当 `AgentLifecycleGuard` 被 drop 时:
|
||||
//! 1. 发送取消信号 (`cancel_token.cancel()`)
|
||||
//! 2. 终止子进程 (`child.kill()`)
|
||||
//! 3. 停止 stderr 任务
|
||||
//!
|
||||
//! 这确保了 Agent 资源的正确清理,即使在异常情况下也不会泄漏。
|
||||
|
||||
mod claude_code_sacp;
|
||||
pub mod lifecycle;
|
||||
#[cfg(windows)]
|
||||
mod windows_launch;
|
||||
|
||||
// ============================================================================
|
||||
// SACP 类型导出(推荐使用)
|
||||
// ============================================================================
|
||||
|
||||
pub use lifecycle::AgentLifecycleGuard;
|
||||
|
||||
// 直接导出 SACP 类型
|
||||
pub use claude_code_sacp::{
|
||||
SacpAgentLaunchConfig, SacpClaudeCodeLauncher, SacpLauncherConnectionInfo,
|
||||
convert_context_servers_sacp, get_default_sacp_agent_config, load_sacp_agent_config,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 兼容性类型别名(向后兼容旧代码)
|
||||
// ============================================================================
|
||||
|
||||
/// 兼容性别名:ClaudeCodeLauncher -> SacpClaudeCodeLauncher
|
||||
pub type ClaudeCodeLauncher<N> = SacpClaudeCodeLauncher<N>;
|
||||
|
||||
/// 兼容性别名:LauncherConnectionInfoComplete -> SacpLauncherConnectionInfo
|
||||
pub type LauncherConnectionInfoComplete = SacpLauncherConnectionInfo;
|
||||
|
||||
/// 兼容性别名:AgentLaunchConfig -> SacpAgentLaunchConfig
|
||||
pub type AgentLaunchConfig = SacpAgentLaunchConfig;
|
||||
|
||||
/// 兼容性别名:load_agent_config -> load_sacp_agent_config
|
||||
pub use claude_code_sacp::load_sacp_agent_config as load_agent_config;
|
||||
|
||||
/// 兼容性别名:get_default_agent_config -> get_default_sacp_agent_config
|
||||
pub use claude_code_sacp::get_default_sacp_agent_config as get_default_agent_config;
|
||||
|
||||
/// 兼容性别名:convert_context_servers -> convert_context_servers_sacp
|
||||
pub use claude_code_sacp::convert_context_servers_sacp as convert_context_servers;
|
||||
@@ -0,0 +1,411 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
pub const CREATE_NO_WINDOW_FLAG: u32 = 0x0800_0000;
|
||||
|
||||
fn resolve_windows_node_exe() -> Option<PathBuf> {
|
||||
// 1. 显式指定的 node 可执行文件路径(最高优先级)
|
||||
if let Ok(path) = std::env::var("NUWAX_NODE_PATH") {
|
||||
let node = PathBuf::from(path);
|
||||
if node.exists() {
|
||||
info!(
|
||||
"[SACP] Windows node resolved via NUWAX_NODE_PATH: {}",
|
||||
node.display()
|
||||
);
|
||||
return Some(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 1.5 Tauri 层设置的 NUWAX_NODE_EXE(sidecar node-runtime.exe)
|
||||
if let Ok(path) = std::env::var("NUWAX_NODE_EXE") {
|
||||
let node = PathBuf::from(&path);
|
||||
if node.exists() {
|
||||
info!(
|
||||
"[SACP] Windows node resolved via NUWAX_NODE_EXE: {}",
|
||||
node.display()
|
||||
);
|
||||
return Some(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 应用集成的 runtime 路径(Tauri 打包后设置的 NUWAX_APP_RUNTIME_PATH,
|
||||
// 包含 runtime\node\bin 等目录,内含集成的 node.exe)
|
||||
if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
|
||||
for dir in runtime_path.split(';') {
|
||||
let dir = dir.trim();
|
||||
if dir.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let node = PathBuf::from(dir).join("node.exe");
|
||||
if node.exists() {
|
||||
info!(
|
||||
"[SACP] Windows node resolved via NUWAX_APP_RUNTIME_PATH: {}",
|
||||
node.display()
|
||||
);
|
||||
return Some(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 从 APPDATA 推导默认的应用 runtime 路径(NUWAX_APP_RUNTIME_PATH 未设置时的回退)
|
||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||||
let default_node = PathBuf::from(&appdata)
|
||||
.join("com.nuwax.agent-tauri-client")
|
||||
.join("runtime")
|
||||
.join("node")
|
||||
.join("bin")
|
||||
.join("node.exe");
|
||||
if default_node.exists() {
|
||||
info!(
|
||||
"[SACP] Windows node resolved via APPDATA default path: {}",
|
||||
default_node.display()
|
||||
);
|
||||
return Some(default_node);
|
||||
}
|
||||
}
|
||||
|
||||
warn!(
|
||||
"[SACP] Windows node resolution failed: NUWAX_NODE_PATH, NUWAX_NODE_EXE, NUWAX_APP_RUNTIME_PATH and APPDATA default paths all missed"
|
||||
);
|
||||
None
|
||||
}
|
||||
|
||||
fn npm_package_entry_from_dir(
|
||||
package_dir: &std::path::Path,
|
||||
package_name: &str,
|
||||
) -> Option<PathBuf> {
|
||||
let package_json = package_dir.join("package.json");
|
||||
let content = std::fs::read_to_string(package_json).ok()?;
|
||||
let package_json: serde_json::Value = serde_json::from_str(&content).ok()?;
|
||||
let bin_field = package_json.get("bin")?;
|
||||
|
||||
let rel_entry = if let Some(bin_str) = bin_field.as_str() {
|
||||
Some(bin_str.to_string())
|
||||
} else if let Some(bin_map) = bin_field.as_object() {
|
||||
bin_map
|
||||
.get(package_name)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_string)
|
||||
.or_else(|| {
|
||||
bin_map
|
||||
.values()
|
||||
.find_map(|v| v.as_str())
|
||||
.map(str::to_string)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}?;
|
||||
|
||||
let entry = package_dir.join(rel_entry);
|
||||
if entry.exists() { Some(entry) } else { None }
|
||||
}
|
||||
|
||||
fn get_windows_cmd_script_path(path: &std::path::Path) -> Option<PathBuf> {
|
||||
match path.extension().and_then(|s| s.to_str()) {
|
||||
Some(ext) if ext.eq_ignore_ascii_case("cmd") => Some(path.to_path_buf()),
|
||||
None => {
|
||||
// 先检查直接路径(绝对路径 or 当前目录)
|
||||
let direct_candidate = path.with_extension("cmd");
|
||||
if direct_candidate.exists() {
|
||||
return Some(direct_candidate);
|
||||
}
|
||||
|
||||
// which 未命中时 path 为裸命令名,需要在已知目录中搜索
|
||||
let program_name = path.file_name().and_then(|s| s.to_str())?;
|
||||
|
||||
// 搜索应用自管的 node_modules/.bin
|
||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||||
let app_private = PathBuf::from(&appdata)
|
||||
.join("com.nuwax.agent-tauri-client")
|
||||
.join("node_modules")
|
||||
.join(".bin")
|
||||
.join(format!("{}.cmd", program_name));
|
||||
if app_private.exists() {
|
||||
return Some(app_private);
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索 NUWAX_APP_RUNTIME_PATH 中的每个目录
|
||||
if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
|
||||
for dir in runtime_path.split(';').filter(|s| !s.trim().is_empty()) {
|
||||
let candidate =
|
||||
std::path::Path::new(dir.trim()).join(format!("{}.cmd", program_name));
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_js_entry_from_cmd_shim(cmd_script: &std::path::Path) -> Option<PathBuf> {
|
||||
let content = match std::fs::read_to_string(cmd_script) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"[SACP] cmd shim read failed: {} ({})",
|
||||
cmd_script.display(),
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let base_dir = cmd_script.parent()?;
|
||||
|
||||
for raw_line in content.lines() {
|
||||
let line = raw_line.trim();
|
||||
// 支持多种 npm shim 格式:%~dp0(旧版)、%dp0%(新版 npm >=7)、%dp0(变体)
|
||||
let base_token = if line.contains("%~dp0") {
|
||||
"%~dp0"
|
||||
} else if line.contains("%dp0%") {
|
||||
"%dp0%"
|
||||
} else if line.contains("%dp0") {
|
||||
"%dp0"
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let clean = line.replace('"', "");
|
||||
let clean_lower = clean.to_ascii_lowercase();
|
||||
let ext_end = [".cjs", ".mjs", ".js"]
|
||||
.iter()
|
||||
.filter_map(|ext| clean_lower.find(ext).map(|pos| pos + ext.len()))
|
||||
.min();
|
||||
let Some(end) = ext_end else {
|
||||
continue;
|
||||
};
|
||||
let start = clean.find(base_token)?;
|
||||
if end <= start + base_token.len() || end > clean.len() {
|
||||
continue;
|
||||
}
|
||||
let rel = clean[start + base_token.len()..end].trim_start_matches(['\\', '/']);
|
||||
if rel.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let rel = rel.replace('/', "\\");
|
||||
let entry = base_dir.join(std::path::Path::new(&rel));
|
||||
if entry.exists() {
|
||||
debug!(
|
||||
"[SACP] cmd shim resolved entry: {} -> {}",
|
||||
cmd_script.display(),
|
||||
entry.display()
|
||||
);
|
||||
return Some(entry);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("[SACP] cmd shim not resolved: {}", cmd_script.display());
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn resolve_windows_node_cli_command(
|
||||
command: &str,
|
||||
args: &[String],
|
||||
) -> Option<(String, Vec<String>)> {
|
||||
let command_path = which::which(command).unwrap_or_else(|_| PathBuf::from(command));
|
||||
let path = command_path.as_path();
|
||||
info!(
|
||||
"[SACP] Windows command resolution started: input={}, resolved={}",
|
||||
command,
|
||||
path.display()
|
||||
);
|
||||
let cmd_script = get_windows_cmd_script_path(path);
|
||||
|
||||
let node_exe = resolve_windows_node_exe()?;
|
||||
info!(
|
||||
"[SACP] Windows node.exe already resolved: {}",
|
||||
node_exe.display()
|
||||
);
|
||||
|
||||
if let Some(cmd_script) = cmd_script.as_ref() {
|
||||
info!("[SACP] using cmd shim: {}", cmd_script.display());
|
||||
if let Some(js_entry) = resolve_js_entry_from_cmd_shim(cmd_script) {
|
||||
let mut actual_args = Vec::with_capacity(args.len() + 1);
|
||||
actual_args.push(js_entry.to_string_lossy().to_string());
|
||||
actual_args.extend(args.iter().cloned());
|
||||
info!(
|
||||
"[SACP] cmd shim resolution succeeded: {} -> {}",
|
||||
cmd_script.display(),
|
||||
js_entry.display()
|
||||
);
|
||||
return Some((node_exe.to_string_lossy().to_string(), actual_args));
|
||||
}
|
||||
info!(
|
||||
"[SACP] cmd shim exists but JS entry not resolved, falling back to package.json bin: {}",
|
||||
cmd_script.display()
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"[SACP] cmd shim not found, falling back to package.json bin: command={}",
|
||||
command
|
||||
);
|
||||
}
|
||||
|
||||
let package_name = match path.extension().and_then(|s| s.to_str()) {
|
||||
Some(ext) if ext.eq_ignore_ascii_case("cmd") => path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(str::to_string),
|
||||
None => path
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(str::to_string),
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
let mut package_dirs: Vec<PathBuf> = Vec::new();
|
||||
|
||||
// 从 which 解析到的 .bin 目录向上推导 node_modules
|
||||
if let Some(parent) = path.parent() {
|
||||
let is_npm_bin = parent
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().eq_ignore_ascii_case(".bin"))
|
||||
.unwrap_or(false);
|
||||
if is_npm_bin {
|
||||
if let Some(node_modules_dir) = parent.parent() {
|
||||
package_dirs.push(node_modules_dir.join(&package_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 仅搜索应用自管的 node_modules
|
||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||||
package_dirs.push(
|
||||
PathBuf::from(appdata)
|
||||
.join("com.nuwax.agent-tauri-client")
|
||||
.join("node_modules")
|
||||
.join(&package_name),
|
||||
);
|
||||
}
|
||||
|
||||
for package_dir in package_dirs {
|
||||
if !package_dir.exists() {
|
||||
info!(
|
||||
"[SACP] package directorynot found, skip: {}",
|
||||
package_dir.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if let Some(js_entry) = npm_package_entry_from_dir(&package_dir, &package_name) {
|
||||
let mut actual_args = Vec::with_capacity(args.len() + 1);
|
||||
actual_args.push(js_entry.to_string_lossy().to_string());
|
||||
actual_args.extend(args.iter().cloned());
|
||||
info!(
|
||||
"[SACP] package.json bin resolved: {} -> {}",
|
||||
package_name,
|
||||
js_entry.display()
|
||||
);
|
||||
return Some((node_exe.to_string_lossy().to_string(), actual_args));
|
||||
}
|
||||
info!(
|
||||
"[SACP] package.json bin resolution failed: package={}, dir={}",
|
||||
package_name,
|
||||
package_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
warn!(
|
||||
"[SACP] Windows command resolution failed, no JS entry found: command={}",
|
||||
command
|
||||
);
|
||||
None
|
||||
}
|
||||
|
||||
/// Windows 下将「命令 + 参数」规范化为不弹 CMD 窗口的形式。
|
||||
///
|
||||
/// 背景:Windows 上 npm 全局安装会创建 .cmd/.bat,直接执行会弹出 CMD 窗口。
|
||||
/// 本函数按扩展名检测并尽可能转换为 `node.exe + JS 入口`,避免弹窗。
|
||||
///
|
||||
/// 检查顺序(从优到劣):
|
||||
/// 1. `.exe` → 原生二进制,直接返回(不修改)
|
||||
/// 2. `.cmd` / `.bat` → 尝试解析为 node.exe + JS,成功则返回新 (path, args)
|
||||
/// 3. 其他扩展名 → 直接返回
|
||||
/// 4. 无扩展名 → 用 `which` 解析后再按 1–3 处理
|
||||
///
|
||||
/// 仅编译于 Windows;调用方需使用 `#[cfg(windows)]`。
|
||||
#[cfg(windows)]
|
||||
pub fn normalize_windows_command_for_no_window(
|
||||
command_path: String,
|
||||
command_args: Vec<String>,
|
||||
) -> (String, Vec<String>) {
|
||||
let mut path = command_path;
|
||||
let mut args = command_args;
|
||||
|
||||
let path_ref = Path::new(&path);
|
||||
let ext = path_ref
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|s| s.to_ascii_lowercase());
|
||||
|
||||
match ext.as_deref() {
|
||||
Some("exe") => {
|
||||
info!(
|
||||
"[SACP] Windows detected native .exe: {} - no popup window",
|
||||
path
|
||||
);
|
||||
}
|
||||
Some("cmd" | "bat") => {
|
||||
info!("[SACP] 🔍 Windows detecting .cmd/.bat: {}", path);
|
||||
info!("[SACP] 🔄 converting to node.exe + JS ...");
|
||||
if let Some((node_path, js_args)) = resolve_windows_node_cli_command(&path, &args) {
|
||||
info!("[SACP] conversion succeeded: {} + {:?}", node_path, js_args);
|
||||
path = node_path;
|
||||
args = js_args;
|
||||
} else {
|
||||
warn!(
|
||||
"[SACP] ⚠️ conversion failed, will run original command (may show popup): {}",
|
||||
path
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(other) => {
|
||||
info!(
|
||||
"[SACP] ℹ️ Windows detected other format .{}: {} - trying direct execution",
|
||||
other, path
|
||||
);
|
||||
}
|
||||
None => {
|
||||
info!("[SACP] 🔍 Windows detecting extension: {}", path);
|
||||
if let Ok(resolved) = which::which(&path) {
|
||||
let resolved_str = resolved.to_string_lossy().to_string();
|
||||
info!("[SACP] 🔄 resolved: {}", resolved_str);
|
||||
|
||||
let resolved_ext = resolved
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|s| s.to_ascii_lowercase());
|
||||
|
||||
match resolved_ext.as_deref() {
|
||||
Some("exe") => {
|
||||
info!("[SACP] detected .exe - no popup window");
|
||||
path = resolved_str;
|
||||
}
|
||||
Some("cmd" | "bat") => {
|
||||
info!("[SACP] 🔍 detected .cmd/.bat, converting ...");
|
||||
if let Some((node_path, js_args)) =
|
||||
resolve_windows_node_cli_command(&resolved_str, &args)
|
||||
{
|
||||
info!("[SACP] conversion succeeded: {} + {:?}", node_path, js_args);
|
||||
path = node_path;
|
||||
args = js_args;
|
||||
} else {
|
||||
warn!("[SACP] ⚠️ conversion failed");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
info!("[SACP] ℹ️ unknown extension, keeping original");
|
||||
path = resolved_str;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("[SACP] ⚠️ unable to resolve path: {}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(path, args)
|
||||
}
|
||||
86
qiming-rcoder/crates/agent_abstraction/src/lib.rs
Normal file
86
qiming-rcoder/crates/agent_abstraction/src/lib.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! # Agent Abstraction Layer (SACP 版本)
|
||||
//!
|
||||
//! 此 crate 提供 Agent 管理的抽象接口,是 RCoder 架构的中间层。
|
||||
//!
|
||||
//! ## SACP 迁移说明
|
||||
//!
|
||||
//! 本模块已迁移至 SACP (Symposium ACP) 实现:
|
||||
//! - **移除 Client trait**: `AcpSessionManager<N, R>` 不再需要 `Client` 泛型参数
|
||||
//! - **支持 Send trait**: 可使用标准 `tokio::spawn`,无需 `LocalSet`
|
||||
//! - **简化架构**: 移除了 channel handler,SACP 内部使用回调处理
|
||||
//!
|
||||
//! ## 架构位置
|
||||
//!
|
||||
//! ```text
|
||||
//! shared_types (基础类型: ProjectAndAgentInfo, AgentLifecycleGuard)
|
||||
//! ↓
|
||||
//! agent_abstraction (抽象接口) ← 你在这里
|
||||
//! ↓
|
||||
//! agent_runner (具体实现: AgentSessionRegistry, SseSessionNotifier)
|
||||
//! ```
|
||||
//!
|
||||
//! ## 模块职责
|
||||
//!
|
||||
//! | 模块 | 职责 | 关键类型 |
|
||||
//! |------|------|---------|
|
||||
//! | [`traits`] | 核心抽象接口定义 | `SessionRegistry`, `SessionNotifier` |
|
||||
//! | [`session`] | 会话管理和 Worker 抽象 | `AcpSessionManager`, `AcpAgentWorker` |
|
||||
//! | [`launcher`] | Agent 启动和生命周期管理 | `SacpClaudeCodeLauncher` |
|
||||
//! | [`acp`] | ACP 协议连接抽象 | `CancelResult` |
|
||||
//!
|
||||
//! ## Trait 实现指南
|
||||
//!
|
||||
//! 本 crate 定义的 trait 预期在 `agent_runner` 中实现:
|
||||
//!
|
||||
//! - [`SessionRegistry`] → `agent_runner::service::AgentSessionRegistry` (全局 `AGENT_REGISTRY`)
|
||||
//! - [`SessionNotifier`] → `agent_runner::service::SseSessionNotifier`
|
||||
//!
|
||||
//! ### 使用模式 (SACP)
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // 在 agent_runner 中:
|
||||
//! // 1. 实现 SessionRegistry trait
|
||||
//! impl SessionRegistry for AgentSessionRegistry { ... }
|
||||
//!
|
||||
//! // 2. 通过依赖注入传入 AcpSessionManager(不再需要 Client 类型参数)
|
||||
//! let session_manager = AcpSessionManager::new(
|
||||
//! Arc::new(notifier), // SessionNotifier 实现
|
||||
//! AGENT_REGISTRY.clone(), // SessionRegistry 实现
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! ## 设计模式
|
||||
//!
|
||||
//! - **依赖反转(DIP)**: Trait 在此定义,实现在上层 crate
|
||||
//! - **泛型参数**: `AcpSessionManager<N, R>` 支持可插拔实现
|
||||
//! - **RAII**: 通过 `AgentLifecycleGuard` 管理 Agent 资源生命周期
|
||||
//!
|
||||
//! ## 与 AGENT_REGISTRY 的关系
|
||||
//!
|
||||
//! `AGENT_REGISTRY` 是 `agent_runner` 中的全局单例,实现了 `SessionRegistry` trait。
|
||||
//! 通过依赖注入传入 `AcpSessionManager`,使得抽象层不依赖具体实现。
|
||||
//!
|
||||
//! - **直接访问 AGENT_REGISTRY**: 仅在 `agent_runner` 内部使用(如清理任务、状态查询)
|
||||
//! - **通过 SessionRegistry trait**: 在 `agent_abstraction` 内部使用(会话管理逻辑)
|
||||
|
||||
pub mod acp;
|
||||
pub mod error;
|
||||
pub mod launcher;
|
||||
pub mod mirror_env;
|
||||
pub mod path_env;
|
||||
pub mod session;
|
||||
pub mod traits;
|
||||
|
||||
// Re-export types from submodules
|
||||
pub use acp::{CancelNotificationRequestWrapper, CancelResult};
|
||||
pub use error::AgentAbstractionError;
|
||||
pub use launcher::{
|
||||
AgentLaunchConfig, AgentLifecycleGuard, ClaudeCodeLauncher, LauncherConnectionInfoComplete,
|
||||
convert_context_servers, get_default_agent_config, load_agent_config,
|
||||
};
|
||||
pub use session::{
|
||||
AcpAgentWorker, AcpSessionManager, AgentWorker, SessionHandles, WorkerRequest, WorkerResponse,
|
||||
};
|
||||
pub use traits::agent::{AgentStartConfig, PromptMessage};
|
||||
pub use traits::session_notifier::SessionNotifier;
|
||||
pub use traits::session_registry::SessionRegistry;
|
||||
54
qiming-rcoder/crates/agent_abstraction/src/mirror_env.rs
Normal file
54
qiming-rcoder/crates/agent_abstraction/src/mirror_env.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! 镜像源环境变量透传
|
||||
//!
|
||||
//! 收集当前进程中的镜像源配置,供 MCP 子进程(npx/bunx/uvx)使用。
|
||||
//!
|
||||
//! 变量来源优先级:
|
||||
//! 1. 直接变量(npm_config_registry / UV_INDEX_URL / PIP_INDEX_URL)
|
||||
//! 2. MCP_PROXY_* 中转变量(MCP_PROXY_NPM_REGISTRY / MCP_PROXY_PYPI_INDEX_URL)
|
||||
|
||||
/// 需要透传的镜像源环境变量映射
|
||||
///
|
||||
/// 格式: (目标变量名, 后备变量名)
|
||||
const MIRROR_MAPPINGS: &[(&str, &str)] = &[
|
||||
("npm_config_registry", "MCP_PROXY_NPM_REGISTRY"),
|
||||
("UV_INDEX_URL", "MCP_PROXY_PYPI_INDEX_URL"),
|
||||
("PIP_INDEX_URL", "MCP_PROXY_PYPI_INDEX_URL"),
|
||||
];
|
||||
|
||||
/// 收集需要透传给子进程的镜像源环境变量
|
||||
///
|
||||
/// 每个目标变量优先读取自身,未设置时从对应的 MCP_PROXY_* 后备变量转换。
|
||||
/// 返回 `Vec<(key, value)>`,仅包含有值的条目。
|
||||
pub fn collect_mirror_env_vars() -> Vec<(String, String)> {
|
||||
MIRROR_MAPPINGS
|
||||
.iter()
|
||||
.filter_map(|&(target, fallback)| {
|
||||
let val = std::env::var(target)
|
||||
.or_else(|_| std::env::var(fallback))
|
||||
.ok()?;
|
||||
Some((target.to_string(), val))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn collect_does_not_panic() {
|
||||
let _ = collect_mirror_env_vars();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mappings_have_correct_targets() {
|
||||
// 确保目标变量名是工具直接使用的名称
|
||||
assert!(
|
||||
MIRROR_MAPPINGS
|
||||
.iter()
|
||||
.any(|(t, _)| *t == "npm_config_registry")
|
||||
);
|
||||
assert!(MIRROR_MAPPINGS.iter().any(|(t, _)| *t == "UV_INDEX_URL"));
|
||||
assert!(MIRROR_MAPPINGS.iter().any(|(t, _)| *t == "PIP_INDEX_URL"));
|
||||
}
|
||||
}
|
||||
120
qiming-rcoder/crates/agent_abstraction/src/path_env.rs
Normal file
120
qiming-rcoder/crates/agent_abstraction/src/path_env.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
//! 子进程环境变量管理 - 供 Agent 子进程使用
|
||||
//!
|
||||
//! 提供 PATH 环境变量构建函数,供 rcoder 启动 Claude Code 等 Agent 时使用。
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Tauri 桌面客户端在 APPDATA 下的应用数据目录名
|
||||
/// Windows: %APPDATA%/{TAURI_APP_DATA_DIR}
|
||||
pub const TAURI_APP_DATA_DIR: &str = "com.nuwax.agent-tauri-client";
|
||||
|
||||
/// 在系统 PATH 中查找可执行文件
|
||||
fn find_in_path(executable: &str) -> Option<String> {
|
||||
let output = if cfg!(windows) {
|
||||
std::process::Command::new("where").arg(executable).output()
|
||||
} else {
|
||||
std::process::Command::new("which").arg(executable).output()
|
||||
};
|
||||
|
||||
match output {
|
||||
Ok(o) if o.status.success() => {
|
||||
let binding = String::from_utf8_lossy(&o.stdout);
|
||||
binding
|
||||
.lines()
|
||||
.next()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 在系统 PATH 中查找可执行文件并返回其父目录
|
||||
fn find_bin_dir(executable: &str) -> Option<String> {
|
||||
find_in_path(executable).and_then(|path| {
|
||||
let parent = PathBuf::from(&path).parent()?.to_path_buf();
|
||||
let parent_str = parent.to_string_lossy().to_string();
|
||||
|
||||
// 如果父目录是 "cmd",尝试找同级的 "bin" 目录
|
||||
// 例如: D:\Program Files\Git\cmd -> D:\Program Files\Git\bin
|
||||
if parent_str.to_lowercase().ends_with("cmd") {
|
||||
let bin_dir = parent
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|n| {
|
||||
if n.eq_ignore_ascii_case("cmd") {
|
||||
parent
|
||||
.parent()
|
||||
.map(|p| p.join("bin").to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten();
|
||||
if let Some(bin) = bin_dir {
|
||||
if PathBuf::from(&bin).join("bash.exe").exists() {
|
||||
return Some(bin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(parent_str)
|
||||
})
|
||||
}
|
||||
|
||||
/// Windows: 查找 Git Bash 路径
|
||||
///
|
||||
/// 查找系统 PATH 中的 bash.exe,返回其 bin 目录路径
|
||||
/// 如果 bash.exe 在 Git\cmd 目录下,会自动查找同级的 Git\bin 目录
|
||||
#[cfg(windows)]
|
||||
pub fn find_git_bash_path() -> Option<String> {
|
||||
find_bin_dir("bash.exe")
|
||||
}
|
||||
|
||||
/// 构建适用于 rcoder 进程的 PATH 环境变量。
|
||||
///
|
||||
/// 优先级:
|
||||
/// 1. NUWAX_APP_RUNTIME_PATH(优先)
|
||||
/// 2. APPDATA 中的 node bin 和 npm bin(回退)
|
||||
///
|
||||
/// 返回 PATH 字符串,如果都未找到则返回 None
|
||||
#[cfg(windows)]
|
||||
pub fn build_rcoder_path_env() -> Option<String> {
|
||||
use tracing::info;
|
||||
|
||||
let sep = ";";
|
||||
|
||||
// 1. 优先从 NUWAX_APP_RUNTIME_PATH 构建
|
||||
if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
|
||||
let runtime_path = runtime_path.trim().to_string();
|
||||
if !runtime_path.is_empty() {
|
||||
info!("[Env] Using NUWAX_APP_RUNTIME_PATH to build PATH");
|
||||
return Some(runtime_path);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 从 APPDATA 推导默认路径
|
||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||||
let app_base = PathBuf::from(&appdata).join(TAURI_APP_DATA_DIR);
|
||||
let mut paths: Vec<String> = Vec::new();
|
||||
|
||||
let node_bin = app_base.join("runtime").join("node").join("bin");
|
||||
if node_bin.exists() {
|
||||
paths.push(node_bin.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
let npm_bin = app_base.join("node_modules").join(".bin");
|
||||
if npm_bin.exists() {
|
||||
paths.push(npm_bin.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
if !paths.is_empty() {
|
||||
let path_value = paths.join(sep);
|
||||
info!("[Env] APPDATA built PATH: {}", path_value);
|
||||
return Some(path_value);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
242
qiming-rcoder/crates/agent_abstraction/src/session/acp_worker.rs
Normal file
242
qiming-rcoder/crates/agent_abstraction/src/session/acp_worker.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
//! ACP Agent Worker 实现 (SACP 版本)
|
||||
//!
|
||||
//! 封装 Agent 请求处理的核心业务逻辑
|
||||
//!
|
||||
//! ## SACP 迁移说明
|
||||
//!
|
||||
//! - 移除了 `Client` trait 泛型参数(SACP 内部处理)
|
||||
//! - 简化了会话创建参数
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_config::{AgentServersConfig, PromptConfigAssembler};
|
||||
use anyhow::Result;
|
||||
use shared_types::{ProjectAndAgentInfo, SessionEntry};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use super::{AcpSessionManager, AgentWorker, SessionHandles, WorkerRequest, WorkerResponse};
|
||||
use crate::launcher::convert_context_servers;
|
||||
use crate::traits::{AgentStartConfig, SessionNotifier, SessionRegistry};
|
||||
|
||||
/// ACP Agent Worker (SACP 版本)
|
||||
///
|
||||
/// 处理 ACP Agent 请求的核心业务逻辑实现
|
||||
///
|
||||
/// # 类型参数
|
||||
/// - `N`: SessionNotifier 实现,用于推送 SSE 消息
|
||||
/// - `R`: SessionRegistry 实现,用于存储会话数据
|
||||
#[derive(Clone)]
|
||||
pub struct AcpAgentWorker<N: SessionNotifier, R: SessionRegistry> {
|
||||
session_manager: Arc<AcpSessionManager<N, R>>,
|
||||
}
|
||||
|
||||
impl<N: SessionNotifier + 'static, R: SessionRegistry> AcpAgentWorker<N, R> {
|
||||
/// 创建新的 ACP Agent Worker
|
||||
pub fn new(session_manager: Arc<AcpSessionManager<N, R>>) -> Self {
|
||||
Self { session_manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<N: SessionNotifier + Send + Sync + 'static, R: SessionRegistry + Send + Sync + 'static>
|
||||
AgentWorker for AcpAgentWorker<N, R>
|
||||
where
|
||||
R::Entry: Into<ProjectAndAgentInfo> + From<ProjectAndAgentInfo>,
|
||||
{
|
||||
fn name(&self) -> &'static str {
|
||||
"AcpAgentWorker"
|
||||
}
|
||||
|
||||
async fn process_request(&self, request: WorkerRequest) -> Result<WorkerResponse> {
|
||||
let project_id = request.project_id().to_string();
|
||||
let project_path = request.project_path().clone();
|
||||
|
||||
info!(
|
||||
"📨 AcpAgentWorker received request, project_id: {}",
|
||||
project_id
|
||||
);
|
||||
|
||||
// 1. 路径规范化
|
||||
let normalized_path = AcpSessionManager::<N, R>::normalize_path(&project_path);
|
||||
debug!("📂 path: {:?}", normalized_path);
|
||||
|
||||
// 2. 确保项目目录存在
|
||||
AcpSessionManager::<N, R>::ensure_project_dir(&normalized_path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("createdprojectdirectoryfailed: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
// 3. 使用 PromptConfigAssembler 组装配置
|
||||
let default_agent_id = "claude-code-acp-ts";
|
||||
|
||||
// 根据请求中的 service_type 加载对应配置
|
||||
let servers_config =
|
||||
AgentServersConfig::load_or_default_for_service(&request.prompt_message.service_type)
|
||||
.await;
|
||||
|
||||
let assembler = PromptConfigAssembler::new(servers_config)
|
||||
.with_system_prompt(request.prompt_message.system_prompt_override.clone())
|
||||
.with_user_prompt_template(request.prompt_message.user_prompt_template_override.clone())
|
||||
.with_agent_config(request.prompt_message.agent_config_override.clone());
|
||||
|
||||
// 🆕 获取用户指定的 agent_id(如果有)
|
||||
let agent_id = assembler.get_agent_id(default_agent_id);
|
||||
debug!("🎯 Resolved Agent ID: {}", agent_id);
|
||||
|
||||
// 获取最终的系统提示词(入参有值则使用入参,否则使用默认配置)
|
||||
// 🆕 使用实际 agent_id 而不是 default_agent_id
|
||||
let system_prompt = assembler.get_system_prompt(&agent_id);
|
||||
// 应用用户提示词模板(如果有)
|
||||
// 🆕 使用实际 agent_id 而不是 default_agent_id
|
||||
let final_user_prompt =
|
||||
assembler.apply_user_prompt(&agent_id, &request.prompt_message.content);
|
||||
|
||||
info!(
|
||||
"📝 Prompt processing - System prompt: has_override={}, length={} | User prompt: has_template={}, original_len={}, final_len={}",
|
||||
assembler.has_system_prompt_override(),
|
||||
system_prompt.len(),
|
||||
assembler.has_user_prompt_template_override(),
|
||||
request.prompt_message.content.len(),
|
||||
final_user_prompt.len()
|
||||
);
|
||||
|
||||
debug!(
|
||||
"📝 System prompt: has_override={}, length={}, content={}",
|
||||
assembler.has_system_prompt_override(),
|
||||
system_prompt.len(),
|
||||
system_prompt
|
||||
);
|
||||
debug!(
|
||||
"📝 User prompt: has_template={}, original_len={}, final_len={}, final_content={}",
|
||||
assembler.has_user_prompt_template_override(),
|
||||
request.prompt_message.content.len(),
|
||||
final_user_prompt.len(),
|
||||
final_user_prompt
|
||||
);
|
||||
|
||||
// 获取 MCP 服务器配置(入参有值则使用入参,否则使用默认配置)
|
||||
let context_servers = assembler.get_context_servers();
|
||||
debug!(
|
||||
"🔌 MCP server config: has_override={}, count={}",
|
||||
assembler.has_agent_config_override(),
|
||||
context_servers.len()
|
||||
);
|
||||
|
||||
// 将 context_servers 转换为 ACP 协议的 McpServer 格式
|
||||
let mcp_servers = convert_context_servers(&context_servers);
|
||||
debug!("🔌 MCP servers: {}", mcp_servers.len());
|
||||
|
||||
// 构建 AgentStartConfig 并传递 MCP 服务器、service_type
|
||||
let mut start_config = AgentStartConfig::new(request.prompt_message.service_type.clone())
|
||||
.with_system_prompt(system_prompt)
|
||||
.with_mcp_servers(mcp_servers);
|
||||
|
||||
// 🆕 如果用户指定了 agent_server 配置,添加到 start_config
|
||||
// 注意:这里直接使用用户传入的配置,由 launcher 层负责与默认配置合并
|
||||
if let Some(ref override_config) = request.prompt_message.agent_config_override {
|
||||
if let Some(ref agent_server) = override_config.agent_server {
|
||||
debug!(
|
||||
"📝 Using user-specified Agent server config: command={:?}, args={:?}",
|
||||
agent_server.command, agent_server.args
|
||||
);
|
||||
start_config = start_config.with_agent_server_override(agent_server.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Resume 策略:直接传递 session_id,由 LoadSessionRequest 在 Agent 层面检查
|
||||
//
|
||||
// 工作原理:
|
||||
// 1. 用户传入 session_id → 直接设置 resume_session_id
|
||||
// 2. claude_code_sacp.rs 尝试 LoadSessionRequest 加载历史会话
|
||||
// 3. 如果 Agent 返回成功 → 恢复上下文
|
||||
// 4. 如果 Agent 返回错误/超时 → 降级到 NewSessionRequest 创建新会话
|
||||
//
|
||||
// 优势:
|
||||
// - Agent 层面统一处理,无需本地检查文件
|
||||
// - 与 Agent 行为保持一致
|
||||
// - 自动降级(有会话就恢复,没有就创建)
|
||||
if let Some(ref session_id) = request.prompt_message.session_id {
|
||||
info!("Setting resume_session_id: {}", session_id);
|
||||
start_config = start_config.with_resume_session_id(session_id.clone());
|
||||
}
|
||||
|
||||
// 4. 更新 prompt_message 的 content 为处理后的用户提示词
|
||||
let mut prompt_message = request.prompt_message.clone();
|
||||
prompt_message.content = final_user_prompt;
|
||||
|
||||
// 5. 获取或创建会话 (SACP 版本 - 不需要 client 和 shared_api_key_manager)
|
||||
let (session_entry, is_new) = self
|
||||
.session_manager
|
||||
.get_or_create_session(
|
||||
&project_id,
|
||||
normalized_path,
|
||||
prompt_message.session_id.clone(),
|
||||
request.model_provider.clone(),
|
||||
start_config.clone(),
|
||||
request.service_uuid.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to create session: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
// 使用 SessionEntry trait 方法访问会话信息
|
||||
info!(
|
||||
"✅ Session ready, session_id: {}, is_new: {}",
|
||||
session_entry.session_id(),
|
||||
is_new
|
||||
);
|
||||
|
||||
// 6. 构建 Prompt 请求
|
||||
let prompt_request = if let Some(ref attachment_blocks) = request.attachment_blocks {
|
||||
debug!("📎 Built Prompt request with attachments");
|
||||
AcpSessionManager::<N, R>::build_prompt_request_with_attachments(
|
||||
&prompt_message,
|
||||
session_entry.session_id().clone(),
|
||||
attachment_blocks.clone(),
|
||||
)?
|
||||
} else {
|
||||
debug!("📝 Built text Prompt request");
|
||||
AcpSessionManager::<N, R>::build_text_prompt_request(
|
||||
&prompt_message,
|
||||
session_entry.session_id().clone(),
|
||||
)?
|
||||
};
|
||||
|
||||
// 7. 发送 Prompt(异步,支持背压)
|
||||
self.session_manager
|
||||
.send_prompt_request(&project_id, prompt_request)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("send Prompt requestfailed: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
info!("Prompt request already sent, project_id: {}", project_id);
|
||||
|
||||
// 8. 构建响应
|
||||
if is_new {
|
||||
Ok(WorkerResponse::new_session_success(
|
||||
project_id,
|
||||
session_entry.session_id().to_string(),
|
||||
Some(request.request_id().to_string()),
|
||||
prompt_message.service_type.clone(),
|
||||
SessionHandles {
|
||||
prompt_tx: session_entry.prompt_tx().clone(),
|
||||
cancel_tx: session_entry.cancel_tx().clone(),
|
||||
lifecycle_handle: session_entry.lifecycle_handle().cloned(),
|
||||
},
|
||||
))
|
||||
} else {
|
||||
Ok(WorkerResponse::reuse_session_success(
|
||||
project_id,
|
||||
session_entry.session_id().to_string(),
|
||||
Some(request.request_id().to_string()),
|
||||
prompt_message.service_type.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
23
qiming-rcoder/crates/agent_abstraction/src/session/mod.rs
Normal file
23
qiming-rcoder/crates/agent_abstraction/src/session/mod.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
//! Session 管理模块
|
||||
//!
|
||||
//! 提供 ACP 会话的统一管理能力,包括:
|
||||
//! - 会话信息存储和查询
|
||||
//! - 会话生命周期管理
|
||||
//! - 模型配置变化检测
|
||||
//! - Agent Worker 抽象
|
||||
//!
|
||||
//! ## 架构说明
|
||||
//!
|
||||
//! 会话信息统一使用 `ProjectAndAgentInfo`(定义在 `shared_types`),
|
||||
//! 通过 `SessionEntry` trait 抽象访问接口。
|
||||
//! 会话存储通过 `SessionRegistry` trait 抽象,允许注入不同实现(如 `AGENT_REGISTRY`)。
|
||||
|
||||
mod acp_worker;
|
||||
mod session_file_scanner;
|
||||
mod session_manager;
|
||||
mod worker;
|
||||
|
||||
pub use acp_worker::AcpAgentWorker;
|
||||
pub use session_file_scanner::check_session_file_exists;
|
||||
pub use session_manager::AcpSessionManager;
|
||||
pub use worker::{AgentWorker, SessionHandles, WorkerRequest, WorkerResponse};
|
||||
@@ -0,0 +1,352 @@
|
||||
//! Session 文件扫描模块
|
||||
//!
|
||||
//! 扫描 ~/.claude/projects/ 目录,验证 session 文件是否存在
|
||||
//! 作为官方 listSessions API 的降级方案
|
||||
//!
|
||||
//! 特性:
|
||||
//! - 使用 moka 缓存扫描结果(TTL 3分钟兜底)
|
||||
//! - 使用 notify 监听文件系统变化,自动刷新缓存
|
||||
//! - 防抖机制:100ms 内的多个事件合并为一次刷新
|
||||
//! - 文件变化时清除缓存,下次查询时重新扫描
|
||||
|
||||
use moka::future::Cache;
|
||||
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
/// 全局 session 文件存在性缓存
|
||||
/// Key: project_path, Value: 该项目下的 session_id 集合
|
||||
static FILE_SCAN_CACHE: OnceLock<Cache<String, HashSet<String>>> = OnceLock::new();
|
||||
|
||||
/// 文件监听器(全局单例)
|
||||
static FILE_WATCHER: OnceLock<Mutex<Option<RecommendedWatcher>>> = OnceLock::new();
|
||||
|
||||
/// 监听器是否已启动
|
||||
static WATCHER_STARTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// 防抖时间(毫秒)
|
||||
const DEBOUNCE_MS: u64 = 100;
|
||||
|
||||
/// 获取或初始化缓存(TTL 3分钟兜底,主要由文件监听触发刷新)
|
||||
fn get_file_scan_cache() -> &'static Cache<String, HashSet<String>> {
|
||||
FILE_SCAN_CACHE.get_or_init(|| {
|
||||
Cache::builder()
|
||||
.time_to_live(Duration::from_secs(180)) // 3分钟兜底 TTL
|
||||
.build()
|
||||
})
|
||||
}
|
||||
|
||||
/// Claude 配置目录
|
||||
fn get_claude_config_dir() -> PathBuf {
|
||||
std::env::var("CLAUDE_CONFIG_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("/home/user"))
|
||||
.join(".claude")
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取 projects 目录路径
|
||||
fn get_projects_dir() -> PathBuf {
|
||||
get_claude_config_dir().join("projects")
|
||||
}
|
||||
|
||||
/// 编码项目路径为目录名
|
||||
/// 规则:将 `/` 替换为 `-`
|
||||
fn encode_project_path(project_path: &str) -> String {
|
||||
project_path.replace('/', "-")
|
||||
}
|
||||
|
||||
/// 从文件路径中提取项目目录名
|
||||
/// 例如:~/.claude/projects/-home-user-project/abc123.jsonl -> -home-user-project
|
||||
fn extract_project_dir_from_path(file_path: &Path) -> Option<String> {
|
||||
let projects_dir = get_projects_dir();
|
||||
|
||||
// 检查文件是否在 projects 目录下
|
||||
if !file_path.starts_with(&projects_dir) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 获取相对于 projects 目录的路径
|
||||
if let Ok(relative) = file_path.strip_prefix(&projects_dir) {
|
||||
// 第一个组件就是项目目录名
|
||||
if let Some(first_component) = relative.components().next() {
|
||||
return Some(first_component.as_os_str().to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// 扫描项目目录,获取所有 session_id(异步版本)
|
||||
///
|
||||
/// 使用 tokio::fs 异步 I/O,避免阻塞 Tokio 工作线程
|
||||
async fn scan_project_sessions(project_path: &str) -> HashSet<String> {
|
||||
let projects_dir = get_projects_dir();
|
||||
let encoded_path = encode_project_path(project_path);
|
||||
|
||||
let mut session_ids = HashSet::new();
|
||||
|
||||
if !projects_dir.exists() {
|
||||
debug!(
|
||||
"🔍 [FILE_SCAN] Projects directory does not exist: {}",
|
||||
projects_dir.display()
|
||||
);
|
||||
return session_ids;
|
||||
}
|
||||
|
||||
// 使用异步 I/O 遍历目录
|
||||
let Ok(mut dir_entries) = tokio::fs::read_dir(&projects_dir).await else {
|
||||
warn!(
|
||||
"🔍 [FILE_SCAN] Unable to read project directory: {}",
|
||||
projects_dir.display()
|
||||
);
|
||||
return session_ids;
|
||||
};
|
||||
|
||||
while let Ok(Some(entry)) = dir_entries.next_entry().await {
|
||||
let dir_name = entry.file_name();
|
||||
|
||||
// 匹配编码后的路径(精确或带哈希后缀)
|
||||
// 使用 OsStr 避免不必要的 String 分配
|
||||
if dir_name
|
||||
.to_string_lossy()
|
||||
.starts_with(encoded_path.as_str())
|
||||
{
|
||||
let Ok(mut files) = tokio::fs::read_dir(entry.path()).await else {
|
||||
warn!(
|
||||
"🔍 [file_scanner] Unable to scan session directory: {}",
|
||||
entry.path().display()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
while let Ok(Some(file)) = files.next_entry().await {
|
||||
if let Some(filename) = file.file_name().to_str() {
|
||||
if filename.ends_with(".jsonl") {
|
||||
// 提取 session_id(去掉 .jsonl 后缀)
|
||||
let session_id = filename.trim_end_matches(".jsonl").to_string();
|
||||
session_ids.insert(session_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!(
|
||||
"🔍 [FILE_SCAN] Scan completed: project_path={}, found {} sessions",
|
||||
project_path,
|
||||
session_ids.len()
|
||||
);
|
||||
|
||||
session_ids
|
||||
}
|
||||
|
||||
/// 清除指定项目目录相关的缓存
|
||||
///
|
||||
/// 由于 moka 不支持按前缀遍历/删除,采用 invalidate_all 策略
|
||||
/// 下次查询时会用原始 project_path 重新扫描
|
||||
async fn invalidate_project_cache(project_dir_name: &str) {
|
||||
let cache = get_file_scan_cache();
|
||||
|
||||
debug!(
|
||||
"🔄 [FILE_WATCH] Detected project directory change: {}, clearing cache",
|
||||
project_dir_name
|
||||
);
|
||||
|
||||
// 由于无法从目录名还原 project_path,直接清除所有缓存
|
||||
// 这是一个权衡:简单但可能影响其他项目的缓存
|
||||
// 实际影响较小,因为缓存会在下次查询时重建
|
||||
cache.invalidate_all();
|
||||
}
|
||||
|
||||
/// 启动文件系统监听器
|
||||
///
|
||||
/// 监听 ~/.claude/projects/ 目录的变化,当 .jsonl 文件创建/删除时刷新缓存
|
||||
pub fn start_file_watcher() {
|
||||
// 确保只启动一次
|
||||
if WATCHER_STARTED.swap(true, Ordering::SeqCst) {
|
||||
debug!("[filelisten] File watcher already started");
|
||||
return;
|
||||
}
|
||||
|
||||
let projects_dir = get_projects_dir();
|
||||
|
||||
// 创建目录(如果不存在)
|
||||
if !projects_dir.exists() {
|
||||
if let Err(e) = std::fs::create_dir_all(&projects_dir) {
|
||||
warn!("[filelisten] Unable to create projects directory: {}", e);
|
||||
WATCHER_STARTED.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建异步通道
|
||||
let (tx, mut rx) = mpsc::channel::<Event>(100);
|
||||
|
||||
// 创建文件监听器
|
||||
let watcher_result = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
|
||||
match res {
|
||||
Ok(event) => {
|
||||
// 只关心 .jsonl 文件的创建和删除
|
||||
let has_jsonl = event
|
||||
.paths
|
||||
.iter()
|
||||
.any(|p| p.extension().is_some_and(|ext| ext == "jsonl"));
|
||||
|
||||
if has_jsonl {
|
||||
match event.kind {
|
||||
EventKind::Create(_) | EventKind::Remove(_) | EventKind::Modify(_) => {
|
||||
if let Err(e) = tx.blocking_send(event) {
|
||||
error!("[filelisten] Failed to send event: {}", e);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("[filelisten] Listener error: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut watcher = match watcher_result {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
error!("[filelisten] Failed to create listener: {}", e);
|
||||
WATCHER_STARTED.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 开始监听目录
|
||||
if let Err(e) = watcher.watch(&projects_dir, RecursiveMode::Recursive) {
|
||||
error!("[filelisten] Failed to watch directory: {}", e);
|
||||
WATCHER_STARTED.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存 watcher 到全局变量(防止被 drop)
|
||||
let watcher_holder = FILE_WATCHER.get_or_init(|| Mutex::new(None));
|
||||
if let Ok(mut guard) = watcher_holder.lock() {
|
||||
*guard = Some(watcher);
|
||||
}
|
||||
|
||||
info!(
|
||||
"👁️ [filelisten] startinglistendirectory: {}",
|
||||
projects_dir.display()
|
||||
);
|
||||
|
||||
// 启动异步任务处理事件(带防抖)
|
||||
tokio::spawn(async move {
|
||||
let mut pending_dirs: HashSet<String> = HashSet::new();
|
||||
let mut debounce_timer: Option<tokio::time::Instant> = None;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// 接收新事件
|
||||
event = rx.recv() => {
|
||||
match event {
|
||||
Some(event) => {
|
||||
// 提取变化文件所属的项目目录
|
||||
for path in &event.paths {
|
||||
if let Some(project_dir) = extract_project_dir_from_path(path) {
|
||||
pending_dirs.insert(project_dir);
|
||||
}
|
||||
}
|
||||
// 重置防抖计时器
|
||||
debounce_timer = Some(tokio::time::Instant::now() + Duration::from_millis(DEBOUNCE_MS));
|
||||
}
|
||||
None => {
|
||||
warn!("[filelisten] Received event but channel already closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 防抖计时器触发
|
||||
_ = async {
|
||||
if let Some(timer) = debounce_timer {
|
||||
tokio::time::sleep_until(timer).await;
|
||||
} else {
|
||||
// 没有计时器时永远等待
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
} => {
|
||||
if !pending_dirs.is_empty() {
|
||||
debug!(
|
||||
"📁 [FILE_WATCH] Debounce triggered, refreshing {} project directories",
|
||||
pending_dirs.len()
|
||||
);
|
||||
// 刷新所有待处理的项目目录
|
||||
for project_dir in pending_dirs.drain() {
|
||||
invalidate_project_cache(&project_dir).await;
|
||||
}
|
||||
}
|
||||
debounce_timer = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
info!("[FILE_WATCH] File watcher started");
|
||||
}
|
||||
|
||||
/// 停止文件系统监听器
|
||||
#[allow(dead_code)]
|
||||
pub fn stop_file_watcher() {
|
||||
if !WATCHER_STARTED.swap(false, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let watcher_holder = FILE_WATCHER.get_or_init(|| Mutex::new(None));
|
||||
if let Ok(mut guard) = watcher_holder.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
|
||||
info!("🛑 [filelisten] File watcher already stopped");
|
||||
}
|
||||
|
||||
/// 通过文件扫描检查 session 是否存在(带缓存)
|
||||
pub async fn check_session_file_exists(session_id: &str, project_path: &str) -> bool {
|
||||
// 确保监听器已启动
|
||||
start_file_watcher();
|
||||
|
||||
let cache = get_file_scan_cache();
|
||||
|
||||
// 尝试从缓存获取
|
||||
if let Some(session_ids) = cache.get(project_path).await {
|
||||
let exists = session_ids.contains(session_id);
|
||||
debug!(
|
||||
"🔍 [FILE_SCAN_CACHE] session={} -> {}",
|
||||
session_id,
|
||||
if exists { "exists" } else { "not found" }
|
||||
);
|
||||
return exists;
|
||||
}
|
||||
|
||||
// 缓存未命中,执行扫描
|
||||
debug!(
|
||||
"🔍 [FILE_SCAN] Cache miss, scanning directory: project_path={}",
|
||||
project_path
|
||||
);
|
||||
let session_ids = scan_project_sessions(project_path).await;
|
||||
let exists = session_ids.contains(session_id);
|
||||
|
||||
// 存入缓存
|
||||
cache.insert(project_path.to_string(), session_ids).await;
|
||||
|
||||
if exists {
|
||||
info!("[file_scan] Session file found: {}", session_id);
|
||||
} else {
|
||||
debug!("[file_scan] Not a session file: {}", session_id);
|
||||
}
|
||||
|
||||
exists
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
//! # ACP Session Manager
|
||||
//!
|
||||
//! 基于 project_id 管理 ACP 会话的核心模块。
|
||||
//!
|
||||
//! ## SACP 迁移说明
|
||||
//!
|
||||
//! 本模块已迁移至 SACP 实现:
|
||||
//! - 移除了 `Client` trait 泛型参数(SACP 内部处理)
|
||||
//! - 简化了 `create_session` 参数(不再需要 client 和 shared_api_key_manager)
|
||||
//! - 使用标准 tokio::spawn(无需 LocalSet)
|
||||
//!
|
||||
//! ## 职责范围
|
||||
//!
|
||||
//! 1. **会话生命周期管理**
|
||||
//! - 创建新会话 (`create_session`)
|
||||
//! - 获取或复用会话 (`get_or_create_session`)
|
||||
//! - 移除会话 (`remove_session`)
|
||||
//! - 检测会话健康状态(channel 是否关闭)
|
||||
//!
|
||||
//! 2. **Prompt 构建**
|
||||
//! - 构建纯文本 Prompt (`build_text_prompt_request`)
|
||||
//! - 构建带附件的 Prompt (`build_prompt_request_with_attachments`)
|
||||
//! - 将 request_id 放入 meta 字段
|
||||
//!
|
||||
//! 3. **路径处理**
|
||||
//! - 路径规范化 (`normalize_path`)
|
||||
//! - 确保项目目录存在 (`ensure_project_dir`)
|
||||
//!
|
||||
//! ## 架构说明
|
||||
//!
|
||||
//! 使用依赖注入的 `SessionRegistry` 进行会话存储:
|
||||
//! - 统一使用注入的 `SessionRegistry`(通常是 `AGENT_REGISTRY`)
|
||||
//!
|
||||
//! ## 与 Worker 的协作
|
||||
//!
|
||||
//! ```text
|
||||
//! AcpAgentWorker (acp_worker.rs)
|
||||
//! │
|
||||
//! │ 1. 调用 get_or_create_session()
|
||||
//! ▼
|
||||
//! AcpSessionManager
|
||||
//! │
|
||||
//! │ 2. 通过 SessionRegistry 获取/创建会话
|
||||
//! │ 3. 通过 SacpClaudeCodeLauncher 启动 Agent
|
||||
//! ▼
|
||||
//! SessionRegistry (注入的 AGENT_REGISTRY)
|
||||
//! ```
|
||||
|
||||
use std::path::{Component, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_config::PromptBuilder;
|
||||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use agent_client_protocol::schema::{ContentBlock, PromptRequest, SessionId, TextContent};
|
||||
use shared_types::{
|
||||
AgentLifecycle, AgentStatus, ModelProviderConfig, ProjectAndAgentInfo, SessionEntry,
|
||||
};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::PromptMessage;
|
||||
use crate::launcher::ClaudeCodeLauncher;
|
||||
use crate::traits::{AgentStartConfig, SessionNotifier, SessionRegistry};
|
||||
|
||||
/// ACP 会话管理器 (SACP 版本)
|
||||
///
|
||||
/// 管理所有活跃的 ACP 会话,提供:
|
||||
/// - 会话创建和复用
|
||||
/// - 模型配置变化检测
|
||||
/// - 会话生命周期管理
|
||||
///
|
||||
/// ## 泛型参数
|
||||
/// - `N`: SessionNotifier 实现,用于推送 SSE 消息
|
||||
/// - `R`: SessionRegistry 实现,用于存储会话数据(通常是 AGENT_REGISTRY)
|
||||
pub struct AcpSessionManager<N: SessionNotifier, R: SessionRegistry> {
|
||||
/// 会话注册表(注入的 SessionRegistry)
|
||||
registry: Arc<R>,
|
||||
/// 会话通知器
|
||||
notifier: Arc<N>,
|
||||
}
|
||||
|
||||
impl<N: SessionNotifier + 'static, R: SessionRegistry> AcpSessionManager<N, R>
|
||||
where
|
||||
R::Entry: Into<ProjectAndAgentInfo> + From<ProjectAndAgentInfo>,
|
||||
{
|
||||
/// 创建新的会话管理器
|
||||
///
|
||||
/// # 参数
|
||||
/// - `notifier`: 会话通知器
|
||||
/// - `registry`: 会话注册表(通常注入 AGENT_REGISTRY)
|
||||
pub fn new(notifier: Arc<N>, registry: Arc<R>) -> Self {
|
||||
Self { registry, notifier }
|
||||
}
|
||||
|
||||
/// 获取会话信息
|
||||
pub fn get_session(&self, project_id: &str) -> Option<R::Entry> {
|
||||
self.registry.get(project_id)
|
||||
}
|
||||
|
||||
/// 检查会话是否存在
|
||||
pub fn contains_session(&self, project_id: &str) -> bool {
|
||||
self.registry.contains(project_id)
|
||||
}
|
||||
|
||||
/// 移除会话
|
||||
pub fn remove_session(&self, project_id: &str) -> Option<R::Entry> {
|
||||
self.registry.remove(project_id)
|
||||
}
|
||||
|
||||
/// 获取所有会话的 project_id 列表
|
||||
pub fn list_sessions(&self) -> Vec<String> {
|
||||
self.registry.list_project_ids()
|
||||
}
|
||||
|
||||
/// 获取会话数量
|
||||
pub fn session_count(&self) -> usize {
|
||||
self.registry.count()
|
||||
}
|
||||
|
||||
/// 获取 registry 的 Arc 引用
|
||||
pub fn registry(&self) -> Arc<R> {
|
||||
self.registry.clone()
|
||||
}
|
||||
|
||||
/// 获取 notifier 的 Arc 引用
|
||||
pub fn notifier(&self) -> Arc<N> {
|
||||
self.notifier.clone()
|
||||
}
|
||||
|
||||
/// 规范化项目路径
|
||||
///
|
||||
/// - 如果是相对路径,先与当前目录拼接
|
||||
/// - 去除路径中的 "./"(CurDir 组件)
|
||||
pub fn normalize_path(path: &PathBuf) -> PathBuf {
|
||||
let joined_path = if path.is_absolute() {
|
||||
path.clone()
|
||||
} else {
|
||||
std::env::current_dir().unwrap_or_default().join(path)
|
||||
};
|
||||
|
||||
joined_path
|
||||
.components()
|
||||
.filter(|c| !matches!(c, Component::CurDir))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 确保项目目录存在
|
||||
pub async fn ensure_project_dir(path: &PathBuf) -> Result<()> {
|
||||
if !path.exists() {
|
||||
info!("Project path does not exist, creating: {:?}", path);
|
||||
tokio::fs::create_dir_all(path).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 构建基础 Prompt 请求(仅文本内容)
|
||||
///
|
||||
/// 使用 ACP 协议分离模式:
|
||||
/// - 系统提示词已在 NewSessionRequest._meta.systemPrompt 中传递
|
||||
/// - 这里只构建纯用户提示词
|
||||
///
|
||||
/// 注意:附件转换由调用方处理(因为涉及文件 I/O)
|
||||
pub fn build_text_prompt_request(
|
||||
prompt: &PromptMessage,
|
||||
session_id: SessionId,
|
||||
) -> Result<PromptRequest> {
|
||||
// 构建纯用户提示词
|
||||
let final_prompt = if prompt.data_source_attachments.is_empty() {
|
||||
PromptBuilder::build_user_prompt(&prompt.content)
|
||||
} else {
|
||||
PromptBuilder::build_user_prompt_with_data_sources(
|
||||
&prompt.content,
|
||||
&prompt.data_source_attachments,
|
||||
)
|
||||
};
|
||||
|
||||
// 创建文本内容块
|
||||
let text_block = ContentBlock::Text(TextContent::new(final_prompt));
|
||||
let content_blocks = vec![text_block];
|
||||
|
||||
// 将 request_id 放入 meta 字段
|
||||
debug!(
|
||||
"🔧 [build_prompt] Putting request_id={} into PromptRequest.meta",
|
||||
prompt.request_id
|
||||
);
|
||||
let mut meta = serde_json::Map::new();
|
||||
meta.insert(
|
||||
"request_id".to_string(),
|
||||
serde_json::Value::String(prompt.request_id.clone()),
|
||||
);
|
||||
|
||||
Ok(PromptRequest::new(session_id, content_blocks).meta(meta))
|
||||
}
|
||||
|
||||
/// 构建带附件的 Prompt 请求
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `prompt` - 提示消息
|
||||
/// * `session_id` - 会话 ID
|
||||
/// * `attachment_blocks` - 已转换的附件内容块
|
||||
pub fn build_prompt_request_with_attachments(
|
||||
prompt: &PromptMessage,
|
||||
session_id: SessionId,
|
||||
attachment_blocks: Vec<ContentBlock>,
|
||||
) -> Result<PromptRequest> {
|
||||
// 构建纯用户提示词
|
||||
let final_prompt = if prompt.data_source_attachments.is_empty() {
|
||||
PromptBuilder::build_user_prompt(&prompt.content)
|
||||
} else {
|
||||
PromptBuilder::build_user_prompt_with_data_sources(
|
||||
&prompt.content,
|
||||
&prompt.data_source_attachments,
|
||||
)
|
||||
};
|
||||
|
||||
// 创建文本内容块
|
||||
let text_block = ContentBlock::Text(TextContent::new(final_prompt));
|
||||
let mut content_blocks = vec![text_block];
|
||||
|
||||
// 添加附件内容块
|
||||
content_blocks.extend(attachment_blocks);
|
||||
|
||||
// 将 request_id 放入 meta 字段
|
||||
debug!(
|
||||
"🔧 [build_prompt] Putting request_id={} into PromptRequest.meta",
|
||||
prompt.request_id
|
||||
);
|
||||
let mut meta = serde_json::Map::new();
|
||||
meta.insert(
|
||||
"request_id".to_string(),
|
||||
serde_json::Value::String(prompt.request_id.clone()),
|
||||
);
|
||||
|
||||
Ok(PromptRequest::new(session_id, content_blocks).meta(meta))
|
||||
}
|
||||
|
||||
/// 创建新的 Agent 会话 (SACP 版本)
|
||||
///
|
||||
/// 启动 Agent 进程并建立 SACP 连接
|
||||
///
|
||||
/// # 参数
|
||||
/// - `project_id`: 项目 ID
|
||||
/// - `project_path`: 项目路径
|
||||
/// - `model_provider`: 模型提供者配置
|
||||
/// - `start_config`: Agent 启动配置
|
||||
/// - `service_uuid`: 与此 Agent 关联的唯一 UUID
|
||||
///
|
||||
/// # 返回值
|
||||
/// - `R::Entry`: 会话条目
|
||||
pub async fn create_session(
|
||||
&self,
|
||||
project_id: String,
|
||||
project_path: PathBuf,
|
||||
_session_id_hint: Option<String>,
|
||||
model_provider: Option<ModelProviderConfig>,
|
||||
start_config: AgentStartConfig,
|
||||
service_uuid: Option<String>,
|
||||
) -> Result<R::Entry> {
|
||||
let agent_info = self
|
||||
.create_session_internal(
|
||||
project_id.clone(),
|
||||
project_path,
|
||||
model_provider,
|
||||
start_config,
|
||||
service_uuid,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 存储会话信息到 registry
|
||||
let session_id_str = agent_info.session_id().to_string();
|
||||
self.registry
|
||||
.insert(&project_id, &session_id_str, agent_info.clone());
|
||||
|
||||
Ok(agent_info)
|
||||
}
|
||||
|
||||
/// 内部方法:创建 Agent 会话但不插入到 registry
|
||||
///
|
||||
/// 用于 entry API 优化,避免重复插入
|
||||
async fn create_session_internal(
|
||||
&self,
|
||||
project_id: String,
|
||||
project_path: PathBuf,
|
||||
model_provider: Option<ModelProviderConfig>,
|
||||
start_config: AgentStartConfig,
|
||||
service_uuid: Option<String>,
|
||||
) -> Result<R::Entry> {
|
||||
info!(
|
||||
"Creating Agent session, project ID: {}",
|
||||
project_id
|
||||
);
|
||||
|
||||
// 创建 SACP 启动器
|
||||
let launcher = ClaudeCodeLauncher::new(self.notifier.clone());
|
||||
|
||||
// 记录是否使用了 resume(仅用于日志)
|
||||
let has_resume = start_config.resume_session_id.is_some();
|
||||
if has_resume {
|
||||
info!(
|
||||
"📌 Starting Agent with resume: session_id={:?}",
|
||||
start_config.resume_session_id
|
||||
);
|
||||
}
|
||||
|
||||
// 启动 Agent (SACP 版本)
|
||||
// 如果 resume 失败,直接返回错误,让上层(rcoder)决定是否降级重试
|
||||
let connection_info = launcher
|
||||
.launch(
|
||||
project_id.clone(),
|
||||
project_path.clone(),
|
||||
model_provider.clone(),
|
||||
start_config.clone(),
|
||||
self.registry.clone(),
|
||||
service_uuid,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!(
|
||||
"✅ Agent session created successfully, session ID: {}",
|
||||
connection_info.session_id
|
||||
);
|
||||
|
||||
// 创建 ProjectAndAgentInfo
|
||||
let lifecycle_handle =
|
||||
Some(connection_info.lifecycle_guard.clone() as Arc<dyn AgentLifecycle>);
|
||||
let now = Utc::now();
|
||||
let agent_info = ProjectAndAgentInfo {
|
||||
project_id: project_id.clone(),
|
||||
session_id: connection_info.session_id.clone(),
|
||||
prompt_tx: connection_info.prompt_tx,
|
||||
cancel_tx: connection_info.cancel_tx,
|
||||
model_provider: model_provider.clone(),
|
||||
request_id: None,
|
||||
status: AgentStatus::Idle,
|
||||
last_activity: now,
|
||||
created_at: now,
|
||||
stop_handle: lifecycle_handle,
|
||||
};
|
||||
|
||||
// 返回 agent_info(不插入 registry,由调用方处理)
|
||||
Ok(agent_info.into())
|
||||
}
|
||||
|
||||
/// 获取或创建会话 (SACP 版本)
|
||||
///
|
||||
/// 如果会话已存在且模型配置未变化,则复用;否则创建新会话
|
||||
///
|
||||
/// # 优化说明
|
||||
/// 获取或创建会话
|
||||
///
|
||||
/// # 参数
|
||||
/// - `project_id`: 项目 ID
|
||||
/// - `project_path`: 项目路径
|
||||
/// - `session_id_hint`: 会话 ID 提示(用于恢复现有会话)
|
||||
/// - `model_provider`: 模型提供者配置
|
||||
/// - `start_config`: Agent 启动配置
|
||||
/// - `service_uuid`: 与此 Agent 关联的唯一 UUID
|
||||
///
|
||||
/// # 返回值
|
||||
/// - `R::Entry`: 会话条目
|
||||
/// - `bool`: 是否是新创建的会话
|
||||
///
|
||||
/// # 并发安全性
|
||||
///
|
||||
/// 使用"检查-创建-插入"三阶段模式避免在持有 entry 期间调用 `.await`:
|
||||
///
|
||||
/// 1. **快速检查**:检查会话是否已存在且有效
|
||||
/// 2. **创建会话**:如果需要创建,在**不持有锁**的情况下创建会话(.await)
|
||||
/// 3. **原子性插入**:使用 entry API 原子性插入,如果其他线程已创建则使用已存在的
|
||||
///
|
||||
/// 这样确保:
|
||||
/// - 不会在持有 DashMap entry 期间跨越 await 点
|
||||
/// - 同一 project_id 最多只会创建一个会话
|
||||
/// - 高并发下不会阻塞其他 project_id 的访问(DashMap 分段锁特性)
|
||||
pub async fn get_or_create_session(
|
||||
&self,
|
||||
project_id: &str,
|
||||
project_path: PathBuf,
|
||||
session_id_hint: Option<String>, // 🔥 修复:使用此参数查找现有会话
|
||||
model_provider: Option<ModelProviderConfig>,
|
||||
start_config: AgentStartConfig,
|
||||
service_uuid: Option<String>,
|
||||
) -> Result<(R::Entry, bool)> {
|
||||
use dashmap::mapref::entry::Entry;
|
||||
|
||||
let project_id_key = project_id.to_string();
|
||||
|
||||
// 🆕 第一阶段:如果提供了 session_id_hint,先尝试通过 session_id 查找现有会话
|
||||
// 🔥 优化:使用 get_entry_by_session() 避免两次调用之间的竞态窗口
|
||||
if let Some(ref hint_sid) = session_id_hint {
|
||||
// 一次性查询:通过 session_id 直接获取 agent_info
|
||||
if let Some(existing) = self.registry.get_entry_by_session(hint_sid) {
|
||||
// 验证 project_id 是否匹配(防御性编程)
|
||||
if existing.project_id() == project_id {
|
||||
let channel_closed = existing.is_channel_closed();
|
||||
let model_changed = existing.is_model_config_changed(&model_provider);
|
||||
|
||||
if !channel_closed && !model_changed {
|
||||
info!(
|
||||
"[SESSION] Reusing existing session via session_id_hint: project_id={}, session_id={}",
|
||||
project_id, hint_sid
|
||||
);
|
||||
return Ok((existing, false));
|
||||
}
|
||||
|
||||
if channel_closed {
|
||||
info!(
|
||||
"⚠️ [SESSION] Session channel closed for session_id_hint, need to rebuild: project_id={}, session_id={}",
|
||||
project_id, hint_sid
|
||||
);
|
||||
}
|
||||
if model_changed {
|
||||
info!(
|
||||
"🔄 [SESSION] Model config changed for session_id_hint, need to rebuild: project_id={}, session_id={}",
|
||||
project_id, hint_sid
|
||||
);
|
||||
}
|
||||
// 会话无效,继续后续逻辑(可能需要重建)
|
||||
} else {
|
||||
info!(
|
||||
"⚠️ [SESSION] session_id_hint belongs to different project: hint_project={}, current_project={}, session_id={}",
|
||||
existing.project_id(),
|
||||
project_id,
|
||||
hint_sid
|
||||
);
|
||||
// session_id 属于其他 project,不能复用,继续后续逻辑
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
"🔍 [SESSION] session_id_hint does not exist in registry: session_id={}",
|
||||
hint_sid
|
||||
);
|
||||
// session_id 不存在,继续后续逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 第一阶段:快速检查现有会话
|
||||
if let Entry::Occupied(occupied_entry) = self.registry.entry(project_id_key.clone()) {
|
||||
let existing = occupied_entry.get();
|
||||
|
||||
// 🔥 显式检查 Pending 状态 - PendingGuard 创建的占位符需要被替换
|
||||
if *existing.status() == AgentStatus::Pending {
|
||||
info!(
|
||||
"🔄 [SESSION] Detected Pending placeholder, preparing to replace: project_id={}",
|
||||
project_id
|
||||
);
|
||||
// 显式释放 entry 锁
|
||||
drop(occupied_entry);
|
||||
|
||||
// 第二阶段:创建新会话(不持有锁)
|
||||
let new_session = self
|
||||
.create_session_internal(
|
||||
project_id_key.clone(),
|
||||
project_path,
|
||||
model_provider,
|
||||
start_config,
|
||||
service_uuid,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 第三阶段:原子性替换(插入新会话,覆盖 pending 占位符)
|
||||
match self.registry.entry(project_id_key.clone()) {
|
||||
Entry::Vacant(entry) => {
|
||||
let new_session_id = new_session.session_id().to_string();
|
||||
entry.insert(new_session.clone());
|
||||
info!(
|
||||
"✅ [SESSION] Inserted new session to replace pending: project_id={}, session_id={}",
|
||||
project_id, new_session_id
|
||||
);
|
||||
return Ok((new_session, true));
|
||||
}
|
||||
Entry::Occupied(mut entry) => {
|
||||
// 检查是 Pending 占位符还是其他线程创建的真实会话
|
||||
let current_session = entry.get();
|
||||
let current_status = current_session.status();
|
||||
|
||||
if *current_status == AgentStatus::Pending {
|
||||
// 仍然是 Pending 占位符,替换它
|
||||
let old_session_id = current_session.session_id().to_string();
|
||||
let new_session_id = new_session.session_id().to_string();
|
||||
entry.insert(new_session.clone());
|
||||
info!(
|
||||
"✅ [SESSION] Replaced pending placeholder: project_id={}, {} → {}",
|
||||
project_id, old_session_id, new_session_id
|
||||
);
|
||||
return Ok((new_session, true));
|
||||
} else {
|
||||
// 其他线程已经创建了真实会话,使用已存在的(丢弃我们创建的)
|
||||
let existing_session = current_session.clone();
|
||||
let created_session_id = new_session.session_id().to_string();
|
||||
let existing_session_id = existing_session.session_id().to_string();
|
||||
info!(
|
||||
"🔄 [SESSION] Detected concurrent creation (other thread already created real session): project_id={}, discarded session_id={}, using session_id={}",
|
||||
project_id, created_session_id, existing_session_id
|
||||
);
|
||||
// new_session 会被 drop,AgentLifecycleGuard 会清理 Agent 进程
|
||||
return Ok((existing_session, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let channel_closed = existing.is_channel_closed();
|
||||
let model_changed = existing.is_model_config_changed(&model_provider);
|
||||
|
||||
if !channel_closed && !model_changed {
|
||||
info!("Reuse Agent session, project ID: {}", project_id);
|
||||
return Ok((existing.clone(), false));
|
||||
}
|
||||
|
||||
// 需要重建会话,先克隆必要数据并释放锁
|
||||
let session_id_str = existing.session_id().to_string();
|
||||
drop(occupied_entry); // 显式释放 entry 锁
|
||||
|
||||
if channel_closed {
|
||||
info!(
|
||||
"⚠️ Detected session channel closed (Agent process exited), rebuilding session, project ID: {}, old session_id: {}",
|
||||
project_id, session_id_str
|
||||
);
|
||||
}
|
||||
if model_changed {
|
||||
info!(
|
||||
"Model config changed, restarting Agent session, project ID: {}, old session_id: {}",
|
||||
project_id, session_id_str
|
||||
);
|
||||
}
|
||||
|
||||
// 第二阶段:在不持有锁的情况下创建新会话
|
||||
let new_session = self
|
||||
.create_session_internal(
|
||||
project_id_key.clone(),
|
||||
project_path,
|
||||
model_provider,
|
||||
start_config,
|
||||
service_uuid,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 第三阶段:原子性插入(使用 entry API 防止并发创建)
|
||||
match self.registry.entry(project_id_key.clone()) {
|
||||
Entry::Vacant(entry) => {
|
||||
// 其他线程还没有创建,使用我们创建的会话
|
||||
let new_session_id = new_session.session_id().to_string();
|
||||
entry.insert(new_session.clone());
|
||||
info!(
|
||||
"✅ Session rebuilt, project ID: {}, new session_id: {}",
|
||||
project_id, new_session_id
|
||||
);
|
||||
return Ok((new_session, true));
|
||||
}
|
||||
Entry::Occupied(entry) => {
|
||||
// 其他线程已经创建了会话,使用已存在的(丢弃我们创建的)
|
||||
let existing_session = entry.get().clone();
|
||||
let created_session_id = new_session.session_id().to_string();
|
||||
let existing_session_id = existing_session.session_id().to_string();
|
||||
info!(
|
||||
"🔄 [SESSION] Detected concurrent creation, using other thread's session: project_id={}, discarded session_id={}, using session_id={}",
|
||||
project_id, created_session_id, existing_session_id
|
||||
);
|
||||
// new_session 会被 drop,Session 的 Drop 实现会清理 Agent 进程
|
||||
return Ok((existing_session, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 会话不存在,需要创建新会话
|
||||
info!(
|
||||
"Session not found, creating new session, project ID: {}",
|
||||
project_id
|
||||
);
|
||||
|
||||
// 第二阶段:在不持有锁的情况下创建新会话
|
||||
let new_session = self
|
||||
.create_session_internal(
|
||||
project_id_key.clone(),
|
||||
project_path,
|
||||
model_provider,
|
||||
start_config,
|
||||
service_uuid,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 第三阶段:原子性插入
|
||||
match self.registry.entry(project_id_key.clone()) {
|
||||
Entry::Vacant(entry) => {
|
||||
// 其他线程还没有创建,使用我们创建的会话
|
||||
let session_id_str = new_session.session_id().to_string();
|
||||
entry.insert(new_session.clone());
|
||||
info!(
|
||||
"✅ New session created, project ID: {}, session_id: {}",
|
||||
project_id, session_id_str
|
||||
);
|
||||
Ok((new_session, true))
|
||||
}
|
||||
Entry::Occupied(entry) => {
|
||||
// 其他线程已经创建了会话,使用已存在的(丢弃我们创建的)
|
||||
let existing_session = entry.get().clone();
|
||||
let created_session_id = new_session.session_id().to_string();
|
||||
let existing_session_id = existing_session.session_id().to_string();
|
||||
info!(
|
||||
"🔄 [SESSION] Detected concurrent creation, using session created by other thread: project_id={}, discarding session_id={}, using session_id={}",
|
||||
project_id, created_session_id, existing_session_id
|
||||
);
|
||||
// new_session 会被 drop,Session 的 Drop 实现会清理 Agent 进程
|
||||
Ok((existing_session, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送 Prompt 到指定会话(仅文本)
|
||||
///
|
||||
/// 使用有界通道,提供背压保护。如果通道已满,会异步等待直到有空间。
|
||||
pub async fn send_text_prompt(&self, project_id: &str, prompt: &PromptMessage) -> Result<()> {
|
||||
let session = self
|
||||
.get_session(project_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Session not found: {}", project_id))?;
|
||||
|
||||
let prompt_request = Self::build_text_prompt_request(prompt, session.session_id().clone())?;
|
||||
|
||||
session
|
||||
.prompt_tx()
|
||||
.send(prompt_request)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("send Prompt requestfailed: {:?}", e);
|
||||
anyhow::anyhow!("Failed to send Prompt request: {:?}", e)
|
||||
})?;
|
||||
|
||||
info!("Prompt request already sent, project ID: {}", project_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 发送 Prompt 请求到指定会话
|
||||
///
|
||||
/// 使用有界通道,提供背压保护。如果通道已满,会异步等待直到有空间。
|
||||
pub async fn send_prompt_request(
|
||||
&self,
|
||||
project_id: &str,
|
||||
prompt_request: PromptRequest,
|
||||
) -> Result<()> {
|
||||
let session = self
|
||||
.get_session(project_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Session not found: {}", project_id))?;
|
||||
|
||||
session
|
||||
.prompt_tx()
|
||||
.send(prompt_request)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("send Prompt requestfailed: {:?}", e);
|
||||
anyhow::anyhow!("Failed to send Prompt request: {:?}", e)
|
||||
})?;
|
||||
|
||||
info!("Prompt request already sent, project ID: {}", project_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: SessionNotifier + 'static, R: SessionRegistry> std::fmt::Debug for AcpSessionManager<N, R> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AcpSessionManager")
|
||||
.field("session_count", &self.registry.count())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
221
qiming-rcoder/crates/agent_abstraction/src/session/worker.rs
Normal file
221
qiming-rcoder/crates/agent_abstraction/src/session/worker.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
//! Agent Worker 抽象
|
||||
//!
|
||||
//! 定义请求处理的通用接口
|
||||
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use dashmap::DashMap;
|
||||
use agent_client_protocol::schema::{ContentBlock, PromptRequest};
|
||||
use shared_types::{
|
||||
AgentLifecycle, CancelNotificationRequestWrapper, ModelProviderConfig, ServiceType,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::PromptMessage;
|
||||
|
||||
/// Worker 请求
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkerRequest {
|
||||
/// Agent 提示消息
|
||||
pub prompt_message: PromptMessage,
|
||||
/// 模型提供商配置
|
||||
pub model_provider: Option<ModelProviderConfig>,
|
||||
/// 预处理的附件内容块
|
||||
/// 由 agent_runner 使用 ContentBuilder 预处理
|
||||
pub attachment_blocks: Option<Vec<ContentBlock>>,
|
||||
/// 🔥 关联的 service UUID(用于 API 密钥管理)
|
||||
pub service_uuid: Option<String>,
|
||||
/// 🔥 共享的 API 密钥管理器(用于自动清理)
|
||||
pub shared_api_key_manager: Option<Arc<DashMap<String, ModelProviderConfig>>>,
|
||||
}
|
||||
|
||||
impl WorkerRequest {
|
||||
/// 创建新的 Worker 请求
|
||||
pub fn new(prompt_message: PromptMessage, model_provider: Option<ModelProviderConfig>) -> Self {
|
||||
Self {
|
||||
prompt_message,
|
||||
model_provider,
|
||||
attachment_blocks: None,
|
||||
service_uuid: None,
|
||||
shared_api_key_manager: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置 service_uuid
|
||||
pub fn with_service_uuid(mut self, service_uuid: Option<String>) -> Self {
|
||||
self.service_uuid = service_uuid;
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置 shared_api_key_manager
|
||||
pub fn with_key_manager(
|
||||
mut self,
|
||||
key_manager: Option<Arc<DashMap<String, ModelProviderConfig>>>,
|
||||
) -> Self {
|
||||
self.shared_api_key_manager = key_manager;
|
||||
self
|
||||
}
|
||||
|
||||
/// 获取项目 ID
|
||||
pub fn project_id(&self) -> &str {
|
||||
&self.prompt_message.project_id
|
||||
}
|
||||
|
||||
/// 获取项目路径
|
||||
pub fn project_path(&self) -> &PathBuf {
|
||||
&self.prompt_message.project_path
|
||||
}
|
||||
|
||||
/// 获取请求 ID
|
||||
pub fn request_id(&self) -> &str {
|
||||
&self.prompt_message.request_id
|
||||
}
|
||||
}
|
||||
|
||||
/// 会话句柄
|
||||
///
|
||||
/// 用于传递会话句柄给 agent_runner 更新全局 MAP
|
||||
#[derive(Clone)]
|
||||
pub struct SessionHandles {
|
||||
pub prompt_tx: mpsc::Sender<PromptRequest>,
|
||||
pub cancel_tx: mpsc::Sender<CancelNotificationRequestWrapper>,
|
||||
pub lifecycle_handle: Option<Arc<dyn AgentLifecycle>>,
|
||||
}
|
||||
|
||||
// 手动实现 Debug,跳过不支持 Debug 的 lifecycle_handle
|
||||
impl std::fmt::Debug for SessionHandles {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SessionHandles")
|
||||
.field("prompt_tx", &"Sender<PromptRequest>")
|
||||
.field("cancel_tx", &"Sender<CancelNotificationRequestWrapper>")
|
||||
.field(
|
||||
"lifecycle_handle",
|
||||
&self
|
||||
.lifecycle_handle
|
||||
.as_ref()
|
||||
.map(|_| "Some(AgentLifecycle)"),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Worker 响应
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkerResponse {
|
||||
/// 项目 ID
|
||||
pub project_id: String,
|
||||
/// 会话 ID
|
||||
pub session_id: String,
|
||||
/// 错误信息(如果有)
|
||||
pub error: Option<String>,
|
||||
/// 请求 ID
|
||||
pub request_id: Option<String>,
|
||||
/// 服务类型
|
||||
pub service_type: ServiceType,
|
||||
/// 标识是否是新创建的会话
|
||||
pub is_new_session: bool,
|
||||
/// 会话句柄(仅新会话时有值)
|
||||
pub session_handles: Option<SessionHandles>,
|
||||
}
|
||||
|
||||
impl WorkerResponse {
|
||||
/// 创建新会话的成功响应
|
||||
pub fn new_session_success(
|
||||
project_id: String,
|
||||
session_id: String,
|
||||
request_id: Option<String>,
|
||||
service_type: ServiceType,
|
||||
handles: SessionHandles,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
session_id,
|
||||
error: None,
|
||||
request_id,
|
||||
service_type,
|
||||
is_new_session: true,
|
||||
session_handles: Some(handles),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建复用会话的成功响应
|
||||
pub fn reuse_session_success(
|
||||
project_id: String,
|
||||
session_id: String,
|
||||
request_id: Option<String>,
|
||||
service_type: ServiceType,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
session_id,
|
||||
error: None,
|
||||
request_id,
|
||||
service_type,
|
||||
is_new_session: false,
|
||||
session_handles: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建成功响应(旧版本,保持向后兼容)
|
||||
#[deprecated(note = "Use new_session_success or reuse_session_success instead")]
|
||||
pub fn success(
|
||||
project_id: String,
|
||||
session_id: String,
|
||||
request_id: Option<String>,
|
||||
service_type: ServiceType,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
session_id,
|
||||
error: None,
|
||||
request_id,
|
||||
service_type,
|
||||
is_new_session: false,
|
||||
session_handles: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建错误响应
|
||||
pub fn error(
|
||||
project_id: String,
|
||||
session_id: String,
|
||||
error: String,
|
||||
request_id: Option<String>,
|
||||
service_type: ServiceType,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
session_id,
|
||||
error: Some(error),
|
||||
request_id,
|
||||
service_type,
|
||||
is_new_session: false,
|
||||
session_handles: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否成功
|
||||
pub fn is_success(&self) -> bool {
|
||||
self.error.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Agent Worker Trait
|
||||
///
|
||||
/// 定义请求处理的抽象接口,允许不同的实现策略
|
||||
#[async_trait]
|
||||
pub trait AgentWorker: Send + Sync {
|
||||
/// 处理单个请求
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request` - Worker 请求
|
||||
///
|
||||
/// # Returns
|
||||
/// 处理结果
|
||||
async fn process_request(&self, request: WorkerRequest) -> Result<WorkerResponse>;
|
||||
|
||||
/// 获取 Worker 名称(用于日志和调试)
|
||||
fn name(&self) -> &'static str;
|
||||
}
|
||||
413
qiming-rcoder/crates/agent_abstraction/src/traits/agent.rs
Normal file
413
qiming-rcoder/crates/agent_abstraction/src/traits/agent.rs
Normal file
@@ -0,0 +1,413 @@
|
||||
//! Agent trait definition.
|
||||
|
||||
use agent_client_protocol::schema::McpServer;
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// Agent startup configuration
|
||||
///
|
||||
/// Contains additional configuration information required to start an Agent,
|
||||
/// such as system prompts, MCP server configurations, etc.
|
||||
/// These configurations are passed to the Agent via ACP protocol's NewSessionRequest.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentStartConfig {
|
||||
/// System prompt (passed via meta.systemPrompt.append)
|
||||
///
|
||||
/// Uses the `_meta.systemPrompt.append` mode of the ACP protocol to preserve
|
||||
/// the base capabilities of the claude_code preset while appending custom system prompts.
|
||||
pub system_prompt: Option<String>,
|
||||
|
||||
/// MCP server configuration
|
||||
pub mcp_servers: Vec<McpServer>,
|
||||
|
||||
/// Additional meta fields
|
||||
///
|
||||
/// Can pass any additional configuration information to the Agent
|
||||
pub extra_meta: Option<serde_json::Map<String, serde_json::Value>>,
|
||||
|
||||
/// Service type (for loading corresponding config, required)
|
||||
pub service_type: shared_types::ServiceType,
|
||||
|
||||
/// Session ID for resuming sessions
|
||||
///
|
||||
/// When resuming a previous session, pass the previous session_id,
|
||||
/// which will be passed to the Agent via `_meta.claudeCode.options.resume`.
|
||||
pub resume_session_id: Option<String>,
|
||||
|
||||
/// 🆕 Agent server config override (for custom Agent startup command)
|
||||
///
|
||||
/// When the user request specifies agent_server config, use this config
|
||||
/// to override the default config. Includes command, args, env, etc.
|
||||
pub agent_server_override: Option<shared_types::ChatAgentServerConfig>,
|
||||
|
||||
/// 🆕 ACP session creation timeout (seconds), default 100
|
||||
///
|
||||
/// Maximum wait time for Agent to create a new session
|
||||
/// (may require more time when there are many MCP tools)
|
||||
pub acp_session_create_timeout_secs: Option<u64>,
|
||||
|
||||
/// 🆕 Agent cancel call timeout (seconds), default 10
|
||||
///
|
||||
/// Maximum wait time for Agent internal cancel operations
|
||||
pub agent_cancel_timeout_secs: Option<u64>,
|
||||
}
|
||||
|
||||
impl AgentStartConfig {
|
||||
/// Create a new AgentStartConfig
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `service_type`: Service type (required)
|
||||
pub fn new(service_type: shared_types::ServiceType) -> Self {
|
||||
Self {
|
||||
system_prompt: None,
|
||||
mcp_servers: Vec::new(),
|
||||
extra_meta: None,
|
||||
service_type,
|
||||
resume_session_id: None,
|
||||
agent_server_override: None,
|
||||
acp_session_create_timeout_secs: None,
|
||||
agent_cancel_timeout_secs: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set system prompt
|
||||
pub fn with_system_prompt(mut self, system_prompt: String) -> Self {
|
||||
self.system_prompt = Some(system_prompt);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set MCP server configuration
|
||||
pub fn with_mcp_servers(mut self, mcp_servers: Vec<McpServer>) -> Self {
|
||||
self.mcp_servers = mcp_servers;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set additional meta fields
|
||||
pub fn with_extra_meta(
|
||||
mut self,
|
||||
extra_meta: serde_json::Map<String, serde_json::Value>,
|
||||
) -> Self {
|
||||
self.extra_meta = Some(extra_meta);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set service type
|
||||
pub fn with_service_type(mut self, service_type: shared_types::ServiceType) -> Self {
|
||||
self.service_type = service_type;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set session ID for resuming sessions
|
||||
pub fn with_resume_session_id(mut self, session_id: String) -> Self {
|
||||
self.resume_session_id = Some(session_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// 🆕 Set Agent server config override
|
||||
pub fn with_agent_server_override(
|
||||
mut self,
|
||||
agent_server: shared_types::ChatAgentServerConfig,
|
||||
) -> Self {
|
||||
self.agent_server_override = Some(agent_server);
|
||||
self
|
||||
}
|
||||
|
||||
/// 🆕 Set ACP session creation timeout
|
||||
pub fn with_acp_session_create_timeout(mut self, timeout_secs: u64) -> Self {
|
||||
self.acp_session_create_timeout_secs = Some(timeout_secs);
|
||||
self
|
||||
}
|
||||
|
||||
/// 🆕 Set Agent cancel call timeout
|
||||
pub fn with_agent_cancel_timeout(mut self, timeout_secs: u64) -> Self {
|
||||
self.agent_cancel_timeout_secs = Some(timeout_secs);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the meta field for NewSessionRequest
|
||||
///
|
||||
/// Merges the system prompt and additional meta fields into a JSON Map
|
||||
/// for passing to the ACP protocol's NewSessionRequest.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns a Meta object containing `systemPrompt: { append: "..." }`
|
||||
pub fn build_meta(&self) -> serde_json::Map<String, serde_json::Value> {
|
||||
let mut meta = serde_json::Map::new();
|
||||
|
||||
// Add system prompt (using append mode)
|
||||
if let Some(ref system_prompt) = self.system_prompt {
|
||||
let mut system_prompt_obj = serde_json::Map::new();
|
||||
system_prompt_obj.insert(
|
||||
"append".to_string(),
|
||||
serde_json::Value::String(system_prompt.clone()),
|
||||
);
|
||||
meta.insert(
|
||||
"systemPrompt".to_string(),
|
||||
serde_json::Value::Object(system_prompt_obj),
|
||||
);
|
||||
info!(
|
||||
"[ACP] Sending system_prompt to agent: length={}",
|
||||
system_prompt.len()
|
||||
);
|
||||
debug!(
|
||||
"[ACP] system_prompt content (first 200 chars): \"{}\"",
|
||||
&system_prompt[..system_prompt.len().min(200)]
|
||||
);
|
||||
} else {
|
||||
info!("[ACP] No system_prompt to send to agent");
|
||||
}
|
||||
|
||||
// Build claudeCode.options structure
|
||||
// settingSources 控制 Claude Code Agent 从哪些来源加载配置和 skills。
|
||||
// 可选值: "user"(全局 ~/.claude/), "project"(项目 .claude/), "local"
|
||||
// 只加载 "project" 级别配置,避免全局 user 配置覆盖 ANTHROPIC_BASE_URL,
|
||||
// 同时允许项目目录下的 .claude/skills/ 自定义技能生效。
|
||||
// Refer to the TypeScript code on the Agent side:
|
||||
// resume: (params._meta as NewSessionMeta | undefined)?.claudeCode?.options?.resume
|
||||
let mut options = serde_json::Map::new();
|
||||
|
||||
// Only load project-level settings (skills), block global settings
|
||||
options.insert(
|
||||
"settingSources".to_string(),
|
||||
serde_json::Value::Array(vec![serde_json::Value::String("project".to_string())]),
|
||||
);
|
||||
|
||||
// Add resume session_id if present
|
||||
if let Some(ref session_id) = self.resume_session_id {
|
||||
options.insert(
|
||||
"resume".to_string(),
|
||||
serde_json::Value::String(session_id.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
let mut claude_code = serde_json::Map::new();
|
||||
claude_code.insert("options".to_string(), serde_json::Value::Object(options));
|
||||
meta.insert(
|
||||
"claudeCode".to_string(),
|
||||
serde_json::Value::Object(claude_code),
|
||||
);
|
||||
|
||||
// Merge additional meta fields
|
||||
if let Some(ref extra) = self.extra_meta {
|
||||
for (key, value) in extra {
|
||||
// Don't overwrite if key already exists (e.g., systemPrompt)
|
||||
if !meta.contains_key(key) {
|
||||
meta.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meta
|
||||
}
|
||||
|
||||
/// Check if there is a system prompt
|
||||
pub fn has_system_prompt(&self) -> bool {
|
||||
self.system_prompt.is_some()
|
||||
}
|
||||
|
||||
/// Check if there are MCP server configurations
|
||||
pub fn has_mcp_servers(&self) -> bool {
|
||||
!self.mcp_servers.is_empty()
|
||||
}
|
||||
|
||||
/// Build meta without resume parameter
|
||||
///
|
||||
/// Used when the list_sessions check finds the session does not exist.
|
||||
/// Similar to build_meta(), but without the claudeCode.options.resume field.
|
||||
///
|
||||
/// # Use Cases
|
||||
/// - User passed a session_id, but verification via list_sessions API found it doesn't exist
|
||||
/// - Create a new session while preserving other configs (system prompt, extra_meta, etc.)
|
||||
pub fn build_meta_without_resume(&self) -> serde_json::Map<String, serde_json::Value> {
|
||||
let mut meta = serde_json::Map::new();
|
||||
|
||||
// Only add system prompt (using append mode, consistent with build_meta)
|
||||
if let Some(ref system_prompt) = self.system_prompt {
|
||||
let mut system_prompt_obj = serde_json::Map::new();
|
||||
system_prompt_obj.insert(
|
||||
"append".to_string(),
|
||||
serde_json::Value::String(system_prompt.clone()),
|
||||
);
|
||||
meta.insert(
|
||||
"systemPrompt".to_string(),
|
||||
serde_json::Value::Object(system_prompt_obj),
|
||||
);
|
||||
info!(
|
||||
"[ACP] Sending system_prompt to agent (no resume): length={}",
|
||||
system_prompt.len()
|
||||
);
|
||||
debug!(
|
||||
"[ACP] system_prompt content (first 200 chars): \"{}\"",
|
||||
&system_prompt[..system_prompt.len().min(200)]
|
||||
);
|
||||
}
|
||||
|
||||
// Only load project-level settings (consistent with build_meta)
|
||||
let mut options = serde_json::Map::new();
|
||||
options.insert(
|
||||
"settingSources".to_string(),
|
||||
serde_json::Value::Array(vec![serde_json::Value::String("project".to_string())]),
|
||||
);
|
||||
|
||||
let mut claude_code = serde_json::Map::new();
|
||||
claude_code.insert("options".to_string(), serde_json::Value::Object(options));
|
||||
meta.insert(
|
||||
"claudeCode".to_string(),
|
||||
serde_json::Value::Object(claude_code),
|
||||
);
|
||||
|
||||
// Merge additional meta fields (don't overwrite existing keys)
|
||||
if let Some(ref extra) = self.extra_meta {
|
||||
for (key, value) in extra {
|
||||
if !meta.contains_key(key) {
|
||||
meta.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meta
|
||||
}
|
||||
}
|
||||
|
||||
/// Agent common Prompt message
|
||||
///
|
||||
/// This is the core data structure of the Agent abstraction layer, containing only
|
||||
/// the core information needed for the Agent to execute tasks.
|
||||
/// Does not contain business layer configurations (such as model_provider, mcp configs, etc.).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PromptMessage {
|
||||
/// Main prompt text entered by the user
|
||||
pub content: String,
|
||||
|
||||
/// Project ID
|
||||
pub project_id: String,
|
||||
|
||||
/// Project working directory path
|
||||
pub project_path: std::path::PathBuf,
|
||||
|
||||
/// Session ID (optional, if None the Agent will create a new session)
|
||||
pub session_id: Option<String>,
|
||||
|
||||
/// Request tracking ID
|
||||
pub request_id: String,
|
||||
|
||||
/// Attachment list (supports text, images, audio, documents, etc.)
|
||||
pub attachments: Vec<shared_types::Attachment>,
|
||||
|
||||
/// Data source attachments (JSON string array)
|
||||
pub data_source_attachments: Vec<String>,
|
||||
|
||||
/// Service type
|
||||
pub service_type: shared_types::ServiceType,
|
||||
|
||||
// === New fields (v2) ===
|
||||
/// System prompt override
|
||||
///
|
||||
/// If provided, will override the default system prompt configuration
|
||||
pub system_prompt_override: Option<String>,
|
||||
|
||||
/// User prompt template override
|
||||
///
|
||||
/// If provided, will use this template to replace the `{user_prompt}` variable
|
||||
pub user_prompt_template_override: Option<String>,
|
||||
|
||||
/// Agent runtime config override (MCP servers, etc.)
|
||||
///
|
||||
/// Contains Agent server configuration and MCP server configuration
|
||||
pub agent_config_override: Option<shared_types::ChatAgentConfig>,
|
||||
}
|
||||
|
||||
impl PromptMessage {
|
||||
/// Create a new PromptMessage
|
||||
pub fn new(
|
||||
content: String,
|
||||
project_id: String,
|
||||
project_path: std::path::PathBuf,
|
||||
request_id: String,
|
||||
service_type: shared_types::ServiceType,
|
||||
) -> Self {
|
||||
Self {
|
||||
content,
|
||||
project_id,
|
||||
project_path,
|
||||
session_id: None,
|
||||
request_id,
|
||||
attachments: Vec::new(),
|
||||
data_source_attachments: Vec::new(),
|
||||
service_type,
|
||||
// New fields default to None
|
||||
system_prompt_override: None,
|
||||
user_prompt_template_override: None,
|
||||
agent_config_override: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set session ID
|
||||
pub fn with_session_id(mut self, session_id: Option<String>) -> Self {
|
||||
self.session_id = session_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set attachment list
|
||||
pub fn with_attachments(mut self, attachments: Vec<shared_types::Attachment>) -> Self {
|
||||
self.attachments = attachments;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set data source attachments
|
||||
pub fn with_data_source_attachments(mut self, data_source_attachments: Vec<String>) -> Self {
|
||||
self.data_source_attachments = data_source_attachments;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set system prompt override
|
||||
pub fn with_system_prompt_override(mut self, system_prompt: Option<String>) -> Self {
|
||||
self.system_prompt_override = system_prompt;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set user prompt template override
|
||||
pub fn with_user_prompt_template_override(mut self, template: Option<String>) -> Self {
|
||||
self.user_prompt_template_override = template;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set Agent config override
|
||||
pub fn with_agent_config_override(
|
||||
mut self,
|
||||
config: Option<shared_types::ChatAgentConfig>,
|
||||
) -> Self {
|
||||
self.agent_config_override = config;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from ChatPrompt to PromptMessage
|
||||
impl From<shared_types::ChatPrompt> for PromptMessage {
|
||||
fn from(chat_prompt: shared_types::ChatPrompt) -> Self {
|
||||
info!(
|
||||
"[agent_abstraction] Converting ChatPrompt to PromptMessage, project_id={:?}, session_id={:?}, has_model_provider={}, has_agent_config_override={}",
|
||||
chat_prompt.project_id,
|
||||
chat_prompt.session_id,
|
||||
chat_prompt.model_provider.is_some(),
|
||||
chat_prompt.agent_config_override.is_some(),
|
||||
);
|
||||
|
||||
Self {
|
||||
content: chat_prompt.prompt,
|
||||
project_id: chat_prompt.project_id,
|
||||
project_path: chat_prompt.project_path,
|
||||
session_id: chat_prompt.session_id,
|
||||
request_id: chat_prompt.request_id.unwrap_or_else(|| {
|
||||
// Generate one if ChatPrompt doesn't have request_id
|
||||
uuid::Uuid::new_v4().to_string().replace("-", "")
|
||||
}),
|
||||
attachments: chat_prompt.attachments,
|
||||
data_source_attachments: chat_prompt.data_source_attachments,
|
||||
service_type: chat_prompt.service_type,
|
||||
// Map new fields
|
||||
system_prompt_override: chat_prompt.system_prompt_override,
|
||||
user_prompt_template_override: chat_prompt.user_prompt_template_override,
|
||||
agent_config_override: chat_prompt.agent_config_override,
|
||||
}
|
||||
}
|
||||
}
|
||||
9
qiming-rcoder/crates/agent_abstraction/src/traits/mod.rs
Normal file
9
qiming-rcoder/crates/agent_abstraction/src/traits/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! Trait definitions for agent abstraction.
|
||||
|
||||
pub mod agent;
|
||||
pub mod session_notifier;
|
||||
pub mod session_registry;
|
||||
|
||||
pub use agent::{AgentStartConfig, PromptMessage};
|
||||
pub use session_notifier::SessionNotifier;
|
||||
pub use session_registry::SessionRegistry;
|
||||
@@ -0,0 +1,113 @@
|
||||
//! # Session Notifier Trait
|
||||
//!
|
||||
//! 定义会话通知的抽象接口,用于推送 SSE 消息到前端。
|
||||
//! `agent_runner` 模块实现此 trait 来完成实际的消息推送。
|
||||
//!
|
||||
//! ## 预期实现
|
||||
//!
|
||||
//! 此 trait 预期在 `agent_runner` 中实现:
|
||||
//!
|
||||
//! - **实现类**: `agent_runner::service::SseSessionNotifier`
|
||||
//! - **包装类**: `agent_runner::service::StateAwareNotifier`(注入 project_id)
|
||||
//!
|
||||
//! ## 架构说明
|
||||
//!
|
||||
//! ```text
|
||||
//! agent_abstraction agent_runner
|
||||
//! ┌─────────────────┐ ┌─────────────────────┐
|
||||
//! │ SessionNotifier │◄────────────────│ SseSessionNotifier │
|
||||
//! │ (trait) │ implements │ (struct) │
|
||||
//! └────────┬────────┘ └──────────┬──────────┘
|
||||
//! │ │
|
||||
//! │ ┌──────────▼──────────┐
|
||||
//! ┌────────▼────────┐ │ StateAwareNotifier │
|
||||
//! │AcpSessionManager│◄────────────────│ (wrapper, 注入 │
|
||||
//! │ notifier: N │ injects │ project_id) │
|
||||
//! └─────────────────┘ └─────────────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## 方法使用场景
|
||||
//!
|
||||
//! | 方法 | 触发时机 | 推送内容 |
|
||||
//! |------|---------|---------|
|
||||
//! | `notify_prompt_start` | Agent 开始处理请求 | 会话开始通知 |
|
||||
//! | `notify_prompt_end` | Agent 完成处理 | 会话结束通知(含停止原因)|
|
||||
//! | `notify_prompt_error` | Agent 处理出错 | 错误通知 |
|
||||
//! | `notify_session_update` | Agent 输出内容 | 内容块更新(文本、工具调用等)|
|
||||
//! | `notify` | 通用通知 | 任意 SessionNotify 类型 |
|
||||
//!
|
||||
//! ## 使用示例
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // 在 agent_runner 中定义实现
|
||||
//! pub struct SseSessionNotifier;
|
||||
//!
|
||||
//! #[async_trait]
|
||||
//! impl SessionNotifier for SseSessionNotifier {
|
||||
//! async fn notify_prompt_start(...) -> Result<...> {
|
||||
//! // 推送到 SESSION_CACHE,由 SSE handler 分发
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // 使用 StateAwareNotifier 包装,自动注入 project_id
|
||||
//! let notifier = StateAwareNotifier::new(project_id.clone());
|
||||
//! let session_manager = AcpSessionManager::new(Arc::new(notifier), registry);
|
||||
//! ```
|
||||
|
||||
use async_trait::async_trait;
|
||||
use shared_types::SessionNotify;
|
||||
|
||||
/// 会话通知器 trait
|
||||
///
|
||||
/// 提供会话消息推送的抽象接口,解耦 agent_abstraction 和具体的 SSE 实现。
|
||||
///
|
||||
/// # 设计说明
|
||||
/// - agent_abstraction 只依赖此 trait,不依赖具体的 SSE 实现
|
||||
/// - agent_runner 实现此 trait,完成实际的消息推送
|
||||
/// - 通过依赖注入的方式,在启动 prompt handler 时传入 notifier
|
||||
#[async_trait]
|
||||
pub trait SessionNotifier: Send + Sync + 'static {
|
||||
/// 推送会话开始通知
|
||||
async fn notify_prompt_start(
|
||||
&self,
|
||||
project_id: &str,
|
||||
session_id: &str,
|
||||
request_id: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
/// 推送会话结束通知
|
||||
async fn notify_prompt_end(
|
||||
&self,
|
||||
project_id: &str,
|
||||
session_id: &str,
|
||||
stop_reason: agent_client_protocol::schema::StopReason,
|
||||
error_message: Option<String>,
|
||||
request_id: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
/// 推送会话错误通知
|
||||
async fn notify_prompt_error(
|
||||
&self,
|
||||
project_id: &str,
|
||||
session_id: &str,
|
||||
error: agent_client_protocol::schema::Error,
|
||||
request_id: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
/// 推送 Agent 会话更新通知
|
||||
async fn notify_session_update(
|
||||
&self,
|
||||
project_id: &str,
|
||||
session_id: &str,
|
||||
session_update: agent_client_protocol::schema::SessionUpdate,
|
||||
request_id: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
/// 推送通用会话通知
|
||||
async fn notify(
|
||||
&self,
|
||||
project_id: &str,
|
||||
session_id: &str,
|
||||
notify: SessionNotify,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
//! # Session Registry Trait
|
||||
//!
|
||||
//! 定义会话注册表的抽象接口,允许 `AcpSessionManager` 使用不同的存储实现。
|
||||
//! 通过依赖注入避免 `agent_abstraction` 和 `agent_runner` 之间的循环依赖。
|
||||
//!
|
||||
//! ## 预期实现
|
||||
//!
|
||||
//! 此 trait 预期在 `agent_runner` 中实现:
|
||||
//!
|
||||
//! - **实现类**: `agent_runner::service::AgentSessionRegistry`
|
||||
//! - **全局单例**: `agent_runner::service::AGENT_REGISTRY`
|
||||
//!
|
||||
//! ## 架构说明
|
||||
//!
|
||||
//! ```text
|
||||
//! agent_abstraction agent_runner
|
||||
//! ┌─────────────────┐ ┌─────────────────────┐
|
||||
//! │ SessionRegistry │◄────────────────│ AgentSessionRegistry│
|
||||
//! │ (trait) │ implements │ (struct) │
|
||||
//! └────────┬────────┘ └──────────┬──────────┘
|
||||
//! │ │
|
||||
//! │ │
|
||||
//! ┌────────▼────────┐ ┌──────────▼──────────┐
|
||||
//! │AcpSessionManager│◄────────────────│ AGENT_REGISTRY │
|
||||
//! │ registry: R │ injects │ (static LazyLock) │
|
||||
//! └─────────────────┘ └─────────────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## 与 AGENT_REGISTRY 的关系
|
||||
//!
|
||||
//! - **AGENT_REGISTRY**: `agent_runner` 中的全局单例,实现了此 trait
|
||||
//! - **何时直接访问 AGENT_REGISTRY**: 在 `agent_runner` 内部进行状态查询、清理任务
|
||||
//! - **何时通过 trait 访问**: 在 `agent_abstraction` 内部,通过泛型参数 `R: SessionRegistry`
|
||||
//!
|
||||
//! ## 使用示例
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // 在 agent_runner 中定义实现
|
||||
//! pub struct AgentSessionRegistry {
|
||||
//! agent_info_map: DashMap<String, ProjectAndAgentInfo>,
|
||||
//! project_to_session: DashMap<String, String>,
|
||||
//! session_to_project: DashMap<String, String>,
|
||||
//! }
|
||||
//!
|
||||
//! impl SessionRegistry for AgentSessionRegistry {
|
||||
//! type Entry = ProjectAndAgentInfo;
|
||||
//! // ...
|
||||
//! }
|
||||
//!
|
||||
//! // 创建全局单例
|
||||
//! pub static AGENT_REGISTRY: LazyLock<Arc<AgentSessionRegistry>> = ...;
|
||||
//!
|
||||
//! // 注入到 AcpSessionManager
|
||||
//! let session_manager = AcpSessionManager::new(notifier, AGENT_REGISTRY.clone());
|
||||
//! ```
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use dashmap::mapref::entry::Entry;
|
||||
use shared_types::SessionEntry;
|
||||
|
||||
/// 会话注册表 trait
|
||||
///
|
||||
/// 抽象 session 存储的 CRUD 操作,允许不同的实现(如 AGENT_REGISTRY)。
|
||||
///
|
||||
/// # 设计说明
|
||||
/// - `SessionEntry` trait 定义在 `shared_types`,描述单个会话条目的数据访问
|
||||
/// - `SessionRegistry` trait 定义在 `agent_abstraction`,描述会话存储的 CRUD 操作
|
||||
/// - `agent_runner` 为 `AGENT_REGISTRY` 实现 `SessionRegistry`,并注入到 `AcpSessionManager`
|
||||
///
|
||||
/// # 使用示例
|
||||
/// ```ignore
|
||||
/// // 定义在 agent_runner
|
||||
/// impl SessionRegistry for AgentSessionRegistry {
|
||||
/// type Entry = ProjectAndAgentInfo;
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// // 注入到 AcpSessionManager
|
||||
/// let session_manager = AcpSessionManager::new(notifier, Arc::new(registry));
|
||||
/// ```
|
||||
pub trait SessionRegistry: Send + Sync + 'static {
|
||||
/// 会话条目类型
|
||||
type Entry: SessionEntry;
|
||||
|
||||
/// 获取会话
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `project_id` - 项目 ID
|
||||
///
|
||||
/// # Returns
|
||||
/// 如果存在则返回会话条目的克隆,否则返回 None
|
||||
fn get(&self, project_id: &str) -> Option<Self::Entry>;
|
||||
|
||||
/// 插入或更新会话
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `project_id` - 项目 ID
|
||||
/// * `session_id` - 会话 ID
|
||||
/// * `entry` - 会话条目
|
||||
fn insert(&self, project_id: &str, session_id: &str, entry: Self::Entry);
|
||||
|
||||
/// 移除会话
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `project_id` - 项目 ID
|
||||
///
|
||||
/// # Returns
|
||||
/// 如果存在则返回被移除的会话条目,否则返回 None
|
||||
fn remove(&self, project_id: &str) -> Option<Self::Entry>;
|
||||
|
||||
/// 检查会话是否存在
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `project_id` - 项目 ID
|
||||
fn contains(&self, project_id: &str) -> bool;
|
||||
|
||||
/// 🆕 通过 session_id 获取 project_id(反向查询)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `session_id` - 会话 ID
|
||||
///
|
||||
/// # Returns
|
||||
/// 如果 session_id 存在,返回对应的 project_id;否则返回 None
|
||||
fn get_project_by_session(&self, session_id: &str) -> Option<String>;
|
||||
|
||||
/// 🆕 通过 session_id 直接获取会话条目(原子性操作)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `session_id` - 会话 ID
|
||||
///
|
||||
/// # Returns
|
||||
/// 如果 session_id 存在,返回对应的会话条目克隆;否则返回 None
|
||||
///
|
||||
/// # 优势
|
||||
/// - 一次性查询,避免两次调用之间的竞态窗口
|
||||
/// - 内部使用 DashMap 的原子性操作
|
||||
fn get_entry_by_session(&self, session_id: &str) -> Option<Self::Entry>;
|
||||
|
||||
/// 获取所有项目 ID 列表
|
||||
fn list_project_ids(&self) -> Vec<String>;
|
||||
|
||||
/// 获取会话数量
|
||||
fn count(&self) -> usize;
|
||||
|
||||
/// 获取 DashMap entry(用于原子性操作)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `project_id` - 项目 ID
|
||||
///
|
||||
/// # Returns
|
||||
/// DashMap Entry,支持原子性的 get/insert/update 操作
|
||||
///
|
||||
/// # 使用示例
|
||||
/// ```ignore
|
||||
/// use dashmap::mapref::entry::Entry;
|
||||
///
|
||||
/// match registry.entry(project_id.to_string()) {
|
||||
/// Entry::Occupied(mut occupied) => {
|
||||
/// // 已存在,可以检查和更新
|
||||
/// let existing = occupied.get();
|
||||
/// if needs_rebuild {
|
||||
/// occupied.insert(new_value);
|
||||
/// }
|
||||
/// }
|
||||
/// Entry::Vacant(vacant) => {
|
||||
/// // 不存在,可以插入
|
||||
/// vacant.insert(new_value);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
fn entry(&self, project_id: String) -> Entry<'_, String, Self::Entry>;
|
||||
}
|
||||
|
||||
/// SessionRegistry 的 Arc 包装器实现
|
||||
///
|
||||
/// 允许 `Arc<R>` 作为 `SessionRegistry` 使用
|
||||
impl<R: SessionRegistry> SessionRegistry for Arc<R> {
|
||||
type Entry = R::Entry;
|
||||
|
||||
fn get(&self, project_id: &str) -> Option<Self::Entry> {
|
||||
(**self).get(project_id)
|
||||
}
|
||||
|
||||
fn insert(&self, project_id: &str, session_id: &str, entry: Self::Entry) {
|
||||
(**self).insert(project_id, session_id, entry)
|
||||
}
|
||||
|
||||
fn remove(&self, project_id: &str) -> Option<Self::Entry> {
|
||||
(**self).remove(project_id)
|
||||
}
|
||||
|
||||
fn contains(&self, project_id: &str) -> bool {
|
||||
(**self).contains(project_id)
|
||||
}
|
||||
|
||||
fn get_project_by_session(&self, session_id: &str) -> Option<String> {
|
||||
(**self).get_project_by_session(session_id)
|
||||
}
|
||||
|
||||
fn get_entry_by_session(&self, session_id: &str) -> Option<Self::Entry> {
|
||||
(**self).get_entry_by_session(session_id)
|
||||
}
|
||||
|
||||
fn list_project_ids(&self) -> Vec<String> {
|
||||
(**self).list_project_ids()
|
||||
}
|
||||
|
||||
fn count(&self) -> usize {
|
||||
(**self).count()
|
||||
}
|
||||
|
||||
fn entry(&self, project_id: String) -> Entry<'_, String, Self::Entry> {
|
||||
(**self).entry(project_id)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user