Files
qiming/qiming-rcoder/crates/agent_runner/src/config.rs
2026-06-01 13:54:52 +08:00

955 lines
32 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"));
}
}