添加qiming-rcoder模块

This commit is contained in:
Codex
2026-06-01 13:54:52 +08:00
parent 8092c4b1f8
commit 4b1a580132
539 changed files with 151650 additions and 0 deletions

View File

@@ -0,0 +1,355 @@
//! Agent Runtime 模块
//!
//! 简化版的 Agent Worker 管理器,利用 SACP 的 Send trait 支持。
//!
//! ## 新架构设计
//!
//! - 移除独立 OS 线程,使用 `tokio::spawn`
//! - 简化 sender 管理,移除 ArcSwap
//! - 保留自动重启功能
//! - 保留心跳检测(僵尸检测)
//! - 简化状态机(使用原子操作)
//!
//! ## 与旧架构对比
//!
//! | 组件 | 旧设计 | 新设计 |
//! |------|--------|--------|
//! | 运行环境 | 独立 OS 线程 + 独立运行时 | 主运行时 + tokio::spawn |
//! | Sender 管理 | ArcSwap<Option<Sender>> | mpsc::Sender (固定) |
//! | Worker 生命周期 | 手动管理线程 | JoinHandle |
//! | Ready 信号 | oneshot::channel | JoinHandle 完成 |
//! | 状态机 | watch::Sender + Mutex | Arc<AtomicState> |
//! | 重启 | 替换 sender | abort + spawn |
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, AtomicU8, AtomicUsize, Ordering};
use std::time::Duration;
use chrono::Utc;
use tokio::sync::{Mutex, mpsc};
use tokio::task::JoinHandle;
use tracing::{info, warn};
use crate::proxy_agent::AgentRequest;
/// Worker 状态
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkerState {
/// 启动中
Starting = 0,
/// 运行中
Running = 1,
/// 停止中
Stopping = 2,
/// 已停止
Stopped = 3,
}
/// 原子状态包装器 (无需 Mutex)
pub struct AtomicState(AtomicU8);
impl AtomicState {
pub fn new(state: WorkerState) -> Self {
Self(AtomicU8::new(state as u8))
}
pub fn get(&self) -> WorkerState {
match self.0.load(Ordering::Acquire) {
0 => WorkerState::Starting,
1 => WorkerState::Running,
2 => WorkerState::Stopping,
3 => WorkerState::Stopped,
invalid => {
tracing::error!(
"[AtomicState] Invalid state value: {}, falling back to Stopped",
invalid
);
WorkerState::Stopped
}
}
}
pub fn set(&self, state: WorkerState) {
self.0.store(state as u8, Ordering::Release);
}
}
/// 心跳包
#[derive(Clone, Debug)]
pub struct Heartbeat {
/// 心跳时间戳
pub timestamp: chrono::DateTime<chrono::Utc>,
}
/// Worker 就绪信号
///
/// Worker 在初始化完成后发送此信号
#[derive(Clone, Debug)]
pub struct WorkerReady {
/// 就绪时间戳
pub timestamp: chrono::DateTime<chrono::Utc>,
}
/// 并发控制配置
///
/// 工作线程池大小 - 决定可以并发处理的 Agent 会话数量
/// 🔥 已改为运行时可配置的全局变量,使用 get_concurrency_limit() 获取
/// 全局并发限制(运行时可配置)
pub static WORKER_THREAD_POOL_SIZE: AtomicUsize = AtomicUsize::new(10);
/// 初始化并发限制(在应用启动时调用)
pub fn init_concurrency_limit(limit: usize) {
WORKER_THREAD_POOL_SIZE.store(limit, Ordering::Release);
info!("🔧 Concurrency limit initialized: {}", limit);
}
/// 获取当前并发限制
pub fn get_concurrency_limit() -> usize {
WORKER_THREAD_POOL_SIZE.load(Ordering::Acquire)
}
/// Agent 运行时
///
/// 替代 AgentWorkerManager使用简化的架构
/// - 直接在主运行时中运行 (SACP 支持 Send)
/// - 使用原子操作管理状态
/// - 使用 JoinHandle 管理生命周期
pub struct AgentRuntime {
/// 请求发送端 (固定不变)
request_tx: mpsc::Sender<AgentRequest>,
/// 当前 Worker 的 JoinHandle
worker_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
/// 当前状态
state: Arc<AtomicState>,
/// 🔥 P1 修复: 最后心跳时间戳毫秒Unix timestamp
/// 使用 AtomicI64 替代 Mutex避免频繁的锁竞争
/// - 0 表示从未收到心跳
/// - 正数表示最后一次心跳的 timestamp_millis()
last_heartbeat_ts: Arc<AtomicI64>,
/// 活跃请求追踪: request_id -> 开始时间
active_requests: Arc<Mutex<HashMap<String, chrono::DateTime<chrono::Utc>>>>,
/// 心跳超时阈值
heartbeat_timeout: Duration,
/// 首次启动宽限期
initial_grace_period: Duration,
}
impl AgentRuntime {
/// 创建新的 AgentRuntime
///
/// 返回 (runtime, request_receiver)
pub fn new(request_buffer: usize) -> (Self, mpsc::Receiver<AgentRequest>) {
let (request_tx, request_rx) = mpsc::channel(request_buffer);
let runtime = Self {
request_tx,
worker_handle: Arc::new(Mutex::new(None)),
state: Arc::new(AtomicState::new(WorkerState::Starting)),
last_heartbeat_ts: Arc::new(AtomicI64::new(0)),
active_requests: Arc::new(Mutex::new(HashMap::new())),
heartbeat_timeout: Duration::from_secs(15),
initial_grace_period: Duration::from_secs(30),
};
(runtime, request_rx)
}
/// 启动 Worker (在主运行时中)
///
/// SACP 支持 Send直接在主运行时中运行无需独立线程
pub async fn start(&self, receiver: mpsc::Receiver<AgentRequest>) {
let state = self.state.clone();
let last_heartbeat_ts = self.last_heartbeat_ts.clone();
let active_requests = self.active_requests.clone();
let handle = tokio::spawn(async move {
// SACP 支持 Send直接在主运行时中运行
if let Err(e) = crate::proxy_agent::agent_worker_with_heartbeat(
receiver,
state.clone(),
last_heartbeat_ts.clone(),
active_requests.clone(),
)
.await
{
tracing::error!("Agent worker failed: {}", e);
}
});
*self.worker_handle.lock().await = Some(handle);
self.state.set(WorkerState::Running);
info!("AgentRuntime: worker started");
}
/// 重启 Worker
pub async fn restart(&self, new_receiver: mpsc::Receiver<AgentRequest>) {
warn!("AgentRuntime: preparing to restart worker...");
// 1. 停止旧 worker
if let Some(handle) = self.worker_handle.lock().await.take() {
handle.abort();
info!("AgentRuntime: previous worker terminated");
}
// 2. 重置状态
self.state.set(WorkerState::Starting);
self.last_heartbeat_ts.store(0, Ordering::Release);
*self.active_requests.lock().await = HashMap::new();
// 3. 启动新 worker
self.start(new_receiver).await;
info!("AgentRuntime: worker restart completed");
}
/// 发送请求
pub async fn send(&self, request: AgentRequest) -> anyhow::Result<()> {
self.request_tx
.send(request)
.await
.map_err(|_| anyhow::anyhow!("Worker is closed"))?;
Ok(())
}
/// 健康检查
pub async fn check_health(&self) -> bool {
let state = self.state.get();
// 检查状态
if state == WorkerState::Stopped {
return false;
}
// 🔥 P1 修复: 使用原子操作检查心跳(无锁)
let last_ts = self.last_heartbeat_ts.load(Ordering::Acquire);
if last_ts > 0 {
let elapsed_ms = Utc::now().timestamp_millis() - last_ts;
elapsed_ms < self.heartbeat_timeout.as_millis() as i64
} else {
// 首次启动宽限期
true
}
}
/// 获取当前状态
pub fn state(&self) -> WorkerState {
self.state.get()
}
/// 🔥 P1 修复: 检查心跳是否超时(无锁)
///
/// ## 返回值
///
/// - `true`: 心跳超时(超过 15 秒未收到心跳)
/// - `false`: 心跳正常或在宽限期内
pub fn check_heartbeat_timeout(&self) -> bool {
let last_ts = self.last_heartbeat_ts.load(Ordering::Acquire);
if last_ts > 0 {
// 有心跳记录,检查是否超过 15 秒
let elapsed_ms = Utc::now().timestamp_millis() - last_ts;
elapsed_ms > 15_000
} else {
// 从未收到心跳,使用首次启动宽限期
false
}
}
/// 🔥 P1 修复: 获取最后心跳时间(无锁)
///
/// ## 返回值
///
/// - `Some(timestamp)`: 最后心跳时间
/// - `None`: 从未收到心跳
pub fn last_heartbeat_time(&self) -> Option<chrono::DateTime<chrono::Utc>> {
let last_ts = self.last_heartbeat_ts.load(Ordering::Acquire);
if last_ts > 0 {
// 将毫秒时间戳转换为 DateTime
use chrono::TimeZone;
// timestamp_millis_opt 返回 LocalResult使用 single() 转换为 Option
match chrono::Utc.timestamp_millis_opt(last_ts).single() {
Some(dt) => Some(dt),
None => {
tracing::warn!(
"[WorkerInfo] Invalid timestamp: {}, using current time",
last_ts
);
Some(chrono::Utc::now())
}
}
} else {
None
}
}
/// 获取活跃请求句柄
pub fn active_requests(&self) -> Arc<Mutex<HashMap<String, chrono::DateTime<chrono::Utc>>>> {
self.active_requests.clone()
}
/// 检查请求通道是否已关闭
pub fn is_closed(&self) -> bool {
self.request_tx.is_closed()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_atomic_state() {
let state = AtomicState::new(WorkerState::Starting);
assert_eq!(state.get(), WorkerState::Starting);
state.set(WorkerState::Running);
assert_eq!(state.get(), WorkerState::Running);
state.set(WorkerState::Stopped);
assert_eq!(state.get(), WorkerState::Stopped);
}
#[tokio::test]
async fn test_agent_runtime_creation() {
let (runtime, _rx) = AgentRuntime::new(100);
assert_eq!(runtime.state(), WorkerState::Starting);
assert!(!runtime.is_closed());
}
#[tokio::test]
async fn test_heartbeat_timeout_detection() {
let (runtime, _rx) = AgentRuntime::new(100);
// 初始状态:从未收到心跳
assert!(!runtime.check_heartbeat_timeout());
// 模拟心跳超时设置20秒前的时间戳
let timestamp_20s_ago = Utc::now().timestamp_millis() - (20 * 1000);
runtime
.last_heartbeat_ts
.store(timestamp_20s_ago, std::sync::atomic::Ordering::Release);
// 心跳超过 15 秒,应检测到超时
assert!(runtime.check_heartbeat_timeout());
}
#[tokio::test]
async fn test_health_check() {
let (runtime, _rx) = AgentRuntime::new(100);
// 初始状态应该是健康的
assert!(runtime.check_health().await);
// 设置状态为 Stopped
runtime.state.set(WorkerState::Stopped);
assert!(!runtime.check_health().await);
}
}

View File

@@ -0,0 +1,317 @@
//! API 密钥管理器
//!
//! 在内存中存储 API 密钥配置,支持通过服务名称快速查询。
//! 密钥配置通过 gRPC 从 rcoder 主服务传递到 agent_runner。
//!
//! ## 使用示例
//!
//! ```rust
//! use agent_runner::api_key_manager::ApiKeyManager;
//! use shared_types::ModelProviderConfig;
//!
//! let manager = ApiKeyManager::new();
//!
//! // 存储 API 配置
//! let config = ModelProviderConfig {
//! id: "anthropic-prov".to_string(),
//! name: "anthropic".to_string(),
//! api_key: "sk-ant-xxx".to_string(),
//! base_url: "https://api.anthropic.com".to_string(),
//! requires_openai_auth: false,
//! default_model: "claude-3-5-sonnet-20241022".to_string(),
//! api_protocol: Some("anthropic".to_string()),
//! };
//! manager.store_config("anthropic", config);
//!
//! // 查询 API 密钥
//! if let Some(key) = manager.get_api_key("anthropic") {
//! println!("API Key: {}", key);
//! }
//! ```
use dashmap::DashMap;
use shared_types::ModelProviderConfig;
use std::sync::Arc;
use tracing::{debug, info};
/// API 密钥管理器
///
/// 使用 DashMap 实现并发安全的 HashMap支持多线程安全访问。
/// 密钥配置存储在内存中,进程重启后清空。
///
/// 可以作为独立存储使用(通过 `new()`),也可以包装共享的 DashMap通过 `from_shared()`)。
#[derive(Debug, Clone)]
pub struct ApiKeyManager {
/// 服务名称 -> 密钥配置
///
/// 键为服务标识符(如 UUID值为完整的 ModelProviderConfig。
/// 包装共享的 DashMap 引用(不拥有数据)或拥有独立 DashMap。
shared: Arc<DashMap<String, ModelProviderConfig>>,
}
impl Default for ApiKeyManager {
fn default() -> Self {
Self::new()
}
}
impl ApiKeyManager {
/// 创建新的 API 密钥管理器(拥有独立的 DashMap
///
/// # 示例
///
/// ```rust
/// use agent_runner::api_key_manager::ApiKeyManager;
///
/// let manager = ApiKeyManager::new();
/// ```
pub fn new() -> Self {
Self {
shared: Arc::new(DashMap::new()),
}
}
/// 从共享 DashMap 创建包装器
///
/// 将 ApiKeyManager 改为包装共享的 DashMap而不是拥有独立的数据。
/// 这样多个组件可以共享同一个配置存储。
///
/// # 参数
///
/// * `shared` - 共享的 DashMap<String, ModelProviderConfig>
///
/// # 示例
///
/// ```rust
/// use agent_runner::api_key_manager::ApiKeyManager;
/// use dashmap::DashMap;
/// use std::sync::Arc;
///
/// let shared_map = Arc::new(DashMap::new());
/// let manager = ApiKeyManager::from_shared(shared_map);
/// ```
pub fn from_shared(shared: Arc<DashMap<String, ModelProviderConfig>>) -> Self {
Self { shared }
}
/// 存储 ModelProviderConfig 到内存
///
/// 如果已存在相同服务名称的配置,将被覆盖。
/// 使用 DashMap 的 insert 方法确保原子性。
///
/// # 参数
///
/// * `service_name` - 服务标识符(如 UUID
/// * `config` - 完整的模型提供商配置
///
/// # 示例
///
/// ```rust
/// # use agent_runner::api_key_manager::ApiKeyManager;
/// # use shared_types::ModelProviderConfig;
/// let manager = ApiKeyManager::new();
/// let config = ModelProviderConfig {
/// id: "anthropic-prov".to_string(),
/// name: "anthropic".to_string(),
/// api_key: "sk-ant-xxx".to_string(),
/// base_url: "https://api.anthropic.com".to_string(),
/// requires_openai_auth: false,
/// default_model: "claude-3-5-sonnet-20241022".to_string(),
/// api_protocol: Some("anthropic".to_string()),
/// };
/// manager.store_config("svc-uuid-123", config);
/// ```
pub fn store_config(&self, service_name: &str, config: ModelProviderConfig) {
debug!(
"🔑 [API_KEY_MANAGER] Storing config: service_name={}",
service_name
);
self.shared.insert(service_name.to_string(), config);
}
/// 获取完整的 ModelProviderConfig
///
/// 使用 DashMap 的 value() 方法避免克隆(返回引用)。
///
/// # 参数
///
/// * `service_name` - 服务标识符
///
/// # 返回
///
/// 如果找到配置则返回 `Some(ModelProviderConfig)`,否则返回 `None`。
pub fn get(&self, service_name: &str) -> Option<ModelProviderConfig> {
self.shared.get(service_name).map(|r| r.value().clone())
}
/// 获取 API 密钥
///
/// # 参数
///
/// * `service_name` - 服务标识符
///
/// # 返回
///
/// 如果找到配置则返回 `Some(api_key)`,否则返回 `None`。
pub fn get_api_key(&self, service_name: &str) -> Option<String> {
self.get(service_name).map(|c| c.api_key)
}
/// 获取 API Base URL
///
/// # 参数
///
/// * `service_name` - 服务标识符
///
/// # 返回
///
/// 如果找到配置则返回 `Some(base_url)`,否则返回 `None`。
pub fn get_base_url(&self, service_name: &str) -> Option<String> {
self.get(service_name).map(|c| c.base_url)
}
/// 移除指定服务的配置
///
/// 使用 DashMap 的 remove 方法这是原子性操作entry API
///
/// # 参数
///
/// * `service_name` - 服务标识符
///
/// # 返回
///
/// 如果找到并移除了配置则返回 `Some(ModelProviderConfig)`,否则返回 `None`。
pub fn remove(&self, service_name: &str) -> Option<ModelProviderConfig> {
debug!(
"🔑 [API_KEY_MANAGER] Removing config: service_name={}",
service_name
);
self.shared.remove(service_name).map(|(_, v)| v)
}
/// 清空所有配置
pub fn clear(&self) {
info!("🔑 [API_KEY_MANAGER] Cleared all configs");
self.shared.clear();
}
/// 获取当前存储的配置数量
pub fn len(&self) -> usize {
self.shared.len()
}
/// 检查是否为空
pub fn is_empty(&self) -> bool {
self.shared.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_config(name: &str) -> ModelProviderConfig {
ModelProviderConfig {
id: format!("test_{}", name),
name: name.to_string(),
base_url: format!("https://api.{}.com", name),
api_key: format!("sk-{}-123", name),
requires_openai_auth: name == "openai",
default_model: "test-model".to_string(),
api_protocol: Some(name.to_string()),
}
}
#[test]
fn test_store_and_get() {
let manager = ApiKeyManager::new();
let config = create_test_config("anthropic");
manager.store_config("anthropic", config.clone());
let retrieved = manager.get("anthropic");
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().api_key, "sk-anthropic-123");
}
#[test]
fn test_get_api_key() {
let manager = ApiKeyManager::new();
let config = create_test_config("openai");
manager.store_config("openai", config);
let api_key = manager.get_api_key("openai");
assert_eq!(api_key, Some("sk-openai-123".to_string()));
}
#[test]
fn test_get_base_url() {
let manager = ApiKeyManager::new();
let config = create_test_config("anthropic");
manager.store_config("anthropic", config);
let base_url = manager.get_base_url("anthropic");
assert_eq!(base_url, Some("https://api.anthropic.com".to_string()));
}
#[test]
fn test_nonexistent() {
let manager = ApiKeyManager::new();
assert!(manager.get("nonexistent").is_none());
assert!(manager.get_api_key("nonexistent").is_none());
assert!(manager.get_base_url("nonexistent").is_none());
}
#[test]
fn test_remove() {
let manager = ApiKeyManager::new();
let config = create_test_config("anthropic");
manager.store_config("anthropic", config);
assert_eq!(manager.len(), 1);
let removed = manager.remove("anthropic");
assert!(removed.is_some());
assert_eq!(manager.len(), 0);
assert!(manager.get("anthropic").is_none());
}
#[test]
fn test_clear() {
let manager = ApiKeyManager::new();
manager.store_config("anthropic", create_test_config("anthropic"));
manager.store_config("openai", create_test_config("openai"));
assert_eq!(manager.len(), 2);
manager.clear();
assert_eq!(manager.len(), 0);
}
#[test]
fn test_overwrite() {
let manager = ApiKeyManager::new();
manager.store_config("anthropic", create_test_config("anthropic"));
let mut new_config = create_test_config("anthropic");
new_config.api_key = "sk-updated".to_string();
manager.store_config("anthropic", new_config);
let api_key = manager.get_api_key("anthropic");
assert_eq!(api_key, Some("sk-updated".to_string()));
}
#[test]
fn test_is_empty() {
let manager = ApiKeyManager::new();
assert!(manager.is_empty());
manager.store_config("anthropic", create_test_config("anthropic"));
assert!(!manager.is_empty());
}
}

View File

@@ -0,0 +1,954 @@
use std::env;
use std::fs;
use std::path::PathBuf;
use clap::Parser;
use serde::{Deserialize, Serialize};
use tracing::{error, info, warn};
/// 命令行参数
#[derive(Parser, Debug)]
#[command(name = "rcoder")]
#[command(about = "AI-powered development platform")]
#[command(version)]
pub struct CliArgs {
/// Service port
#[arg(short, long, help = "Service port")]
pub port: Option<u16>,
/// Project workspace directory
#[arg(short = 'd', long, help = "Root directory for project workspace")]
pub projects_dir: Option<PathBuf>,
/// Enable port-based reverse proxy
#[arg(long, help = "Enable port-based reverse proxy")]
pub enable_proxy: bool,
/// Proxy listener port
#[arg(long, help = "Proxy service listener port")]
pub proxy_port: Option<u16>,
/// Default backend port
#[arg(long, help = "Default backend service port")]
pub default_backend_port: Option<u16>,
}
/// 应用配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
/// 默认使用的 Agent ID
#[serde(default = "default_agent_id")]
pub default_agent_id: String,
/// 项目工作的根目录,根据启动命令的当前目录来确定
pub projects_dir: PathBuf,
/// 服务端口
pub port: u16,
/// 代理配置
pub proxy_config: Option<ProxyConfig>,
/// Agent 清理配置
#[serde(default)]
pub agent_cleanup: Option<AgentCleanupConfig>,
/// gRPC 超时配置
#[serde(default)]
pub grpc_timeouts: Option<GrpcTimeoutConfig>,
/// Agent 并发配置
#[serde(default)]
pub agent_concurrency: Option<AgentConcurrencyConfig>,
/// mcp-proxy 日志目录(可选)
/// 当设置此值且日志级别为 debug 时mcp-proxy convert 命令会自动追加
/// --diagnostic 和 --log-dir 参数
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mcp_proxy_log_dir: Option<String>,
}
fn default_agent_id() -> String {
"claude-code-acp-ts".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheckConfig {
pub enabled: bool,
pub interval_seconds: u64,
pub timeout_seconds: u64,
pub healthy_threshold: u32,
pub unhealthy_threshold: u32,
}
/// 代理配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyConfig {
/// 代理监听端口
pub listen_port: u16,
/// 默认后端端口
pub default_backend_port: u16,
/// 后端服务主机
pub backend_host: String,
/// URL 中端口参数的名称
pub port_param: String,
/// 健康检查配置
pub health_check: HealthCheckConfig,
}
/// Agent cleanup configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCleanupConfig {
/// Idle timeout (seconds), default 300 (5 minutes)
#[serde(default = "default_idle_timeout")]
pub idle_timeout_secs: u64,
/// Cleanup check interval (seconds), default 30
#[serde(default = "default_cleanup_interval")]
pub cleanup_interval_secs: u64,
}
/// Agent 并发配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConcurrencyConfig {
/// 并发会话槽位数量,默认 10
#[serde(default = "default_concurrency_limit")]
pub concurrency_limit: usize,
}
/// Agent 并发配置常量
impl AgentConcurrencyConfig {
/// 最小并发限制
pub const MIN_CONCURRENCY_LIMIT: usize = 1;
/// 验证配置值是否在有效范围内
pub fn validate(&self) -> Result<(), String> {
if self.concurrency_limit < Self::MIN_CONCURRENCY_LIMIT {
return Err(format!(
"concurrency_limit must be >= {}, current value: {}",
Self::MIN_CONCURRENCY_LIMIT,
self.concurrency_limit
));
}
Ok(())
}
}
fn default_concurrency_limit() -> usize {
10
}
impl Default for AgentConcurrencyConfig {
fn default() -> Self {
Self {
concurrency_limit: default_concurrency_limit(),
}
}
}
/// gRPC timeout configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrpcTimeoutConfig {
/// Cancel session timeout (seconds), default 30
#[serde(default = "default_cancel_timeout")]
pub cancel_session_timeout_secs: u64,
/// ACP session creation timeout (seconds), default 100
#[serde(default = "default_acp_session_timeout")]
pub acp_session_create_timeout_secs: u64,
/// Agent cancel call timeout (seconds), default 10
#[serde(default = "default_agent_cancel_timeout")]
pub agent_cancel_timeout_secs: u64,
/// Port check timeout (milliseconds), default 500
#[serde(default = "default_port_check_timeout")]
pub port_check_timeout_millis: u64,
}
/// gRPC timeout configuration constants
impl GrpcTimeoutConfig {
/// Minimum cancel session timeout (5 seconds)
pub const MIN_CANCEL_TIMEOUT: u64 = 5;
/// Maximum cancel session timeout (300 seconds = 5 minutes)
pub const MAX_CANCEL_TIMEOUT: u64 = 300;
/// Minimum ACP session creation timeout (10 seconds)
pub const MIN_ACP_SESSION_TIMEOUT: u64 = 10;
/// Maximum ACP session creation timeout (300 seconds = 5 minutes)
pub const MAX_ACP_SESSION_TIMEOUT: u64 = 300;
/// Minimum Agent cancel call timeout (5 seconds)
pub const MIN_AGENT_CANCEL_TIMEOUT: u64 = 5;
/// Maximum Agent cancel call timeout (60 seconds)
pub const MAX_AGENT_CANCEL_TIMEOUT: u64 = 60;
/// Minimum port check timeout (100 milliseconds)
pub const MIN_PORT_CHECK_TIMEOUT: u64 = 100;
/// Maximum port check timeout (10000 milliseconds = 10 seconds)
pub const MAX_PORT_CHECK_TIMEOUT: u64 = 10000;
/// Validate that configuration values are within valid ranges
pub fn validate(&self) -> Result<(), String> {
if self.cancel_session_timeout_secs < Self::MIN_CANCEL_TIMEOUT
|| self.cancel_session_timeout_secs > Self::MAX_CANCEL_TIMEOUT
{
return Err(format!(
"cancel_session_timeout_secs must be between {} and {}, current: {}",
Self::MIN_CANCEL_TIMEOUT,
Self::MAX_CANCEL_TIMEOUT,
self.cancel_session_timeout_secs
));
}
if self.acp_session_create_timeout_secs < Self::MIN_ACP_SESSION_TIMEOUT
|| self.acp_session_create_timeout_secs > Self::MAX_ACP_SESSION_TIMEOUT
{
return Err(format!(
"acp_session_create_timeout_secs must be between {} and {}, current: {}",
Self::MIN_ACP_SESSION_TIMEOUT,
Self::MAX_ACP_SESSION_TIMEOUT,
self.acp_session_create_timeout_secs
));
}
if self.agent_cancel_timeout_secs < Self::MIN_AGENT_CANCEL_TIMEOUT
|| self.agent_cancel_timeout_secs > Self::MAX_AGENT_CANCEL_TIMEOUT
{
return Err(format!(
"agent_cancel_timeout_secs must be between {} and {}, current: {}",
Self::MIN_AGENT_CANCEL_TIMEOUT,
Self::MAX_AGENT_CANCEL_TIMEOUT,
self.agent_cancel_timeout_secs
));
}
if self.port_check_timeout_millis < Self::MIN_PORT_CHECK_TIMEOUT
|| self.port_check_timeout_millis > Self::MAX_PORT_CHECK_TIMEOUT
{
return Err(format!(
"port_check_timeout_millis must be between {} and {}, current: {}",
Self::MIN_PORT_CHECK_TIMEOUT,
Self::MAX_PORT_CHECK_TIMEOUT,
self.port_check_timeout_millis
));
}
Ok(())
}
}
/// Agent 清理配置常量
impl AgentCleanupConfig {
/// 最小闲置超时时间10 秒)
pub const MIN_IDLE_TIMEOUT: u64 = 10;
/// 最大闲置超时时间24 小时)
pub const MAX_IDLE_TIMEOUT: u64 = 24 * 60 * 60;
/// 最小清理检查间隔5 秒)
pub const MIN_CLEANUP_INTERVAL: u64 = 5;
/// 最大清理检查间隔1 小时)
pub const MAX_CLEANUP_INTERVAL: u64 = 60 * 60;
/// 验证配置值是否在有效范围内
pub fn validate(&self) -> Result<(), String> {
if self.idle_timeout_secs < Self::MIN_IDLE_TIMEOUT
|| self.idle_timeout_secs > Self::MAX_IDLE_TIMEOUT
{
return Err(format!(
"idle_timeout_secs must be between {} and {}, current value: {}",
Self::MIN_IDLE_TIMEOUT,
Self::MAX_IDLE_TIMEOUT,
self.idle_timeout_secs
));
}
if self.cleanup_interval_secs < Self::MIN_CLEANUP_INTERVAL
|| self.cleanup_interval_secs > Self::MAX_CLEANUP_INTERVAL
{
return Err(format!(
"cleanup_interval_secs must be between {} and {}, current value: {}",
Self::MIN_CLEANUP_INTERVAL,
Self::MAX_CLEANUP_INTERVAL,
self.cleanup_interval_secs
));
}
Ok(())
}
}
#[cfg(feature = "http-server")]
fn default_idle_timeout() -> u64 {
24 * 60 * 60 // 24 小时Tauri 客户端模式)
}
#[cfg(not(feature = "http-server"))]
fn default_idle_timeout() -> u64 {
300 // 5 分钟CLI 模式)
}
fn default_cleanup_interval() -> u64 {
30 // 30 秒
}
fn default_cancel_timeout() -> u64 {
30 // 30 秒
}
fn default_acp_session_timeout() -> u64 {
100 // 100 秒
}
fn default_agent_cancel_timeout() -> u64 {
10 // 10 秒
}
fn default_port_check_timeout() -> u64 {
500 // 500 毫秒
}
impl Default for AgentCleanupConfig {
fn default() -> Self {
Self {
idle_timeout_secs: default_idle_timeout(),
cleanup_interval_secs: default_cleanup_interval(),
}
}
}
impl Default for GrpcTimeoutConfig {
fn default() -> Self {
Self {
cancel_session_timeout_secs: default_cancel_timeout(),
acp_session_create_timeout_secs: default_acp_session_timeout(),
agent_cancel_timeout_secs: default_agent_cancel_timeout(),
port_check_timeout_millis: default_port_check_timeout(),
}
}
}
/// 配置文件路径
const CONFIG_FILE: &str = "config.yml";
impl Default for AppConfig {
fn default() -> Self {
Self {
default_agent_id: default_agent_id(),
projects_dir: PathBuf::from("./project_workspace"),
port: 8086,
proxy_config: Some(ProxyConfig::default()),
agent_cleanup: Some(AgentCleanupConfig::default()),
grpc_timeouts: Some(GrpcTimeoutConfig::default()),
agent_concurrency: Some(AgentConcurrencyConfig::default()),
mcp_proxy_log_dir: None,
}
}
}
impl Default for ProxyConfig {
fn default() -> Self {
Self {
listen_port: 8088,
default_backend_port: 8086,
backend_host: "127.0.0.1".to_string(),
port_param: "port".to_string(),
health_check: HealthCheckConfig {
enabled: true,
interval_seconds: 5,
timeout_seconds: 1,
healthy_threshold: 2,
unhealthy_threshold: 3,
},
}
}
}
/// 加载配置
/// 配置优先级:命令行参数 > 环境变量 > 配置文件 > 默认配置
pub fn load_config_with_args(cli_args: CliArgs) -> AppConfig {
// 1. 首先加载默认配置
let mut config = AppConfig::default();
// 2. 尝试从当前目录读取配置文件
match load_config_from_file() {
Ok(file_config) => {
config = file_config;
info!("Loaded config from {}", CONFIG_FILE);
}
Err(e) => {
warn!(
"Failed to read config file {}: {}, using defaults",
CONFIG_FILE, e
);
// 创建默认配置文件
if let Err(create_err) = create_default_config_file(&config) {
error!("Failed to create default config file: {}", create_err);
} else {
info!("Created default config file: {}", CONFIG_FILE);
}
}
}
// 3. 环境变量覆盖配置
if let Ok(port) = env::var("RCODER_PORT") {
match port.parse::<u16>() {
Ok(p) => {
config.port = p;
info!("Set port from env RCODER_PORT: {}", p);
}
Err(_) => {
warn!(
"Invalid RCODER_PORT env value: {}, keeping config port: {}",
port, config.port
);
}
}
}
// 🆕 Agent 清理配置:支持环境变量覆盖
if let Ok(idle_timeout) = env::var("RCODER_AGENT_IDLE_TIMEOUT_SECS") {
match idle_timeout.parse::<u64>() {
Ok(timeout) => {
// 🔒 验证范围
if (AgentCleanupConfig::MIN_IDLE_TIMEOUT..=AgentCleanupConfig::MAX_IDLE_TIMEOUT)
.contains(&timeout)
{
config
.agent_cleanup
.get_or_insert_with(Default::default)
.idle_timeout_secs = timeout;
info!(
"Set idle timeout from env RCODER_AGENT_IDLE_TIMEOUT_SECS: {} seconds",
timeout
);
} else {
warn!(
"Invalid RCODER_AGENT_IDLE_TIMEOUT_SECS: {} seconds, out of range [{}, {}], keeping config value",
timeout,
AgentCleanupConfig::MIN_IDLE_TIMEOUT,
AgentCleanupConfig::MAX_IDLE_TIMEOUT
);
}
}
Err(_) => {
warn!(
"Invalid RCODER_AGENT_IDLE_TIMEOUT_SECS format: {}, keeping config value",
idle_timeout
);
}
}
}
if let Ok(cleanup_interval) = env::var("RCODER_AGENT_CLEANUP_INTERVAL_SECS") {
match cleanup_interval.parse::<u64>() {
Ok(interval) => {
// 🔒 验证范围
if (AgentCleanupConfig::MIN_CLEANUP_INTERVAL
..=AgentCleanupConfig::MAX_CLEANUP_INTERVAL)
.contains(&interval)
{
config
.agent_cleanup
.get_or_insert_with(Default::default)
.cleanup_interval_secs = interval;
info!(
"Set cleanup interval from env RCODER_AGENT_CLEANUP_INTERVAL_SECS: {} seconds",
interval
);
} else {
warn!(
"Invalid RCODER_AGENT_CLEANUP_INTERVAL_SECS: {} seconds, out of range [{}, {}], keeping config value",
interval,
AgentCleanupConfig::MIN_CLEANUP_INTERVAL,
AgentCleanupConfig::MAX_CLEANUP_INTERVAL
);
}
}
Err(_) => {
warn!(
"Invalid RCODER_AGENT_CLEANUP_INTERVAL_SECS format: {}, keeping config value",
cleanup_interval
);
}
}
}
// 🆕 Agent 并发配置:支持环境变量覆盖
if let Ok(concurrency_limit) = env::var("RCODER_AGENT_CONCURRENCY_LIMIT") {
match concurrency_limit.parse::<usize>() {
Ok(limit) => {
if limit >= AgentConcurrencyConfig::MIN_CONCURRENCY_LIMIT {
config
.agent_concurrency
.get_or_insert_with(Default::default)
.concurrency_limit = limit;
info!(
"Set concurrency limit from env RCODER_AGENT_CONCURRENCY_LIMIT: {}",
limit
);
} else {
warn!(
"Invalid RCODER_AGENT_CONCURRENCY_LIMIT: {}, must be >= {}, keeping config value",
limit,
AgentConcurrencyConfig::MIN_CONCURRENCY_LIMIT
);
}
}
Err(_) => {
warn!(
"Invalid RCODER_AGENT_CONCURRENCY_LIMIT format: {}, keeping config value",
concurrency_limit
);
}
}
}
// 🆕 gRPC 超时配置:支持环境变量覆盖
if let Ok(cancel_timeout) = env::var("RCODER_CANCEL_SESSION_TIMEOUT_SECS") {
match cancel_timeout.parse::<u64>() {
Ok(timeout) => {
if (GrpcTimeoutConfig::MIN_CANCEL_TIMEOUT..=GrpcTimeoutConfig::MAX_CANCEL_TIMEOUT)
.contains(&timeout)
{
config
.grpc_timeouts
.get_or_insert_with(Default::default)
.cancel_session_timeout_secs = timeout;
info!(
"Set cancel-session timeout from env RCODER_CANCEL_SESSION_TIMEOUT_SECS: {} seconds",
timeout
);
} else {
warn!(
"Invalid RCODER_CANCEL_SESSION_TIMEOUT_SECS: {} seconds, out of range [{}, {}]",
timeout,
GrpcTimeoutConfig::MIN_CANCEL_TIMEOUT,
GrpcTimeoutConfig::MAX_CANCEL_TIMEOUT
);
}
}
Err(_) => {
warn!(
"Invalid RCODER_CANCEL_SESSION_TIMEOUT_SECS format: {}",
cancel_timeout
);
}
}
}
if let Ok(acp_timeout) = env::var("RCODER_ACP_SESSION_CREATE_TIMEOUT_SECS") {
match acp_timeout.parse::<u64>() {
Ok(timeout) => {
if (GrpcTimeoutConfig::MIN_ACP_SESSION_TIMEOUT
..=GrpcTimeoutConfig::MAX_ACP_SESSION_TIMEOUT)
.contains(&timeout)
{
config
.grpc_timeouts
.get_or_insert_with(Default::default)
.acp_session_create_timeout_secs = timeout;
info!(
"Set ACP session-create timeout from env RCODER_ACP_SESSION_CREATE_TIMEOUT_SECS: {} seconds",
timeout
);
} else {
warn!(
"Invalid RCODER_ACP_SESSION_CREATE_TIMEOUT_SECS: {} seconds, out of range [{}, {}]",
timeout,
GrpcTimeoutConfig::MIN_ACP_SESSION_TIMEOUT,
GrpcTimeoutConfig::MAX_ACP_SESSION_TIMEOUT
);
}
}
Err(_) => {
warn!(
"Invalid RCODER_ACP_SESSION_CREATE_TIMEOUT_SECS format: {}",
acp_timeout
);
}
}
}
if let Ok(agent_cancel_timeout) = env::var("RCODER_AGENT_CANCEL_TIMEOUT_SECS") {
match agent_cancel_timeout.parse::<u64>() {
Ok(timeout) => {
if (GrpcTimeoutConfig::MIN_AGENT_CANCEL_TIMEOUT
..=GrpcTimeoutConfig::MAX_AGENT_CANCEL_TIMEOUT)
.contains(&timeout)
{
config
.grpc_timeouts
.get_or_insert_with(Default::default)
.agent_cancel_timeout_secs = timeout;
info!(
"Set agent-cancel timeout from env RCODER_AGENT_CANCEL_TIMEOUT_SECS: {} seconds",
timeout
);
} else {
warn!(
"Invalid RCODER_AGENT_CANCEL_TIMEOUT_SECS: {} seconds, out of range [{}, {}]",
timeout,
GrpcTimeoutConfig::MIN_AGENT_CANCEL_TIMEOUT,
GrpcTimeoutConfig::MAX_AGENT_CANCEL_TIMEOUT
);
}
}
Err(_) => {
warn!(
"Invalid RCODER_AGENT_CANCEL_TIMEOUT_SECS format: {}",
agent_cancel_timeout
);
}
}
}
if let Ok(port_check_timeout) = env::var("RCODER_PORT_CHECK_TIMEOUT_MILLIS") {
match port_check_timeout.parse::<u64>() {
Ok(timeout) => {
if (GrpcTimeoutConfig::MIN_PORT_CHECK_TIMEOUT
..=GrpcTimeoutConfig::MAX_PORT_CHECK_TIMEOUT)
.contains(&timeout)
{
config
.grpc_timeouts
.get_or_insert_with(Default::default)
.port_check_timeout_millis = timeout;
info!(
"Set port-check timeout from env RCODER_PORT_CHECK_TIMEOUT_MILLIS: {} ms",
timeout
);
} else {
warn!(
"Invalid RCODER_PORT_CHECK_TIMEOUT_MILLIS: {} ms, out of range [{}, {}]",
timeout,
GrpcTimeoutConfig::MIN_PORT_CHECK_TIMEOUT,
GrpcTimeoutConfig::MAX_PORT_CHECK_TIMEOUT
);
}
}
Err(_) => {
warn!(
"Invalid RCODER_PORT_CHECK_TIMEOUT_MILLIS format: {}",
port_check_timeout
);
}
}
}
// 🆕 验证最终配置的有效性
if let Some(ref cleanup_config) = config.agent_cleanup
&& let Err(e) = cleanup_config.validate()
{
warn!(
"Agent cleanup config validation failed: {}, using defaults",
e
);
config.agent_cleanup = Some(AgentCleanupConfig::default());
}
// 🆕 验证 Agent 并发配置的有效性
if let Some(ref concurrency_config) = config.agent_concurrency
&& let Err(e) = concurrency_config.validate()
{
warn!(
"Agent concurrency config validation failed: {}, using defaults",
e
);
config.agent_concurrency = Some(AgentConcurrencyConfig::default());
}
// 4. 处理代理配置
if cli_args.enable_proxy {
let proxy_config = ProxyConfig {
listen_port: cli_args.proxy_port.unwrap_or(8080),
default_backend_port: cli_args.default_backend_port.unwrap_or(config.port),
backend_host: "127.0.0.1".to_string(),
port_param: "port".to_string(),
health_check: HealthCheckConfig {
enabled: true,
interval_seconds: 5,
timeout_seconds: 1,
healthy_threshold: 2,
unhealthy_threshold: 3,
},
};
info!(
"Reverse proxy enabled, listening on port: {}",
proxy_config.listen_port
);
config.proxy_config = Some(proxy_config);
}
// 5. 命令行参数覆盖配置(优先级最高)
if let Some(port) = cli_args.port {
config.port = port;
info!("Set port from CLI arg: {}", port);
}
if let Some(projects_dir) = cli_args.projects_dir {
config.projects_dir = projects_dir.clone();
info!("Set projects directory from CLI arg: {:?}", projects_dir);
}
info!(
"最终配置: port={}, projects_dir={:?}, default_agent_id={}, proxy_enabled={}",
config.port,
config.projects_dir,
config.default_agent_id,
config.proxy_config.is_some()
);
// 🆕 验证 gRPC 超时配置的有效性
if let Some(ref grpc_timeouts) = config.grpc_timeouts
&& let Err(e) = grpc_timeouts.validate()
{
warn!(
"gRPC timeout config validation failed: {}, using defaults",
e
);
config.grpc_timeouts = Some(GrpcTimeoutConfig::default());
}
config
}
/// 加载配置(保留旧接口以保持兼容性)
pub fn load_config() -> AppConfig {
let cli_args = CliArgs {
port: None,
projects_dir: None,
enable_proxy: false,
proxy_port: None,
default_backend_port: None,
};
load_config_with_args(cli_args)
}
/// 从文件加载配置
fn load_config_from_file() -> anyhow::Result<AppConfig> {
let config_content = fs::read_to_string(CONFIG_FILE)
.map_err(|e| anyhow::anyhow!("Failed to read config file: {}", e))?;
let config: AppConfig = serde_yaml::from_str(&config_content)
.map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?;
Ok(config)
}
/// 创建默认配置文件
fn create_default_config_file(config: &AppConfig) -> anyhow::Result<()> {
// 获取 proxy_config如果不存在则使用默认值
let proxy_config = config.proxy_config.as_ref().cloned().unwrap_or_default();
// 获取 agent_cleanup 配置,如果不存在则使用默认值
let agent_cleanup = config.agent_cleanup.as_ref().cloned().unwrap_or_default();
// 获取 grpc_timeouts 配置,如果不存在则使用默认值
let grpc_timeouts = config.grpc_timeouts.as_ref().cloned().unwrap_or_default();
// 获取 agent_concurrency 配置,如果不存在则使用默认值
let agent_concurrency = config
.agent_concurrency
.as_ref()
.cloned()
.unwrap_or_default();
// 手动构建带注释的 YAML 内容
let content_with_comments = format!(
r#"# rcoder 配置文件
# 该文件在首次启动时自动生成
# 默认使用的 Agent ID
default_agent_id: {}
# 项目工作目录
projects_dir: {}
# 主服务端口
port: {}
# Pingora 反向代理配置
proxy_config:
# 代理服务监听端口 (用于接收外部请求)
listen_port: {}
# 默认后端服务端口 (当请求未指定端口时使用)
default_backend_port: {}
# 后端服务主机地址
backend_host: "{}"
# URL 中端口参数的名称 (用于从路径中提取端口号)
port_param: "{}"
# 健康检查配置
health_check:
enabled: {}
interval_seconds: {}
timeout_seconds: {}
healthy_threshold: {}
unhealthy_threshold: {}
# Agent 清理配置
# 如果省略此配置块,将使用以下默认值:
# - idle_timeout_secs: 300 (5分钟)
# - cleanup_interval_secs: 30 (30秒)
agent_cleanup:
# Agent 闲置超时时间(秒)
# Agent 在闲置超过此时间后会被自动清理以释放资源
# 有效范围: 10 - 86400 秒10秒 - 24小时
# 可通过环境变量 RCODER_AGENT_IDLE_TIMEOUT_SECS 覆盖
idle_timeout_secs: {}
# 清理检查间隔(秒)
# 系统每隔此时间检查一次是否有闲置的 Agent 需要清理
# 有效范围: 5 - 3600 秒5秒 - 1小时
# 可通过环境变量 RCODER_AGENT_CLEANUP_INTERVAL_SECS 覆盖
cleanup_interval_secs: {}
# gRPC 超时配置
# 如果省略此配置块,将使用以下默认值:
# - cancel_session_timeout_secs: 30 (30秒)
# - acp_session_create_timeout_secs: 100 (100秒)
# - agent_cancel_timeout_secs: 10 (10秒)
# - port_check_timeout_millis: 500 (500毫秒)
grpc_timeouts:
# 取消会话超时(秒)
# gRPC 取消会话请求的最大等待时间
# 有效范围: 5 - 300 秒
# 可通过环境变量 RCODER_CANCEL_SESSION_TIMEOUT_SECS 覆盖
cancel_session_timeout_secs: {}
# ACP 会话创建超时(秒)
# Agent 创建新会话的最大等待时间MCP 工具较多时可能需要更长时间)
# 有效范围: 10 - 300 秒
# 可通过环境变量 RCODER_ACP_SESSION_CREATE_TIMEOUT_SECS 覆盖
acp_session_create_timeout_secs: {}
# Agent 取消调用超时(秒)
# Agent 内部取消操作的最大等待时间
# 有效范围: 5 - 60 秒
# 可通过环境变量 RCODER_AGENT_CANCEL_TIMEOUT_SECS 覆盖
agent_cancel_timeout_secs: {}
# 端口检查超时(毫秒)
# 检查端口可用性的最大等待时间
# 有效范围: 100 - 10000 毫秒
# 可通过环境变量 RCODER_PORT_CHECK_TIMEOUT_MILLIS 覆盖
port_check_timeout_millis: {}
# Agent 并发配置
# 如果省略此配置块,将使用以下默认值:
# - concurrency_limit: 10 (10个并发会话)
agent_concurrency:
# Agent 并发会话槽位数量
# 决定可以同时处理的 Agent 会话数量
# 有效范围: >= 1
# 可通过环境变量 RCODER_AGENT_CONCURRENCY_LIMIT 覆盖
concurrency_limit: {}
"#,
config.default_agent_id,
config.projects_dir.display(),
config.port,
proxy_config.listen_port,
proxy_config.default_backend_port,
proxy_config.backend_host,
proxy_config.port_param,
proxy_config.health_check.enabled,
proxy_config.health_check.interval_seconds,
proxy_config.health_check.timeout_seconds,
proxy_config.health_check.healthy_threshold,
proxy_config.health_check.unhealthy_threshold,
agent_cleanup.idle_timeout_secs,
agent_cleanup.cleanup_interval_secs,
grpc_timeouts.cancel_session_timeout_secs,
grpc_timeouts.acp_session_create_timeout_secs,
grpc_timeouts.agent_cancel_timeout_secs,
grpc_timeouts.port_check_timeout_millis,
agent_concurrency.concurrency_limit
);
fs::write(CONFIG_FILE, content_with_comments)
.map_err(|e| anyhow::anyhow!("Failed to write config file: {}", e))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_cleanup_default_values() {
let config = AgentCleanupConfig::default();
assert_eq!(config.idle_timeout_secs, 300);
assert_eq!(config.cleanup_interval_secs, 30);
}
#[test]
fn test_agent_cleanup_validate_valid_range() {
let config = AgentCleanupConfig {
idle_timeout_secs: 600, // 10 分钟
cleanup_interval_secs: 60, // 1 分钟
};
assert!(config.validate().is_ok());
}
#[test]
fn test_agent_cleanup_validate_min_boundaries() {
// 测试最小边界值
let config = AgentCleanupConfig {
idle_timeout_secs: AgentCleanupConfig::MIN_IDLE_TIMEOUT,
cleanup_interval_secs: AgentCleanupConfig::MIN_CLEANUP_INTERVAL,
};
assert!(config.validate().is_ok());
}
#[test]
fn test_agent_cleanup_validate_max_boundaries() {
// 测试最大边界值
let config = AgentCleanupConfig {
idle_timeout_secs: AgentCleanupConfig::MAX_IDLE_TIMEOUT,
cleanup_interval_secs: AgentCleanupConfig::MAX_CLEANUP_INTERVAL,
};
assert!(config.validate().is_ok());
}
#[test]
fn test_agent_cleanup_validate_idle_timeout_too_small() {
let config = AgentCleanupConfig {
idle_timeout_secs: 5, // 小于最小值 10
cleanup_interval_secs: 30,
};
assert!(config.validate().is_err());
let err = config.validate().unwrap_err();
assert!(err.contains("idle_timeout_secs"));
}
#[test]
fn test_agent_cleanup_validate_idle_timeout_too_large() {
let config = AgentCleanupConfig {
idle_timeout_secs: 100000, // 大于最大值 86400
cleanup_interval_secs: 30,
};
assert!(config.validate().is_err());
let err = config.validate().unwrap_err();
assert!(err.contains("idle_timeout_secs"));
}
#[test]
fn test_agent_cleanup_validate_cleanup_interval_too_small() {
let config = AgentCleanupConfig {
idle_timeout_secs: 180,
cleanup_interval_secs: 2, // 小于最小值 5
};
assert!(config.validate().is_err());
let err = config.validate().unwrap_err();
assert!(err.contains("cleanup_interval_secs"));
}
#[test]
fn test_agent_cleanup_validate_cleanup_interval_too_large() {
let config = AgentCleanupConfig {
idle_timeout_secs: 180,
cleanup_interval_secs: 5000, // 大于最大值 3600
};
assert!(config.validate().is_err());
let err = config.validate().unwrap_err();
assert!(err.contains("cleanup_interval_secs"));
}
#[test]
fn test_agent_cleanup_validate_both_invalid() {
let config = AgentCleanupConfig {
idle_timeout_secs: 0,
cleanup_interval_secs: 0,
};
assert!(config.validate().is_err());
// 应该先检测到 idle_timeout_secs 的错误
let err = config.validate().unwrap_err();
assert!(err.contains("idle_timeout_secs"));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
//! gRPC 服务模块
//!
//! 提供 agent_runner 的 gRPC 服务端实现,用于替代原有的 HTTP 接口
pub mod agent_service_impl;
pub use agent_service_impl::AgentServiceImpl;

View File

@@ -0,0 +1,35 @@
//! 健康检查处理器
use axum::Json;
use chrono::Utc;
use utoipa::ToSchema;
/// 健康检查响应结构
#[derive(serde::Serialize, ToSchema)]
pub struct HealthResponse {
/// 服务状态
pub status: String,
/// 时间戳
pub timestamp: chrono::DateTime<chrono::Utc>,
/// 服务名称
pub service: String,
}
/// 健康检查端点
///
/// 检查服务的健康状态
#[utoipa::path(
get,
path = "/health",
responses(
(status = 200, description = "服务健康状态", body = HealthResponse)
),
tag = "system"
)]
pub async fn health_check() -> Json<HealthResponse> {
Json(HealthResponse {
status: "healthy".to_string(),
timestamp: Utc::now(),
service: "rcoder-ai-service".to_string(),
})
}

