use std::path::PathBuf;
use std::collections::HashMap;
use anyhow::{Result, Context};
use serde::{Deserialize, Serialize};
use crate::env_config::*;
use crate::types::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub session_id: String,
pub models: ModelConfig,
pub sandbox: SandboxConfig,
pub tool_registry: ToolRegistryConfig,
pub deepseek_api: DeepSeekApiConfig,
pub system_prompt_config: Option<SystemPromptConfig>,
pub workspace_root: PathBuf,
pub mcp_config: Option<McpServerConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
pub default_model: String,
pub max_tokens: usize,
pub temperature: f64,
pub request_timeout: u64,
pub max_retries: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxConfig {
pub enabled: bool,
pub directory: Option<PathBuf>,
pub allowed_commands: Vec<String>,
pub forbidden_commands: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolRegistryConfig {
pub enabled: bool,
pub tool_timeout: u64,
pub max_concurrent_tools: usize,
pub tool_retries: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeepSeekApiConfig {
pub api_key: String,
pub api_endpoint: String,
pub streaming: bool,
pub headers: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemPromptConfig {
pub system_prompt_mappings: Option<Vec<ModelTemplateMapping>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelTemplateMapping {
pub base_urls: Option<Vec<String>>,
pub model_names: Option<Vec<String>>,
pub template: Option<String>,
}
impl Config {
pub fn new() -> Result<Self> {
init_env_config()?;
validate_required_env()?;
let workspace_root = get_workspace_root();
let models = ModelConfig {
default_model: get_openai_model(),
max_tokens: get_max_tokens(),
temperature: get_temperature(),
request_timeout: get_request_timeout(),
max_retries: get_max_retries(),
};
let sandbox = SandboxConfig {
enabled: is_sandbox_mode(),
directory: if get_sandbox().is_empty() {
None
} else {
Some(PathBuf::from(get_sandbox()))
},
allowed_commands: vec![
"ls".to_string(),
"pwd".to_string(),
"cat".to_string(),
"grep".to_string(),
"find".to_string(),
"git".to_string(),
],
forbidden_commands: vec![
"rm".to_string(),
"rmdir".to_string(),
"del".to_string(),
"format".to_string(),
"fdisk".to_string(),
],
};
let tool_registry = ToolRegistryConfig {
enabled: true,
tool_timeout: 30,
max_concurrent_tools: 5,
tool_retries: 3,
};
let deepseek_api = DeepSeekApiConfig {
api_key: get_deepseek_api_key().unwrap_or_else(|_| {
get_openai_api_key().unwrap_or_else(|_| String::new())
}),
api_endpoint: get_deepseek_api_endpoint(),
streaming: true,
headers: {
let mut headers = HashMap::new();
headers.insert("User-Agent".to_string(), get_user_agent());
headers.insert("Content-Type".to_string(), "application/json".to_string());
headers
},
};
let system_prompt_config = load_system_prompt_config()?;
let mcp_config = crate::mcp_config::McpConfigLoader::load_mcp_config()?;
Ok(Config {
session_id: get_session_id(),
models,
sandbox,
tool_registry,
deepseek_api,
system_prompt_config,
workspace_root,
mcp_config,
})
}
pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path.as_ref())
.with_context(|| format!("无法读取配置文件: {}", path.as_ref().display()))?;
let config: Config = toml::from_str(&content)
.with_context(|| "无法解析配置文件")?;
Ok(config)
}
pub fn save_to_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
let content = toml::to_string_pretty(self)
.with_context(|| "无法序列化配置")?;
std::fs::write(path.as_ref(), content)
.with_context(|| format!("无法写入配置文件: {}", path.as_ref().display()))?;
Ok(())
}
pub fn get_api_key(&self) -> &str {
&self.deepseek_api.api_key
}
pub fn get_api_endpoint(&self) -> &str {
&self.deepseek_api.api_endpoint
}
pub fn get_default_model(&self) -> &str {
&self.models.default_model
}
pub fn get_max_tokens(&self) -> usize {
self.models.max_tokens
}
pub fn get_temperature(&self) -> f64 {
self.models.temperature
}
pub fn get_request_timeout(&self) -> u64 {
self.models.request_timeout
}
pub fn get_max_retries(&self) -> usize {
self.models.max_retries
}
pub fn is_sandbox_enabled(&self) -> bool {
self.sandbox.enabled
}
pub fn get_sandbox_directory(&self) -> Option<&PathBuf> {
self.sandbox.directory.as_ref()
}
pub fn is_command_allowed(&self, command: &str) -> bool {
if self.sandbox.forbidden_commands.contains(&command.to_string()) {
return false;
}
if self.sandbox.allowed_commands.is_empty() {
return true;
}
self.sandbox.allowed_commands.contains(&command.to_string())
}
pub fn get_tool_timeout(&self) -> u64 {
self.tool_registry.tool_timeout
}
pub fn get_max_concurrent_tools(&self) -> usize {
self.tool_registry.max_concurrent_tools
}
pub fn get_tool_retries(&self) -> usize {
self.tool_registry.tool_retries
}
pub fn get_workspace_root(&self) -> &PathBuf {
&self.workspace_root
}
pub fn get_mcp_config(&self) -> Option<&McpServerConfig> {
self.mcp_config.as_ref()
}
pub fn get_system_prompt_config(&self) -> Option<&SystemPromptConfig> {
self.system_prompt_config.as_ref()
}
pub fn update_api_key(&mut self, api_key: String) {
self.deepseek_api.api_key = api_key;
}
pub fn update_api_endpoint(&mut self, api_endpoint: String) {
self.deepseek_api.api_endpoint = api_endpoint;
}
pub fn update_default_model(&mut self, model: String) {
self.models.default_model = model;
}
pub fn update_max_tokens(&mut self, max_tokens: usize) {
self.models.max_tokens = max_tokens;
}
pub fn update_temperature(&mut self, temperature: f64) {
self.models.temperature = temperature;
}
pub fn update_sandbox(&mut self, enabled: bool, directory: Option<PathBuf>) {
self.sandbox.enabled = enabled;
self.sandbox.directory = directory;
}
pub fn add_allowed_command(&mut self, command: String) {
if !self.sandbox.allowed_commands.contains(&command) {
self.sandbox.allowed_commands.push(command);
}
}
pub fn add_forbidden_command(&mut self, command: String) {
if !self.sandbox.forbidden_commands.contains(&command) {
self.sandbox.forbidden_commands.push(command);
}
}
pub fn remove_allowed_command(&mut self, command: &str) {
self.sandbox.allowed_commands.retain(|c| c != command);
}
pub fn remove_forbidden_command(&mut self, command: &str) {
self.sandbox.forbidden_commands.retain(|c| c != command);
}
pub fn update_tool_registry(&mut self, enabled: bool, timeout: u64, max_concurrent: usize, retries: usize) {
self.tool_registry.enabled = enabled;
self.tool_registry.tool_timeout = timeout;
self.tool_registry.max_concurrent_tools = max_concurrent;
self.tool_registry.tool_retries = retries;
}
pub fn get_summary(&self) -> HashMap<String, String> {
let mut summary = HashMap::new();
summary.insert("session_id".to_string(), self.session_id.clone());
summary.insert("default_model".to_string(), self.models.default_model.clone());
summary.insert("max_tokens".to_string(), self.models.max_tokens.to_string());
summary.insert("temperature".to_string(), self.models.temperature.to_string());
summary.insert("request_timeout".to_string(), self.models.request_timeout.to_string());
summary.insert("max_retries".to_string(), self.models.max_retries.to_string());
summary.insert("sandbox_enabled".to_string(), self.sandbox.enabled.to_string());
summary.insert("tool_registry_enabled".to_string(), self.tool_registry.enabled.to_string());
summary.insert("tool_timeout".to_string(), self.tool_registry.tool_timeout.to_string());
summary.insert("max_concurrent_tools".to_string(), self.tool_registry.max_concurrent_tools.to_string());
summary.insert("tool_retries".to_string(), self.tool_registry.tool_retries.to_string());
summary.insert("api_endpoint".to_string(), self.deepseek_api.api_endpoint.clone());
summary.insert("streaming".to_string(), self.deepseek_api.streaming.to_string());
summary.insert("workspace_root".to_string(), self.workspace_root.to_string_lossy().to_string());
if let Some(sandbox_dir) = &self.sandbox.directory {
summary.insert("sandbox_directory".to_string(), sandbox_dir.to_string_lossy().to_string());
}
summary
}
pub fn validate(&self) -> Result<()> {
if self.deepseek_api.api_key.is_empty() {
return Err(anyhow::anyhow!("API密钥不能为空"));
}
if self.deepseek_api.api_endpoint.is_empty() {
return Err(anyhow::anyhow!("API端点不能为空"));
}
if self.models.default_model.is_empty() {
return Err(anyhow::anyhow!("默认模型不能为空"));
}
if self.models.max_tokens == 0 {
return Err(anyhow::anyhow!("最大令牌数必须大于0"));
}
if self.models.temperature < 0.0 || self.models.temperature > 2.0 {
return Err(anyhow::anyhow!("温度设置必须在0.0到2.0之间"));
}
if self.models.request_timeout == 0 {
return Err(anyhow::anyhow!("请求超时时间必须大于0"));
}
if self.models.max_retries == 0 {
return Err(anyhow::anyhow!("最大重试次数必须大于0"));
}
if self.tool_registry.tool_timeout == 0 {
return Err(anyhow::anyhow!("工具超时时间必须大于0"));
}
if self.tool_registry.max_concurrent_tools == 0 {
return Err(anyhow::anyhow!("最大并发工具调用数必须大于0"));
}
if self.tool_registry.tool_retries == 0 {
return Err(anyhow::anyhow!("工具重试次数必须大于0"));
}
if !self.workspace_root.exists() {
return Err(anyhow::anyhow!(
"工作区根目录不存在: {}",
self.workspace_root.display()
));
}
Ok(())
}
}
impl Default for Config {
fn default() -> Self {
Self::new().unwrap_or_else(|_| {
Config {
session_id: "default".to_string(),
models: ModelConfig {
default_model: "gpt-4o".to_string(),
max_tokens: 4096,
temperature: 0.7,
request_timeout: 30,
max_retries: 3,
},
sandbox: SandboxConfig {
enabled: false,
directory: None,
allowed_commands: vec![],
forbidden_commands: vec![],
},
tool_registry: ToolRegistryConfig {
enabled: true,
tool_timeout: 30,
max_concurrent_tools: 5,
tool_retries: 3,
},
deepseek_api: DeepSeekApiConfig {
api_key: String::new(),
api_endpoint: "https://api.deepseek.com".to_string(),
streaming: true,
headers: HashMap::new(),
},
system_prompt_config: None,
workspace_root: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
mcp_config: None,
}
})
}
}
fn load_system_prompt_config() -> Result<Option<SystemPromptConfig>> {
if let Some(config_str) = std::env::var("SYSTEM_PROMPT_CONFIG").ok() {
let config: SystemPromptConfig = serde_json::from_str(&config_str)
.with_context(|| "无法解析系统提示配置")?;
return Ok(Some(config));
}
let config_path = get_workspace_root().join("system-prompt-config.json");
if config_path.exists() {
let content = std::fs::read_to_string(&config_path)
.with_context(|| format!("无法读取系统提示配置文件: {}", config_path.display()))?;
let config: SystemPromptConfig = serde_json::from_str(&content)
.with_context(|| "无法解析系统提示配置文件")?;
return Ok(Some(config));
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_config_new() {
env::set_var("DEEPSEEK_API_KEY", "test-key");
env::set_var("DEEPSEEK_API_ENDPOINT", "https://api.deepseek.com");
env::set_var("OPENAI_MODEL", "gpt-4");
let config = Config::new();
assert!(config.is_ok());
let config = config.unwrap();
assert_eq!(config.get_default_model(), "gpt-4");
assert_eq!(config.get_api_endpoint(), "https://api.deepseek.com");
assert!(config.get_api_key().contains("test-key"));
env::remove_var("DEEPSEEK_API_KEY");
env::remove_var("DEEPSEEK_API_ENDPOINT");
env::remove_var("OPENAI_MODEL");
}
#[test]
fn test_config_validation() {
let mut config = Config::default();
assert!(config.validate().is_ok());
config.deepseek_api.api_key = String::new();
assert!(config.validate().is_err());
config.deepseek_api.api_key = "test-key".to_string();
config.models.max_tokens = 0;
assert!(config.validate().is_err());
}
#[test]
fn test_sandbox_commands() {
let mut config = Config::default();
assert!(config.is_command_allowed("ls"));
assert!(config.is_command_allowed("pwd"));
config.add_forbidden_command("rm".to_string());
assert!(!config.is_command_allowed("rm"));
config.remove_forbidden_command("rm");
assert!(config.is_command_allowed("rm"));
}
#[test]
fn test_config_updates() {
let mut config = Config::default();
config.update_api_key("new-key".to_string());
assert_eq!(config.get_api_key(), "new-key");
config.update_default_model("gpt-3.5-turbo".to_string());
assert_eq!(config.get_default_model(), "gpt-3.5-turbo");
config.update_temperature(0.5);
assert_eq!(config.get_temperature(), 0.5);
}
#[test]
fn test_config_summary() {
let config = Config::default();
let summary = config.get_summary();
assert!(summary.contains_key("session_id"));
assert!(summary.contains_key("default_model"));
assert!(summary.contains_key("max_tokens"));
assert!(summary.contains_key("temperature"));
assert!(summary.contains_key("workspace_root"));
}
}