View File

@@ -0,0 +1,4 @@
//! HTTP 路由和处理器模块
mod health_handler;
pub use health_handler::*;

View File

@@ -0,0 +1,146 @@
//! Computer Agent Cancel Handler
//!
//! 处理 POST /computer/agent/session/cancel 请求
use axum::{
Json,
extract::State,
http::HeaderMap,
};
use agent_client_protocol::schema::{CancelNotification, SessionId};
use std::sync::Arc;
use tokio::sync::oneshot;
use tracing::{info, warn};
use crate::CancelNotificationRequestWrapper;
use crate::http_server::router::AppState;
use crate::service::AGENT_REGISTRY;
use shared_types::{
ComputerAgentCancelRequest, ComputerAgentCancelResponse, HttpResult, I18nJsonOrQuery,
error_codes::ERR_VALIDATION, get_i18n_message, AppError,
};
use super::locale_from_headers;
/// 取消 Computer Agent 会话任务
///
/// 直接操作 AGENT_REGISTRY 发送取消信号
#[utoipa::path(
post,
path = "/computer/agent/session/cancel",
params(
ComputerAgentCancelRequest
),
responses(
(status = 200, description = "Cancel request successful", body = HttpResult<ComputerAgentCancelResponse>),
(status = 400, description = "Bad request - missing fields"),
(status = 500, description = "Internal server error")
),
tag = "Computer Agent"
)]
pub async fn handle_computer_cancel(
State(_state): State<Arc<AppState>>,
headers: HeaderMap,
I18nJsonOrQuery(request): I18nJsonOrQuery<ComputerAgentCancelRequest>,
) -> Result<Json<HttpResult<ComputerAgentCancelResponse>>, AppError> {
let locale = locale_from_headers(&headers);
info!(
"🚫 [HTTP] Computer Agent 取消请求: user_id={:?}, project_id={}, session_id={:?}",
request.user_id, request.project_id, request.session_id
);
// 1. 验证必填字段
// user_id 是 Option<String>,需要用 as_ref() 或直接检查
if request.user_id.as_ref().map_or(true, |s| s.is_empty()) {
return Err(AppError::with_i18n_key(
ERR_VALIDATION,
&get_i18n_message("error.user_id_required", locale),
));
}
if request.project_id.is_empty() {
return Err(AppError::with_i18n_key(
ERR_VALIDATION,
&get_i18n_message("error.project_id_required", locale),
));
}
// 2. 查找 session_id (如果未提供,从 AGENT_REGISTRY 获取)
let session_id = if let Some(sid) = request.session_id {
sid
} else {
// 从 AGENT_REGISTRY 查找
match AGENT_REGISTRY.get_agent_info(&request.project_id) {
Some(info) => {
// session_id 是 SessionId 类型,直接转换为 String
info.session_id.to_string()
}
None => {
// Agent 不存在,幂等返回成功
info!(
" [HTTP] Agent 不存在,幂等返回成功: project_id={}",
request.project_id
);
let response = ComputerAgentCancelResponse {
success: true,
session_id: String::new(),
};
return Ok(Json(HttpResult::success(response)));
}
}
};
// 3. 从 AGENT_REGISTRY 获取 Agent 信息并发送取消信号
if let Some(agent_info) = AGENT_REGISTRY.get_agent_info(&request.project_id) {
// 获取 cancel_tx
let cancel_tx = agent_info.cancel_tx.clone();
// 释放读锁
drop(agent_info);
// 检查是否已经空闲或停止中(幂等性)
if cancel_tx.is_closed() {
info!(
" [HTTP] Agent stopped, cancel channel is closed: session_id={}",
session_id
);
} else {
// 创建取消通知
let session_id_obj = SessionId::new(Arc::from(session_id.as_str()));
let cancel_notification = CancelNotification::new(session_id_obj);
// 创建 oneshot channel 接收取消结果 (HTTP 不等待结果,直接丢弃)
let (result_tx, _result_rx) = oneshot::channel();
let cancel_request = CancelNotificationRequestWrapper {
cancel_notification,
result_tx,
};
// 发送取消信号 (异步)
match cancel_tx.send(cancel_request).await {
Ok(_) => {
info!("[HTTP] Cancel signal sent: session_id={}", session_id);
}
Err(e) => {
warn!(
"⚠️ [HTTP] Failed to send cancel signal: session_id={}, error={}",
session_id, e
);
}
}
}
} else {
// Session 不存在,幂等返回成功
info!(
" [HTTP] Agent not found, returning success idempotently: session_id={}",
session_id
);
}
let response = ComputerAgentCancelResponse {
success: true,
session_id: session_id.clone(),
};
Ok(Json(HttpResult::success(response)))
}

View File

@@ -0,0 +1,211 @@
//! Computer Chat Handler
//!
//! 处理 POST /computer/chat 请求
use axum::{extract::State, http::HeaderMap, Json};
use std::sync::Arc;
use tracing::{error, info};
use crate::http_server::router::AppState;
use crate::service::AGENT_REGISTRY;
use crate::service::chat_handler::{ChatHandlerContext, ChatHandlerInput, handle_chat_core};
use crate::service::{SESSION_CACHE, SessionData};
use dashmap::mapref::entry::Entry;
use shared_types::{
ChatResponse, ComputerChatRequest, HttpResult, I18nJsonOrQuery, ServiceType,
error_codes::ERR_INTERNAL_SERVER_ERROR, error_codes::ERR_VALIDATION, get_i18n_message,
};
use super::locale_from_headers;
/// 处理 Computer Agent Chat 请求
///
/// 直接调用 agent_runner 本地的 handle_chat_core(),无需 gRPC 转发
#[utoipa::path(
post,
path = "/computer/chat",
request_body = ComputerChatRequest,
responses(
(status = 200, description = "Chat request successful", body = HttpResult<ChatResponse>),
(status = 400, description = "Bad request - missing user_id"),
(status = 500, description = "Internal server error")
),
tag = "Computer Agent"
)]
pub async fn handle_computer_chat(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
I18nJsonOrQuery(request): I18nJsonOrQuery<ComputerChatRequest>,
) -> Result<Json<HttpResult<ChatResponse>>, shared_types::AppError> {
let locale = locale_from_headers(&headers);
info!(
"📨 [HTTP] Received Computer Chat request:\n\
├─ user_id: {:?}\n\
├─ project_id: {:?}\n\
├─ session_id: {:?}\n\
├─ request_id: {:?}\n\
├─ prompt ({}chars): {:?}\n\
├─ pod_id: {:?}\n\
├─ tenant_id: {:?}\n\
├─ space_id: {:?}\n\
├─ isolation_type: {:?}\n\
├─ attachments: {:?}\n\
├─ data_source_attachments: {:?}\n\
├─ model_provider: {:#?}\n\
├─ agent_config: {:#?}\n\
├─ system_prompt: {:?}\n\
└─ user_prompt: {:?}",
request.user_id,
request.project_id,
request.session_id,
request.request_id,
request.prompt.len(),
request.prompt,
request.pod_id,
request.tenant_id,
request.space_id,
request.isolation_type,
request.attachments,
request.data_source_attachments,
request.model_provider,
request.agent_config,
request.system_prompt,
request.user_prompt
);
// 1. 验证必填字段
if request.user_id.is_empty() {
let error_msg = get_i18n_message("error.user_id_required", locale);
error!("[HTTP] {}", error_msg);
return Err(shared_types::AppError::with_i18n_key(
ERR_VALIDATION,
&error_msg,
));
}
let user_id = request.user_id.clone();
// 2. 生成或使用提供的 project_id (直接用 UUID去掉连字符与 rcoder 保持一致)
let project_id = request
.project_id
.clone()
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string().replace('-', ""));
// 3. 自动查找现有 session_id (如果未提供)
let session_id = request.session_id.or_else(|| {
AGENT_REGISTRY
.get_agent_info(&project_id)
.map(|info| info.session_id.to_string())
});
// 4. 创建项目工作目录(使用配置中的 projects_dir支持外部配置
// Docker 挂载:宿主机 /computer-project-workspace/{user_id} → 容器 /home/user
// Agent 工作目录:/home/user/{project_id}
let project_dir = state.config.projects_dir.join(&project_id);
if let Err(e) = tokio::fs::create_dir_all(&project_dir).await {
let error_msg = format!(
"{}: {}",
get_i18n_message("error.project_dir_create_failed", locale),
e
);
error!("[HTTP] {}", error_msg);
return Err(shared_types::AppError::with_message(
ERR_INTERNAL_SERVER_ERROR,
&error_msg,
));
}
// 5. 生成或使用提供的 request_id
let request_id = request
.request_id
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
// 6. 构建 ChatHandlerInput
let input = ChatHandlerInput {
project_id: project_id.clone(),
project_dir,
session_id,
prompt: request.prompt,
request_id: request_id.clone(),
attachments: request.attachments,
data_source_attachments: request.data_source_attachments,
model_config: request.model_provider,
service_type: ServiceType::ComputerAgentRunner,
agent_config_override: request.agent_config,
system_prompt_override: request.system_prompt,
user_prompt_template_override: request.user_prompt,
skip_slot_limit: true, // HTTP Server 部署,跳过槽位限制
};
// 7. 构建 ChatHandlerContext
let context = ChatHandlerContext {
agent_runtime: state.agent_runtime.clone(),
shared_api_key_manager: state.shared_api_key_manager.clone(),
project_uuid_map: state.project_uuid_map.clone(),
};
// 8. 调用核心 Chat 处理逻辑
let output = handle_chat_core(input, &context).await;
// 🔧 关键修复:将 session 写入 SESSION_CACHESSE 进度流需要从这里读取)
let session_id_str = output.session_id.clone();
match SESSION_CACHE.entry(session_id_str.clone()) {
Entry::Occupied(entry) => {
info!(
"[HTTP] SESSION_CACHE already exists, reusing: session_id={}",
session_id_str
);
entry.get().clone()
}
Entry::Vacant(entry) => {
let data = SessionData::new(1000);
info!(
"[HTTP] SESSION_CACHE created: session_id={}",
session_id_str
);
entry.insert(data.clone());
data
}
};
// 9. 构建响应
let response = ChatResponse {
project_id: output.project_id.clone(),
session_id: output.session_id.clone(),
error: output.error.clone(),
request_id: Some(request_id),
need_fallback: None,
fallback_reason: None,
};
// 10. 根据执行结果返回成功或错误
if output.error.is_some() || !output.success {
error!(
"❌ [HTTP] Computer Chat failed: session_id={}, error={:?}",
response.session_id, response.error
);
// 返回成功的 HTTP 状态码,但 HttpResult 包含错误信息
// 这与 rcoder 的行为一致HTTP 200 + HttpResult.error
return Ok(Json(HttpResult::error(
output
.error_code
.as_deref()
.unwrap_or(ERR_INTERNAL_SERVER_ERROR),
&shared_types::get_error_message(
output
.error_code
.as_deref()
.unwrap_or(ERR_INTERNAL_SERVER_ERROR),
locale,
),
)));
}
info!(
"✅ [HTTP] Computer Chat response: session_id={}, error={:?}",
response.session_id, response.error
);
Ok(Json(HttpResult::success(response)))
}

View File

@@ -0,0 +1,247 @@
//! Computer Agent Progress Handler
//!
//! 处理 GET /computer/progress/{session_id} 请求 (SSE 流)
use axum::{
Json,
extract::{Path, State},
http::{HeaderMap, StatusCode},
response::sse::{Event, KeepAlive, Sse},
};
use chrono::Utc;
use futures_util::stream::{Stream, StreamExt};
use std::convert::Infallible;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use tokio_stream::wrappers::ReceiverStream;
use tracing::{error, info, warn};
use crate::http_server::router::AppState;
use crate::service::{AGENT_REGISTRY, SESSION_CACHE};
use shared_types::{
AgentStatus, HttpResult, SessionMessageType, UnifiedSessionMessage,
error_codes::{ERR_INTERNAL_SERVER_ERROR, ERR_SESSION_NOT_FOUND},
get_i18n_message,
};
use super::locale_from_headers;
/// 统一的 SSE 流类型
type SseStream = Pin<Box<dyn Stream<Item = Result<Event, Infallible>> + Send>>;
/// 检查 Agent 是否处于 idle 状态
///
/// 返回 true 如果:
/// - Agent 不存在,或
/// - Agent 状态为 Idle
/// - Agent 的 session_id 与当前请求的 session_id 不匹配
async fn is_agent_idle(session_id: &str) -> bool {
// 通过 session_id 获取 agent_info
if let Some(info) = AGENT_REGISTRY.get_agent_info_by_session(session_id) {
// 检查状态是否为 Idle或者 session_id 是否匹配
info.status == AgentStatus::Idle || info.session_id.to_string() != session_id
} else {
// Agent 不存在,视为 idle
true
}
}
/// 创建 Agent idle 时的结束事件
///
/// 当 Agent 处于闲置状态时,发送此事件通知前端没有正在执行的任务
fn create_idle_end_event(session_id: &str) -> Event {
let unified_message = UnifiedSessionMessage {
session_id: session_id.to_string(),
message_type: SessionMessageType::SessionPromptEnd,
sub_type: "end_turn".to_string(),
data: serde_json::json!({
"reason": "EndTurn",
"description": "Agent 当前无在执行任务"
}),
timestamp: Utc::now(),
};
let json_data = match serde_json::to_string(&unified_message) {
Ok(json) => json,
Err(e) => {
warn!(
"⚠️ [HTTP] 序列化 SessionPromptEnd 消息失败: session_id={}, error={}",
session_id, e
);
// 返回包含 session_id 的最小可用结构
format!(
r#"{{"sessionId":"{}","messageType":"sessionPromptEnd","subType":"end_turn","data":{{"reason":"EndTurn","description":"Agent 当前无在执行任务"}}}}"#,
session_id
)
}
};
Event::default().event("end_turn").data(json_data)
}
/// 创建心跳消息流
///
/// 定期发送符合 UnifiedSessionMessage 格式的心跳消息
fn create_heartbeat_stream(
session_id: String,
) -> impl Stream<Item = Result<Event, Infallible>> + Send {
let heartbeat_interval = Duration::from_secs(15);
let (tx, rx) = tokio::sync::mpsc::channel(10);
tokio::spawn(async move {
let mut interval = tokio::time::interval(heartbeat_interval);
// 立即发送第一个心跳,然后按间隔发送
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
interval.tick().await;
// 创建心跳消息
let heartbeat_msg = UnifiedSessionMessage::heartbeat(session_id.clone());
let json_str = match serde_json::to_string(&heartbeat_msg) {
Ok(s) => s,
Err(e) => {
error!("[HTTP] Failed to serialize heartbeat message: {}", e);
continue;
}
};
// 使用 sub_type ("ping") 作为事件名
if tx
.send(Ok(Event::default().event("ping").data(json_str)))
.await
.is_err()
{
// 接收端已关闭,停止发送心跳
break;
}
}
});
ReceiverStream::new(rx)
}
/// Computer Agent 进度流 (SSE)
///
/// 直接从 SESSION_CACHE 订阅消息流,无需 gRPC
#[utoipa::path(
get,
path = "/computer/progress/{session_id}",
responses(
(status = 200, description = "SSE progress stream", content_type = "text/event-stream"),
(status = 404, description = "Session not found"),
(status = 500, description = "Internal server error")
),
tag = "Computer Agent"
)]
pub async fn handle_computer_progress(
State(_state): State<Arc<AppState>>,
headers: HeaderMap,
Path(session_id): Path<String>,
) -> Result<Sse<SseStream>, (StatusCode, Json<HttpResult<String>>)> {
let locale = locale_from_headers(&headers);
info!(
"📡 [HTTP] Computer Agent progress stream subscribed: session_id={}",
session_id
);
// 0. 检查 Agent 状态(必须在 SESSION_CACHE 查找之前检查)
if is_agent_idle(&session_id).await {
info!(
"💤 [HTTP] Agent is idle, sending SessionPromptEnd and closing: session_id={}",
session_id
);
let end_event = create_idle_end_event(&session_id);
let stream: SseStream = Box::pin(futures_util::stream::iter([Ok(end_event)]));
return Ok(Sse::new(stream));
}
// 1. 从 SESSION_CACHE 获取 session_data
// 🛡️ 关键修复:先 clone Arc<SessionData>,立即释放 DashMap shard 读锁
// 之前直接在 Ref 上调用 create_new_connection().await导致 DashMap 读锁跨 await 持有
// 可能造成与 SESSION_CACHE.entry()/remove() 等写操作的死锁
let session_data = match SESSION_CACHE.get(&session_id) {
Some(data) => data.value().clone(),
None => {
warn!(" [HTTP] Session not found: session_id={}", session_id);
return Err((
StatusCode::NOT_FOUND,
Json(HttpResult::error_with_message(
ERR_SESSION_NOT_FOUND,
locale,
&format!(
"{}: {}",
get_i18n_message("error.session_not_found", locale),
session_id
),
)),
));
}
};
// 2. 创建新的消息订阅DashMap 锁已释放,此处 await 安全)
let (message_rx, _cancel_token) = match session_data.create_new_connection(1000).await {
Ok(conn) => conn,
Err(e) => {
error!(
"❌ [HTTP] Failed to create session connection: session_id={}, error={}",
session_id, e
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(HttpResult::error_with_message(
ERR_INTERNAL_SERVER_ERROR,
locale,
&format!(
"{}: {}",
get_i18n_message("error.internal_server_error", locale),
e
),
)),
));
}
};
// 3. 创建消息流和心跳流
// UnifiedSessionMessage 已使用 #[serde(rename_all = "camelCase")], 序列化后符合 RCoder 约定
let message_stream = ReceiverStream::new(message_rx).map(|msg| {
let is_terminal = matches!(msg.message_type, SessionMessageType::SessionPromptEnd);
let json_str = match serde_json::to_string(&msg) {
Ok(s) => s,
Err(e) => {
error!("[HTTP] Failed to serialize message: {}", e);
return (Ok(Event::default().data("{}")), false);
}
};
(Ok(Event::default().event(msg.sub_type).data(json_str)), is_terminal)
});
// 4. 创建心跳流(标记为非终端)
let heartbeat_stream = create_heartbeat_stream(session_id.clone())
.map(|event| (event, false));
// 5. 合并两个流,并用 scan 监测终止条件
// select 会继续轮询心跳流(永不结束),所以必须在合并流层面检测终止
// 终止条件:
// - 收到 SessionPromptEnd终端消息→ 发送后结束流
// - channel 关闭 → message_stream 返回 Nonescan 最终也会结束
let merged_stream = futures_util::stream::select(message_stream, heartbeat_stream)
.scan(false, |seen_terminal, (event, is_terminal)| {
if *seen_terminal {
// 已发送终端消息,结束流
return std::future::ready(None);
}
if is_terminal {
*seen_terminal = true;
}
std::future::ready(Some(event))
});
info!("[HTTP] SSE stream established: session_id={}", session_id);
let stream: SseStream = Box::pin(merged_stream);
Ok(Sse::new(stream))
}

View File

@@ -0,0 +1,95 @@
//! Computer Agent Status Handler
//!
//! 处理 POST /computer/agent/status 请求
use axum::{extract::State, http::HeaderMap, Json};
use std::sync::Arc;
use tracing::{info, warn};
use crate::http_server::router::AppState;
use crate::service::AGENT_REGISTRY;
use shared_types::{
ComputerAgentStatusRequest, ComputerAgentStatusResponse, HttpResult, I18nJsonOrQuery,
error_codes::ERR_VALIDATION, get_i18n_message,
};
use super::locale_from_headers;
/// 查询 Computer Agent 状态
///
/// 直接使用 AGENT_REGISTRY 查询,无需 gRPC 调用
#[utoipa::path(
post,
path = "/computer/agent/status",
request_body = ComputerAgentStatusRequest,
responses(
(status = 200, description = "Status query successful", body = HttpResult<ComputerAgentStatusResponse>),
(status = 400, description = "Bad request - missing fields"),
(status = 500, description = "Internal server error")
),
tag = "Computer Agent"
)]
pub async fn handle_computer_status(
State(_state): State<Arc<AppState>>,
headers: HeaderMap,
I18nJsonOrQuery(request): I18nJsonOrQuery<ComputerAgentStatusRequest>,
) -> Result<Json<HttpResult<ComputerAgentStatusResponse>>, shared_types::AppError> {
let locale = locale_from_headers(&headers);
// 使用 garde 进行字段校验
let I18nJsonOrQuery(request) = I18nJsonOrQuery(request).validate_into_app_error()?;
let project_id = request.project_id.as_ref().expect("validated: project_id is required and non-empty");
// 验证 user_id 或 project_id 至少有一个
let user_id_empty = request.user_id.as_ref().map_or(true, |s| s.is_empty());
if user_id_empty && project_id.is_empty() {
return Err(shared_types::AppError::with_i18n_key(
ERR_VALIDATION,
&get_i18n_message("error.user_id_or_project_id_required", locale),
));
}
info!(
"🔍 [HTTP] Computer Agent 状态查询: user_id={:?}, project_id={}, pod_id={:?}, tenant_id={:?}, space_id={:?}, isolation_type={:?}",
request.user_id, project_id, request.pod_id, request.tenant_id, request.space_id, request.isolation_type
);
// 从 AGENT_REGISTRY 查询 Agent 状态
let agent_info = AGENT_REGISTRY.get_agent_info(project_id);
let response = match agent_info {
Some(info) => {
// Agent 存在且活跃
info!(
"✅ [HTTP] Agent status: project_id={}, is_alive=true, session_id={:?}",
project_id, info.session_id
);
ComputerAgentStatusResponse {
user_id: request.user_id.clone(),
project_id: project_id.to_string(),
is_alive: true,
session_id: Some(info.session_id.to_string()),
status: Some(format!("{:?}", info.status)),
last_activity: Some(info.last_activity),
created_at: Some(info.created_at),
}
}
None => {
// Agent 不存在
warn!(" [HTTP] Agent not found: project_id={}", project_id);
ComputerAgentStatusResponse {
user_id: request.user_id.clone(),
project_id: project_id.to_string(),
is_alive: false,
session_id: None,
status: None,
last_activity: None,
created_at: None,
}
}
};
Ok(Json(HttpResult::success(response)))
}

View File

@@ -0,0 +1,135 @@
//! Computer Agent Stop Handler
//!
//! 处理 POST /computer/agent/stop 请求
use axum::{extract::State, http::HeaderMap, Json};
use agent_client_protocol::schema::{CancelNotification, SessionId};
use std::sync::Arc;
use tokio::sync::oneshot;
use tracing::{info, warn};
use crate::CancelNotificationRequestWrapper;
use crate::http_server::router::AppState;
use crate::service::AGENT_REGISTRY;
use shared_types::{
ComputerAgentStopRequest, ComputerAgentStopResponse, HttpResult, I18nJsonOrQuery,
error_codes::{ERR_VALIDATION, SUCCESS},
get_error_message, get_i18n_message,
};
use super::locale_from_headers;
/// 停止 Computer Agent
///
/// 1. 发送取消信号停止正在运行的任务
/// 2. 从 AGENT_REGISTRY 移除 Agent 状态
#[utoipa::path(
post,
path = "/computer/agent/stop",
request_body = ComputerAgentStopRequest,
responses(
(status = 200, description = "Stop request successful", body = HttpResult<ComputerAgentStopResponse>),
(status = 400, description = "Bad request - missing fields"),
(status = 500, description = "Internal server error")
),
tag = "Computer Agent"
)]
pub async fn handle_computer_stop(
State(_state): State<Arc<AppState>>,
headers: HeaderMap,
I18nJsonOrQuery(request): I18nJsonOrQuery<ComputerAgentStopRequest>,
) -> Result<Json<HttpResult<ComputerAgentStopResponse>>, shared_types::AppError> {
let locale = locale_from_headers(&headers);
// 使用 garde 进行字段校验
let I18nJsonOrQuery(request) = I18nJsonOrQuery(request).validate_into_app_error()?;
let project_id = request.project_id.as_ref().expect("validated: project_id is required and non-empty");
// 验证 user_id 或 project_id 至少有一个
let user_id_empty = request.user_id.as_ref().map_or(true, |s| s.is_empty());
if user_id_empty && project_id.is_empty() {
return Err(shared_types::AppError::with_i18n_key(
ERR_VALIDATION,
&get_i18n_message("error.user_id_or_project_id_required", locale),
));
}
info!(
"🛑 [HTTP] Computer Agent 停止请求: user_id={:?}, project_id={}, pod_id={:?}, tenant_id={:?}, space_id={:?}, isolation_type={:?}",
request.user_id, project_id, request.pod_id, request.tenant_id, request.space_id, request.isolation_type
);
// 获取 Agent 信息并发送取消信号
let (success, message) =
if let Some(agent_info) = AGENT_REGISTRY.get_agent_info(project_id) {
let session_id = agent_info.session_id.to_string();
let cancel_tx = agent_info.cancel_tx.clone();
// 释放读锁
drop(agent_info);
// 发送取消信号(如果 channel 仍然打开)
if !cancel_tx.is_closed() {
let session_id_obj = SessionId::new(Arc::from(session_id.as_str()));
let cancel_notification = CancelNotification::new(session_id_obj);
let (result_tx, _result_rx) = oneshot::channel();
let cancel_request = CancelNotificationRequestWrapper {
cancel_notification,
result_tx,
};
match cancel_tx.send(cancel_request).await {
Ok(_) => {
info!("[HTTP] Cancel signal sent: session_id={}", session_id);
}
Err(e) => {
warn!(
"⚠️ [HTTP] Failed to send cancel signal: session_id={}, error={}",
session_id, e
);
}
}
}
// 从 AGENT_REGISTRY 移除 Agent
let removed = AGENT_REGISTRY
.remove_by_project(project_id)
.is_some();
if removed {
info!("[HTTP] Agent stopped: project_id={}", project_id);
(true, get_error_message(SUCCESS, locale))
} else {
// 可能在取消期间已被清理
info!(
" [HTTP] Agent already cleaned up: project_id={}",
project_id
);
(
true,
get_i18n_message("success.agent_already_stopped", locale),
)
}
} else {
// Agent 不存在,幂等返回成功
info!(
" [HTTP] Agent not found, returning success idempotently: project_id={}",
project_id
);
(
true,
get_i18n_message("success.agent_already_stopped", locale),
)
};
let response = ComputerAgentStopResponse {
success,
message,
user_id: request.user_id.clone(),
pod_id: None,
project_id: project_id.to_string(),
};
Ok(Json(HttpResult::success(response)))
}

View File

@@ -0,0 +1,17 @@
//! HTTP 请求处理器
//!
//! 仅在 `http-server` feature 启用时编译
use axum::http::HeaderMap;
pub mod computer_cancel;
pub mod computer_chat;
pub mod computer_progress;
pub mod computer_status;
pub mod computer_stop;
pub mod rcoder_progress; // RCoder 模式的 SSE 进度流
pub(super) fn locale_from_headers(headers: &HeaderMap) -> &'static str {
let accept_language = headers.get("accept-language").and_then(|v| v.to_str().ok());
shared_types::parse_accept_language(accept_language)
}

View File

@@ -0,0 +1,247 @@
//! RCoder Agent Progress Handler
//!
//! 处理 GET /agent/progress/{session_id} 请求 (SSE 流)
//!
//! 与 computer_progress.rs 使用相同的 SSE 流模式,直接从 SESSION_CACHE 订阅消息
use std::convert::Infallible;
use std::pin::Pin;
use std::time::Duration;
use axum::{
extract::Path,
http::{HeaderMap, StatusCode},
Json,
response::sse::{Event, Sse},
};
use chrono::Utc;
use futures_util::stream::{Stream, StreamExt};
use tokio_stream::wrappers::ReceiverStream;
use tracing::{error, info, warn};
use crate::service::{AGENT_REGISTRY, SESSION_CACHE};
use shared_types::{
AgentStatus, HttpResult, SessionMessageType, UnifiedSessionMessage,
error_codes::{ERR_INTERNAL_SERVER_ERROR, ERR_SESSION_NOT_FOUND},
get_i18n_message,
};
use super::locale_from_headers;
/// 统一的 SSE 流类型
type SseStream = Pin<Box<dyn Stream<Item = Result<Event, Infallible>> + Send>>;
/// 检查 Agent 是否处于 idle 状态
///
/// 返回 true 如果:
/// - Agent 不存在,或
/// - Agent 状态为 Idle
/// - Agent 的 session_id 与当前请求的 session_id 不匹配
async fn is_agent_idle(session_id: &str) -> bool {
// 通过 session_id 获取 agent_info
if let Some(info) = AGENT_REGISTRY.get_agent_info_by_session(session_id) {
// 检查状态是否为 Idle或者 session_id 是否匹配
info.status == AgentStatus::Idle || info.session_id.to_string() != session_id
} else {
// Agent 不存在,视为 idle
true
}
}
/// 创建 Agent idle 时的结束事件
///
/// 当 Agent 处于闲置状态时,发送此事件通知前端没有正在执行的任务
fn create_idle_end_event(session_id: &str) -> Event {
let unified_message = UnifiedSessionMessage {
session_id: session_id.to_string(),
message_type: SessionMessageType::SessionPromptEnd,
sub_type: "end_turn".to_string(),
data: serde_json::json!({
"reason": "EndTurn",
"description": "Agent 当前无在执行任务"
}),
timestamp: Utc::now(),
};
let json_data = match serde_json::to_string(&unified_message) {
Ok(json) => json,
Err(e) => {
warn!(
"⚠️ [RCoder] 序列化 SessionPromptEnd 消息失败: session_id={}, error={}",
session_id, e
);
// 返回包含 session_id 的最小可用结构
format!(
r#"{{"sessionId":"{}","messageType":"sessionPromptEnd","subType":"end_turn","data":{{"reason":"EndTurn","description":"Agent 当前无在执行任务"}}}}"#,
session_id
)
}
};
Event::default().event("end_turn").data(json_data)
}
/// 创建心跳消息流
///
/// 定期发送符合 UnifiedSessionMessage 格式的心跳消息
fn create_heartbeat_stream(
session_id: String,
) -> impl Stream<Item = Result<Event, Infallible>> + Send {
let heartbeat_interval = Duration::from_secs(15);
let (tx, rx) = tokio::sync::mpsc::channel(10);
tokio::spawn(async move {
let mut interval = tokio::time::interval(heartbeat_interval);
// 立即发送第一个心跳,然后按间隔发送
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
interval.tick().await;
// 创建心跳消息
let heartbeat_msg = UnifiedSessionMessage::heartbeat(session_id.clone());
let json_str = match serde_json::to_string(&heartbeat_msg) {
Ok(s) => s,
Err(e) => {
error!("[RCoder] Failed to serialize heartbeat message: {}", e);
continue;
}
};
// 使用 sub_type ("ping") 作为事件名
if tx
.send(Ok(Event::default().event("ping").data(json_str)))
.await
.is_err()
{
// 接收端已关闭,停止发送心跳
break;
}
}
});
ReceiverStream::new(rx)
}
/// RCoder Agent 进度流 (SSE)
///
/// 直接从 SESSION_CACHE 订阅消息流,无需 gRPC
#[utoipa::path(
get,
path = "/agent/progress/{session_id}",
params(
("session_id" = String, Path, description = "会话ID")
),
responses(
(status = 200, description = "SSE progress stream", content_type = "text/event-stream"),
(status = 404, description = "Session not found"),
(status = 500, description = "Internal server error")
),
tag = "RCoder Agent"
)]
pub async fn handle_rcoder_progress(
headers: HeaderMap,
Path(session_id): Path<String>,
) -> Result<Sse<SseStream>, (StatusCode, Json<HttpResult<String>>)> {
let locale = locale_from_headers(&headers);
info!(
"📡 [RCoder] Progress stream subscribed: session_id={}",
session_id
);
// 0. 检查 Agent 状态(必须在 SESSION_CACHE 查找之前检查)
if is_agent_idle(&session_id).await {
info!(
"💤 [RCoder] Agent is idle, sending SessionPromptEnd and closing: session_id={}",
session_id
);
let end_event = create_idle_end_event(&session_id);
let stream: SseStream = Box::pin(futures_util::stream::iter([Ok(end_event)]));
return Ok(Sse::new(stream));
}
// 1. 从 SESSION_CACHE 获取 session_data
// 🛡️ 关键:先 clone Arc<SessionData>,立即释放 DashMap shard 读锁
let session_data = match SESSION_CACHE.get(&session_id) {
Some(data) => data.value().clone(),
None => {
warn!(" [RCoder] Session not found: session_id={}", session_id);
return Err((
StatusCode::NOT_FOUND,
Json(HttpResult::error_with_message(
ERR_SESSION_NOT_FOUND,
locale,
&format!(
"{}: {}",
get_i18n_message("error.session_not_found", locale),
session_id
),
)),
));
}
};
// 2. 创建新的消息订阅DashMap 锁已释放,此处 await 安全)
let (message_rx, _cancel_token) = match session_data.create_new_connection(1000).await {
Ok(conn) => conn,
Err(e) => {
error!(
"❌ [RCoder] Failed to create session connection: session_id={}, error={}",
session_id, e
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(HttpResult::error_with_message(
ERR_INTERNAL_SERVER_ERROR,
locale,
&format!(
"{}: {}",
get_i18n_message("error.internal_server_error", locale),
e
),
)),
));
}
};
// 3. 创建消息流和心跳流
let message_stream = ReceiverStream::new(message_rx).map(|msg| {
let is_terminal = matches!(msg.message_type, SessionMessageType::SessionPromptEnd);
let json_str = match serde_json::to_string(&msg) {
Ok(s) => s,
Err(e) => {
error!("[RCoder] Failed to serialize message: {}", e);
return (Ok(Event::default().data("{}")), false);
}
};
(Ok(Event::default().event(msg.sub_type).data(json_str)), is_terminal)
});
// 4. 创建心跳流(标记为非终端)
let heartbeat_stream = create_heartbeat_stream(session_id.clone())
.map(|event| (event, false));
// 5. 合并两个流,并用 scan 监测终止条件
// select 会继续轮询心跳流(永不结束),所以必须在合并流层面检测终止
// 终止条件:
// - 收到 SessionPromptEnd终端消息→ 发送后结束流
// - channel 关闭 → message_stream 返回 Nonescan 最终也会结束
let merged_stream = futures_util::stream::select(message_stream, heartbeat_stream)
.scan(false, |seen_terminal, (event, is_terminal)| {
if *seen_terminal {
// 已发送终端消息,结束流
return std::future::ready(None);
}
if is_terminal {
*seen_terminal = true;
}
std::future::ready(Some(event))
});
info!("[RCoder] SSE stream established: session_id={}", session_id);
let stream: SseStream = Box::pin(merged_stream);
Ok(Sse::new(stream))
}

View File

@@ -0,0 +1,11 @@
//! HTTP 服务器模块
//!
//! 仅在 `http-server` feature 启用时编译
//! 提供 Computer Agent HTTP REST API
pub mod handlers;
pub mod router;
pub mod start;
pub use router::{AppState, create_router};
pub use start::{HttpServerConfig, start_http_server};

View File

@@ -0,0 +1,186 @@
//! HTTP 路由定义
//!
//! 定义所有 HTTP 端点和路由
use axum::{
Json, Router,
routing::{get, post},
};
use dashmap::DashMap;
use std::sync::Arc;
use tower_http::limit::RequestBodyLimitLayer;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use crate::agent_runtime::AgentRuntime;
use crate::api_key_manager::ApiKeyManager;
use crate::config::AppConfig;
use crate::service::local_agent_service::LocalAgentHttpService;
/// HTTP 应用状态
#[derive(Clone)]
pub struct AppState {
pub config: AppConfig,
pub agent_runtime: Arc<AgentRuntime>,
pub api_key_manager: Arc<ApiKeyManager>,
pub shared_api_key_manager: Arc<DashMap<String, shared_types::ModelProviderConfig>>,
pub project_uuid_map: Arc<DashMap<String, String>>,
}
impl AppState {
pub fn new(
config: AppConfig,
agent_runtime: Arc<AgentRuntime>,
shared_api_key_manager: Arc<DashMap<String, shared_types::ModelProviderConfig>>,
) -> Self {
Self {
config,
agent_runtime: agent_runtime.clone(),
api_key_manager: Arc::new(ApiKeyManager::from_shared(shared_api_key_manager.clone())),
shared_api_key_manager,
project_uuid_map: Arc::new(DashMap::new()),
}
}
/// 创建 LocalAgentHttpService 实例用于 RCoder 模式
pub fn create_local_agent_service(&self) -> Arc<LocalAgentHttpService> {
Arc::new(LocalAgentHttpService::new(
self.agent_runtime.clone(),
self.shared_api_key_manager.clone(),
self.project_uuid_map.clone(),
self.config.projects_dir.clone(),
))
}
}
/// 创建 HTTP 路由
///
/// 组合 Computer Agent 路由和 RCoder Agent 路由
pub fn create_router(state: Arc<AppState>) -> Router {
use super::handlers::{
computer_cancel, computer_chat, computer_progress, computer_status, computer_stop,
rcoder_progress,
};
use shared_types::http_handlers;
// 创建 LocalAgentHttpService 实例
let local_agent_service = state.create_local_agent_service();
// Computer Agent 路由
let computer_routes = Router::new()
.route("/computer/chat", post(computer_chat::handle_computer_chat))
.route(
"/computer/agent/stop",
post(computer_stop::handle_computer_stop),
)
.route(
"/computer/agent/status",
post(computer_status::handle_computer_status),
)
.route(
"/computer/agent/session/cancel",
post(computer_cancel::handle_computer_cancel),
)
.route(
"/computer/progress/{session_id}",
get(computer_progress::handle_computer_progress),
)
.with_state(state.clone());
// RCoder Agent 路由(使用 LocalAgentHttpService
let rcoder_routes = Router::new()
.route("/chat", post(http_handlers::handle_chat::<LocalAgentHttpService>))
.route(
"/agent/session/cancel",
post(http_handlers::handle_cancel::<LocalAgentHttpService>),
)
.route("/agent/stop", post(http_handlers::handle_stop::<LocalAgentHttpService>))
.route(
"/agent/status/{project_id}",
get(http_handlers::handle_status::<LocalAgentHttpService>),
)
.route(
"/agent/progress/{session_id}",
get(rcoder_progress::handle_rcoder_progress),
)
.with_state(local_agent_service);
// 通用路由
let api_routes = Router::new()
.route("/health", get(health_check))
.with_state(state.clone());
// 组合路由
Router::new()
.merge(computer_routes)
.merge(rcoder_routes)
.merge(api_routes)
.merge(create_swagger_ui())
.layer(RequestBodyLimitLayer::new(50 * 1024 * 1024)) // 🔥 50MB body 限制
}
/// 健康检查端点
///
/// 检查服务的健康状态
#[utoipa::path(
get,
path = "/health",
responses(
(status = 200, description = "服务健康状态", body = shared_types::HealthResponse)
),
tag = "system"
)]
pub async fn health_check() -> Json<shared_types::HealthResponse> {
Json(shared_types::HealthResponse::new("agent-runner"))
}
/// 创建 Swagger UI
fn create_swagger_ui() -> SwaggerUi {
use super::handlers::{
computer_cancel::__path_handle_computer_cancel, computer_chat::__path_handle_computer_chat,
computer_progress::__path_handle_computer_progress,
computer_status::__path_handle_computer_status, computer_stop::__path_handle_computer_stop,
};
#[derive(OpenApi)]
#[openapi(
paths(
// Computer Agent 端点
handle_computer_chat,
handle_computer_status,
handle_computer_stop,
handle_computer_cancel,
handle_computer_progress,
// 健康检查
health_check,
),
components(schemas(
// Computer Agent 类型
shared_types::ComputerChatRequest,
shared_types::ChatResponse,
shared_types::ComputerAgentStatusRequest,
shared_types::ComputerAgentStatusResponse,
shared_types::ComputerAgentStopRequest,
shared_types::ComputerAgentStopResponse,
shared_types::ComputerAgentCancelRequest,
shared_types::ComputerAgentCancelResponse,
// RCoder Agent 类型
shared_types::RcoderChatRequest,
shared_types::RcoderAgentCancelRequest,
shared_types::RcoderAgentCancelResponse,
shared_types::RcoderAgentStopRequest,
shared_types::RcoderAgentStopResponse,
shared_types::AgentStatusResponse,
// 通用类型
shared_types::HealthResponse,
)),
tags(
(name = "Computer Agent", description = "Computer Agent HTTP API"),
(name = "RCoder Agent", description = "RCoder Agent HTTP API"),
(name = "System", description = "系统管理接口")
)
)]
struct ApiDoc;
SwaggerUi::new("/api/docs").url("/api-docs/openapi.json", ApiDoc::openapi())
}

View File

@@ -0,0 +1,266 @@
//! HTTP 服务器启动模块
//!
//! 提供便捷的 HTTP 服务器启动 API
//! 支持 HTTP REST API 和可选的 Pingora 代理服务
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use tracing::{error, info, warn};
use crate::agent_runtime::AgentRuntime;
use crate::config::AppConfig;
use crate::http_server::router::{AppState, create_router};
use crate::proxy_agent::cleanup_task::{CleanupConfig, start_cleanup_task};
use crate::proxy_agent::set_unlimited_mode;
#[cfg(feature = "proxy")]
use crate::proxy_agent::start_pingora;
/// HTTP 服务器配置
pub struct HttpServerConfig {
/// HTTP 监听端口
pub port: u16,
/// 应用配置
pub app_config: AppConfig,
/// Agent 运行时
pub agent_runtime: Arc<AgentRuntime>,
/// 共享 API Key Manager
pub shared_api_key_manager: Arc<dashmap::DashMap<String, shared_types::ModelProviderConfig>>,
}
/// HTTP 服务器控制柄
///
/// 用于控制 HTTP 服务器的生命周期
#[derive(Clone)]
pub struct HttpServerHandle {
/// 关闭信号令牌
shutdown_token: CancellationToken,
/// 活跃任务集合
join_set: Arc<tokio::sync::Mutex<JoinSet<()>>>,
/// Pingora 结果(用于调用 stop
#[cfg(feature = "proxy")]
pingora_result: Arc<tokio::sync::Mutex<Option<crate::proxy_agent::PingoraStartResult>>>,
}
impl HttpServerHandle {
/// 检查是否收到关闭信号
pub fn is_shutdown(&self) -> bool {
self.shutdown_token.is_cancelled()
}
/// 停止 HTTP 服务器并等待所有任务完成
pub async fn stop(&self) {
info!("Stopping HTTP server...");
// 1. 发送关闭信号
self.shutdown_token.cancel();
// 2. 停止 Pingora 服务
#[cfg(feature = "proxy")]
{
let mut pingora_guard = self.pingora_result.lock().await;
if let Some(mut pingora) = pingora_guard.take() {
pingora.stop().await;
}
}
// 3. 等待所有任务完成(带超时)
// 使用 3 秒超时清理任务会立即退出axum 有 3 秒进行连接排空
let timeout = Duration::from_secs(3);
let mut join_set = self.join_set.lock().await;
loop {
match tokio::time::timeout(timeout, join_set.join_next()).await {
Ok(Some(Ok(()))) => {
info!("Task exited normally");
}
Ok(Some(Err(e))) => {
warn!("Task error: {:?}", e);
}
Ok(None) => {
// JoinSet 为空,所有任务已完成
break;
}
Err(_) => {
warn!("Timed out waiting for tasks (3s), aborting remaining tasks");
join_set.abort_all();
break;
}
}
}
info!("HTTP server stopped");
}
}
/// 启动 HTTP 服务器
///
/// # 示例
///
/// ```no_run
/// use agent_runner::{AgentRuntime, start_http_server, HttpServerConfig, AppConfig, ProxyConfig, HealthCheckConfig};
/// use std::sync::Arc;
/// use std::path::PathBuf;
///
/// #[tokio::main]
/// async fn main() {
/// // 创建 Agent Runtime
/// let (runtime, receiver) = AgentRuntime::new(1000);
/// let runtime = Arc::new(runtime);
/// runtime.start(receiver).await;
///
/// // 配置 HTTP Server
/// let config = HttpServerConfig {
/// port: 8080,
/// app_config: AppConfig {
/// port: 8080,
/// projects_dir: PathBuf::from("/app/computer-project-workspace"),
/// // 可选:启用 Pingora 代理服务
/// proxy_config: Some(ProxyConfig {
/// listen_port: 8088,
/// default_backend_port: 8080,
/// backend_host: "127.0.0.1".to_string(),
/// port_param: "port".to_string(),
/// health_check: HealthCheckConfig::default(),
/// }),
/// ..Default::default()
/// },
/// agent_runtime: runtime,
/// shared_api_key_manager: Arc::new(dashmap::DashMap::new()),
/// };
///
/// // 启动 HTTP Server
/// let handle = start_http_server(config).await.unwrap();
///
/// // 优雅停止
/// handle.stop().await;
/// }
/// ```
pub async fn start_http_server(config: HttpServerConfig) -> Result<HttpServerHandle> {
// 设置 mcp-proxy 日志目录环境变量(如果配置了的话)
if let Some(ref log_dir) = config.app_config.mcp_proxy_log_dir {
// SAFETY: 在服务启动时设置环境变量是安全的,此时尚未启动多线程任务
unsafe {
std::env::set_var("MCP_PROXY_LOG_DIR", log_dir);
}
info!("🔧 Set MCP_PROXY_LOG_DIR={}", log_dir);
}
// 设置无限制模式HTTP Server 部署不限制槽位)
set_unlimited_mode(true);
// 创建关闭信号令牌
let shutdown_token = CancellationToken::new();
let join_set = Arc::new(tokio::sync::Mutex::new(JoinSet::new()));
#[cfg(feature = "proxy")]
let pingora_result = Arc::new(tokio::sync::Mutex::new(None));
// 1. 启动 Agent 清理任务
let cleanup_config = CleanupConfig {
idle_timeout: Duration::from_secs(
config
.app_config
.agent_cleanup
.clone()
.unwrap_or_default()
.idle_timeout_secs,
),
cleanup_interval: Duration::from_secs(
config
.app_config
.agent_cleanup
.clone()
.unwrap_or_default()
.cleanup_interval_secs,
),
};
info!(
"🧹 [HTTP] Agent cleanup config: idle_timeout={}s, cleanup_interval={}s",
cleanup_config.idle_timeout.as_secs(),
cleanup_config.cleanup_interval.as_secs()
);
let cleanup_token = shutdown_token.child_token();
join_set.lock().await.spawn(async move {
tokio::select! {
_ = start_cleanup_task(cleanup_config) => {}
_ = cleanup_token.cancelled() => {
info!("Cleanup task received shutdown signal");
}
}
});
// 2. 启动 Pingora 代理服务(如果配置了且启用了 proxy feature
#[cfg(feature = "proxy")]
if let Some(proxy_config) = &config.app_config.proxy_config {
let result = start_pingora(proxy_config, config.shared_api_key_manager.clone());
// 保存 Pingora 结果以便后续调用 stop
*pingora_result.lock().await = Some(result);
} else {
info!("Pingora proxy service is not configured, skipping startup");
}
#[cfg(not(feature = "proxy"))]
info!("Pingora proxy service is disabled (proxy feature not enabled)");
// 3. 创建 HTTP 应用状态
let state = Arc::new(AppState::new(
config.app_config.clone(),
config.agent_runtime,
config.shared_api_key_manager,
));
// 4. 创建路由
let app = create_router(state.clone());
// 5. 绑定地址并启动 HTTP 服务器
let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
let listener = tokio::net::TcpListener::bind(addr).await?;
info!("HTTP server started on port {}", config.port);
info!("HTTP API endpoints:");
info!(" POST /computer/chat - Computer Agent chat");
info!(" POST /computer/agent/status - Computer Agent status");
info!(" POST /computer/agent/stop - Computer Agent stop");
info!(" POST /computer/agent/session/cancel - Computer Agent cancel");
info!(" GET /computer/progress/:session_id - SSE progress stream");
info!(" -- RCoder Agent endpoints (new) --");
info!(" POST /chat - RCoder Agent chat");
info!(" GET /agent/status/:project_id - RCoder Agent status");
info!(" POST /agent/stop - RCoder Agent stop");
info!(" POST /agent/session/cancel - RCoder Agent cancel");
info!(" GET /agent/progress/:session_id - RCoder SSE progress stream");
info!(" -- Common endpoints --");
info!(" GET /health - Health check");
info!(" GET /api/docs - Swagger API documentation");
// 6. 启动 HTTP 服务任务
let http_token = shutdown_token.child_token();
// 将 listener 和 app 移入任务中
let http_app = app;
let http_listener = listener;
join_set.lock().await.spawn(async move {
// 使用 graceful shutdown wrapper
let server = axum::serve(http_listener, http_app).with_graceful_shutdown(async move {
let _ = http_token.cancelled().await;
});
match server.await {
Ok(()) => info!("HTTP service exited normally"),
Err(e) => error!("HTTP service error: {:?}", e),
}
});
// 创建 handle
let handle = HttpServerHandle {
shutdown_token,
join_set,
#[cfg(feature = "proxy")]
pingora_result,
};
Ok(handle)
}

View File

@@ -0,0 +1,37 @@
//! agent_runner 库
//!
//! 提供 AI 代理运行时和 ACP 协议集成
pub mod agent_runtime;
pub mod api_key_manager;
mod config;
pub mod grpc;
mod handler;
mod model;
pub mod otel_tracing; // 🔥 设为 public供其他模块使用
mod proxy_agent;
pub mod router;
pub mod service; // 🔥 设为 public供测试使用
mod utils;
// 条件性编译HTTP 服务器模块
#[cfg(feature = "http-server")]
pub mod http_server;
// 测试辅助模块 (仅在 testing feature 启用时编译)
#[cfg(feature = "testing")]
pub mod testing;
// 重新导出主要的类型和函数
pub use agent_runtime::*;
pub use config::*;
pub use model::*;
pub use otel_tracing::*;
pub use proxy_agent::*;
pub use service::*; // 重新导出 service 模块
pub use utils::*;
#[cfg(feature = "http-server")]
pub use http_server::start::HttpServerHandle;
#[cfg(feature = "http-server")]
pub use http_server::{HttpServerConfig, start_http_server};

View File

@@ -0,0 +1,599 @@
use clap::Parser;
use dashmap::DashMap;
use std::sync::Arc;
use std::time::Duration;
use tracing::{error, info, warn};
// 🆕 使用共享的遥测模块
use rcoder_telemetry::{TelemetryConfig, TelemetryGuard};
mod agent_runtime;
mod api_key_manager;
mod config;
mod grpc;
mod handler;
mod model;
mod process_reaper;
mod proxy_agent;
// 🔥 Pyroscope Profiler 模块(可选:需要 pyroscope feature
#[cfg(feature = "pyroscope")]
mod profiler;
// 🔥 OpenTelemetry 追踪模块(可选:保留用于向后兼容)
#[allow(dead_code)]
mod otel_tracing;
mod router;
mod service;
mod utils;
// HTTP 服务器模块 (仅在 http-server feature 启用时)
#[cfg(feature = "http-server")]
mod http_server;
use agent_runtime::AgentRuntime;
use config::{CliArgs, load_config_with_args};
use model::*;
use proxy_agent::cleanup_task::{CleanupConfig, start_cleanup_task};
#[cfg(feature = "proxy")]
use rcoder_proxy::{PingoraServerManager, ProxyConfig};
use router::AppState;
use std::fs::OpenOptions;
use std::io::Write;
use std::panic;
use std::path::PathBuf;
#[cfg(unix)]
use tokio::signal::unix::{SignalKind, signal};
/// 🔥 设置自定义 Panic Hook
///
/// 当 agent_runner panic 时,将完整的 panic 信息(包括 backtrace写入日志文件
/// 这样即使容器被销毁,也能通过挂载的日志目录找到崩溃原因
fn set_panic_hook() {
let default_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
// 🔥 立即写入日志文件(不依赖 tracing确保在 panic 时也能写入)
if let Err(e) = write_panic_to_file(panic_info) {
// 如果文件写入失败,尝试输出到 stderr
eprintln!("❌ [PANIC] Failed to write panic log file: {}", e);
}
// 🔥 同时输出到 stderrDocker 会捕获到容器日志)
eprintln!("═══════════════════════════════════════════════════════════");
eprintln!("❌ [PANIC] agent_runner encountered a fatal error!");
eprintln!("═══════════════════════════════════════════════════════════");
if let Some(location) = panic_info.location() {
eprintln!(
"panic.location: {}:{}:{}",
location.file(),
location.line(),
location.column()
);
}
eprintln!("panic.payload: {}", panic_info);
eprintln!("═══════════════════════════════════════════════════════════");
// 调用默认 hook会终止进程
default_hook(panic_info);
}));
}
/// 将 panic 信息写入日志文件
fn write_panic_to_file(panic_info: &panic::PanicHookInfo) -> std::io::Result<()> {
// 🔥 日志文件路径:/app/container-logs/agent_runner_panic.log使用已有的挂载目录
let log_path = PathBuf::from("/app/container-logs/agent_runner_panic.log");
// 确保目录存在
if let Some(parent) = log_path.parent() {
std::fs::create_dir_all(parent)?;
}
// 打开文件(追加模式)
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)?;
// 获取当前时间
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
// 写入 panic 信息
writeln!(
file,
"═══════════════════════════════════════════════════════════"
)?;
writeln!(file, "❌ [PANIC] agent_runner encountered a fatal error!")?;
writeln!(file, "time: {}", now)?;
writeln!(
file,
"═══════════════════════════════════════════════════════════"
)?;
if let Some(location) = panic_info.location() {
writeln!(
file,
"panic.location: {}:{}:{}",
location.file(),
location.line(),
location.column()
)?;
}
writeln!(file, "panic.payload: {}", panic_info)?;
// 写入 backtrace如果启用
#[cfg(feature = "backtrace")]
{
if let Ok(backtrace) = std::backtrace::Backtrace::capture() {
writeln!(file, "Backtrace:\n{}", backtrace)?;
}
}
writeln!(
file,
"═══════════════════════════════════════════════════════════\n"
)?;
// 强制刷新到磁盘
file.flush()?;
eprintln!("✅ Panic info written to: {}", log_path.display());
Ok(())
}
/// 🔥 设置优雅关闭信号处理器
///
/// 监听系统信号,实现优雅关闭:
/// - Unix: SIGTERM (Docker stop) + SIGINT (Ctrl+C)
/// - Windows: Ctrl+C
fn setup_shutdown_handler() -> tokio::task::JoinHandle<()> {
#[cfg(unix)]
{
tokio::spawn(async move {
// 监听 SIGTERMDocker stop
let mut sigterm = match signal(SignalKind::terminate()) {
Ok(s) => s,
Err(e) => {
eprintln!("❌ [SIGNAL] Failed to register SIGTERM handler: {}", e);
return;
}
};
// 监听 SIGINTCtrl+C
let mut sigint = match signal(SignalKind::interrupt()) {
Ok(s) => s,
Err(e) => {
eprintln!("❌ [SIGNAL] Failed to register SIGINT handler: {}", e);
return;
}
};
tokio::select! {
_ = sigterm.recv() => {
eprintln!("📨 [SIGNAL] Received SIGTERM (Docker stop), starting graceful shutdown...");
write_shutdown_log("SIGTERM");
}
_ = sigint.recv() => {
eprintln!("📨 [SIGNAL] Received SIGINT (Ctrl+C), starting graceful shutdown...");
write_shutdown_log("SIGINT");
}
}
eprintln!("🧹 [SIGNAL] Cleaning up resources...");
eprintln!("✅ [SIGNAL] Graceful shutdown completed, exiting");
std::process::exit(0);
})
}
#[cfg(not(unix))]
{
tokio::spawn(async move {
// Windows: 仅监听 Ctrl+C
if let Ok(()) = tokio::signal::ctrl_c().await {
eprintln!("📨 [SIGNAL] Received Ctrl+C, starting graceful shutdown...");
write_shutdown_log("Ctrl+C");
}
eprintln!("🧹 [SIGNAL] Cleaning up resources...");
eprintln!("✅ [SIGNAL] Graceful shutdown completed, exiting");
std::process::exit(0);
})
}
}
/// 将关闭事件写入日志文件
fn write_shutdown_log(signal: &str) {
use std::fs::OpenOptions;
use std::io::Write;
let log_path = PathBuf::from("/app/container-logs/agent_runner_shutdown.log");
if let Some(parent) = log_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&log_path) {
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
let _ = writeln!(
file,
"═══════════════════════════════════════════════════════════"
);
let _ = writeln!(
file,
"📨 [SHUTDOWN] agent_runner received a shutdown signal"
);
let _ = writeln!(file, "signal: {}", signal);
let _ = writeln!(file, "time: {}", now);
let _ = writeln!(
file,
"═══════════════════════════════════════════════════════════\n"
);
let _ = file.flush();
eprintln!("✅ Shutdown info written to: {}", log_path.display());
}
}
// 路由创建函数已移动到 handler 模块
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 🔥 设置自定义 Panic Hook确保 panic 信息被记录
set_panic_hook();
// 🔥 设置信号处理器实现优雅关闭Docker stop、Ctrl+C
let _shutdown_handle = setup_shutdown_handler();
// ✅ 初始化 Rustls CryptoProvider必须在最前面在任何可能使用 TLS 的代码之前)
// 🔥 如果这里失败,会导致 panic但 panic hook 会捕获并记录
rustls::crypto::ring::default_provider()
.install_default()
.expect(
"❌ [FATAL] Rustls CryptoProvider initialization failed. The process cannot continue. This is usually an environment issue.",
);
// 🆕 Initializing telemetry system使用 rcoder-telemetry包含控制台 + 文件日志)
let telemetry_config = TelemetryConfig::from_env("agent_runner").with_file_log("agent-runner"); // 启用文件日志,前缀为 agent-runner
let telemetry: TelemetryGuard = rcoder_telemetry::init(telemetry_config).await?;
let telemetry = Arc::new(telemetry);
// 🆕 Pyroscope Profiler 初始化(可选:需要 pyroscope feature
#[cfg(feature = "pyroscope")]
let _pyroscope_guard: Option<profiler::ProfilerGuard> = {
info!("Pyroscope profiling feature enabled");
match profiler::init_pyroscope_profiler_default() {
Ok(guard) => {
info!("Pyroscope profiler initialized successfully");
Some(guard)
}
Err(e) => {
warn!("Failed to initialize Pyroscope profiler: {}", e);
warn!("Continuing without Pyroscope profiling");
None
}
}
};
#[cfg(not(feature = "pyroscope"))]
let _pyroscope_guard: Option<()> = None;
info!("Starting rcoder - AI-powered development platform");
// 解析命令行参数
let cli_args = CliArgs::parse();
// 加载配置(包含命令行参数)
let config = load_config_with_args(cli_args);
// 🔥 初始化并发限制(从配置读取)
if let Some(ref concurrency_config) = config.agent_concurrency {
agent_runtime::init_concurrency_limit(concurrency_config.concurrency_limit);
}
// 🔥 创建 AgentRuntime新架构
let (agent_runtime, task_receiver) = AgentRuntime::new(1000);
let agent_runtime = Arc::new(agent_runtime);
info!("🔧 [MAIN] AgentRuntime created");
// 🔥 启动 Worker在主运行时中无需独立线程
agent_runtime.start(task_receiver).await;
info!("📌 [MAIN] Agent Worker started");
// 🔥 启动健康检查和重启任务
let health_monitor = spawn_health_monitor(agent_runtime.clone());
info!("[MAIN] Worker health monitor started");
// 🔥 启动僵尸进程回收器PID 1 必须回收孤儿进程)
let _reaper_handle = process_reaper::start_process_reaper();
info!("🧹 [MAIN] Process reaper started (PID 1 mode)");
// 🆕 从配置中获取 Agent 清理配置,或使用默认值
let agent_cleanup_config = config.agent_cleanup.clone().unwrap_or_default();
let cleanup_config = CleanupConfig {
idle_timeout: Duration::from_secs(agent_cleanup_config.idle_timeout_secs),
cleanup_interval: Duration::from_secs(agent_cleanup_config.cleanup_interval_secs),
};
info!(
"🧹 [MAIN] Agent cleanup config: idle_timeout={}s, cleanup_interval={}s",
agent_cleanup_config.idle_timeout_secs, agent_cleanup_config.cleanup_interval_secs
);
// 在主异步运行时中启动清理任务
let _cleanup_handle = start_cleanup_task(cleanup_config.clone());
// proxy_manager 不需要直接访问 app_state通过参数传递即可
// 🔒 创建共享的 API 密钥 DashMap
let shared_api_key_manager =
Arc::new(dashmap::DashMap::<String, shared_types::ModelProviderConfig>::new());
info!("🔑 [MAIN] Shared API key DashMap created");
// 🔥 创建 ApiKeyManager 包装器(包装共享 DashMap消除双重存储
let api_key_manager = Arc::new(api_key_manager::ApiKeyManager::from_shared(
shared_api_key_manager.clone(),
));
// 🔒 project_id -> service_uuid 映射
let project_uuid_map: Arc<DashMap<String, String>> = Arc::new(DashMap::new());
// 🔥 http-server 模式:启动 HTTP + (可选 gRPC) + Pingora
#[cfg(feature = "http-server")]
{
use http_server::{HttpServerConfig, start_http_server};
use proxy_agent::set_unlimited_mode;
// 设置为无限制模式HTTP Server 部署,不限制槽位)
set_unlimited_mode(true);
// 🔥 1. 可选:启动 gRPC 服务(当 grpc-server feature 启用时)
#[cfg(feature = "grpc-server")]
let grpc_handle = {
info!(" HTTP server mode: starting HTTP + gRPC + Pingora");
let grpc_port = shared_types::GRPC_DEFAULT_PORT;
let grpc_addr = format!("[::]:{}", grpc_port)
.parse()
.map_err(|e| anyhow::anyhow!("Failed to parse gRPC address: {}", e))?;
// 为 gRPC 创建 state
let grpc_state = Arc::new(AppState {
sessions: Arc::new(DashMap::new()),
config: config.clone(),
local_task_sender: agent_runtime.clone(),
agent_runtime: agent_runtime.clone(),
#[cfg(feature = "proxy")]
pingora_service: None,
api_key_manager: api_key_manager.clone(),
shared_api_key_manager: shared_api_key_manager.clone(),
project_uuid_map: project_uuid_map.clone(),
});
// gRPC 消息大小限制
let grpc_service = shared_types::grpc::agent_service_server::AgentServiceServer::new(
grpc::AgentServiceImpl::new(grpc_state.clone()),
)
.max_decoding_message_size(shared_types::GRPC_MAX_MESSAGE_SIZE)
.max_encoding_message_size(shared_types::GRPC_MAX_MESSAGE_SIZE);
let handle = tokio::spawn(async move {
info!("gRPC service started, listening on port: {}", grpc_port);
info!("gRPC endpoints (port {}):", grpc_port);
info!(" agent.AgentService/Chat - gRPC chat");
info!(" agent.AgentService/SubscribeProgress - gRPC progress stream");
info!(" agent.AgentService/CancelSession - gRPC cancel");
info!(" agent.AgentService/GetStatus - gRPC status");
if let Err(e) = tonic::transport::Server::builder()
.add_service(grpc_service)
.serve(grpc_addr)
.await
{
error!("gRPC server error: {}", e);
}
});
Some(handle)
};
// 无 gRPC 模式
#[cfg(not(feature = "grpc-server"))]
{
info!(" HTTP server mode: starting HTTP + Pingora only (no gRPC)");
}
// 🔥 2. 创建 HttpServerConfig包含所有配置
let http_config = HttpServerConfig {
port: config.port,
app_config: config.clone(),
agent_runtime: agent_runtime.clone(),
shared_api_key_manager: shared_api_key_manager.clone(),
};
// 🔥 3. 启动 HTTP 服务器(内部会启动 Pingora
let _handle = start_http_server(http_config).await?;
// 🔥 4. 同时等待 gRPC如果有和信号
info!("HTTP + Pingora services started; running until shutdown signal is received");
#[cfg(feature = "grpc-server")]
{
tokio::select! {
_ = grpc_handle.unwrap() => {
info!("gRPC service ended unexpectedly, shutting down...");
}
_ = tokio::signal::ctrl_c() => {
info!("📨 Received shutdown signal, preparing graceful shutdown...");
}
}
}
#[cfg(not(feature = "grpc-server"))]
{
tokio::signal::ctrl_c().await?;
info!("📨 Received shutdown signal, preparing graceful shutdown...");
}
Ok(())
}
// 🔥 non-http-server 模式:启动 gRPC + Pingora用于 Docker 容器内)
#[cfg(not(feature = "http-server"))]
{
info!(" Container mode: starting gRPC + Pingora");
// 启动 gRPC 服务
let grpc_port = shared_types::GRPC_DEFAULT_PORT;
let grpc_addr = format!("[::]:{}", grpc_port)
.parse()
.map_err(|e| anyhow::anyhow!("Failed to parse gRPC address: {}", e))?;
// 为 gRPC 创建 state
let grpc_state = Arc::new(AppState {
sessions: Arc::new(DashMap::new()),
config: config.clone(),
local_task_sender: agent_runtime.clone(),
agent_runtime: agent_runtime.clone(),
#[cfg(feature = "proxy")]
pingora_service: None,
api_key_manager: api_key_manager.clone(),
shared_api_key_manager: shared_api_key_manager.clone(),
project_uuid_map: project_uuid_map.clone(),
});
// gRPC 消息大小限制
let grpc_service = shared_types::grpc::agent_service_server::AgentServiceServer::new(
grpc::AgentServiceImpl::new(grpc_state.clone()),
)
.max_decoding_message_size(shared_types::GRPC_MAX_MESSAGE_SIZE)
.max_encoding_message_size(shared_types::GRPC_MAX_MESSAGE_SIZE);
let grpc_handle = tokio::spawn(async move {
info!("gRPC service started, listening on port: {}", grpc_port);
info!("gRPC endpoints (port {}):", grpc_port);
info!(" agent.AgentService/Chat - gRPC chat");
info!(" agent.AgentService/SubscribeProgress - gRPC progress stream");
info!(" agent.AgentService/CancelSession - gRPC cancel");
info!(" agent.AgentService/GetStatus - gRPC status");
if let Err(e) = tonic::transport::Server::builder()
.add_service(grpc_service)
.serve(grpc_addr)
.await
{
error!("gRPC server error: {}", e);
}
});
// 启动轻量 HTTP 健康检查服务(供 docker_manager 健康检查使用)
let health_port = config.port; // 默认 8086来自 --port 参数
let _health_handle = tokio::spawn(async move {
use axum::{Json, Router, routing::get};
async fn health_check() -> Json<shared_types::HealthResponse> {
Json(shared_types::HealthResponse::new("agent-runner"))
}
let app = Router::new().route("/health", get(health_check));
let addr = format!("0.0.0.0:{}", health_port);
info!(
"🏥 HTTP health check service started, listening on port: {}",
health_port
);
let listener = match tokio::net::TcpListener::bind(&addr).await {
Ok(l) => l,
Err(e) => {
error!(
"❌ Failed to bind HTTP health check service: {} (port: {})",
e, health_port
);
return;
}
};
if let Err(e) = axum::serve(listener, app).await {
error!("HTTP health check service error: {}", e);
}
});
// 启动 Pingora如有配置且启用了 proxy feature
#[cfg(feature = "proxy")]
let pingora_result = {
use proxy_agent::start_pingora;
if let Some(proxy_config) = &config.proxy_config {
Some(start_pingora(proxy_config, shared_api_key_manager.clone()))
} else {
info!(" Pingora proxy service is not configured");
None
}
};
#[cfg(not(feature = "proxy"))]
let pingora_result: Option<()> = {
info!(" Pingora proxy service is disabled (proxy feature not enabled)");
None
};
// 等待 gRPC 服务
let _ = grpc_handle.await;
// 停止 Pingora 服务
#[cfg(feature = "proxy")]
if let Some(mut result) = pingora_result {
result.stop().await;
}
#[cfg(not(feature = "proxy"))]
let _ = pingora_result;
Ok(())
}
}
/// 🔥 健康监控任务 (新架构)
///
/// 定期检查 Agent Worker 健康状态,自动重启不健康的 Worker
async fn spawn_health_monitor(runtime: Arc<AgentRuntime>) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(5));
let mut consecutive_failures: u32 = 0;
const MAX_RESTART_ATTEMPTS: u32 = 5;
const RESTART_COOLDOWN_SECS: u64 = 60;
info!("[HealthMonitor] Health monitor started");
loop {
interval.tick().await;
// 检查健康状态
if !runtime.check_health().await {
error!("[HealthMonitor] Worker reported unhealthy");
// 检查冷却期
if consecutive_failures >= MAX_RESTART_ATTEMPTS {
warn!(
"⏳ [HealthMonitor] {} consecutive restart failures, entering cooldown",
consecutive_failures
);
tokio::time::sleep(Duration::from_secs(RESTART_COOLDOWN_SECS)).await;
consecutive_failures = 0;
info!("[HealthMonitor] Cooldown ended, reset failure counter");
}
// 创建新的通道
let (new_tx, new_rx) = tokio::sync::mpsc::channel(1000);
// 重启 worker
runtime.restart(new_rx).await;
consecutive_failures += 1;
info!(
"🔄 [HealthMonitor] Worker restart completed (attempt #{})",
consecutive_failures
);
} else {
consecutive_failures = 0;
}
}
})
}

View File

@@ -0,0 +1,26 @@
// 重新导出 shared_types 中的模型,保持向后兼容
pub use agent_abstraction::PromptMessage;
pub use shared_types::{
// Agent model exports
AgentStatus,
// Session and message exports
Attachment,
AttachmentSource,
AudioAttachment,
// 取消相关类型
CancelNotificationRequestWrapper,
CancelResult,
ChatPromptResponse,
DocumentAttachment,
ImageAttachment,
ProjectAndAgentInfo,
SessionMessageType,
SessionNotify,
TextAttachment,
UnifiedSessionMessage,
};
// 重新导出 ACP 类型,供客户端构造取消请求使用
pub use agent_client_protocol::schema::{CancelNotification, SessionId};

View File

@@ -0,0 +1,457 @@
//! OpenTelemetry 追踪模块
//!
//! 集成 `rcoder-telemetry` 提供完整的分布式追踪功能。
use opentelemetry::trace::Status;
use tracing::{Level, Span, error, info, span};
use tracing_opentelemetry::OpenTelemetrySpanExt;
/// OpenTelemetry 追踪配置
#[derive(Debug, Clone)]
pub struct TraceConfig {
/// 是否启用追踪
pub enabled: bool,
/// OTLP 导出端点(如 Jaeger、OTLP
pub exporter_endpoint: Option<String>,
/// 是否启用 Prometheus 指标
pub prometheus_enabled: bool,
/// 服务名称
pub service_name: String,
}
impl Default for TraceConfig {
fn default() -> Self {
Self {
enabled: true,
exporter_endpoint: None,
prometheus_enabled: true,
service_name: "agent-runner".to_string(),
}
}
}
/// 初始化 OpenTelemetry 追踪
///
/// 集成 `rcoder-telemetry` 提供完整的分布式追踪功能:
/// - OTLP 导出器(支持 Jaeger、Zipkin、OTLP Collector
/// - Prometheus 指标
/// - Trace context 传播
/// - Console 日志
///
/// # Arguments
///
/// * `config` - 追踪配置
///
/// # Returns
///
/// 返回遥测系统 Guard需要在应用运行期间保持存活
///
/// # Environment Variables
///
/// 支持以下环境变量:
/// - `OTEL_EXPORTER_OTLP_ENDPOINT`: OTLP 端点(如 http://jaeger:4317
/// - `OTEL_TRACES_SAMPLER_ARG`: 采样率 (0.0-1.0,默认 1.0)
/// - `TELEMETRY_PROMETHEUS_ENABLED`: 是否启用 Prometheus默认 true
pub async fn init_tracing(config: TraceConfig) -> anyhow::Result<rcoder_telemetry::TelemetryGuard> {
if !config.enabled {
info!("📍 [OTel] Tracing is disabled");
// 即使追踪禁用仍然Initializing Prometheus如果启用
if config.prometheus_enabled {
return rcoder_telemetry::init_prometheus_only(&config.service_name);
}
// 创建一个空的 guard仅保留服务名称用于后续渲染指标
// 使用空的 telemetry_config 创建一个没有任何功能的 guard
let telemetry_config = rcoder_telemetry::TelemetryConfig::new(&config.service_name);
return rcoder_telemetry::init(telemetry_config).await;
}
// 使用 from_env 从环境变量读取配置,然后覆盖必要字段
let mut telemetry_config = rcoder_telemetry::TelemetryConfig::from_env(&config.service_name);
// 如果配置中指定了端点,覆盖环境变量中的值
if let Some(ref endpoint) = config.exporter_endpoint {
telemetry_config = telemetry_config.with_otlp_endpoint(endpoint);
}
// 根据配置设置 Prometheus
if config.prometheus_enabled {
telemetry_config = telemetry_config.with_prometheus();
} else {
telemetry_config = telemetry_config.without_prometheus();
}
// Initializing telemetry system
let guard = rcoder_telemetry::init(telemetry_config).await?;
info!(
"✅ [OTel] Tracing initialized: OTLP={}, Prometheus={}",
guard.is_otlp_enabled(),
guard.is_prometheus_enabled()
);
Ok(guard)
}
impl TraceConfig {
/// 从环境变量构建配置
pub fn from_env() -> Self {
Self {
enabled: std::env::var("OTEL_TRACING_ENABLED")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(true),
exporter_endpoint: std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok(),
prometheus_enabled: std::env::var("TELEMETRY_PROMETHEUS_ENABLED")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(true),
service_name: "agent-runner".to_string(),
}
}
/// 设置 OTLP 端点
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.exporter_endpoint = Some(endpoint.into());
self
}
/// 禁用 Prometheus
pub fn without_prometheus(mut self) -> Self {
self.prometheus_enabled = false;
self
}
/// 禁用追踪
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
/// 设置服务名称
pub fn with_service_name(mut self, name: impl Into<String>) -> Self {
self.service_name = name.into();
self
}
}
/// 请求追踪 Guard自动管理 span 生命周期)
///
/// 使用 `OpenTelemetrySpanExt` 支持动态属性设置
///
/// 注意:此结构体实现 Send + Sync可在 tokio::spawn 中安全使用
pub struct RequestSpan {
/// 底层 tracing span用于 OpenTelemetrySpanExt 方法)
span: Span,
}
impl RequestSpan {
/// 创建新的请求 span
///
/// # Arguments
///
/// * `project_id` - 项目 ID
/// * `request_id` - 请求 ID
/// * `operation` - 操作名称(如 "process_prompt"
pub fn new(project_id: &str, request_id: &str, operation: &str) -> Self {
let span = span!(
Level::INFO,
"agent_request",
project_id = %project_id,
request_id = %request_id,
operation = %operation,
);
info!(
"📍 [OTel] Span created: project_id={}, request_id={}, operation={}",
project_id, request_id, operation
);
Self { span }
}
/// 设置动态属性(使用 OpenTelemetrySpanExt
///
/// # Arguments
///
/// * `key` - 属性键
/// * `value` - 属性值
///
/// # Example
///
/// ```rust
/// use agent_runner::RequestSpan;
///
/// let span = RequestSpan::new("proj", "req", "op");
/// span.set_attribute("http.status_code", 200);
/// span.set_attribute("user.id", "user-123");
/// ```
pub fn set_attribute<K, V>(&self, key: K, value: V)
where
K: Into<opentelemetry::Key>,
V: Into<opentelemetry::Value>,
{
self.span.set_attribute(key, value);
}
/// 批量设置动态属性
///
/// # Arguments
///
/// * `attributes` - 属性列表 (key, value)
pub fn set_attributes(&self, attributes: &[(&str, &str)]) {
for (key, value) in attributes {
// 需要转换为 String 以满足 'static 生命周期要求
self.span.set_attribute(key.to_string(), value.to_string());
}
}
/// 设置 span 状态为成功
pub fn set_ok(&self) {
self.span.set_status(Status::Ok);
}
/// 设置 span 状态为错误
///
/// # Arguments
///
/// * `description` - 错误描述
pub fn set_error(&self, description: impl Into<std::borrow::Cow<'static, str>>) {
self.span.set_status(Status::error(description));
}
/// 添加事件到当前 span使用 OpenTelemetrySpanExt
///
/// # Arguments
///
/// * `name` - 事件名称
/// * `attributes` - 事件属性
///
/// # Example
///
/// ```rust
/// use agent_runner::RequestSpan;
/// use opentelemetry::KeyValue;
///
/// let span = RequestSpan::new("proj", "req", "op");
/// span.add_event("cache_hit", vec![
/// KeyValue::new("cache.key", "user:123"),
/// KeyValue::new("cache.ttl", 300),
/// ]);
/// ```
pub fn add_event(
&self,
name: impl Into<std::borrow::Cow<'static, str>>,
attributes: Vec<opentelemetry::KeyValue>,
) {
self.span.add_event(name, attributes);
}
/// 记录事件(简化版本,兼容旧 API
pub fn event(&self, name: &str, attributes: &[(&str, String)]) {
// 转换为 String 以满足 'static 生命周期要求
let kv_attrs: Vec<opentelemetry::KeyValue> = attributes
.iter()
.map(|(k, v)| opentelemetry::KeyValue::new(k.to_string(), v.clone()))
.collect();
self.span.add_event(name.to_string(), kv_attrs);
info!(
"📍 [OTel] Event: {}, attributes: {:?}",
name,
attributes
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(", ")
);
}
/// 记录错误到当前 span
pub fn error(&self, err: &anyhow::Error) {
self.set_error(err.to_string());
error!(
error = %err,
"📍 [OTel] Request error"
);
}
/// 获取 OpenTelemetry 上下文(用于跨服务传播)
pub fn context(&self) -> opentelemetry::Context {
self.span.context()
}
/// 完成 span手动关闭
pub fn finish(self) {
self.set_ok();
// span 在 drop 时会自动关闭
}
}
impl Drop for RequestSpan {
fn drop(&mut self) {
info!("📍 [OTel] Span closed");
}
}
/// 创建子 span用于追踪子操作
///
/// # Arguments
///
/// * `_parent` - 父 span 的引用(子 span 会自动继承父 span 的上下文)
/// * `name` - 子 span 名称
/// * `attributes` - 附加属性
///
/// # Example
///
/// ```rust
/// use agent_runner::{RequestSpan, child_span};
///
/// let parent = RequestSpan::new("proj", "req", "parent_op");
/// let child = child_span(&parent, "child_op", &[("key", "value".to_string())]);
/// child.finish();
/// parent.finish();
/// ```
pub fn child_span(_parent: &RequestSpan, name: &str, attributes: &[(&str, String)]) -> RequestSpan {
let span = span!(
Level::INFO,
"child_operation",
otel.name = %name,
);
// 使用 OpenTelemetrySpanExt 设置动态属性
// 需要转换为 String 以满足 'static 生命周期要求
for (key, value) in attributes {
span.set_attribute(key.to_string(), value.clone());
}
info!("📍 [OTel] Child span created: {}", name);
RequestSpan { span }
}
/// 从上下文中提取当前 span用于跨线程传递
pub fn current_span() -> RequestSpan {
let span = Span::current();
RequestSpan { span }
}
/// 创建带属性的 span
///
/// # Example
///
/// ```rust
/// use agent_runner::otel_tracing::span_with_attributes;
///
/// let span = span_with_attributes(
/// "process_attachment",
/// &[
/// ("file_name", "test.pdf".to_string()),
/// ("file_size", "1024".to_string()),
/// ]
/// );
/// ```
pub fn span_with_attributes(name: &str, attributes: &[(&str, String)]) -> RequestSpan {
let span = span!(
Level::INFO,
"custom_operation",
otel.name = %name,
);
// 使用 OpenTelemetrySpanExt 设置动态属性
// 需要转换为 String 以满足 'static 生命周期要求
for (key, value) in attributes {
span.set_attribute(key.to_string(), value.clone());
}
RequestSpan { span }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trace_config_default() {
let config = TraceConfig::default();
assert!(config.enabled);
assert!(config.exporter_endpoint.is_none());
assert!(config.prometheus_enabled);
}
#[test]
fn test_request_span_creation() {
let span = RequestSpan::new("test_project", "test_request", "test_operation");
span.event("test_event", &[("key", "value".to_string())]);
span.finish();
}
#[test]
fn test_request_span_with_attributes() {
let span = RequestSpan::new("test_project", "test_request", "test_operation");
// 使用 OpenTelemetrySpanExt 设置动态属性
span.set_attribute("http.method", "GET");
span.set_attribute("http.status_code", 200i64);
span.set_attribute("user.id", "user-123");
span.set_ok();
span.finish();
}
#[test]
fn test_request_span_with_error() {
let span = RequestSpan::new("test_project", "test_request", "test_operation");
span.set_error("Connection timeout");
// span 会在 drop 时自动关闭
}
#[test]
fn test_request_span_auto_drop() {
let _span = RequestSpan::new("test_project", "test_request", "test_operation");
// Span 会在 drop 时自动关闭
}
#[test]
fn test_child_span() {
let parent = RequestSpan::new("test_project", "test_request", "parent_operation");
let child = child_span(&parent, "child_operation", &[("attr", "value".to_string())]);
child.finish();
parent.finish();
}
#[test]
fn test_span_with_attributes() {
let span = span_with_attributes(
"custom_op",
&[
("file_name", "test.pdf".to_string()),
("file_size", "1024".to_string()),
],
);
span.finish();
}
#[test]
fn test_add_event() {
let span = RequestSpan::new("test_project", "test_request", "test_operation");
span.add_event(
"cache_hit",
vec![
opentelemetry::KeyValue::new("cache.key", "user:123"),
opentelemetry::KeyValue::new("cache.ttl", 300i64),
],
);
span.finish();
}
#[tokio::test]
async fn test_init_tracing() {
let config = TraceConfig::default();
let result = init_tracing(config).await;
assert!(result.is_ok());
}
}

View File

@@ -0,0 +1,672 @@
//! 僵尸进程回收器 (Zombie Process Reaper)
//!
//! 当 agent_runner 作为容器的 PID 1 运行时,它需要负责回收孤儿进程。
//! 此模块实现了一个基于 SIGCHLD 信号的子进程回收机制。
//!
//! # 设计原理
//!
//! 在 Linux 容器中,如果 PID 1 不调用 wait() 回收子进程,这些子进程
//! 退出后会变成僵尸进程Zombie占用系统资源。
//!
//! # 使用方式
//!
//! ```rust
//! use process_reaper::start_process_reaper;
//!
//! // 在主函数中启动回收器
//! let _reaper_handle = start_process_reaper();
//! ```
use std::collections::HashMap;
use std::fs;
use tokio::process::Child;
#[cfg(unix)]
use tokio::signal::unix::{SignalKind, signal};
use tracing::{debug, error, info, warn};
/// 进程回收器配置
#[derive(Debug, Clone)]
pub struct ReaperConfig {
/// 是否启用详细日志
pub verbose: bool,
/// 是否启用主动僵尸进程检测
pub enable_zombie_detection: bool,
/// 僵尸进程检测间隔(秒)
pub zombie_detection_interval_secs: u64,
}
impl Default for ReaperConfig {
fn default() -> Self {
Self {
verbose: false,
enable_zombie_detection: true,
zombie_detection_interval_secs: 10,
}
}
}
/// 僵尸进程信息
#[derive(Debug, Clone)]
pub struct ZombieProcessInfo {
pub pid: u32,
pub ppid: u32,
pub comm: String,
pub state: char,
}
/// 进程回收器状态
#[derive(Debug)]
struct ReaperState {
/// 追踪活跃的子进程
/// 存储格式: pid -> Child
active_children: HashMap<u32, Child>,
/// 回收的进程总数
reaped_count: u64,
/// 检测到的僵尸进程数
zombie_detected_count: u64,
/// 配置
config: ReaperConfig,
}
impl ReaperState {
fn new(config: ReaperConfig) -> Self {
Self {
active_children: HashMap::new(),
reaped_count: 0,
zombie_detected_count: 0,
config,
}
}
/// 注册一个子进程,稍后自动回收
fn register_child(&mut self, child: Child) {
let id = child.id().unwrap_or(0);
if id > 0 {
self.active_children.insert(id, child);
if self.config.verbose {
debug!("[ProcessReaper] Registered child process PID={}", id);
}
}
}
/// 尝试回收所有已退出的子进程
fn reap_all(&mut self) {
let mut reaped_now = 0;
// 使用 entry API 避免 DashMap/RwLock 问题(虽然这里是普通 HashMap
self.active_children.retain(|pid, child| {
// 尝试查询进程状态(非阻塞)
match child.try_wait() {
Ok(Some(status)) => {
// 进程已退出
reaped_now += 1;
if self.config.verbose {
debug!(
"[ProcessReaper] Reaped child process PID={}, exit_status={:?}",
pid, status
);
}
false // 移除已回收的进程
}
Ok(None) => {
// 进程仍在运行
true
}
Err(e) => {
// 查询失败,可能进程已不存在
warn!("[ProcessReaper] Failed to query child PID={}: {}", pid, e);
false // 移除无法查询的进程
}
}
});
if reaped_now > 0 {
self.reaped_count += reaped_now;
info!(
"[ProcessReaper] Reaped {} child processes (total: {})",
reaped_now, self.reaped_count
);
}
}
/// 🔍 主动检测系统中的僵尸进程
///
/// 扫描 /proc 文件系统,查找状态为 'Z' (Zombie) 的进程
fn detect_zombie_processes(&mut self) -> Vec<ZombieProcessInfo> {
let mut zombies = Vec::new();
#[cfg(unix)]
{
let proc_path = "/proc";
// 读取 /proc 目录下的所有 PID 目录
if let Ok(entries) = fs::read_dir(proc_path) {
for entry in entries.flatten() {
let name = entry.file_name();
// 检查是否是数字PID 目录)
if let Ok(pid) = name.to_string_lossy().parse::<u32>() {
// 读取 /proc/[pid]/stat 文件
let stat_path = entry.path().join("stat");
if let Ok(content) = fs::read_to_string(&stat_path)
&& let Some(info) = parse_stat_file(pid, &content)
&& info.state == 'Z'
{
zombies.push(info);
}
}
}
}
}
if !zombies.is_empty() {
self.zombie_detected_count += zombies.len() as u64;
warn!(
"[ProcessReaper] Detected {} zombie processes (total detected: {})",
zombies.len(),
self.zombie_detected_count
);
for zombie in &zombies {
warn!(
"[ProcessReaper] Zombie process: PID={}, PPID={}, CMD={}",
zombie.pid, zombie.ppid, zombie.comm
);
}
}
zombies
}
/// 🔧 主动清理所有僵尸进程
///
/// 使用 waitpid 循环回收所有可能的僵尸进程,不仅仅是追踪的子进程
/// 这是 PID 1 的责任:回收所有孤儿进程
fn reap_all_zombies_blocking(&mut self) {
#[cfg(unix)]
{
use nix::sys::wait::{WaitPidFlag, WaitStatus, waitpid};
use nix::unistd::Pid;
let mut reaped_this_round = 0;
// 循环调用 waitpid直到没有更多僵尸进程
loop {
match waitpid(
Pid::from_raw(-1), // -1 表示等待任意子进程
Some(WaitPidFlag::WNOHANG),
) {
Ok(WaitStatus::Exited(pid, exit_code)) => {
reaped_this_round += 1;
debug!(
"[ProcessReaper] Reaped zombie proactively: PID={}, exit_code={}",
pid, exit_code
);
}
Ok(WaitStatus::Signaled(pid, signal, _)) => {
reaped_this_round += 1;
debug!(
"[ProcessReaper] Reaped zombie proactively: PID={}, signal={:?}",
pid, signal
);
}
Ok(WaitStatus::StillAlive) => {
// WNOHANG: 没有更多的僵尸进程
break;
}
Ok(WaitStatus::Stopped(pid, signal)) => {
// 进程被停止(不是退出),不计入回收
debug!(
"[ProcessReaper] Process stopped: PID={}, signal={:?}",
pid, signal
);
// 继续循环,可能还有其他僵尸进程
continue;
}
Ok(WaitStatus::Continued(pid)) => {
// 进程被恢复SIGCONT不计入回收
debug!("[ProcessReaper] Process resumed: PID={}", pid);
// 继续循环,可能还有其他僵尸进程
continue;
}
#[cfg(linux_android)]
Ok(WaitStatus::PtraceEvent(pid, signal, event)) => {
// ptrace 事件,不计入回收
debug!(
"[ProcessReaper] ptrace event: PID={}, signal={:?}, event={}",
pid, signal, event
);
continue;
}
#[cfg(linux_android)]
Ok(WaitStatus::PtraceSyscall(pid)) => {
// ptrace 系统调用,不计入回收
debug!("[ProcessReaper] ptrace syscall: PID={}", pid);
continue;
}
// 非Linux平台忽略 ptrace 相关状态macOS 上 WaitStatus 包含这些变体但不会实际触发)
Ok(_) => {
debug!("[ProcessReaper] Ignored waitpid status");
continue;
}
Err(nix::errno::Errno::ECHILD) => {
// 没有子进程
break;
}
Err(e) => {
warn!("[ProcessReaper] waitpid error: {}", e);
break;
}
}
}
if reaped_this_round > 0 {
self.reaped_count += reaped_this_round;
info!(
"[ProcessReaper] Reaped {} zombies proactively (total: {})",
reaped_this_round, self.reaped_count
);
}
}
#[cfg(not(unix))]
{
debug!("[ProcessReaper] Non-Unix platform, skipping zombie detection");
}
}
}
/// 解析 /proc/[pid]/stat 文件
///
/// 文件格式pid (comm) state ppid ...
/// 示例1 (init) S 0 0 0 0 ...
fn parse_stat_file(pid: u32, content: &str) -> Option<ZombieProcessInfo> {
// stat 文件格式pid (comm) state ppid ...
// 需要找到 comm 的结束括号
let content = content.trim();
// 找到第一个 '(' 和最后一个 ')'
let open_paren = content.find('(')?;
let close_paren = content.rfind(')')?;
let comm = content[open_paren + 1..close_paren].to_string();
let after_comm = &content[close_paren + 1..];
// 解析 state 和 ppid
// 格式:) state ppid ...
let parts: Vec<&str> = after_comm.split_whitespace().collect();
if parts.len() < 2 {
return None;
}
let state = parts.first()?.chars().next()?;
let ppid: u32 = parts.get(1)?.parse().ok()?;
Some(ZombieProcessInfo {
pid,
ppid,
comm,
state,
})
}
/// 启动进程回收器任务
///
/// 此函数会:
/// 1. 注册 SIGCHLD 信号处理器
/// 2. 在后台循环中等待信号并回收子进程
/// 3. 定期主动检测和清理僵尸进程
///
/// # 返回值
///
/// 返回一个 JoinHandle可以用于等待回收器任务退出通常不需要
pub fn start_process_reaper() -> tokio::task::JoinHandle<()> {
tokio::spawn(async move { run_reaper(ReaperConfig::default()).await })
}
/// 启动进程回收器任务(带配置)
pub fn start_process_reaper_with_config(config: ReaperConfig) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move { run_reaper(config).await })
}
/// 核心回收逻辑
#[cfg(unix)]
async fn run_reaper(config: ReaperConfig) {
info!("[ProcessReaper] Zombie process reaper started (PID 1 mode)");
if config.enable_zombie_detection {
info!(
"[ProcessReaper] Zombie detection enabled, interval: {} seconds",
config.zombie_detection_interval_secs
);
}
// 创建 SIGCHLD 信号监听器
let sigchld = match signal(SignalKind::child()) {
Ok(sig) => sig,
Err(e) => {
error!("[ProcessReaper] Failed to register SIGCHLD handler: {}", e);
error!("[ProcessReaper] Falling back to polling mode");
// 回退模式:使用轮询
run_reaper_polling(config).await;
return;
}
};
let state = ReaperState::new(config.clone());
// 启动定期轮询任务(作为信号机制的补充)
let mut poll_interval = tokio::time::interval(std::time::Duration::from_secs(5));
poll_interval.tick().await; // 跳过第一次立即触发
// 根据配置决定是否启用僵尸进程检测
if config.enable_zombie_detection {
run_reaper_with_detection(
sigchld,
poll_interval,
state,
config.zombie_detection_interval_secs,
)
.await
} else {
run_reaper_without_detection(sigchld, poll_interval, state).await
}
}
/// Windows 上的回收逻辑无操作Windows 没有僵尸进程问题)
#[cfg(not(unix))]
async fn run_reaper(_config: ReaperConfig) {
info!("[ProcessReaper] Non-Unix platform: zombie reaper not applicable");
}
/// 🔍 启用僵尸进程检测的回收循环
#[cfg(unix)]
async fn run_reaper_with_detection(
mut sigchld: tokio::signal::unix::Signal,
mut poll_interval: tokio::time::Interval,
mut state: ReaperState,
detect_interval_secs: u64,
) {
let mut zombie_detect_interval =
tokio::time::interval(std::time::Duration::from_secs(detect_interval_secs));
zombie_detect_interval.tick().await; // 跳过第一次立即触发
loop {
tokio::select! {
// 等待 SIGCHLD 信号
_ = sigchld.recv() => {
if state.config.verbose {
debug!("[ProcessReaper] Received SIGCHLD");
}
state.reap_all();
state.reap_all_zombies_blocking();
}
// 定期轮询(每 5 秒)
_ = poll_interval.tick() => {
state.reap_all();
}
// 🔍 定期主动检测和清理僵尸进程
_ = zombie_detect_interval.tick() => {
debug!("[ProcessReaper] Running scheduled zombie detection...");
// 先检测有哪些僵尸进程
let zombies = state.detect_zombie_processes();
// 然后主动清理所有僵尸进程
if !zombies.is_empty() {
state.reap_all_zombies_blocking();
}
}
}
}
}
/// 🚫 不启用僵尸进程检测的回收循环
#[cfg(unix)]
async fn run_reaper_without_detection(
mut sigchld: tokio::signal::unix::Signal,
mut poll_interval: tokio::time::Interval,
mut state: ReaperState,
) {
loop {
tokio::select! {
// 等待 SIGCHLD 信号
_ = sigchld.recv() => {
if state.config.verbose {
debug!("[ProcessReaper] Received SIGCHLD");
}
state.reap_all();
state.reap_all_zombies_blocking();
}
// 定期轮询(每 5 秒)
_ = poll_interval.tick() => {
state.reap_all();
}
}
}
}
/// 轮询模式回退(当信号机制不可用时)
#[cfg(unix)]
async fn run_reaper_polling(config: ReaperConfig) {
info!("[ProcessReaper] Using polling mode for zombie reaping");
let state = ReaperState::new(config.clone());
// 根据配置决定是否启用僵尸进程检测
if config.enable_zombie_detection {
run_reaper_polling_with_detection(state, config.zombie_detection_interval_secs).await
} else {
run_reaper_polling_without_detection(state).await
}
}
/// 🔍 轮询模式 + 僵尸进程检测
#[cfg(unix)]
async fn run_reaper_polling_with_detection(mut state: ReaperState, detect_interval_secs: u64) {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(2));
let mut zombie_detect_interval =
tokio::time::interval(std::time::Duration::from_secs(detect_interval_secs));
zombie_detect_interval.tick().await;
loop {
tokio::select! {
_ = interval.tick() => {
state.reap_all();
state.reap_all_zombies_blocking();
}
_ = zombie_detect_interval.tick() => {
let zombies = state.detect_zombie_processes();
if !zombies.is_empty() {
state.reap_all_zombies_blocking();
}
}
}
}
}
/// 🚫 轮询模式(无僵尸进程检测)
#[cfg(unix)]
async fn run_reaper_polling_without_detection(mut state: ReaperState) {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(2));
loop {
interval.tick().await;
state.reap_all();
state.reap_all_zombies_blocking();
}
}
/// 回收器句柄(可选:用于外部注册子进程)
///
/// 注意:当前实现中,子进程由各自创建者管理。
/// 此结构保留用于未来扩展,例如中央化子进程管理。
#[derive(Debug, Clone)]
pub struct ProcessReaperHandle {
_config: ReaperConfig,
}
impl ProcessReaperHandle {
pub fn new() -> Self {
Self {
_config: ReaperConfig::default(),
}
}
/// 🔍 手动触发僵尸进程检测(仅用于调试)
pub fn detect_zombies_now(&self) -> Vec<ZombieProcessInfo> {
#[cfg(unix)]
{
let proc_path = "/proc";
let mut zombies = Vec::new();
if let Ok(entries) = fs::read_dir(proc_path) {
for entry in entries.flatten() {
let name = entry.file_name();
if let Ok(pid) = name.to_string_lossy().parse::<u32>() {
let stat_path = entry.path().join("stat");
if let Ok(content) = fs::read_to_string(&stat_path)
&& let Some(info) = parse_stat_file(pid, &content)
&& info.state == 'Z'
{
zombies.push(info);
}
}
}
}
zombies
}
#[cfg(not(unix))]
{
Vec::new()
}
}
/// 注册一个子进程(未来扩展)
#[allow(dead_code)]
pub fn register(&self, _child: Child) {
// 当前实现中,子进程由各自的创建者负责回收
// 此方法保留用于未来中央化管理的扩展
}
}
impl Default for ProcessReaperHandle {
fn default() -> Self {
Self::new()
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::process::Stdio;
use tokio::time::Duration;
#[tokio::test]
async fn test_reaper_state() {
let config = ReaperConfig {
verbose: true,
..Default::default()
};
let mut state = ReaperState::new(config);
// 创建一个长时间运行的进程
let child = tokio::process::Command::new("sleep")
.arg("10")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
let pid = child.id().unwrap();
state.register_child(child);
// 立即调用 reap_all进程应该还在运行
state.reap_all();
// 验证进程仍在列表中(因为还没退出)
assert!(state.active_children.contains_key(&pid));
assert_eq!(state.reaped_count, 0); // 还没有回收任何进程
// 清理:杀死进程
if let Some(mut child) = state.active_children.remove(&pid) {
let _ = child.kill().await;
let _ = child.wait().await;
}
}
#[tokio::test]
async fn test_long_running_process() {
let config = ReaperConfig::default();
let mut state = ReaperState::new(config);
// 创建一个长时间运行的进程
let child = tokio::process::Command::new("sleep")
.arg("10")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
let pid = child.id().unwrap();
state.register_child(child);
// 立即尝试回收,进程应该还在运行
state.reap_all();
// 验证进程仍在列表中
assert!(state.active_children.contains_key(&pid));
// 清理:杀死进程
if let Some(mut child) = state.active_children.remove(&pid) {
let _ = child.kill().await;
let _ = child.wait().await;
}
}
#[tokio::test]
async fn test_start_process_reaper() {
let handle = start_process_reaper();
// 创建几个快速退出的子进程
for _ in 0..3 {
let _ = tokio::process::Command::new("true")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
// 等待回收器处理
tokio::time::sleep(Duration::from_millis(500)).await;
// 回收器应该仍在运行
assert!(!handle.is_finished());
// 取消任务
handle.abort();
}
#[test]
fn test_parse_stat_file() {
let content = "1 (init) S 0 0 0 0 -1 4194560 667 5569406 8 23660837 1 0 0 0 0 0 0 0 20 0 1 0 3642608 1340 18446744073709551615 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0";
let info = parse_stat_file(1, content).unwrap();
assert_eq!(info.pid, 1);
assert_eq!(info.ppid, 0);
assert_eq!(info.comm, "init");
assert_eq!(info.state, 'S');
}
#[test]
fn test_parse_stat_file_with_parentheses_in_comm() {
// 进程名包含括号的情况
let content = "1234 (test(a)b)) Z 1 1234 1234 0 -1 4194560 0 0 0 0 0 0 0 0 20 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0";
let info = parse_stat_file(1234, content).unwrap();
assert_eq!(info.pid, 1234);
assert_eq!(info.ppid, 1);
assert_eq!(info.comm, "test(a)b)");
assert_eq!(info.state, 'Z'); // 僵尸进程
}
}

View File

@@ -0,0 +1,111 @@
//! Pyroscope Profiler 集成模块
//!
//! 提供连续性能分析功能,包括:
//! - CPU Profiling
//! - Memory Profiling (Heap/Allocs)
//! - Stack trace sampling
use anyhow::{Context, Result};
use pyroscope::PyroscopeAgent;
use pyroscope_pprofrs::{PprofConfig, pprof_backend};
use tracing::{debug, info};
/// Profiler 配置
#[derive(Debug, Clone)]
pub struct ProfilerConfig {
/// Pyroscope Server 地址
pub server_url: String,
/// 应用名称(支持标签格式)
pub application_name: String,
/// 采样频率Hz
pub sample_rate: u32,
/// 是否启用内存 profiling
pub enable_memory: bool,
/// 是否启用 CPU profiling
pub enable_cpu: bool,
}
impl Default for ProfilerConfig {
fn default() -> Self {
Self {
server_url: "http://pyroscope:4040".to_string(),
application_name: "agent_runner{env=dev}".to_string(),
sample_rate: 100,
enable_memory: true,
enable_cpu: true,
}
}
}
impl ProfilerConfig {
/// 从环境变量加载配置
pub fn from_env() -> Self {
let mut config = Self::default();
if let Ok(url) = std::env::var("PYROSCOPE_URL") {
config.server_url = url;
}
if let Ok(name) = std::env::var("PYROSCOPE_APP_NAME") {
config.application_name = name;
}
if let Ok(project_id) = std::env::var("PROJECT_ID") {
config.application_name = format!("agent_runner{{project_id={}}}", project_id);
}
config
}
}
/// Profiler Guard
///
/// 当 dropped 时自动停止 profiler
pub struct ProfilerGuard {
_agent: Option<pyroscope::PyroscopeAgent<pyroscope::pyroscope::PyroscopeAgentRunning>>,
}
/// 初始化并启动 Pyroscope Profiler
///
/// # Errors
///
/// 如果 profiler 初始化失败,返回错误
pub fn init_pyroscope_profiler(config: ProfilerConfig) -> Result<ProfilerGuard> {
if !config.enable_cpu && !config.enable_memory {
debug!("Pyroscope profiler disabled, skipping initialization");
return Ok(ProfilerGuard { _agent: None });
}
info!(
"Initializing Pyroscope profiler: {}",
config.application_name
);
info!(" Server URL: {}", config.server_url);
info!(" Sample rate: {} Hz", config.sample_rate);
info!(" CPU profiling: {}", config.enable_cpu);
info!(" Memory profiling: {}", config.enable_memory);
// 使用正确的 API: builder() -> backend() -> build()
let agent = PyroscopeAgent::builder(config.server_url, config.application_name)
.backend(pprof_backend(
PprofConfig::new().sample_rate(config.sample_rate),
))
.build()
.context("Failed to build Pyroscope agent")?;
// 启动 profiling返回 PyroscopeAgent<PyroscopeAgentRunning>
let agent_running = agent
.start()
.context("Failed to start Pyroscope profiler")?;
info!("Pyroscope profiler started successfully");
Ok(ProfilerGuard {
_agent: Some(agent_running),
})
}
/// 便捷函数:使用默认配置初始化 profiler
pub fn init_pyroscope_profiler_default() -> Result<ProfilerGuard> {
init_pyroscope_profiler(ProfilerConfig::from_env())
}

View File

@@ -0,0 +1,618 @@
//! ACP Agent Worker 模块 (SACP 版本)
//!
//! 负责处理 Agent 请求队列,管理 Agent 会话的创建和复用。
//! 使用 AcpSessionManager 进行会话管理。
//!
//! ## SACP 迁移说明
//!
//! - 移除了 `spawn_blocking` + `LocalSet` 模式SACP 支持 Send trait
//! - 使用标准 `tokio::spawn` 进行并发处理
//! - 简化了并发模型,提高性能
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use dashmap::DashMap;
use agent_abstraction::session::AcpSessionManager;
use anyhow::Result;
use chrono::Utc;
use shared_types::ModelProviderConfig;
use tokio::sync::{mpsc, oneshot};
use tracing::{debug, error, info, warn};
use agent_client_protocol::schema::SessionId;
use crate::{
agent_runtime::get_concurrency_limit,
model::{AgentStatus, ChatPromptResponse, ProjectAndAgentInfo},
proxy_agent::SESSION_REQUEST_CONTEXT,
service::{AGENT_REGISTRY, AgentSessionRegistry, StateAwareNotifier},
utils::ContentBuilder,
};
/// 🔥 配置标志是否为无限制模式HTTP Server 部署)
static IS_UNLIMITED_MODE: AtomicBool = AtomicBool::new(false);
/// 设置运行模式(供 main.rs 调用)
pub fn set_unlimited_mode(enabled: bool) {
IS_UNLIMITED_MODE.store(enabled, Ordering::SeqCst);
}
// 🔥 OpenTelemetry 追踪
#[cfg(feature = "otel")]
use crate::otel_tracing::RequestSpan;
// 🔥 简化版本:如果没有 OpenTelemetry使用空的 span
#[cfg(not(feature = "otel"))]
struct RequestSpan;
#[cfg(not(feature = "otel"))]
impl RequestSpan {
fn new(_project_id: &str, _request_id: &str, _operation: &str) -> Self {
Self
}
}
/// Agent 请求结构 (SACP 版本)
///
/// 不再需要 LocalSet直接在 tokio::spawn 中运行
#[derive(Debug)]
pub struct AgentRequest {
/// Agent 抽象层的 prompt 消息
prompt_message: agent_abstraction::PromptMessage,
/// 发送回执消息的通道
chat_prompt_tx: oneshot::Sender<ChatPromptResponse>,
/// 模型提供商配置
model_provider: Option<ModelProviderConfig>,
/// 🔥 关联的 service UUID用于 API 密钥管理)
service_uuid: Option<String>,
/// 🔥 共享的 API 密钥管理器(用于自动清理)
shared_api_key_manager: Option<Arc<DashMap<String, ModelProviderConfig>>>,
/// 是否跳过槽位限制HTTP Server 宿主机部署时为 true
skip_slot_limit: bool,
}
impl AgentRequest {
pub fn new(
prompt_message: agent_abstraction::PromptMessage,
model_provider: Option<ModelProviderConfig>,
) -> (Self, oneshot::Receiver<ChatPromptResponse>) {
let (chat_prompt_tx, chat_prompt_rx) = oneshot::channel();
(
Self {
prompt_message,
chat_prompt_tx,
model_provider,
service_uuid: None,
shared_api_key_manager: None,
skip_slot_limit: false,
},
chat_prompt_rx,
)
}
/// 设置 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
}
/// 设置是否跳过槽位限制
pub fn with_skip_slot_limit(mut self, skip: bool) -> Self {
self.skip_slot_limit = skip;
self
}
}
/// Agent Worker 任务
///
/// 使用标准 tokio::spawn 处理 Agent 请求队列。
/// SACP 支持 Send trait无需 LocalSet。
pub async fn agent_worker(mut request_rx: mpsc::UnboundedReceiver<AgentRequest>) -> Result<()> {
use agent_abstraction::session::{AcpAgentWorker, AgentWorker, WorkerRequest};
info!("agent_worker started (SACP version), listening for requests...");
// 创建 AcpSessionManager注入 AGENT_REGISTRY 作为 SessionRegistry
// SACP 版本只需要 2 个泛型参数N (SessionNotifier) 和 R (SessionRegistry)
let session_manager = Arc::new(
AcpSessionManager::<StateAwareNotifier, AgentSessionRegistry>::new(
Arc::new(StateAwareNotifier::new()),
AGENT_REGISTRY.clone(),
),
);
// 创建 AcpAgentWorker
let worker = AcpAgentWorker::new(session_manager);
while let Some(request) = request_rx.recv().await {
let project_id = request.prompt_message.project_id.clone();
let request_id = request.prompt_message.request_id.clone();
info!(
"📨 Received request, project_id: {}, request_id: {}",
project_id, request_id
);
// 1. 预处理附件agent_runner 特有逻辑)
let attachment_blocks = if !request.prompt_message.attachments.is_empty() {
match ContentBuilder::attachments_to_content_blocks(
&request.prompt_message.attachments,
&request.prompt_message.project_path,
)
.await
{
Ok(blocks) => Some(blocks),
Err(e) => {
error!("Attachment processing failed: {:?}", e);
if let Err(send_err) = request.chat_prompt_tx.send(ChatPromptResponse {
project_id: project_id.clone(),
session_id: String::new(),
code: shared_types::error_codes::ERR_AGENT_ERROR.to_string(),
error: Some(format!(
"{}: {:?}",
shared_types::error_codes::get_i18n_message_default("error.attachment_processing_failed"),
e
)),
request_id: Some(request_id),
service_type: request.prompt_message.service_type.clone(),
}) {
error!(
"Failed to send error response (receiver closed): {:?}",
send_err
);
}
continue;
}
}
} else {
None
};
// 2. 创建 WorkerRequest
let worker_request = WorkerRequest {
prompt_message: request.prompt_message.clone(),
model_provider: request.model_provider.clone(),
attachment_blocks,
service_uuid: request.service_uuid.clone(),
shared_api_key_manager: request.shared_api_key_manager.clone(),
};
// 3. 调用 AcpAgentWorker 处理(核心业务逻辑)
let worker_response = match worker.process_request(worker_request).await {
Ok(response) => response,
Err(e) => {
error!("Worker processing failed: {:?}", e);
if let Err(send_err) = request.chat_prompt_tx.send(ChatPromptResponse {
project_id: project_id.clone(),
session_id: String::new(),
code: shared_types::error_codes::ERR_AGENT_ERROR.to_string(),
error: Some(format!(
"{}: {:?}",
shared_types::error_codes::get_i18n_message_default("error.processing_failed"),
e
)),
request_id: Some(request_id.clone()),
service_type: request.prompt_message.service_type.clone(),
}) {
error!(
"Failed to send error response (receiver closed): {:?}",
send_err
);
}
continue;
}
};
// 4. 更新全局状态(使用统一的 AGENT_REGISTRY
if worker_response.is_new_session {
if let Some(handles) = &worker_response.session_handles {
debug!("🆕 New session, registering in AGENT_REGISTRY");
let project_and_agent_info = ProjectAndAgentInfo {
project_id: project_id.clone(),
session_id: SessionId::new(Arc::from(worker_response.session_id.as_str())),
prompt_tx: handles.prompt_tx.clone(),
cancel_tx: handles.cancel_tx.clone(),
model_provider: request.model_provider.clone(),
request_id: Some(request_id.clone()),
status: AgentStatus::Active, // 🆕 修复Worker 处理中应为 Active而非 Idle
last_activity: Utc::now(),
created_at: Utc::now(),
stop_handle: handles.lifecycle_handle.clone(),
};
// 使用统一的 AGENT_REGISTRY 注册(自动处理所有映射)
AGENT_REGISTRY.register(
&project_id,
&worker_response.session_id,
project_and_agent_info,
);
info!(
"🔗 Agent 已注册到 AGENT_REGISTRY: project_id={}, session_id={}",
project_id, worker_response.session_id
);
}
} else {
debug!("♻️ Reusing session, no global Registry update needed");
}
// 5. 更新 SESSION_REQUEST_CONTEXT请求追踪
SESSION_REQUEST_CONTEXT.insert(project_id, request_id.clone());
// 6. 转换并发送回执
let chat_prompt_response = ChatPromptResponse {
project_id: worker_response.project_id,
session_id: worker_response.session_id,
code: if worker_response.error.is_none() {
shared_types::error_codes::SUCCESS.to_string()
} else {
shared_types::error_codes::ERR_AGENT_ERROR.to_string()
},
error: worker_response.error,
request_id: worker_response.request_id,
service_type: worker_response.service_type,
};
if let Err(e) = request.chat_prompt_tx.send(chat_prompt_response) {
error!("Failed to send acknowledgment: {:?}", e);
}
}
info!("🛑 agent_worker stopped");
Ok(())
}
/// 带心跳的 Agent Worker (SACP 版本) - 新架构
///
/// 使用标准 tokio::spawn 进行并发处理(无需 LocalSet
/// SACP 的 Component<L> trait 要求 Send + 'static因此可以安全地在多线程环境中使用
///
/// ## 参数变化
///
/// - `request_rx`: 使用有界 channel 代替 unbounded
/// - `state`: 使用 `Arc<AtomicState>` 代替 `WorkerHandle`
/// - `last_heartbeat_ts`: 🔥 P1 修复: 使用 `Arc<AtomicI64>` 代替 `Arc<Mutex<Option<DateTime>>>`
/// - `active_requests`: 直接访问,用于请求追踪
pub async fn agent_worker_with_heartbeat(
mut request_rx: mpsc::Receiver<AgentRequest>,
state: Arc<crate::agent_runtime::AtomicState>,
last_heartbeat_ts: Arc<std::sync::atomic::AtomicI64>,
active_requests: Arc<tokio::sync::Mutex<HashMap<String, chrono::DateTime<chrono::Utc>>>>,
) -> Result<()> {
info!("agent_worker started (SACP version with heartbeat), listening for requests...");
use agent_abstraction::session::{AcpAgentWorker, AgentWorker, WorkerRequest};
use tokio::time::{Duration, interval};
// 创建 AcpSessionManager注入 AGENT_REGISTRY 作为 SessionRegistry
// SACP 版本只需要 2 个泛型参数N (SessionNotifier) 和 R (SessionRegistry)
let session_manager = Arc::new(
AcpSessionManager::<StateAwareNotifier, AgentSessionRegistry>::new(
Arc::new(StateAwareNotifier::new()),
AGENT_REGISTRY.clone(),
),
);
// 创建 AcpAgentWorker
let worker = AcpAgentWorker::new(session_manager);
// 设置状态为 Running就绪信号
state.set(crate::agent_runtime::WorkerState::Running);
info!("[Worker] SACP Worker initialized, state set to Running");
// 启动心跳任务 - 🔥 P1 修复: 使用原子操作直接更新 last_heartbeat_ts
let last_heartbeat_ts_clone = last_heartbeat_ts.clone();
let heartbeat_task = tokio::spawn(async move {
let mut heartbeat_interval = interval(Duration::from_secs(5));
loop {
heartbeat_interval.tick().await;
let timestamp = Utc::now();
// 📊 打印当前 Worker 占用情况
if IS_UNLIMITED_MODE.load(Ordering::SeqCst) {
// 无限制模式HTTP Server 部署)- 不显示具体数量,避免误解
info!("💓 [Worker] Heartbeat - active sessions: (unlimited)");
} else {
// 限制模式Docker 容器部署)
let active = AGENT_REGISTRY.stats().agent_count;
let limit = get_concurrency_limit();
info!(
"💓 [Worker] Heartbeat - active sessions: {}/{}",
active, limit
);
}
// 🔥 P1 修复: 使用原子操作直接更新时间戳(无锁)
last_heartbeat_ts_clone.store(
timestamp.timestamp_millis(),
std::sync::atomic::Ordering::Release,
);
}
});
// 主处理循环 - SACP 版本:使用标准 tokio::spawn 进行并发处理
while let Some(request) = request_rx.recv().await {
let project_id = request.prompt_message.project_id.clone();
let request_id = request.prompt_message.request_id.clone();
info!(
"📨 Received request, project_id: {}, request_id: {} - SACP concurrent processing",
project_id, request_id
);
// 克隆需要的变量,用于 spawn 任务
let worker_clone = worker.clone();
// 🚀 SACP 版本:直接使用 tokio::spawn无需 spawn_blocking + LocalSet
// SACP 的 Component<L> trait 要求 Send + 'static因此可以安全地在多线程环境中使用
tokio::spawn(async move {
info!(
"🔵 [SACP] 开始处理请求 project_id={}, request_id={}",
project_id, request_id
);
// 🔥 OpenTelemetry 追踪: 创建请求 span
let _otel_span = RequestSpan::new(&project_id, &request_id, "process_agent_request");
// 1. 预处理附件agent_runner 特有逻辑)
let attachment_blocks = if !request.prompt_message.attachments.is_empty() {
match ContentBuilder::attachments_to_content_blocks(
&request.prompt_message.attachments,
&request.prompt_message.project_path,
)
.await
{
Ok(blocks) => Some(blocks),
Err(e) => {
error!("Attachment processing failed: {:?}", e);
// 🔥 DeferGuard 自动清理,无需手动调用 clear_pending_if_exists
if let Err(send_err) = request.chat_prompt_tx.send(ChatPromptResponse {
project_id: project_id.clone(),
session_id: String::new(),
code: shared_types::error_codes::ERR_AGENT_ERROR.to_string(),
error: Some(format!(
"{}: {:?}",
shared_types::error_codes::get_i18n_message_default("error.attachment_processing_failed"),
e
)),
request_id: Some(request_id.clone()),
service_type: request.prompt_message.service_type.clone(),
}) {
error!(
"Failed to send error response (receiver closed): {:?}",
send_err
);
}
return;
}
}
} else {
None
};
// 2. 创建 WorkerRequest
let worker_request = WorkerRequest {
prompt_message: request.prompt_message.clone(),
model_provider: request.model_provider.clone(),
attachment_blocks,
service_uuid: request.service_uuid.clone(),
shared_api_key_manager: request.shared_api_key_manager.clone(),
};
// 3. 调用 AcpAgentWorker 处理(核心业务逻辑)
let worker_response = match worker_clone.process_request(worker_request).await {
Ok(response) => response,
Err(e) => {
error!("Worker processing failed: {:?}", e);
// 🔥 DeferGuard 自动清理,无需手动调用 clear_pending_if_exists
if let Err(send_err) = request.chat_prompt_tx.send(ChatPromptResponse {
project_id: project_id.clone(),
session_id: String::new(),
code: shared_types::error_codes::ERR_AGENT_ERROR.to_string(),
error: Some(format!(
"{}: {:?}",
shared_types::error_codes::get_i18n_message_default("error.processing_failed"),
e
)),
request_id: Some(request_id.clone()),
service_type: request.prompt_message.service_type.clone(),
}) {
error!(
"Failed to send error response (receiver closed): {:?}",
send_err
);
}
return;
}
};
// 4. 提取 session_handles在移动 worker_response 之前)
// 关键:需要保存 lifecycle_handle 用于后续等待会话结束
let session_handles = worker_response.session_handles.clone();
let is_new_session = worker_response.is_new_session;
let response_session_id = worker_response.session_id.clone();
// 5. 更新全局状态(使用统一的 AGENT_REGISTRY
if is_new_session {
// 🔥 修复:槽位对应 Agent 生命周期,只在创建新 Agent 时获取槽位
// HTTP Server 部署模式跳过槽位限制
if request.skip_slot_limit {
info!(
"⏭️ [原子槽位] 跳过限制(无限制模式): project_id={}",
project_id
);
} else if !AGENT_REGISTRY.try_acquire_session_slot() {
let limit = get_concurrency_limit();
error!(
"🛡️ [原子并发限制] Agent 会话槽位已满 ({}/{}), 拒绝新请求 - project_id={}, request_id={}",
AGENT_REGISTRY.active_sessions_count(),
limit,
project_id,
request_id
);
// 清理 Pending 状态(如果已设置)
AGENT_REGISTRY.clear_pending_if_exists(&project_id);
if let Err(send_err) = request.chat_prompt_tx.send(ChatPromptResponse {
project_id: project_id.clone(),
session_id: String::new(),
code: shared_types::error_codes::ERR_TOO_MANY_REQUESTS.to_string(),
error: Some(format!(
"{}",
shared_types::error_codes::get_i18n_message_default("error.system_busy")
).replace("{}", &limit.to_string())),
request_id: Some(request_id.clone()),
service_type: request.prompt_message.service_type.clone(),
}) {
error!(
"Failed to send reject response (receiver closed): {:?}",
send_err
);
}
return;
} else {
info!(
"✅ [原子槽位] 成功获取槽位: {}/{} - project_id={}",
AGENT_REGISTRY.active_sessions_count(),
get_concurrency_limit(),
project_id
);
}
if let Some(ref handles) = session_handles {
debug!("🆕 New session, registering in AGENT_REGISTRY");
let project_and_agent_info = ProjectAndAgentInfo {
project_id: project_id.clone(),
session_id: SessionId::new(Arc::from(response_session_id.as_str())),
prompt_tx: handles.prompt_tx.clone(),
cancel_tx: handles.cancel_tx.clone(),
model_provider: request.model_provider.clone(),
request_id: Some(request_id.clone()),
status: AgentStatus::Active,
last_activity: Utc::now(),
created_at: Utc::now(),
stop_handle: handles.lifecycle_handle.clone(),
};
AGENT_REGISTRY.register(
&project_id,
&response_session_id,
project_and_agent_info,
);
info!(
"🔗 Agent 已注册到 AGENT_REGISTRY: project_id={}, session_id={}",
project_id, response_session_id
);
}
} else {
debug!("♻️ Reusing session, no new slot needed (Agent already holds slot)");
}
// 6. 更新 SESSION_REQUEST_CONTEXT请求追踪
SESSION_REQUEST_CONTEXT.insert(project_id.clone(), request_id.clone());
// 7. 转换并发送回执
let chat_prompt_response = ChatPromptResponse {
project_id: worker_response.project_id,
session_id: worker_response.session_id,
code: if worker_response.error.is_none() {
shared_types::error_codes::SUCCESS.to_string()
} else {
shared_types::error_codes::ERR_AGENT_ERROR.to_string()
},
error: worker_response.error,
request_id: worker_response.request_id,
service_type: worker_response.service_type,
};
if let Err(e) = request.chat_prompt_tx.send(chat_prompt_response) {
error!("Failed to send acknowledgment: {:?}", e);
} else {
info!(
"✅ 回执已发送project_id: {}",
request.prompt_message.project_id
);
}
// SACP 版本:生命周期管理简化
//
// 架构设计:
// - 槽位对应 Agent 生命周期,而非每次请求
// - 新会话:等待 Agent 生命周期结束,然后清理
// - 复用会话:立即退出,不释放槽位(因为 Agent 还在运行)
if is_new_session {
if let Some(ref handles) = session_handles {
if let Some(ref lifecycle) = handles.lifecycle_handle {
info!(
"🔄 [SACP] 新会话:等待 Agent 生命周期 - project_id={}, session_id={}",
project_id, response_session_id
);
// 等待以下任一事件:
// 1. 用户调用 stop_agent → lifecycle.cancel()
// 2. 清理任务停止闲置 Agent5分钟→ lifecycle.graceful_stop()
// 3. Agent 进程异常退出
lifecycle.cancellation_token().cancelled().await;
// 🔥 关键修复lifecycle 结束后,主动清理 Agent 并释放槽位
// 使用 session-aware 移除,避免旧 session 的 cleanup 误删新 session 的 registry 条目。
// 场景:用户快速发送多条消息时,旧 session 被取消,新 session 注册。
// 旧 session 的 spawned task 退出时registry 中已是新 session 的条目。
// 如果用 remove_by_project 会误删新 session导致新请求超时。
AGENT_REGISTRY.remove_by_project_if_session_matches(&project_id, &response_session_id);
info!(
"🛑 [SACP] Agent 生命周期结束,已清理 Registry - project_id={}, session_id={}",
project_id, response_session_id
);
} else {
warn!(
"⚠️ [SACP] 新会话缺少 lifecycle_handle - project_id={}",
project_id
);
// 缺少 lifecycle_handle立即清理
AGENT_REGISTRY.remove_by_project_if_session_matches(&project_id, &response_session_id);
}
}
} else {
info!(
"🔵 [SACP] 复用会话:请求处理完成 - project_id={}, session_id={}",
project_id, response_session_id
);
// 复用会话时不释放槽位,因为槽位对应 Agent 生命周期
// 而不是每次请求。Agent 还在运行,槽位应保持占用状态
}
});
// 立即继续循环,接收下一个请求 - 不等待上面的 spawn 完成
}
// 清理心跳任务
heartbeat_task.abort();
info!("🛑 agent_worker stopped");
Ok(())
}

View File

@@ -0,0 +1,315 @@
//! 定期清理闲置agent的任务
//!
//! 基于AgentLifecycleGuard的RAII原则简化清理逻辑
//! 1. 定时扫描识别闲置的agent
//! 2. 从PROJECT_AND_AGENT_INFO_MAP中移除
//! 3. AgentLifecycleGuard自动drop并清理资源
use anyhow::Result;
use chrono::{DateTime, Utc};
use std::time::Duration;
use tracing::{debug, info, warn};
use crate::model::AgentStatus;
use crate::service::{AGENT_REGISTRY, SESSION_CACHE};
/// 清理配置
#[derive(Debug, Clone)]
pub struct CleanupConfig {
/// 闲置超时时间默认30分钟
pub idle_timeout: Duration,
/// 清理检查间隔默认5分钟
pub cleanup_interval: Duration,
// 注意force_terminate_timeout 字段已移除
// 因为采用RAII模式AgentLifecycleGuard会自动处理资源清理
// 不需要强制终止超时机制
}
impl Default for CleanupConfig {
fn default() -> Self {
Self {
idle_timeout: Duration::from_secs(3 * 60), // 🆕 默认3分钟
cleanup_interval: Duration::from_secs(30), // 默认30秒
}
}
}
/// 清理任务统计信息
#[derive(Debug, Clone, Default)]
pub struct CleanupStats {
/// 总共清理的agent数量
pub total_cleaned: u64,
/// 成功清理的agent数量
pub success_cleaned: u64,
/// 清理失败的agent数量
pub failed_cleaned: u64,
/// 清理的孤立session数量
pub orphaned_sessions_cleaned: u64,
/// 清理的SSE消息数量
pub sse_messages_cleaned: u64,
/// 最后清理时间
pub last_cleanup: Option<DateTime<Utc>>,
}
/// Agent清理器 - 基于RAII的简化版本
pub struct AgentCleaner {
config: CleanupConfig,
stats: CleanupStats,
}
impl AgentCleaner {
/// 创建新的清理器
pub fn new(config: CleanupConfig) -> Self {
Self {
config,
stats: CleanupStats::default(),
}
}
/// 检查agent是否闲置超时
fn is_agent_idle_timeout(
&self,
last_activity: DateTime<Utc>,
current_time: DateTime<Utc>,
) -> bool {
let duration = current_time.signed_duration_since(last_activity);
duration.num_seconds() > 0
&& duration.num_seconds() as u64 > self.config.idle_timeout.as_secs()
}
/// 清理孤立的SSE消息数据
/// 清理没有project_id引用的session和长期未活跃的session
async fn cleanup_orphaned_sse_sessions(&mut self) -> (u64, u64) {
let mut orphaned_count = 0;
let mut messages_cleared = 0;
// 使用统一 Registry 收集所有活跃的 session_id
let active_session_ids: std::collections::HashSet<String> = AGENT_REGISTRY
.iter_agents()
.map(|entry| entry.value().session_id.to_string())
.collect();
// 检查SESSION_CACHE中的所有session
let mut sessions_to_remove = Vec::new();
let session_ids: Vec<String> = SESSION_CACHE
.iter()
.map(|entry| entry.key().clone())
.collect();
for session_id in session_ids {
// 如果session_id不在活跃映射中则为孤立session
if !active_session_ids.contains(&session_id) {
// 检查session中是否有消息
if let Some(session_data_ref) = SESSION_CACHE.get(&session_id) {
let session_data = session_data_ref.clone();
drop(session_data_ref);
let message_count = session_data.message_count().await;
if message_count > 0 {
info!(
"Found orphaned session: session_id={}, message_count={}",
session_id, message_count
);
// 清理这个session的消息 - 直接移除条目
if SESSION_CACHE.remove(&session_id).is_some() {
messages_cleared += 1;
}
// 如果清理后session为空标记为待删除
if session_data.message_count().await == 0 {
sessions_to_remove.push(session_id.clone());
}
orphaned_count += 1;
} else {
// 没有消息的空session直接标记删除
sessions_to_remove.push(session_id.clone());
}
} else {
// session_data不存在也标记删除
sessions_to_remove.push(session_id.clone());
}
}
}
// 删除空的孤立session
for session_id in sessions_to_remove {
if let Some((_, _)) = SESSION_CACHE.remove(&session_id) {
debug!("Removed empty session: {}", session_id);
}
}
if orphaned_count > 0 {
info!(
"Orphaned SSE session cleanup completed: session_count={}, message_count={}",
orphaned_count, messages_cleared
);
}
(orphaned_count, messages_cleared)
}
/// 执行一次清理操作 - 基于RAII的简化版
/// 只需要从 MAP 中移除闲置agentAgentLifecycleGuard 会自动清理资源
async fn cleanup_idle_agents(&mut self) -> Result<CleanupStats> {
let current_time = Utc::now();
let mut cleaned_count = 0;
let mut success_count = 0;
let mut failed_count = 0;
// 使用统一 Registry 获取统计信息
let registry_stats = AGENT_REGISTRY.stats();
let total_agents = registry_stats.agent_count;
info!(
"Starting cleanup for idle agents and SSE messages, current_time: {}, active_agent_count: {}",
current_time, total_agents
);
// 先清理孤立的SSE消息数据
let (orphaned_sessions, sse_messages) = self.cleanup_orphaned_sse_sessions().await;
// 收集需要清理的agent ID使用统一 Registry 遍历)
let mut agents_to_remove = Vec::new();
for entry in AGENT_REGISTRY.iter_agents() {
let project_id = entry.key();
let agent_info = entry.value();
// 只清理Idle状态的agent避免中断正在执行的任务
if agent_info.status == AgentStatus::Idle
&& self.is_agent_idle_timeout(agent_info.last_activity, current_time)
{
let idle_duration = (current_time - agent_info.last_activity).num_seconds();
info!(
"Found idle agent: project_id={}, status={:?}, last_activity: {}, idle_duration_seconds: {}, created_at: {}",
project_id,
agent_info.status,
agent_info.last_activity,
idle_duration,
agent_info.created_at
);
agents_to_remove.push(project_id.clone());
}
}
// 执行清理 - RAII版直接从 MAP 中移除AgentLifecycleGuard 会自动清理
for project_id in agents_to_remove {
match self.cleanup_agent_raii(&project_id) {
Ok(_) => {
success_count += 1;
info!("Agent cleaned successfully: {}", project_id);
}
Err(e) => {
failed_count += 1;
warn!("Failed to clean agent: {} - {}", project_id, e);
}
}
cleaned_count += 1;
}
// 更新统计信息
self.stats.total_cleaned += cleaned_count;
self.stats.success_cleaned += success_count;
self.stats.failed_cleaned += failed_count;
self.stats.orphaned_sessions_cleaned += orphaned_sessions;
self.stats.sse_messages_cleaned += sse_messages;
self.stats.last_cleanup = Some(current_time);
// 清理完成后的统计(使用统一 Registry
let final_stats = AGENT_REGISTRY.stats();
let remaining_agents = final_stats.agent_count;
let active_sessions = final_stats.session_count;
let cached_sessions = SESSION_CACHE.len();
info!(
"Cleanup completed: agent(total={}, success={}, failed={}, remaining={}) | session(active={}, cached={}) | sse_messages(cleared={})",
cleaned_count,
success_count,
failed_count,
remaining_agents,
active_sessions,
cached_sessions,
sse_messages
);
Ok(CleanupStats {
total_cleaned: cleaned_count,
success_cleaned: success_count,
failed_cleaned: failed_count,
orphaned_sessions_cleaned: orphaned_sessions,
sse_messages_cleaned: sse_messages,
last_cleanup: Some(current_time),
})
}
/// 基于RAII的简化清理方法
/// 只需要从MAP中移除agentAgentLifecycleGuard会自动清理所有资源
fn cleanup_agent_raii(&self, project_id: &str) -> Result<()> {
debug!("Starting RAII cleanup for agent: {}", project_id);
// 使用统一 Registry 检查并移除(内部自动同步清理所有映射)
if AGENT_REGISTRY.contains_project(project_id) {
// 通过统一 Registry 移除,自动清理:
// - agent_info_map
// - project_to_session 映射
// - session_to_project 反向映射
let removed = AGENT_REGISTRY.remove_by_project(project_id);
// 同步清理 SESSION_REQUEST_CONTEXT 中的 request_id
crate::proxy_agent::SESSION_REQUEST_CONTEXT.remove(project_id);
debug!(
"🧼 [cleanup] Cleared project_id from SESSION_REQUEST_CONTEXT: {}",
project_id
);
if removed.is_some() {
info!(
"Agent removed from Registry; AgentLifecycleGuard will clean up resources automatically: {}",
project_id
);
} else {
warn!("Tried to remove agent but it was not found: {}", project_id);
}
} else {
warn!("Agent not found in Registry: {}", project_id);
}
Ok(())
}
/// 运行清理任务 - 简化版,只做定时清理
pub async fn run(&mut self) {
info!("Cleanup task started, config: {:?}", self.config);
let mut interval = tokio::time::interval(self.config.cleanup_interval);
loop {
interval.tick().await;
match self.cleanup_idle_agents().await {
Ok(stats) => debug!("Periodic cleanup completed: {:?}", stats),
Err(e) => warn!("Periodic cleanup failed: {}", e),
}
}
}
/// 获取统计信息
pub fn get_stats(&self) -> &CleanupStats {
&self.stats
}
}
/// 启动清理任务 - 普通异步版本
///
/// 清理任务只操作 Send 数据结构,可以在普通异步线程中运行
pub fn start_cleanup_task(config: CleanupConfig) -> tokio::task::JoinHandle<()> {
let mut cleaner = AgentCleaner::new(config);
tokio::task::spawn(async move {
cleaner.run().await;
})
}

View File

@@ -0,0 +1,115 @@
mod acp_agent;
pub mod cleanup_task;
use crate::CancelNotificationRequestWrapper;
// 导出 agent_worker 相关类型和函数
pub use acp_agent::{AgentRequest, agent_worker_with_heartbeat, set_unlimited_mode};
use shared_types::AgentLifecycleGuard;
// SACP 类型导入
#[cfg(feature = "proxy")]
use crate::config::ProxyConfig;
use dashmap::DashMap;
#[cfg(feature = "proxy")]
use rcoder_proxy::{PingoraServerManager, ProxyConfig as PingoraProxyConfig};
use agent_client_protocol::schema::{PromptRequest, SessionId};
use std::sync::{Arc, LazyLock};
use tokio::sync::mpsc;
#[cfg(feature = "proxy")]
use tracing::{error, info};
/// Pingora 启动结果
///
/// 持有关闭信号的发送端,`stop()` 时直接发送信号,无需 Mutex 锁。
#[cfg(feature = "proxy")]
pub struct PingoraStartResult {
/// 关闭信号发送端
shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
}
#[cfg(feature = "proxy")]
impl PingoraStartResult {
/// 停止 Pingora 服务器
pub async fn stop(&mut self) {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(());
}
}
}
/// 启动 Pingora 代理服务
///
/// 封装 Pingora 的创建和启动逻辑,供 main.rs 和 http_server/start.rs 复用。
/// shutdown 通道在外部创建,`stop()` 直接发送信号,不经过 Mutex消除死锁风险。
#[cfg(feature = "proxy")]
#[must_use]
pub fn start_pingora(
proxy_config: &ProxyConfig,
shared_api_key_manager: Arc<dashmap::DashMap<String, shared_types::ModelProviderConfig>>,
) -> PingoraStartResult {
info!(
"Starting Pingora reverse proxy service, listening on port: {}",
proxy_config.listen_port
);
info!(
"Proxy route format: /proxy/{{port}}{{/path}} - e.g.: /proxy/{}/health",
proxy_config.default_backend_port
);
let pingora_config = PingoraProxyConfig {
listen_port: proxy_config.listen_port,
default_backend_port: proxy_config.default_backend_port,
backend_host: proxy_config.backend_host.clone(),
port_param: proxy_config.port_param.clone(),
config_file: None,
verbose: false,
};
// 创建 Pingora 服务器管理器
let mut server_manager =
PingoraServerManager::new(pingora_config).with_api_key_manager(shared_api_key_manager);
let pingora_service = server_manager.service();
// 启动健康检查循环(按配置)
if proxy_config.health_check.enabled {
let hc = &proxy_config.health_check;
pingora_service.start_health_check_loop(hc.interval_seconds, hc.timeout_seconds * 1000);
}
// 在外部创建 shutdown 通道,避免通过 Mutex 发送信号导致死锁
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
// 在后台任务中启动 Pingora直接 move server_manager无需 Arc<Mutex<>>
tokio::spawn(async move {
if let Err(e) = server_manager.start(shutdown_rx).await {
error!("Failed to start Pingora proxy server: {}", e);
}
});
info!(
"✅ Pingora 代理服务已启动在端口 {}",
proxy_config.listen_port
);
PingoraStartResult {
shutdown_tx: Some(shutdown_tx),
}
}
/// 会话级别的 request_id 上下文映射project_id -> request_id
/// 用于在 session_notification 回调中获取当前请求的 request_id
/// 避免使用 PROJECT_AND_AGENT_INFO_MAP 导致的锁竞争问题
/// 注意:使用 project_id 而非 session_id确保同一项目的多次请求能自动覆盖为最新值
pub static SESSION_REQUEST_CONTEXT: LazyLock<DashMap<String, String>> = LazyLock::new(DashMap::new);
/// ACP协议的连接信息
pub struct AcpConnectionInfo {
/// 会话ID
pub session_id: SessionId,
/// 用于发送 Prompt 的通道
pub prompt_tx: mpsc::UnboundedSender<PromptRequest>,
/// 用于发送取消通知的通道(使用新类型)
pub cancel_tx: mpsc::UnboundedSender<CancelNotificationRequestWrapper>,
/// Agent停止句柄将被包装为守卫并放入 ProjectAndAgentInfo
pub stop_handle: Option<Arc<AgentLifecycleGuard>>,
}

View File

@@ -0,0 +1,166 @@
use std::sync::Arc;
use crate::agent_runtime::AgentRuntime;
use crate::{config::AppConfig, handler};
use axum::{Router, response::IntoResponse, routing::get};
use dashmap::DashMap;
use rcoder_telemetry::{HttpMetricsLayer, TelemetryGuard};
use serde::Serialize;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
/// 会话信息结构
#[derive(Debug, Clone, Serialize)]
pub struct SessionInfo {
pub session_id: String,
pub user_id: String,
pub project_id: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub last_activity: chrono::DateTime<chrono::Utc>,
}
/// 应用状态
#[derive(Clone)]
pub struct AppState {
/// 活跃的会话映射, project_id -> SessionInfo
pub sessions: Arc<DashMap<String, SessionInfo>>,
/// 应用配置
pub config: AppConfig,
/// 🆕 Agent Runtime新架构
///
/// **推荐**: 使用 `agent_runtime.send().await` 发送任务
/// - 支持自动重启
/// - 提供健康状态检查
/// - 线程安全的原子操作
pub local_task_sender: Arc<AgentRuntime>,
/// 🆕 Agent Runtime用于健康检查和状态监控
pub agent_runtime: Arc<AgentRuntime>,
/// Pingora 代理服务引用(用于读取真实指标)
#[cfg(feature = "proxy")]
pub pingora_service: Option<Arc<rcoder_proxy::PingoraProxyService>>,
/// 🔒 API 密钥管理器(用于 Pingora 代理注入真实密钥)
/// 注意:此现在是 shared_api_key_manager 的包装器
pub api_key_manager: Arc<crate::api_key_manager::ApiKeyManager>,
/// 🔒 共享的 API 密钥 DashMap直接与 Pingora 共享)
/// 存储格式:<UUID> -> ModelProviderConfig
pub shared_api_key_manager: Arc<DashMap<String, shared_types::ModelProviderConfig>>,
/// 🔒 project_id -> service_uuid 映射(用于清理时查找对应的配置)
pub project_uuid_map: Arc<DashMap<String, String>>,
}
/// 创建 Axum 路由
pub fn create_router(state: Arc<AppState>, telemetry: Option<Arc<TelemetryGuard>>) -> Router {
let api_routes = Router::new()
.route("/health", get(handler::health_check))
.with_state(state.clone());
let mut router = Router::new().merge(api_routes).merge(create_swagger_ui());
// 添加 /metrics 端点(如果启用了 Prometheus
if let Some(ref guard) = telemetry {
let guard_clone = Arc::clone(guard);
router = router.route(
"/metrics",
get(move || {
let guard = Arc::clone(&guard_clone);
async move { metrics_handler(guard).await }
}),
);
}
// 应用 HTTP 指标中间件
router.layer(HttpMetricsLayer::new())
}
/// Prometheus 指标处理器
async fn metrics_handler(telemetry: Arc<TelemetryGuard>) -> impl IntoResponse {
match telemetry.render_metrics() {
Some(metrics) => (
axum::http::StatusCode::OK,
[(
axum::http::header::CONTENT_TYPE,
"text/plain; charset=utf-8",
)],
metrics,
),
None => (
axum::http::StatusCode::SERVICE_UNAVAILABLE,
[(
axum::http::header::CONTENT_TYPE,
"text/plain; charset=utf-8",
)],
"Prometheus metrics not enabled".to_string(),
),
}
}
/// OpenAPI 文档结构
#[derive(OpenApi)]
#[openapi(
paths(
handler::health_check,
),
components(
schemas(
// 响应结构体
handler::HealthResponse,
)
),
tags(
(name = "system", description = "系统健康检查和状态监控接口"),
),
info(
description = r#"
RCoder AI 服务 API
基于 ACP (Agent Client Protocol) 的 AI 驱动开发平台,提供完整的 AI 代理集成解决方案。
## 主要功能
- **智能对话**: 支持文本、图像、音频、文档等多媒体内容的 AI 交互
- **实时通知**: 通过 SSE 协议提供 AI 代理执行进度的实时推送
- **会话管理**: 完整的会话生命周期管理,支持任务取消
- **项目隔离**: 每个对话在独立的项目工作空间中进行,确保安全性
## 技术架构
- **协议**: ACP (Agent Client Protocol) v0.4
- **gRPC 通信**: rcoder 通过 gRPC 与 agent_runner 通信
- **健康检查**: 提供服务健康状态查询
## gRPC 接口
agent_runner 主要通过 gRPC 提供服务:
- `Chat` - 聊天对话
- `SubscribeProgress` - 订阅进度事件Server Streaming
- `CancelSession` - 取消会话
- `GetStatus` - 查询状态
"#,
title = "RCoder AI API",
version = "2.0.0",
license(name = "MIT OR Apache-2.0", url = "https://opensource.org/licenses/MIT"),
contact(
name = "RCoder Team",
email = "team@rcoder.com",
url = "https://github.com/rcoder/rcoder"
)
),
servers(
(url = "http://localhost:50051", description = "gRPC 服务 (agent_runner)"),
(url = "http://localhost:8087", description = "HTTP API 服务 (rcoder)"),
)
)]
pub struct ApiDoc;
/// 创建 Swagger UI 路由
pub fn create_swagger_ui() -> SwaggerUi {
SwaggerUi::new("/api/docs")
.url("/api/docs/openapi.json", ApiDoc::openapi())
.config(utoipa_swagger_ui::Config::new(["/api/docs/openapi.json"]))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,582 @@
//! Chat Handler 共享逻辑
//!
//! 封装 chat 请求处理的核心业务逻辑,供 gRPC 和 HTTP 复用。
//!
//! ## 设计原则
//!
//! - **RAII 状态管理**: 使用 PendingGuard 自动管理 Pending 状态
//! - **DashMap Entry API**: 避免读写锁竞态条件
//! - **Fail Fast**: 尽早暴露问题,便于定位修复
use std::path::PathBuf;
use std::sync::Arc;
use dashmap::DashMap;
use shared_types::{
Attachment, CancelNotificationRequestWrapper, CancelResult, ChatAgentConfig, ChatPromptBuilder,
ModelProviderConfig, ServiceType, error_codes,
};
use agent_client_protocol::schema::{CancelNotification, SessionId};
use tokio::time::Duration;
use tracing::{debug, error, info, warn};
use crate::AgentRuntime;
use crate::agent_runtime::WorkerState;
use crate::proxy_agent::AgentRequest;
use crate::service::{AGENT_REGISTRY, PendingGuard, SESSION_CACHE};
/// Chat Handler 输入参数
///
/// 包含处理 chat 请求所需的所有参数与协议无关gRPC/HTTP
#[derive(Debug, Clone)]
pub struct ChatHandlerInput {
/// 项目 ID
pub project_id: String,
/// 项目工作目录(由调用方根据环境决定)
pub project_dir: PathBuf,
/// 会话 ID可选用于复用会话
pub session_id: Option<String>,
/// 用户提示词
pub prompt: String,
/// 请求 ID用于追踪
pub request_id: String,
/// 附件列表
pub attachments: Vec<Attachment>,
/// 数据源附件列表
pub data_source_attachments: Vec<String>,
/// 模型配置(可选)
pub model_config: Option<ModelProviderConfig>,
/// 服务类型
pub service_type: ServiceType,
/// Agent 配置覆盖(可选)
pub agent_config_override: Option<ChatAgentConfig>,
/// 系统提示覆盖(可选)
pub system_prompt_override: Option<String>,
/// 用户提示模板覆盖(可选)
pub user_prompt_template_override: Option<String>,
/// 是否跳过槽位限制HTTP Server 宿主机部署时为 true
pub skip_slot_limit: bool,
}
/// Chat Handler 输出结果
///
/// 统一的响应结构,可转换为 gRPC 或 HTTP 响应
#[derive(Debug, Clone)]
pub struct ChatHandlerOutput {
/// 项目 ID
pub project_id: String,
/// 会话 ID
pub session_id: String,
/// 是否成功
pub success: bool,
/// 错误消息(可选)
pub error: Option<String>,
/// 错误码(可选)
pub error_code: Option<String>,
/// 请求 ID可选
pub request_id: Option<String>,
/// 是否需要降级处理
pub need_fallback: bool,
/// 降级原因(可选)
pub fallback_reason: Option<String>,
}
impl ChatHandlerOutput {
/// 创建错误响应
pub fn error(
project_id: String,
session_id: String,
error_msg: String,
error_code: String,
) -> Self {
Self {
project_id,
session_id,
success: false,
error: Some(error_msg),
error_code: Some(error_code),
request_id: None,
need_fallback: false,
fallback_reason: None,
}
}
/// 创建 Agent Busy 错误响应
pub fn agent_busy(project_id: String, session_id: Option<String>) -> Self {
Self {
project_id,
session_id: session_id.unwrap_or_default(),
success: false,
error: Some(error_codes::get_i18n_message_default("error.agent_busy")),
error_code: Some(error_codes::ERR_AGENT_BUSY.to_string()),
request_id: None,
need_fallback: false,
fallback_reason: None,
}
}
}
/// Chat Handler 依赖上下文
///
/// 包含处理 chat 请求所需的运行时依赖
pub struct ChatHandlerContext {
/// Agent 运行时
pub agent_runtime: Arc<AgentRuntime>,
/// 共享的 API 密钥管理器
pub shared_api_key_manager: Arc<DashMap<String, ModelProviderConfig>>,
/// project_id -> UUID 映射
pub project_uuid_map: Arc<DashMap<String, String>>,
}
/// 取消当前正在执行的 Agent 任务
///
/// 发送取消通知并等待取消完成,超时时间为 10 秒
///
/// # Arguments
/// * `cancel_tx` - 取消通知发送通道
/// * `session_id` - 当前会话 ID
/// * `project_id` - 项目 ID
///
/// # Returns
/// * `Ok(())` - 取消成功Agent 状态已恢复为 Idle
/// * `Err(ChatHandlerOutput)` - 取消失败,包含错误响应
async fn cancel_current_task(
cancel_tx: &tokio::sync::mpsc::Sender<CancelNotificationRequestWrapper>,
session_id: &str,
project_id: &str,
) -> Result<(), ChatHandlerOutput> {
info!(
"[ChatHandler] Cancelling current task: project_id={}, session_id={}",
project_id, session_id
);
// 1. 检查 cancel_tx 是否有效
if cancel_tx.is_closed() {
error!(
"[ChatHandler] Cancel channel closed: project_id={}, session_id={}",
project_id, session_id
);
return Err(ChatHandlerOutput::error(
project_id.to_string(),
session_id.to_string(),
error_codes::get_i18n_message_default("error.cancel_channel_closed"),
error_codes::ERR_SERVICE_UNAVAILABLE.to_string(),
));
}
// 2. 创建 oneshot channel 等待取消结果
let (result_tx, result_rx) = tokio::sync::oneshot::channel::<CancelResult>();
let cancel_notification = CancelNotification::new(SessionId::new(Arc::from(session_id)));
let cancel_request = CancelNotificationRequestWrapper {
cancel_notification,
result_tx,
};
// 3. 发送取消通知
if let Err(e) = cancel_tx.send(cancel_request).await {
error!(
"[ChatHandler] Failed to send cancel notification: project_id={}, error={}",
project_id, e
);
return Err(ChatHandlerOutput::error(
project_id.to_string(),
session_id.to_string(),
format!(
"{}: {}",
error_codes::get_i18n_message_default("error.cancel_failed"),
e
),
error_codes::ERR_INTERNAL_SERVER_ERROR.to_string(),
));
}
// 4. 等待取消结果(超时 10 秒)
match tokio::time::timeout(Duration::from_secs(10), result_rx).await {
Ok(Ok(cancel_result)) => {
if cancel_result.is_success() {
info!(
"[ChatHandler] Cancel notification sent successfully, proceeding with new request: project_id={}, session_id={}",
project_id, session_id
);
// 🎯 关键设计cancel 后立即返回,不等待 session 移除
//
// 上下文连续性保证:
// - 不等待 session 移除 → session 保持在 Registry 中
// - get_or_create_session → is_channel_closed()=false → 复用同一 session
// - 新 prompt 发送到同一 session 的 prompt_tx → 同一 Agent 子进程处理
// - Agent 子进程保持存活 → 内存中的对话上下文连续
//
// 时序:
// 1. CancelResult::Success → cancel 通知已发送给 Agent
// 2. SACP inner loop 收到 cancel → is_cancelled=true → 等待 Agent 响应或超时
// 3. inner loop 退出 → outer loop 继续等待 prompt_rx
// 4. 新请求的 prompt 到达 → session_cancelled 重置 → 处理新 prompt
// 5. 同一 Agent 子进程处理新 prompt → 上下文连续
//
// 最坏情况延迟inner cancel timeout (10s) — Agent 不响应 cancel 时
Ok(())
} else {
let error_msg = cancel_result.error_message().unwrap_or("Unknown error");
error!(
"[ChatHandler] Cancel failed: project_id={}, error={}",
project_id, error_msg
);
Err(ChatHandlerOutput::error(
project_id.to_string(),
session_id.to_string(),
format!(
"{}: {}",
error_codes::get_i18n_message_default("error.cancel_failed"),
error_msg
),
error_codes::ERR_AGENT_ERROR.to_string(),
))
}
}
Ok(Err(_)) => {
error!(
"[ChatHandler] Cancel result channel dropped: project_id={}",
project_id
);
Err(ChatHandlerOutput::error(
project_id.to_string(),
session_id.to_string(),
error_codes::get_i18n_message_default("error.cancel_channel_dropped"),
error_codes::ERR_INTERNAL_SERVER_ERROR.to_string(),
))
}
Err(_) => {
error!(
"[ChatHandler] Cancel timeout (10s): project_id={}",
project_id
);
Err(ChatHandlerOutput::error(
project_id.to_string(),
session_id.to_string(),
error_codes::get_i18n_message_default("error.cancel_timeout"),
error_codes::ERR_CANCEL_FAILED.to_string(),
))
}
}
}
/// 执行 Chat 请求的核心逻辑
///
/// 封装了 chat 请求的完整处理流程:
/// 1. Agent Busy 检查
/// 2. PendingGuard RAII 状态管理
/// 3. Session 清理逻辑
/// 4. 目录创建
/// 5. ChatPrompt 构建
/// 6. API Key 管理
/// 7. AgentRequest 创建和发送
/// 8. 等待响应
///
/// # Arguments
///
/// * `input` - Chat 请求输入参数
/// * `context` - 运行时上下文依赖
///
/// # Returns
///
/// 返回统一的 `ChatHandlerOutput` 结果
pub async fn handle_chat_core(
input: ChatHandlerInput,
context: &ChatHandlerContext,
) -> ChatHandlerOutput {
let project_id = input.project_id.clone();
let session_id = input.session_id.clone();
let request_id = input.request_id.clone();
info!(
"[ChatHandler] Starting to process request: project_id={}, session_id={:?}, prompt_len={}, has_model_config={}",
project_id,
session_id,
input.prompt.len(),
input.model_config.is_some()
);
// ========== 步骤1: 查询现有 Agent 状态 ==========
// 优先通过 session_id 查找,回退到 project_id 查找
let agent_info_ref = if let Some(ref sid) = session_id {
info!(
"[ChatHandler] Looking up Agent by session_id: session_id={}",
sid
);
AGENT_REGISTRY.get_agent_info_by_session(sid)
} else {
None
};
let agent_info_ref = agent_info_ref.or_else(|| {
info!(
"[ChatHandler] Looking up Agent by project_id: project_id={}",
project_id
);
AGENT_REGISTRY.get_agent_info(&project_id)
});
// ========== 步骤2: 检查 Agent Busy 状态,如果忙则取消当前任务 ==========
use crate::model::AgentStatus;
if let Some(agent_info) = agent_info_ref {
if agent_info.status == AgentStatus::Active || agent_info.status == AgentStatus::Pending {
info!(
"[ChatHandler] Agent Busy, cancelling current task: project_id={}, status={:?}, session_id={:?}",
project_id, agent_info.status, session_id
);
// 获取 cancel_tx 和 session_id并释放 DashMap 读锁(防死锁)
let cancel_tx = agent_info.cancel_tx.clone();
// 优先使用请求中的 session_id如果为空则从 agent_info 中获取
let actual_session_id = session_id
.clone()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| agent_info.session_id.to_string());
drop(agent_info);
// 取消当前任务
if let Err(cancel_error) =
cancel_current_task(&cancel_tx, &actual_session_id, &project_id).await
{
// 取消失败,返回错误
error!(
"[ChatHandler] Failed to cancel current task: project_id={}, error={:?}",
project_id, cancel_error
);
return cancel_error;
}
info!(
"[ChatHandler] Current task cancelled, proceeding with new request: project_id={}",
project_id
);
}
}
// ========== 步骤3: 创建 PendingGuardRAII==========
// 自动在作用域结束时清理,避免状态泄漏
let pending_guard = PendingGuard::new(&AGENT_REGISTRY, &project_id);
info!(
"[ChatHandler] Created PendingGuard: project_id={}",
project_id
);
// ========== 步骤4: 清理无效 session ==========
// 只在 session 不存在时才清理无效的 session_id
if let Some(ref sid) = session_id {
let session_exists = AGENT_REGISTRY.contains_session(sid);
if !session_exists && SESSION_CACHE.remove(sid).is_some() {
info!(
"[ChatHandler] session does not exist, removing invalid session: session_id={}",
sid
);
} else if session_exists {
info!("[ChatHandler] Reusing existing session: session_id={}", sid);
}
}
// ========== 步骤5: 获取项目工作目录 ==========
let project_dir = input.project_dir.clone();
info!(
"[ChatHandler] Project working directory: {:?}, service_type={:?}",
project_dir, input.service_type
);
// 确保目录存在
if !project_dir.exists() {
if let Err(e) = tokio::fs::create_dir_all(&project_dir).await {
error!("[ChatHandler] Failed to create project directory: {}", e);
return ChatHandlerOutput::error(
project_id,
session_id.unwrap_or_default(),
format!(
"{}: {}",
error_codes::get_i18n_message_default("error.create_project_dir_failed"),
e
),
error_codes::ERR_INTERNAL_SERVER_ERROR.to_string(),
);
}
}
// ========== 步骤6: 构建 ChatPrompt 和 PromptMessage ==========
let chat_prompt = match ChatPromptBuilder::default()
.project_id(project_id.clone())
.project_path(project_dir)
.session_id(session_id.clone())
.prompt(input.prompt)
.attachments(input.attachments)
.data_source_attachments(input.data_source_attachments)
.service_type(input.service_type)
.request_id(request_id.clone())
.system_prompt_override(input.system_prompt_override)
.user_prompt_template_override(input.user_prompt_template_override)
.agent_config_override(input.agent_config_override)
.build()
{
Ok(prompt) => prompt,
Err(e) => {
error!("[ChatHandler] Failed to build ChatPrompt: {}", e);
return ChatHandlerOutput::error(
project_id,
session_id.unwrap_or_default(),
format!(
"{}: {}",
error_codes::get_i18n_message_default("error.build_chat_prompt_failed"),
e
),
error_codes::ERR_INTERNAL_SERVER_ERROR.to_string(),
);
}
};
// 转换为 PromptMessage
let prompt_message = agent_abstraction::PromptMessage::from(chat_prompt);
// ========== 步骤7: 管理 API 密钥配置 ==========
let model_provider = input.model_config;
// 生成唯一的 service UUID用于 API 密钥管理)
let service_uuid = if model_provider.is_some() {
Some(uuid::Uuid::new_v4().to_string())
} else {
None
};
// 存储 API 配置到共享 DashMap
if let (Some(ref provider), Some(ref service_uuid_ref)) =
(model_provider.as_ref(), service_uuid.as_ref())
{
debug!(
"[ChatHandler] 使用模型配置: provider={}, model={}, base_url={}, api_protocol={:?}, requires_openai_auth={}, service_uuid={}",
provider.name,
provider.default_model,
provider.base_url,
provider.api_protocol,
provider.requires_openai_auth,
service_uuid_ref
);
// 存储 ModelProviderConfig 到共享 DashMap使用 UUID 作为 key
context
.shared_api_key_manager
.insert(service_uuid_ref.to_string(), (*provider).clone());
// 存储 project_id -> UUID 映射(用于后续清理时查找)
context
.project_uuid_map
.insert(project_id.clone(), service_uuid_ref.to_string());
info!(
"[ChatHandler] Stored API config: service_uuid={}, provider_name={}, base_url={}",
service_uuid_ref,
provider.name,
shared_types::mask_url(&provider.base_url)
);
} else {
warn!("[ChatHandler] No model config provided; falling back to env vars or defaults");
}
// ========== 步骤8: 检查 Worker 状态 ==========
match context.agent_runtime.state() {
WorkerState::Running => {
// 正常操作,继续处理
}
WorkerState::Starting => {
warn!("[ChatHandler] Agent Worker is starting; request may be delayed");
}
WorkerState::Stopping | WorkerState::Stopped => {
// PendingGuard 自动清理(在 drop 时)
error!("[ChatHandler] Agent Worker unavailable");
return ChatHandlerOutput::error(
project_id,
session_id.unwrap_or_default(),
error_codes::get_i18n_message_default("error.agent_worker_unavailable"),
error_codes::ERR_SERVICE_UNAVAILABLE.to_string(),
);
}
}
// ========== 步骤9: 发送任务到 Agent Worker ==========
// 创建请求并设置 UUID 和密钥管理器
let (agent_request, chat_prompt_rx) = AgentRequest::new(prompt_message, model_provider);
let agent_request = agent_request
.with_service_uuid(service_uuid)
.with_key_manager(Some(context.shared_api_key_manager.clone()))
.with_skip_slot_limit(input.skip_slot_limit);
if let Err(e) = context.agent_runtime.send(agent_request).await {
// PendingGuard 自动清理(在 drop 时)
error!("[ChatHandler] Failed to send task: {}", e);
return ChatHandlerOutput::error(
project_id,
session_id.unwrap_or_default(),
format!(
"{}: {}",
error_codes::get_i18n_message_default("error.send_task_failed"),
e
),
error_codes::ERR_INTERNAL_SERVER_ERROR.to_string(),
);
}
// ========== 步骤10: 等待响应5 分钟超时)==========
match tokio::time::timeout(std::time::Duration::from_secs(300), chat_prompt_rx).await {
Ok(Ok(response)) => {
let output = ChatHandlerOutput {
project_id: response.project_id,
session_id: response.session_id,
success: response.error.is_none(),
error: response.error,
error_code: if response.code != error_codes::SUCCESS {
Some(response.code)
} else {
None
},
request_id: Some(request_id),
need_fallback: false,
fallback_reason: None,
};
info!(
"[ChatHandler] Chat completed: success={}, session_id={}",
output.success, output.session_id
);
// 只有请求成功时才提交 PendingGuard 保留 Pending 状态
// 失败时 PendingGuard 自动 drop 清理,允许下次请求重新创建 Agent
if output.success {
pending_guard.commit_success();
}
output
}
Ok(Err(e)) => {
// PendingGuard 自动清理(在 drop 时)
error!("[ChatHandler] Chat response channel dropped: {}", e);
ChatHandlerOutput::error(
project_id,
session_id.unwrap_or_default(),
format!(
"{}: {}",
error_codes::get_i18n_message_default("error.request_processing_failed"),
e
),
error_codes::ERR_INTERNAL_SERVER_ERROR.to_string(),
)
}
Err(_elapsed) => {
// PendingGuard 自动清理(在 drop 时)
error!("[ChatHandler] ⏰ Chat request timeout (300s): project_id={}", project_id);
ChatHandlerOutput::error(
project_id,
session_id.unwrap_or_default(),
error_codes::get_i18n_message_default("error.request_processing_failed")
+ ": request timeout (300s)",
error_codes::ERR_INTERNAL_SERVER_ERROR.to_string(),
)
}
}
}

View File

@@ -0,0 +1,351 @@
//! Agent Runner 本地服务实现
//!
//! 实现 `AgentHttpService` trait直接调用本地服务AGENT_REGISTRY, SESSION_CACHE, handle_chat_core
//! 适用于 Agent Runner 的 HTTP Server 模式
use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use dashmap::DashMap;
use agent_client_protocol::schema::{CancelNotification, SessionId};
use shared_types::{
agent_http_service::AgentHttpService,
rcoder_agent_types::{
RcoderAgentCancelRequest, RcoderAgentCancelResponse, RcoderAgentStopRequest,
RcoderAgentStopResponse,
},
AgentStatusResponse, ChatResponse, HttpResult, RcoderChatRequest, ServiceType,
};
use tokio::sync::oneshot;
use tracing::{info, warn};
use crate::AgentRuntime;
use crate::service::{
chat_handler::{ChatHandlerContext, ChatHandlerInput},
handle_chat_core, AGENT_REGISTRY, SESSION_CACHE, SessionData,
};
/// Agent Runner 本地服务实现
///
/// 直接调用本地 AGENT_REGISTRY、SESSION_CACHE、handle_chat_core()
/// 不需要 gRPC 转发
pub struct LocalAgentHttpService {
/// Agent 运行时
pub agent_runtime: Arc<AgentRuntime>,
/// 共享 API Key 管理器
pub shared_api_key_manager: Arc<DashMap<String, shared_types::ModelProviderConfig>>,
/// project_id -> UUID 映射
pub project_uuid_map: Arc<DashMap<String, String>>,
/// 项目工作目录根路径
pub projects_dir: PathBuf,
}
impl LocalAgentHttpService {
/// 创建新的 LocalAgentHttpService
pub fn new(
agent_runtime: Arc<AgentRuntime>,
shared_api_key_manager: Arc<DashMap<String, shared_types::ModelProviderConfig>>,
project_uuid_map: Arc<DashMap<String, String>>,
projects_dir: PathBuf,
) -> Self {
Self {
agent_runtime,
shared_api_key_manager,
project_uuid_map,
projects_dir,
}
}
}
#[async_trait]
impl AgentHttpService for LocalAgentHttpService {
/// Chat 对话请求
async fn chat(&self, request: RcoderChatRequest) -> HttpResult<ChatResponse> {
// 0. 验证 prompt 不为空
if request.prompt.is_empty() {
warn!("[LocalAgent] Empty prompt received");
return HttpResult::error(
shared_types::error_codes::ERR_VALIDATION,
"Prompt cannot be empty",
);
}
info!(
"📨 [LocalAgent] Chat request: prompt_len={}, project_id={:?}, session_id={:?}",
request.prompt.len(),
request.project_id,
request.session_id
);
// 1. 生成 project_id如果未提供
let project_id = request
.project_id
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string().replace('-', ""));
// 2. 自动查找现有 session_id如果未提供
let session_id = request.session_id.or_else(|| {
AGENT_REGISTRY
.get_agent_info(&project_id)
.map(|info| info.session_id.to_string())
});
// 3. 创建项目工作目录
let project_dir = self.projects_dir.join(&project_id);
if let Err(e) = tokio::fs::create_dir_all(&project_dir).await {
let error_msg = format!("Failed to create project dir: {}", e);
warn!("[LocalAgent] {}", error_msg);
return HttpResult::error(shared_types::error_codes::ERR_INTERNAL_SERVER_ERROR, &error_msg);
}
// 4. 生成 request_id如果未提供
let request_id = request
.request_id
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
// 5. 构建 ChatHandlerInput
let input = ChatHandlerInput {
project_id: project_id.clone(),
project_dir,
session_id,
prompt: request.prompt,
request_id: request_id.clone(),
attachments: request.attachments,
data_source_attachments: request.data_source_attachments,
model_config: request.model_provider,
service_type: ServiceType::RCoder,
agent_config_override: request.agent_config,
system_prompt_override: request.system_prompt,
user_prompt_template_override: request.user_prompt,
skip_slot_limit: true, // HTTP Server 部署,跳过槽位限制
};
// 6. 构建 ChatHandlerContext
let context = ChatHandlerContext {
agent_runtime: self.agent_runtime.clone(),
shared_api_key_manager: self.shared_api_key_manager.clone(),
project_uuid_map: self.project_uuid_map.clone(),
};
// 7. 调用 handle_chat_core
let output = handle_chat_core(input, &context).await;
// 8. 将 session 写入 SESSION_CACHESSE 进度流需要)
let session_id_str = output.session_id.clone();
if SESSION_CACHE.get(&session_id_str).is_none() {
let session_data = SessionData::new(1000);
SESSION_CACHE.insert(session_id_str, session_data);
}
// 9. 构建响应
if output.error.is_some() || !output.success {
let error_code = output.error_code.as_deref().unwrap_or(shared_types::error_codes::ERR_INTERNAL_SERVER_ERROR);
let error_msg = output.error.unwrap_or_else(|| "Unknown error".to_string());
HttpResult::error(error_code, &error_msg)
} else {
HttpResult::success(ChatResponse {
project_id: output.project_id,
session_id: output.session_id,
error: output.error,
request_id: Some(request_id),
need_fallback: None,
fallback_reason: None,
})
}
}
/// 查询 Agent 状态
async fn get_status(&self, project_id: &str) -> HttpResult<AgentStatusResponse> {
info!(
"🔍 [LocalAgent] Status query: project_id={}",
project_id
);
if let Some(info) = AGENT_REGISTRY.get_agent_info(project_id) {
// Agent 存在且活跃
let response = AgentStatusResponse {
project_id: project_id.to_string(),
is_alive: true,
session_id: Some(info.session_id.to_string()),
status: Some(info.status.clone()),
last_activity: Some(info.last_activity),
created_at: Some(info.created_at),
model_provider: None, // AgentRegistry 不存储 model_provider
};
info!(
"✅ [LocalAgent] Agent status: project_id={}, is_alive=true, status={:?}",
project_id,
info.status
);
HttpResult::success(response)
} else {
// Agent 不存在
info!(
"📭 [LocalAgent] Agent not found: project_id={}",
project_id
);
let response = AgentStatusResponse {
project_id: project_id.to_string(),
is_alive: false,
session_id: None,
status: None,
last_activity: None,
created_at: None,
model_provider: None,
};
HttpResult::success(response)
}
}
/// 停止 Agent
async fn stop(&self, request: RcoderAgentStopRequest) -> HttpResult<RcoderAgentStopResponse> {
info!(
"🛑 [LocalAgent] Stop request: project_id={}",
request.project_id
);
// 1. 获取 Agent 信息
if let Some(agent_info) = AGENT_REGISTRY.get_agent_info(&request.project_id) {
let session_id = agent_info.session_id.to_string();
let cancel_tx = agent_info.cancel_tx.clone();
// 释放读锁
drop(agent_info);
// 2. 发送取消信号(如果 channel 仍然打开)
if !cancel_tx.is_closed() {
let session_id_obj = SessionId::new(Arc::from(session_id.as_str()));
let cancel_notification = CancelNotification::new(session_id_obj);
let (result_tx, _result_rx) = oneshot::channel();
let cancel_request = shared_types::CancelNotificationRequestWrapper {
cancel_notification,
result_tx,
};
match cancel_tx.send(cancel_request).await {
Ok(_) => {
info!("[LocalAgent] Cancel signal sent: session_id={}", session_id);
}
Err(e) => {
warn!(
"⚠️ [LocalAgent] Failed to send cancel signal: session_id={}, error={}",
session_id, e
);
}
}
}
// 3. 从 AGENT_REGISTRY 移除 Agent
AGENT_REGISTRY.remove_by_project(&request.project_id);
info!(
"✅ [LocalAgent] Agent stopped: project_id={}",
request.project_id
);
HttpResult::success(RcoderAgentStopResponse {
success: true,
project_id: request.project_id,
session_id: Some(session_id),
message: "Agent stopped successfully".to_string(),
})
} else {
// Agent 不存在,幂等返回成功
info!(
" [LocalAgent] Agent not found, returning success idempotently: project_id={}",
request.project_id
);
HttpResult::success(RcoderAgentStopResponse {
success: true,
project_id: request.project_id,
session_id: None,
message: "Agent not found".to_string(),
})
}
}
/// 取消正在执行的任务
async fn cancel(&self, request: RcoderAgentCancelRequest) -> HttpResult<RcoderAgentCancelResponse> {
info!(
"🚫 [LocalAgent] Cancel request: project_id={}, session_id={:?}",
request.project_id, request.session_id
);
// 1. 查找 session_id如果未提供从 AGENT_REGISTRY 获取)
let session_id = if let Some(sid) = request.session_id {
sid
} else {
match AGENT_REGISTRY.get_agent_info(&request.project_id) {
Some(info) => info.session_id.to_string(),
None => {
// Agent 不存在,幂等返回成功
info!(
" [LocalAgent] Agent not found, returning success idempotently: project_id={}",
request.project_id
);
return HttpResult::success(RcoderAgentCancelResponse {
success: true,
session_id: String::new(),
});
}
}
};
// 2. 获取 Agent 信息并发送取消信号
if let Some(agent_info) = AGENT_REGISTRY.get_agent_info(&request.project_id) {
let cancel_tx = agent_info.cancel_tx.clone();
// 释放读锁
drop(agent_info);
// 检查是否已经空闲或停止中(幂等性)
if cancel_tx.is_closed() {
info!(
" [LocalAgent] Agent stopped, cancel channel is closed: session_id={}",
session_id
);
} else {
// 创建取消通知
let session_id_obj = SessionId::new(Arc::from(session_id.as_str()));
let cancel_notification = CancelNotification::new(session_id_obj);
// 创建 oneshot channel 接收取消结果HTTP 不等待结果,直接丢弃)
let (result_tx, _result_rx) = oneshot::channel();
let cancel_request = shared_types::CancelNotificationRequestWrapper {
cancel_notification,
result_tx,
};
// 发送取消信号(异步)
match cancel_tx.send(cancel_request).await {
Ok(_) => {
info!("[LocalAgent] Cancel signal sent: session_id={}", session_id);
}
Err(e) => {
warn!(
"⚠️ [LocalAgent] Failed to send cancel signal: session_id={}, error={}",
session_id, e
);
}
}
}
} else {
// Session 不存在,幂等返回成功
info!(
" [LocalAgent] Agent not found, returning success idempotently: session_id={}",
session_id
);
}
HttpResult::success(RcoderAgentCancelResponse {
success: true,
session_id,
})
}
}

View File

@@ -0,0 +1,11 @@
pub mod agent_registry;
pub mod chat_handler;
pub mod local_agent_service;
mod session_cache;
mod session_notifier;
mod state_aware_notifier;
pub use agent_registry::{AGENT_REGISTRY, AgentSessionRegistry, PendingGuard};
pub use chat_handler::{ChatHandlerContext, ChatHandlerInput, ChatHandlerOutput, handle_chat_core};
pub use session_cache::{SESSION_CACHE, SessionData, push_session_update_with_project};
pub use state_aware_notifier::StateAwareNotifier;

View File

@@ -0,0 +1,463 @@
//! 全局Session缓存模块
//!
//! 使用LazyLock初始化全局DashMap按session_id分组缓存统一会话消息到ringbuf循环缓冲区
use crate::{SessionNotify, UnifiedSessionMessage};
use anyhow::Result;
use dashmap::DashMap;
use ringbuf::HeapRb;
use ringbuf::traits::{Consumer, Observer, Producer, Split};
use std::sync::{Arc, LazyLock};
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
use super::AGENT_REGISTRY;
/// 日志截取的最大长度默认值
const MAX_LOG_TRUNCATE_LEN: usize = 50;
/// 截取消息内容用于日志打印(防止日志膨胀)
///
/// 社区常见做法:
/// - tracing: 通过 Subscriber 配置限制字段大小
/// - serde: 自定义 Serialize 实现截断
/// - 简单场景: chars().take() + 长度检查(本实现)
fn truncate_message_for_log(data: &serde_json::Value, max_len: usize) -> String {
// 边界检查max_len 为 0 时返回空,避免无效计算
if max_len == 0 {
return String::new();
}
// 优先提取原始字符串内容,避免 JSON 序列化后的引号包裹
let s = match data.as_str() {
Some(inner) => inner.to_string(),
None => data.to_string(),
};
// chars().count() 是一次性遍历,可以与 take() 合并优化但牺牲可读性
// 当前实现清晰且性能可接受(短字符串场景)
let char_count = s.chars().count();
if char_count <= max_len {
return s;
}
// 使用 chars() 安全截取 UTF-8 字符边界
let truncated: String = s.chars().take(max_len).collect();
format!("{}... (truncated)", truncated)
}
/// 全局Session缓存 - LazyLock初始化
pub static SESSION_CACHE: LazyLock<DashMap<String, Arc<SessionData>>> = LazyLock::new(DashMap::new);
/// Session数据包装 - 极简版本,专注消息传输
pub struct SessionData {
command_tx: mpsc::UnboundedSender<SessionCommand>,
// 🎯 极简优化:直接存储当前连接,无需命令传递
current_sender: Arc<tokio::sync::Mutex<Option<mpsc::Sender<UnifiedSessionMessage>>>>,
current_cancel: Arc<tokio::sync::Mutex<Option<CancellationToken>>>,
}
impl SessionData {
pub fn new(max_size: usize) -> Arc<Self> {
let start_time = std::time::Instant::now();
debug!(
"⏱️ [SessionData::new] Starting creation, max_size={}",
max_size
);
let channel_start = std::time::Instant::now();
let (command_tx, command_rx) = mpsc::unbounded_channel();
debug!(
"⏱️ [SessionData::new] Channel creation took: {:?}",
channel_start.elapsed()
);
let arc_start = std::time::Instant::now();
let session = Arc::new(SessionData {
command_tx,
current_sender: Arc::new(tokio::sync::Mutex::new(None)),
current_cancel: Arc::new(tokio::sync::Mutex::new(None)),
});
debug!(
"⏱️ [SessionData::new] Arc creation took: {:?}",
arc_start.elapsed()
);
let spawn_start = std::time::Instant::now();
SessionWorker::spawn(
max_size,
command_rx,
session.current_sender.clone(),
session.current_cancel.clone(),
);
debug!(
"⏱️ [SessionData::new] SessionWorker::spawn took: {:?}",
spawn_start.elapsed()
);
debug!(
"⏱️ [SessionData::new] Total creation took: {:?}",
start_time.elapsed()
);
session
}
pub async fn message_count(&self) -> usize {
let (tx, rx) = oneshot::channel();
if self
.command_tx
.send(SessionCommand::MessageCount { ack: tx })
.is_err()
{
warn!("Failed to send message_count command; worker has exited");
return 0;
}
rx.await.unwrap_or(0)
}
pub async fn create_new_connection(
&self,
buffer_size: usize,
) -> Result<(mpsc::Receiver<UnifiedSessionMessage>, CancellationToken)> {
let start_time = std::time::Instant::now();
debug!(
"⏱️ [create_new_connection] Starting connection creation, buffer_size={}",
buffer_size
);
let token_start = std::time::Instant::now();
let cancellation_token = CancellationToken::new();
debug!(
"⏱️ [create_new_connection] CancellationToken creation took: {:?}",
token_start.elapsed()
);
let channel_start = std::time::Instant::now();
let (tx, rx) = mpsc::channel(buffer_size);
debug!(
"⏱️ [create_new_connection] mpsc channel creation took: {:?}",
channel_start.elapsed()
);
let setup_start = std::time::Instant::now();
// 🛡️ 关键修复:使用 lock() 而非 try_lock(),确保连接一定被设置
// try_lock() 可能失败导致 current_sender 未设置,造成消息丢失
{
// 取消之前的连接
let mut current_cancel_guard = self.current_cancel.lock().await;
if let Some(token) = current_cancel_guard.take() {
token.cancel();
}
// 设置新的取消令牌
*current_cancel_guard = Some(cancellation_token.clone());
// 设置新的发送器
let mut current_sender_guard = self.current_sender.lock().await;
*current_sender_guard = Some(tx);
}
debug!(
"⏱️ [create_new_connection] Connection state setup took: {:?}",
setup_start.elapsed()
);
debug!(
"⏱️ [create_new_connection] Total connection creation took: {:?}",
start_time.elapsed()
);
Ok((rx, cancellation_token))
}
pub fn push_message(&self, message: UnifiedSessionMessage) {
if self
.command_tx
.send(SessionCommand::Push { message })
.is_err()
{
warn!("Failed to push message; worker has exited");
}
}
/// 主动关闭当前 SSE 连接
///
/// 当用户取消任务时,需要主动关闭 SSE 连接,而不是让客户端一直等待
///
/// 关闭机制:
/// 1. 触发 CancellationToken让 SSE 流立即退出循环
/// 2. 显式关闭 channel 发送端,让 rx.recv() 立即返回 None
/// 3. 清空连接状态,防止新的消息被发送
pub async fn close_current_connection(&self) {
// 🎯 主动触发取消令牌,关闭 SSE 连接
let mut current_cancel_guard = self.current_cancel.lock().await;
if let Some(token) = current_cancel_guard.take() {
info!("🔌 [SessionData] Triggering CancellationToken to close SSE connection");
token.cancel();
}
drop(current_cancel_guard);
// 🎯 显式关闭 channel 发送端,让接收端立即感知到连接关闭
let mut current_sender_guard = self.current_sender.lock().await;
if current_sender_guard.take().is_some() {
info!(
"🔌 [SessionData] Explicitly closed channel sender; receiver disconnects immediately"
);
// 当 Sender 被 drop 时Receiver 的 recv() 会返回 None
// 这里通过 take() 将 sender 从 Option 中移除,触发 drop
}
}
}
struct SessionWorker {
max_size: usize,
command_rx: mpsc::UnboundedReceiver<SessionCommand>,
// 🎯 极简优化:直接共享连接状态,无需命令传递
current_sender: Arc<tokio::sync::Mutex<Option<mpsc::Sender<UnifiedSessionMessage>>>>,
current_cancel: Arc<tokio::sync::Mutex<Option<CancellationToken>>>,
}
impl SessionWorker {
fn spawn(
max_size: usize,
command_rx: mpsc::UnboundedReceiver<SessionCommand>,
current_sender: Arc<tokio::sync::Mutex<Option<mpsc::Sender<UnifiedSessionMessage>>>>,
current_cancel: Arc<tokio::sync::Mutex<Option<CancellationToken>>>,
) {
let start_time = std::time::Instant::now();
debug!(
"⏱️ [SessionWorker::spawn] Starting SessionWorker creation, max_size={}",
max_size
);
let worker = SessionWorker {
max_size,
command_rx,
current_sender,
current_cancel,
};
let spawn_start = std::time::Instant::now();
tokio::spawn(worker.run());
debug!(
"⏱️ [SessionWorker::spawn] tokio::spawn took: {:?}",
spawn_start.elapsed()
);
debug!(
"⏱️ [SessionWorker::spawn] Total spawn took: {:?}",
start_time.elapsed()
);
}
async fn run(mut self) {
let (mut producer, mut consumer) = HeapRb::new(self.max_size).split();
let mut buffered_len = 0usize;
while let Some(cmd) = self.command_rx.recv().await {
match cmd {
SessionCommand::Push { message } => {
let should_buffer = !matches!(
message.message_type,
crate::model::SessionMessageType::Heartbeat
);
if should_buffer {
if producer.is_full() {
let _ = consumer.try_pop();
buffered_len = buffered_len.saturating_sub(1);
}
if producer.try_push(message.clone()).is_ok() {
buffered_len += 1;
} else {
warn!("Ring buffer push failed; real-time delivery only");
}
}
// 🛡️ 关键修复:使用 lock().await 确保消息一定被发送
// try_lock() 可能失败导致消息丢失,造成 SSE 卡死
let mut current_sender_guard = self.current_sender.lock().await;
if let Some(sender) = current_sender_guard.as_mut() {
if sender.try_send(message.clone()).is_err() {
// 如果发送失败,可能是缓冲区满了或连接已关闭
// 注意truncate 在锁内执行,但 50 字符开销可忽略
warn!(
"⚠️ SSE sender send failed, disabling real-time delivery: message_type={:?}, sub_type={}, data={}",
message.message_type,
message.sub_type,
truncate_message_for_log(&message.data, MAX_LOG_TRUNCATE_LEN)
);
*current_sender_guard = None;
}
} else {
// 连接不存在,跳过实时推送(记录为 info 级别,便于排查问题)
info!(
"📭 SSE sender missing, skipping real-time delivery (message buffered in ring buffer): message_type={:?}, sub_type={}, data={}",
message.message_type,
message.sub_type,
truncate_message_for_log(&message.data, MAX_LOG_TRUNCATE_LEN)
);
}
}
SessionCommand::Clear { ack } => {
let mut cleared = 0usize;
while consumer.try_pop().is_some() {
cleared += 1;
}
buffered_len = 0;
let _ = ack.send(cleared);
}
SessionCommand::MessageCount { ack } => {
let _ = ack.send(buffered_len);
}
}
}
debug!("🔚 SessionWorker stopped");
}
}
#[derive(Debug)]
enum SessionCommand {
Push { message: UnifiedSessionMessage },
Clear { ack: oneshot::Sender<usize> },
MessageCount { ack: oneshot::Sender<usize> },
}
/// 便捷函数添加SessionNotify消息自动转换为统一格式
///
/// 如果 SESSION_CACHE 中不存在该 session_id 的条目,会自动创建。
/// 这解决了 Agent 开始推送消息时 SESSION_CACHE 条目尚未由 HTTP 处理器创建的竞态问题。
pub async fn push_session_update(session_id: &str, notify: SessionNotify) -> Result<()> {
use dashmap::mapref::entry::Entry;
// 🛡️ 关键修复:使用 entry API 原子性地获取或创建 SessionData
// 之前直接用 get(),如果 session 不存在就丢弃消息。
// 竞态场景Agent 开始推送消息 → push_session_update 查找 SESSION_CACHE → 不存在(因为
// handle_chat_core 尚未返回computer_chat.rs 还未创建条目)→ 消息被丢弃
let session_data = {
match SESSION_CACHE.entry(session_id.to_string()) {
Entry::Occupied(entry) => entry.get().clone(),
Entry::Vacant(entry) => {
let data = SessionData::new(1000);
info!(
"📦 [push_session_update] SESSION_CACHE auto-created: session_id={}",
session_id
);
entry.insert(data.clone());
data
}
}
};
let unified_message = notify.to_unified_message();
debug!(
"📥 Pushing message to cache: session_id={}, message_type={:?}, sub_type={}",
session_id, unified_message.message_type, unified_message.sub_type
);
session_data.push_message(unified_message);
Ok(())
}
/// 便捷函数添加SessionNotify消息并管理Project-Session映射
///
/// 这个函数会自动确保project_id只对应一个活跃的session_id
///
/// 这个函数会自动确保project_id只对应一个活跃的session_id
/// 当检测到session_id变化时会自动清理旧session的数据
pub async fn push_session_update_with_project(
project_id: &str,
session_id: &str,
notify: SessionNotify,
) -> Result<()> {
// 确保project_id对应正确的session_id如果变化则清理旧数据
let cleared_count = ensure_project_session(project_id, session_id).await;
if cleared_count > 0 {
info!(
"📝 [push_session_update_with_project] Session changed, cleaned {} old messages: project_id={}, new_session_id={}",
cleared_count, project_id, session_id
);
}
// 推送消息到新的session
push_session_update(session_id, notify).await
}
/// 确保project_id对应正确的session_id
///
/// 使用统一的 AGENT_REGISTRY 管理 project-session 映射
/// 如果project_id对应的session_id发生变化会自动清理旧session的数据
/// 如果session_id相同则不做任何操作
///
/// 参数:
/// - project_id: 项目ID
/// - session_id: 当前会话ID
///
/// 返回值: 如果清理了旧数据则返回清理的消息数量否则返回0
pub async fn ensure_project_session(project_id: &str, session_id: &str) -> usize {
// 使用统一 Registry 检查当前映射
let mapped_session_id = AGENT_REGISTRY.get_session_by_project(project_id);
match mapped_session_id {
Some(mapped_sid) if mapped_sid == session_id => {
// session_id 相同,不需要做任何操作
debug!(
"📋 Project session mapping unchanged: project_id={}, session_id={}",
project_id, session_id
);
0
}
Some(old_session_id) => {
// session_id 发生变化,需要清理旧 session 的数据
info!(
"🔄 Detected project session change: project_id={}, old_session_id={}, new_session_id={}",
project_id, old_session_id, session_id
);
// 🛡️ 关键修复:先主动关闭旧 session 的 SSE 连接,再移除缓存
// 之前直接 remove 导致旧 SSE 连接的心跳流继续发送但不再收到业务消息,
// 前端如果没有及时关闭旧连接,会看到孤立的心跳流
let cleared_count = if let Some((_, old_session_data)) =
SESSION_CACHE.remove(&old_session_id)
{
old_session_data.close_current_connection().await;
info!(
"🔌 [ensure_project_session] Closed old session SSE connection: old_session_id={}",
old_session_id
);
1 // 移除了1个session
} else {
0 // session不存在
};
// 更新 AGENT_REGISTRY 中的映射关系
let _ = AGENT_REGISTRY.update_session(project_id, session_id);
if cleared_count > 0 {
info!(
"🧹 Cleared old session data and updated mapping: project_id={}, old_session_id={}, new_session_id={}, cleared_count={}",
project_id, old_session_id, session_id, cleared_count
);
} else {
info!(
"📝 Updated project session mapping: project_id={}, old_session_id={}, new_session_id={}",
project_id, old_session_id, session_id
);
}
cleared_count
}
None => {
// 第一次建立映射关系(无旧映射)
// 注意:此时 AGENT_REGISTRY 中可能还没有这个 project 的记录
// 这种情况下不需要调用 update_session因为 agent 注册时会调用 register
info!(
"🆕 Project session first seen: project_id={}, session_id={}",
project_id, session_id
);
0
}
}
}

View File

@@ -0,0 +1,116 @@
//! SessionNotifier 实现
//!
//! 实现 agent_abstraction 定义的 SessionNotifier trait
//! 用于推送 SSE 消息到前端。
use agent_abstraction::SessionNotifier;
use async_trait::async_trait;
use shared_types::{
AgentSessionUpdate, SessionNotify, SessionPromptEnd, SessionPromptError, SessionPromptStart,
};
use super::push_session_update_with_project;
/// SSE 消息推送器
///
/// 实现 SessionNotifier trait将会话消息推送到 SSE 连接。
#[derive(Debug, Clone, Default)]
pub struct SseSessionNotifier;
impl SseSessionNotifier {
/// 创建新的 SSE 消息推送器
pub fn new() -> Self {
Self
}
}
/// 将 anyhow::Error 转换为 Box<dyn std::error::Error + Send + Sync>
fn convert_error(e: anyhow::Error) -> Box<dyn std::error::Error + Send + Sync> {
Box::new(std::io::Error::other(e.to_string()))
}
#[async_trait]
impl SessionNotifier for SseSessionNotifier {
async fn notify_prompt_start(
&self,
project_id: &str,
session_id: &str,
request_id: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let notify = SessionNotify::SessionPromptStart(SessionPromptStart {
session_id: session_id.to_string(),
request_id,
});
push_session_update_with_project(project_id, session_id, notify)
.await
.map_err(convert_error)
}
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>> {
let notify = SessionNotify::SessionPromptEnd(SessionPromptEnd {
session_id: session_id.to_string(),
stop_reason,
error_message,
request_id,
});
push_session_update_with_project(project_id, session_id, notify)
.await
.map_err(convert_error)
}
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>> {
let notify = SessionNotify::SessionPromptError(SessionPromptError {
session_id: session_id.to_string(),
error,
request_id,
});
push_session_update_with_project(project_id, session_id, notify)
.await
.map_err(convert_error)
}
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>> {
let notify = SessionNotify::AgentSessionUpdate(Box::new(AgentSessionUpdate {
session_id: session_id.to_string(),
session_update,
request_id,
}));
push_session_update_with_project(project_id, session_id, notify)
.await
.map_err(convert_error)
}
async fn notify(
&self,
project_id: &str,
session_id: &str,
notify: SessionNotify,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
push_session_update_with_project(project_id, session_id, notify)
.await
.map_err(convert_error)
}
}

View File

@@ -0,0 +1,207 @@
//! StateAwareNotifier 实现
//!
//! 状态感知的 SessionNotifier 包装器,在推送 SSE 消息的同时同步更新 Agent 状态。
//!
//! ## 核心职责
//! 1. 委托给 SseSessionNotifier 推送 SSE 消息
//! 2. 同步更新 AGENT_REGISTRY 状态
//! 3. 保持状态转换的原子性和一致性
use agent_abstraction::SessionNotifier;
use async_trait::async_trait;
use std::sync::Arc;
use tracing::{debug, error, info};
use super::AGENT_REGISTRY;
use super::session_notifier::SseSessionNotifier;
use shared_types::{AgentStatus, SessionNotify};
/// 状态感知的 SessionNotifier 包装器
///
/// 通过委托模式包装 SseSessionNotifier在推送 SSE 消息的同时同步更新
/// AGENT_REGISTRY 中的 Agent 状态。
///
/// # 设计特点
/// - **委托模式**:所有 SSE 推送操作委托给内部的 SseSessionNotifier
/// - **原子性状态更新**:使用 AGENT_REGISTRY 确保状态更新的原子性
/// - **状态同步顺序**
/// - PromptStart: 先更新状态为 Active再推送 SSE
/// - PromptEnd: 先推送 SSE再恢复状态为 Idle
/// - PromptError: 先推送错误消息,再恢复状态为 Idle
#[derive(Debug, Clone)]
pub struct StateAwareNotifier {
/// 内部 SSE 推送器
inner: Arc<SseSessionNotifier>,
}
impl StateAwareNotifier {
/// 创建新的 StateAwareNotifier 实例
pub fn new() -> Self {
Self {
inner: Arc::new(SseSessionNotifier::new()),
}
}
/// 🔥 P1 修复: 更新 Agent 状态(原子操作)
///
/// 使用 `try_update_agent_info` 方法实现原子性状态更新,
/// 避免 TOCTOU 竞态条件:读锁释放 → 时间窗口 → 写锁更新。
///
/// # 参数
/// - `project_id`: 项目 ID
/// - `status`: 新的 Agent 状态
fn update_agent_status(&self, project_id: &str, status: AgentStatus) {
AGENT_REGISTRY.try_update_agent_info(project_id, |info| {
let old_status = info.status;
if old_status != status {
info.status = status;
info.last_activity = chrono::Utc::now();
debug!(
"🔄 [atomic_status] Project[{}] status: {:?} -> {:?}",
project_id, old_status, status
);
true
} else {
false
}
});
}
}
impl Default for StateAwareNotifier {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl SessionNotifier for StateAwareNotifier {
/// 推送会话开始通知
///
/// 顺序:
/// 1. 更新 Agent 状态为 Active
/// 2. 推送 SessionPromptStart 到 SSE
async fn notify_prompt_start(
&self,
project_id: &str,
session_id: &str,
request_id: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
info!(
"📨 Project[{}] sending SessionPromptStart notification, session_id={}, request_id={:?}",
project_id, session_id, request_id
);
// 1. 更新状态为 Active在推送 SSE 之前)
self.update_agent_status(project_id, AgentStatus::Active);
// 2. 推送 SSE 消息
self.inner
.notify_prompt_start(project_id, session_id, request_id)
.await
}
/// 推送会话结束通知
///
/// 顺序:
/// 1. 推送 SessionPromptEnd 到 SSE
/// 2. 恢复 Agent 状态为 Idle
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>> {
info!(
"✅ Project[{}] sending SessionPromptEnd notification, session_id={}, stop_reason={:?}, request_id={:?}",
project_id, session_id, stop_reason, request_id
);
// 1. 推送 SSE 消息(在恢复状态之前)
let result = self
.inner
.notify_prompt_end(
project_id,
session_id,
stop_reason,
error_message,
request_id,
)
.await;
// 2. 恢复状态为 Idle
self.update_agent_status(project_id, AgentStatus::Idle);
result
}
/// 推送会话错误通知
///
/// 顺序:
/// 1. 推送 SessionPromptError 到 SSE
/// 2. 恢复 Agent 状态为 Idle
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>> {
error!(
"❌ Project[{}] sending SessionPromptError notification, session_id={}, error_code={}, error_message={}, request_id={:?}",
project_id, session_id, error.code, error.message, request_id
);
// 1. 推送错误消息(在恢复状态之前)
let result = self
.inner
.notify_prompt_error(project_id, session_id, error, request_id)
.await;
// 2. 恢复状态为 Idle
self.update_agent_status(project_id, AgentStatus::Idle);
result
}
/// 推送 Agent 会话更新通知
///
/// 不更新 Agent 状态,仅推送 SSE 消息。
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>> {
debug!(
"🔄 Project[{}] sending SessionUpdate notification, session_id={}",
project_id, session_id
);
// 委托给内部 notifier不更新状态
self.inner
.notify_session_update(project_id, session_id, session_update, request_id)
.await
}
/// 推送通用会话通知
///
/// 不更新 Agent 状态,仅推送 SSE 消息。
async fn notify(
&self,
project_id: &str,
session_id: &str,
notify: SessionNotify,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
debug!(
"📢 Project[{}] sending generic session notification, session_id={}",
project_id, session_id
);
// 委托给内部 notifier
self.inner.notify(project_id, session_id, notify).await
}
}

View File

@@ -0,0 +1,126 @@
//! 测试用阻塞注入
//!
//! 用于模拟极端场景 (Agent 阻塞、超时等)
use std::sync::Arc;
use std::time::Duration;
/// 阻塞配置
#[derive(Debug, Clone)]
pub struct BlockingConfig {
/// 是否阻塞 new_session
pub block_new_session: bool,
/// 是否阻塞 prompt
pub block_prompt: bool,
/// 阻塞持续时间
pub block_duration: Duration,
}
impl Default for BlockingConfig {
fn default() -> Self {
Self {
block_new_session: false,
block_prompt: false,
block_duration: Duration::from_secs(30),
}
}
}
/// 全局阻塞配置 (线程安全)
pub static BLOCKING_CONFIG: std::sync::LazyLock<Arc<std::sync::RwLock<BlockingConfig>>> =
std::sync::LazyLock::new(|| Arc::new(std::sync::RwLock::new(BlockingConfig::default())));
/// 注入阻塞 (用于测试)
///
/// # Example
///
/// ```rust
/// use agent_runner::testing::blocking::{BlockingConfig, inject_blocking};
/// use std::time::Duration;
///
/// // 注入 30 秒阻塞
/// inject_blocking(BlockingConfig {
/// block_prompt: true,
/// block_duration: Duration::from_secs(30),
/// ..Default::default()
/// });
/// ```
pub fn inject_blocking(config: BlockingConfig) {
tracing::warn!("🧪 [TEST] Blocking config updated: {:?}", config);
let mut global = BLOCKING_CONFIG.write().unwrap();
*global = config;
}
/// 检查并执行阻塞 (在 agent_worker_with_heartbeat 中调用)
///
/// # Arguments
///
/// * `blocking_type` - 阻塞类型: "new_session" 或 "prompt"
///
/// # Example
///
/// ```rust
/// # use agent_runner::testing::blocking::maybe_block;
/// # async fn test() {
/// // 在 agent_worker_with_heartbeat 中调用
/// maybe_block("prompt").await;
/// # }
/// ```
pub async fn maybe_block(blocking_type: &str) {
let config = BLOCKING_CONFIG.read().unwrap();
let should_block = match blocking_type {
"new_session" => config.block_new_session,
"prompt" => config.block_prompt,
_ => false,
};
if should_block {
tracing::warn!(
"🧪 [TEST] Injected blocking: {}, duration: {:?}",
blocking_type,
config.block_duration
);
tokio::time::sleep(config.block_duration).await;
}
}
/// 重置阻塞配置 (用于测试清理)
pub fn reset_blocking() {
inject_blocking(BlockingConfig::default());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blocking_config_default() {
let config = BlockingConfig::default();
assert!(!config.block_new_session);
assert!(!config.block_prompt);
assert_eq!(config.block_duration, Duration::from_secs(30));
}
#[test]
fn test_inject_and_reset_blocking() {
// 注入配置
inject_blocking(BlockingConfig {
block_prompt: true,
block_duration: Duration::from_secs(10),
..Default::default()
});
let config = BLOCKING_CONFIG.read().unwrap();
assert!(config.block_prompt);
assert_eq!(config.block_duration, Duration::from_secs(10));
// 重置配置
drop(config);
reset_blocking();
let config = BLOCKING_CONFIG.read().unwrap();
assert!(!config.block_prompt);
assert_eq!(config.block_duration, Duration::from_secs(30));
}
}

View File

@@ -0,0 +1,226 @@
//! 测试 Fixtures
//!
//! 提供测试用的构造器和辅助工具
use crate::proxy_agent::AgentRequest;
use agent_abstraction::PromptMessage;
use shared_types::{Attachment, ServiceType};
use std::path::PathBuf;
/// 测试请求构造器
///
/// # Example
///
/// ```rust
/// use agent_runner::testing::fixtures::TestRequestBuilder;
///
/// // 创建基础测试请求
/// let (req, resp_rx) = TestRequestBuilder::new()
/// .project_id("test-project")
/// .content("Hello, Agent!")
/// .build();
/// ```
pub struct TestRequestBuilder {
project_id: String,
content: String,
request_id: String,
attachments: Vec<Attachment>,
session_id: Option<String>,
project_path: Option<PathBuf>,
}
impl Default for TestRequestBuilder {
fn default() -> Self {
Self::new()
}
}
impl TestRequestBuilder {
/// 创建新的测试请求构造器
pub fn new() -> Self {
Self {
project_id: "test-project".to_string(),
content: "Hello, Agent!".to_string(),
request_id: uuid::Uuid::new_v4().to_string().replace("-", ""),
attachments: vec![],
session_id: None,
project_path: None,
}
}
/// 设置 project_id
pub fn project_id(mut self, id: &str) -> Self {
self.project_id = id.to_string();
self
}
/// 设置请求内容
pub fn content(mut self, content: &str) -> Self {
self.content = content.to_string();
self
}
/// 设置 request_id
pub fn request_id(mut self, request_id: &str) -> Self {
self.request_id = request_id.to_string();
self
}
/// 设置 session_id
pub fn session_id(mut self, session_id: &str) -> Self {
self.session_id = Some(session_id.to_string());
self
}
/// 设置项目路径
pub fn project_path(mut self, path: &str) -> Self {
self.project_path = Some(PathBuf::from(path));
self
}
/// 添加附件
pub fn add_attachment(mut self, attachment: Attachment) -> Self {
self.attachments.push(attachment);
self
}
/// 构建 PromptMessage用于测试验证
///
/// 返回构建的 PromptMessage可以直接访问字段进行断言验证
pub fn build_prompt_message(&self) -> PromptMessage {
PromptMessage {
content: self.content.clone(),
project_id: self.project_id.clone(),
project_path: self
.project_path
.clone()
.unwrap_or_else(|| PathBuf::from("/tmp/test")),
session_id: self.session_id.clone(),
request_id: self.request_id.clone(),
attachments: self.attachments.clone(),
data_source_attachments: vec![],
service_type: ServiceType::RCoder,
system_prompt_override: None,
user_prompt_template_override: None,
agent_config_override: None,
}
}
/// 构建测试请求
pub fn build(
self,
) -> (
AgentRequest,
tokio::sync::oneshot::Receiver<crate::model::ChatPromptResponse>,
) {
let prompt_message = self.build_prompt_message();
AgentRequest::new(prompt_message, None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_test_request_builder_default() {
let builder = TestRequestBuilder::new();
let prompt = builder.build_prompt_message();
// 验证默认值
assert_eq!(prompt.project_id, "test-project");
assert_eq!(prompt.content, "Hello, Agent!");
assert!(!prompt.request_id.is_empty(), "request_id 应该自动生成");
assert_eq!(prompt.project_path, PathBuf::from("/tmp/test"));
assert!(prompt.session_id.is_none());
assert!(prompt.attachments.is_empty());
assert_eq!(prompt.service_type, ServiceType::RCoder);
}
#[test]
fn test_test_request_builder_with_project_id() {
let prompt = TestRequestBuilder::new()
.project_id("custom-project")
.build_prompt_message();
assert_eq!(prompt.project_id, "custom-project");
}
#[test]
fn test_test_request_builder_with_content() {
let prompt = TestRequestBuilder::new()
.content("Custom content")
.build_prompt_message();
assert_eq!(prompt.content, "Custom content");
}
#[test]
fn test_test_request_builder_with_session_id() {
let prompt = TestRequestBuilder::new()
.session_id("test-session-123")
.build_prompt_message();
assert_eq!(prompt.session_id, Some("test-session-123".to_string()));
}
#[test]
fn test_test_request_builder_with_request_id() {
let prompt = TestRequestBuilder::new()
.request_id("custom-request-id")
.build_prompt_message();
assert_eq!(prompt.request_id, "custom-request-id");
}
#[test]
fn test_test_request_builder_with_project_path() {
let prompt = TestRequestBuilder::new()
.project_path("/home/user/project")
.build_prompt_message();
assert_eq!(prompt.project_path, PathBuf::from("/home/user/project"));
}
#[test]
fn test_test_request_builder_chain() {
// 验证链式调用和所有字段设置
let prompt = TestRequestBuilder::new()
.project_id("my-project")
.content("Test content")
.request_id("custom-request-id")
.session_id("session-456")
.project_path("/home/user/project")
.build_prompt_message();
assert_eq!(prompt.project_id, "my-project");
assert_eq!(prompt.content, "Test content");
assert_eq!(prompt.request_id, "custom-request-id");
assert_eq!(prompt.session_id, Some("session-456".to_string()));
assert_eq!(prompt.project_path, PathBuf::from("/home/user/project"));
}
#[test]
fn test_test_request_builder_build_returns_receiver() {
// 验证 build() 返回的接收器可用
let (_req, mut rx) = TestRequestBuilder::new().build();
// 验证接收器尚未收到消息
assert!(rx.try_recv().is_err(), "接收器应该为空");
}
#[test]
fn test_test_request_builder_request_id_auto_generated() {
// 验证每次构建生成不同的 request_id
let builder1 = TestRequestBuilder::new();
let builder2 = TestRequestBuilder::new();
let prompt1 = builder1.build_prompt_message();
let prompt2 = builder2.build_prompt_message();
assert_ne!(
prompt1.request_id, prompt2.request_id,
"每次构建应该生成不同的 request_id"
);
}
}

View File

@@ -0,0 +1,14 @@
//! 测试辅助模块
//!
//! 仅在 `testing` feature 启用时编译
// 阻塞注入模块 (用于极端场景测试)
#[cfg(feature = "test-blocking")]
pub mod blocking;
#[cfg(feature = "test-blocking")]
pub use blocking::{BlockingConfig, inject_blocking};
// 测试 Fixtures (通用测试辅助工具)
pub mod fixtures;
pub use fixtures::TestRequestBuilder;

View File

@@ -0,0 +1,428 @@
//! Content Builder 工具
//!
//! 将 Attachment 转换为 ACP 协议的 ContentBlock
use anyhow::Result;
use base64::{Engine as _, engine::general_purpose};
use agent_client_protocol::schema::{
AudioContent, BlobResourceContents, ContentBlock, EmbeddedResource, EmbeddedResourceResource,
ImageContent, ResourceLink, TextContent, TextResourceContents,
};
use std::path::Path;
use crate::model::{Attachment, AttachmentSource};
/// Content Builder - 将附件转换为 ACP ContentBlock
pub struct ContentBuilder;
impl ContentBuilder {
/// 将单个附件转换为 ContentBlock
pub async fn attachment_to_content_block(
attachment: &Attachment,
project_path: &std::path::Path,
) -> Result<Option<ContentBlock>> {
match attachment {
Attachment::Text(text_attachment) => {
Self::text_to_content_block(text_attachment, project_path).await
}
Attachment::Image(image_attachment) => {
Self::image_to_content_block(image_attachment, project_path).await
}
Attachment::Audio(audio_attachment) => {
Self::audio_to_content_block(audio_attachment, project_path).await
}
Attachment::Document(document_attachment) => {
Self::document_to_content_block(document_attachment, project_path).await
}
}
}
/// 将多个附件转换为 ContentBlock 列表
/// 文件不存在或无法读取的附件会被静默忽略
pub async fn attachments_to_content_blocks(
attachments: &[Attachment],
project_path: &std::path::Path,
) -> Result<Vec<ContentBlock>> {
let mut content_blocks = Vec::new();
for attachment in attachments {
match Self::attachment_to_content_block(attachment, project_path).await {
Ok(Some(content_block)) => {
content_blocks.push(content_block);
}
Ok(None) => {
// 文件不存在或无法读取,静默忽略
tracing::warn!(
"Attachment could not be loaded and was ignored: attachment_id={}",
attachment.id()
);
}
Err(e) => {
// 其他错误(如网络错误),记录警告但继续处理
tracing::warn!(
"⚠️ Attachment conversion failed and was ignored: attachment_id={}, error={:?}",
attachment.id(),
e
);
}
}
}
Ok(content_blocks)
}
/// 文本附件转换为 ContentBlock
/// 如果文件不存在或是压缩文件,返回 Ok(None)
async fn text_to_content_block(
text_attachment: &crate::model::TextAttachment,
project_path: &std::path::Path,
) -> Result<Option<ContentBlock>> {
let text_content = match &text_attachment.source {
AttachmentSource::FilePath { path } => {
let file_path = project_path.join(path);
// 检查文件是否存在
if !file_path.exists() {
tracing::warn!("Attachment file not found, ignored: {:?}", file_path);
return Ok(None);
}
// 检查是否为二进制文件(常见压缩格式)
if let Some(ext) = file_path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
if matches!(
ext_str.as_str(),
"gz" | "zip" | "tar" | "bz2" | "xz" | "7z" | "rar"
) {
tracing::warn!(
"⚠️ Compressed files are not supported as Text attachments, ignored: {:?} (extension: {})",
file_path,
ext_str
);
return Ok(None);
}
}
// 尝试读取文本文件
let text = match tokio::fs::read_to_string(&file_path).await {
Ok(text) => text,
Err(e) => {
tracing::warn!(
"⚠️ Failed to read text file, ignored: {:?}, error: {} (may be binary or not UTF-8 encoded)",
file_path,
e
);
return Ok(None);
}
};
TextContent::new(text)
}
AttachmentSource::Base64 { data, mime_type: _ } => {
let text = String::from_utf8(general_purpose::STANDARD.decode(data)?)?;
TextContent::new(text)
}
AttachmentSource::Url { url } => {
let client = reqwest::Client::new();
let response = client.get(url).send().await?;
let text = response.text().await?;
TextContent::new(text)
}
};
Ok(Some(ContentBlock::Text(text_content)))
}
/// 图像附件转换为 ContentBlock
/// 如果文件不存在,返回 Ok(None)
async fn image_to_content_block(
image_attachment: &crate::model::ImageAttachment,
project_path: &std::path::Path,
) -> Result<Option<ContentBlock>> {
let (data, uri) = match &image_attachment.source {
AttachmentSource::FilePath { path } => {
let file_path = project_path.join(path);
// 检查文件是否存在
if !file_path.exists() {
tracing::warn!("Image file not found, ignored: {:?}", file_path);
return Ok(None);
}
// 尝试读取文件
let data = match tokio::fs::read(&file_path).await {
Ok(data) => data,
Err(e) => {
tracing::warn!(
"Failed to read image file, ignored: {:?}, error: {}",
file_path,
e
);
return Ok(None);
}
};
let base64_data = general_purpose::STANDARD.encode(data);
let uri = file_path.to_string_lossy().to_string();
(base64_data, Some(uri))
}
AttachmentSource::Base64 { data, .. } => (data.clone(), None),
AttachmentSource::Url { url } => {
let client = reqwest::Client::new();
let response = client.get(url).send().await?;
let data = response.bytes().await?;
let base64_data = general_purpose::STANDARD.encode(data);
(base64_data, Some(url.clone()))
}
};
let mut image_content = ImageContent::new(data, image_attachment.mime_type.clone());
if let Some(u) = uri {
image_content = image_content.uri(u);
}
Ok(Some(ContentBlock::Image(image_content)))
}
/// 音频附件转换为 ContentBlock
/// 如果文件不存在,返回 Ok(None)
async fn audio_to_content_block(
audio_attachment: &crate::model::AudioAttachment,
project_path: &std::path::Path,
) -> Result<Option<ContentBlock>> {
let data = match &audio_attachment.source {
AttachmentSource::FilePath { path } => {
let file_path = project_path.join(path);
// 检查文件是否存在
if !file_path.exists() {
tracing::warn!("Audio file not found, ignored: {:?}", file_path);
return Ok(None);
}
// 尝试读取文件
let data = match tokio::fs::read(&file_path).await {
Ok(data) => data,
Err(e) => {
tracing::warn!(
"Failed to read audio file, ignored: {:?}, error: {}",
file_path,
e
);
return Ok(None);
}
};
general_purpose::STANDARD.encode(data)
}
AttachmentSource::Base64 { data, .. } => data.clone(),
AttachmentSource::Url { url } => {
let client = reqwest::Client::new();
let response = client.get(url).send().await?;
let data = response.bytes().await?;
general_purpose::STANDARD.encode(data)
}
};
Ok(Some(ContentBlock::Audio(AudioContent::new(
data,
audio_attachment.mime_type.clone(),
))))
}
/// 文档附件转换为 ContentBlock
/// 如果文件不存在,返回 Ok(None)
async fn document_to_content_block(
document_attachment: &crate::model::DocumentAttachment,
project_path: &std::path::Path,
) -> Result<Option<ContentBlock>> {
match &document_attachment.source {
AttachmentSource::FilePath { path } => {
let file_path = project_path.join(path);
// 检查文件是否存在
if !file_path.exists() {
tracing::warn!("Document file not found, ignored: {:?}", file_path);
return Ok(None);
}
let uri = file_path.to_string_lossy().to_string();
// 尝试读取为文本,如果失败则作为二进制处理
if document_attachment.mime_type.starts_with("text/") {
match tokio::fs::read_to_string(&file_path).await {
Ok(text) => {
let mut text_contents = TextResourceContents::new(text, uri);
if let Some(mime_type) = Some(document_attachment.mime_type.clone()) {
text_contents = text_contents.mime_type(mime_type);
}
Ok(Some(ContentBlock::Resource(EmbeddedResource::new(
EmbeddedResourceResource::TextResourceContents(text_contents),
))))
}
Err(_) => {
// 文本读取失败,尝试作为二进制处理
Self::handle_binary_document(
&file_path,
&document_attachment.mime_type,
&uri,
)
.await
}
}
} else {
Self::handle_binary_document(&file_path, &document_attachment.mime_type, &uri)
.await
}
}
AttachmentSource::Base64 { data, mime_type } => {
let uri = format!("base64://{}", document_attachment.id);
if mime_type.starts_with("text/") {
match String::from_utf8(general_purpose::STANDARD.decode(data)?) {
Ok(text) => {
let mut text_contents = TextResourceContents::new(text, uri);
text_contents = text_contents.mime_type(mime_type.clone());
Ok(Some(ContentBlock::Resource(EmbeddedResource::new(
EmbeddedResourceResource::TextResourceContents(text_contents),
))))
}
Err(_) => {
// 文本解码失败,作为二进制处理
let mut blob_contents = BlobResourceContents::new(data.clone(), uri);
blob_contents = blob_contents.mime_type(mime_type.clone());
Ok(Some(ContentBlock::Resource(EmbeddedResource::new(
EmbeddedResourceResource::BlobResourceContents(blob_contents),
))))
}
}
} else {
let mut blob_contents = BlobResourceContents::new(data.clone(), uri);
blob_contents = blob_contents.mime_type(mime_type.clone());
Ok(Some(ContentBlock::Resource(EmbeddedResource::new(
EmbeddedResourceResource::BlobResourceContents(blob_contents),
))))
}
}
AttachmentSource::Url { url } => {
// URL 资源作为 ResourceLink 处理
let name = document_attachment
.filename
.clone()
.unwrap_or_else(|| "document".to_string());
let mut resource_link = ResourceLink::new(name, url.clone());
resource_link = resource_link.mime_type(document_attachment.mime_type.clone());
// 只有当值存在时才设置
if let Some(description) = &document_attachment.description {
resource_link = resource_link.description(description.clone());
}
if let Some(size) = document_attachment.size {
resource_link = resource_link.size(size as i64);
}
if let Some(filename) = &document_attachment.filename {
resource_link = resource_link.title(filename.clone());
}
Ok(Some(ContentBlock::ResourceLink(resource_link)))
}
}
}
/// 处理二进制文档
/// 如果文件无法读取,返回 Ok(None)
async fn handle_binary_document(
file_path: &Path,
mime_type: &str,
uri: &str,
) -> Result<Option<ContentBlock>> {
let data = match tokio::fs::read(file_path).await {
Ok(data) => data,
Err(e) => {
tracing::warn!(
"⚠️ Cannot read binary document, ignored: {:?}, error: {}",
file_path,
e
);
return Ok(None);
}
};
let blob = general_purpose::STANDARD.encode(data);
let mut blob_contents = BlobResourceContents::new(blob, uri.to_string());
blob_contents = blob_contents.mime_type(mime_type.to_string());
Ok(Some(ContentBlock::Resource(EmbeddedResource::new(
EmbeddedResourceResource::BlobResourceContents(blob_contents),
))))
}
/// 从文件扩展名推断 MIME 类型
pub fn infer_mime_type_from_extension(filename: &str) -> &'static str {
let path = std::path::Path::new(filename);
match path.extension().and_then(|ext| ext.to_str()) {
Some("txt") => "text/plain",
Some("md") => "text/markdown",
Some("json") => "application/json",
Some("xml") => "application/xml",
Some("html") => "text/html",
Some("css") => "text/css",
Some("js") => "application/javascript",
Some("ts") => "application/typescript",
Some("pdf") => "application/pdf",
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("png") => "image/png",
Some("gif") => "image/gif",
Some("svg") => "image/svg+xml",
Some("webp") => "image/webp",
Some("mp3") => "audio/mpeg",
Some("wav") => "audio/wav",
Some("ogg") => "audio/ogg",
Some("m4a") => "audio/mp4",
Some("mp4") => "video/mp4",
Some("webm") => "video/webm",
Some("zip") => "application/zip",
Some("tar") => "application/x-tar",
Some("gz") => "application/gzip",
Some("rar") => "application/x-rar-compressed",
Some("7z") => "application/x-7z-compressed",
Some("doc") => "application/msword",
Some("docx") => {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
}
Some("xls") => "application/vnd.ms-excel",
Some("xlsx") => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
Some("ppt") => "application/vnd.ms-powerpoint",
Some("pptx") => {
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
}
_ => "application/octet-stream",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_infer_mime_type() {
assert_eq!(
ContentBuilder::infer_mime_type_from_extension("test.txt"),
"text/plain"
);
assert_eq!(
ContentBuilder::infer_mime_type_from_extension("image.jpg"),
"image/jpeg"
);
assert_eq!(
ContentBuilder::infer_mime_type_from_extension("audio.mp3"),
"audio/mpeg"
);
assert_eq!(
ContentBuilder::infer_mime_type_from_extension("document.pdf"),
"application/pdf"
);
assert_eq!(
ContentBuilder::infer_mime_type_from_extension("unknown.xyz"),
"application/octet-stream"
);
}
}

View File

@@ -0,0 +1,370 @@
//! 文件处理工具函数
//!
//! 提供文件读取、验证、转换等实用功能
use anyhow::{Context, Result};
use base64::{Engine as _, engine::general_purpose};
use std::path::{Path, PathBuf};
use tokio::fs;
use tracing::warn;
use super::ContentBuilder;
use shared_types::{Attachment, AttachmentError, AttachmentSource};
/// 文件处理配置
pub struct FileConfig {
/// 最大文件大小(字节)
pub max_file_size: u64,
/// 允许的文件扩展名
pub allowed_extensions: Vec<String>,
/// 临时文件目录
pub temp_dir: PathBuf,
}
impl Default for FileConfig {
fn default() -> Self {
Self {
max_file_size: 50 * 1024 * 1024, // 50MB
allowed_extensions: vec![
"txt", "md", "json", "xml", "html", "css", "js", "ts", // 文本文件
"jpg", "jpeg", "png", "gif", "svg", "webp", // 图像文件
"mp3", "wav", "ogg", "m4a", // 音频文件
"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", // 文档文件
"zip", "tar", "gz", "rar", "7z", // 压缩文件
]
.iter()
.map(|s| s.to_string())
.collect(),
temp_dir: std::env::temp_dir(),
}
}
}
/// 文件处理工具
pub struct FileUtils {
config: FileConfig,
}
impl FileUtils {
/// 创建新的文件处理工具实例
pub fn new() -> Self {
Self {
config: FileConfig::default(),
}
}
/// 使用自定义配置创建文件处理工具
pub fn with_config(config: FileConfig) -> Self {
Self { config }
}
/// 验证文件扩展名是否允许
pub fn validate_extension(&self, filename: &str) -> Result<()> {
let path = Path::new(filename);
let extension = path
.extension()
.and_then(|ext| ext.to_str())
.ok_or_else(|| anyhow::anyhow!("File has no extension"))?;
if !self
.config
.allowed_extensions
.contains(&extension.to_lowercase())
{
return Err(anyhow::anyhow!("Unsupported file extension: {}", extension));
}
Ok(())
}
/// 验证文件大小
pub async fn validate_file_size(&self, file_path: &Path) -> Result<()> {
let metadata = fs::metadata(file_path)
.await
.context("unable to get file metadata")?;
if metadata.len() > self.config.max_file_size {
return Err(AttachmentError::FileSizeExceeded(metadata.len()).into());
}
Ok(())
}
/// 读取文件内容为 base64
pub async fn read_file_as_base64(&self, file_path: &Path) -> Result<String> {
self.validate_file_size(file_path).await?;
let content = fs::read(file_path).await.context("Failed to read file")?;
Ok(general_purpose::STANDARD.encode(content))
}
/// 读取文本文件
pub async fn read_text_file(&self, file_path: &Path) -> Result<String> {
self.validate_file_size(file_path).await?;
let content = fs::read_to_string(file_path)
.await
.context("Failed to read text file")?;
Ok(content)
}
/// 从文件路径创建附件
pub async fn create_attachment_from_file(
&self,
file_path: &Path,
project_path: &Path,
description: Option<String>,
) -> Result<Attachment> {
// 验证文件扩展名
let filename = file_path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| anyhow::anyhow!("Invalid filename"))?;
self.validate_extension(filename)?;
// 获取相对于项目路径的路径
let relative_path = file_path
.strip_prefix(project_path)
.unwrap_or(file_path)
.to_string_lossy()
.to_string();
// 获取 MIME 类型
let mime_type = ContentBuilder::infer_mime_type_from_extension(filename);
// 根据文件类型创建相应的附件
let attachment = if mime_type.starts_with("image/") {
Attachment::new_image(
AttachmentSource::FilePath {
path: relative_path,
},
mime_type.to_string(),
)
} else if mime_type.starts_with("audio/") {
Attachment::new_audio(
AttachmentSource::FilePath {
path: relative_path,
},
mime_type.to_string(),
)
} else if mime_type.starts_with("text/") || mime_type == "application/json" {
let mut attachment = Attachment::new_text(AttachmentSource::FilePath {
path: relative_path,
});
if let Attachment::Text(ref mut text_attachment) = attachment {
text_attachment.description = description;
}
attachment
} else {
Attachment::new_document(
AttachmentSource::FilePath {
path: relative_path,
},
mime_type.to_string(),
)
};
Ok(attachment)
}
/// 从 base64 数据创建附件
pub fn create_attachment_from_base64(
&self,
data: String,
mime_type: String,
filename: Option<String>,
description: Option<String>,
) -> Result<Attachment> {
// 验证数据大小
let decoded_size = general_purpose::STANDARD.decode(&data)?.len() as u64;
if decoded_size > self.config.max_file_size {
return Err(AttachmentError::FileSizeExceeded(decoded_size).into());
}
let source = AttachmentSource::Base64 {
data,
mime_type: mime_type.clone(),
};
let attachment = if mime_type.starts_with("image/") {
let mut attachment = Attachment::new_image(source, mime_type);
if let Attachment::Image(ref mut image_attachment) = attachment {
image_attachment.filename = filename;
image_attachment.description = description;
}
attachment
} else if mime_type.starts_with("audio/") {
let mut attachment = Attachment::new_audio(source, mime_type);
if let Attachment::Audio(ref mut audio_attachment) = attachment {
audio_attachment.filename = filename;
audio_attachment.description = description;
}
attachment
} else if mime_type.starts_with("text/") || mime_type == "application/json" {
let mut attachment = Attachment::new_text(source);
if let Attachment::Text(ref mut text_attachment) = attachment {
text_attachment.filename = filename;
text_attachment.description = description;
}
attachment
} else {
let mut attachment = Attachment::new_document(source, mime_type);
if let Attachment::Document(ref mut doc_attachment) = attachment {
doc_attachment.filename = filename;
doc_attachment.description = description;
doc_attachment.size = Some(decoded_size);
}
attachment
};
Ok(attachment)
}
/// 从 URL 创建附件
pub async fn create_attachment_from_url(
&self,
url: &str,
filename: Option<String>,
description: Option<String>,
) -> Result<Attachment> {
// 获取 URL 头信息以确定文件类型和大小
let client = reqwest::Client::new();
let response = client.head(url).send().await?;
// 检查文件大小
if let Some(content_length) = response.headers().get(reqwest::header::CONTENT_LENGTH) {
let length: u64 = content_length.to_str()?.parse()?;
if length > self.config.max_file_size {
return Err(AttachmentError::FileSizeExceeded(length).into());
}
}
// 获取 MIME 类型
let mime_type = response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or("application/octet-stream");
let source = AttachmentSource::Url {
url: url.to_string(),
};
let attachment = if mime_type.starts_with("image/") {
let mut attachment = Attachment::new_image(source, mime_type.to_string());
if let Attachment::Image(ref mut image_attachment) = attachment {
image_attachment.filename = filename;
image_attachment.description = description;
}
attachment
} else if mime_type.starts_with("audio/") {
let mut attachment = Attachment::new_audio(source, mime_type.to_string());
if let Attachment::Audio(ref mut audio_attachment) = attachment {
audio_attachment.filename = filename;
audio_attachment.description = description;
}
attachment
} else if mime_type.starts_with("text/") || mime_type == "application/json" {
let mut attachment = Attachment::new_text(source);
if let Attachment::Text(ref mut text_attachment) = attachment {
text_attachment.filename = filename;
text_attachment.description = description;
}
attachment
} else {
let mut attachment = Attachment::new_document(source, mime_type.to_string());
if let Attachment::Document(ref mut doc_attachment) = attachment {
doc_attachment.filename = filename;
doc_attachment.description = description;
}
attachment
};
Ok(attachment)
}
/// 批量处理文件创建附件
pub async fn create_attachments_from_files(
&self,
file_paths: &[PathBuf],
project_path: &Path,
) -> Result<Vec<Attachment>> {
let mut attachments = Vec::new();
for file_path in file_paths {
match self
.create_attachment_from_file(file_path, project_path, None)
.await
{
Ok(attachment) => attachments.push(attachment),
Err(e) => {
warn!(
"Skipping file {:?}; failed to create attachment: {}",
file_path, e
);
}
}
}
Ok(attachments)
}
/// 清理临时文件
pub async fn cleanup_temp_files(&self, temp_files: &[PathBuf]) -> Result<()> {
for temp_file in temp_files {
if temp_file.exists() {
fs::remove_file(temp_file)
.await
.with_context(|| format!("failed to delete temp file: {:?}", temp_file))?;
}
}
Ok(())
}
}
impl Default for FileUtils {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_validate_extension() {
let file_utils = FileUtils::new();
// 测试允许的扩展名
assert!(file_utils.validate_extension("test.txt").is_ok());
assert!(file_utils.validate_extension("image.jpg").is_ok());
assert!(file_utils.validate_extension("audio.mp3").is_ok());
// 测试不允许的扩展名
assert!(file_utils.validate_extension("test.exe").is_err());
assert!(file_utils.validate_extension("test").is_err());
}
#[tokio::test]
async fn test_read_file_as_base64() {
let file_utils = FileUtils::new();
// 创建临时文件
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "Hello, World!").unwrap();
let temp_path = temp_file.path();
let base64_content = file_utils.read_file_as_base64(temp_path).await.unwrap();
// 验证 base64 编码是否正确
let decoded = general_purpose::STANDARD.decode(base64_content).unwrap();
let decoded_text = String::from_utf8(decoded).unwrap();
assert_eq!(decoded_text.trim(), "Hello, World!");
}
}

View File

@@ -0,0 +1,6 @@
//! 工具函数模块
mod content_builder;
mod file_utils;
pub use content_builder::*;