//! SaaS 服务器配置 use serde::{Deserialize, Serialize}; use std::path::PathBuf; use secrecy::SecretString; #[cfg(not(debug_assertions))] use secrecy::ExposeSecret; #[cfg(not(debug_assertions))] use sha2::Digest; /// SaaS 服务器完整配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SaaSConfig { pub server: ServerConfig, pub database: DatabaseConfig, pub auth: AuthConfig, pub relay: RelayConfig, #[serde(default)] pub rate_limit: RateLimitConfig, #[serde(default)] pub scheduler: SchedulerConfig, } /// Scheduler 定时任务配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchedulerConfig { #[serde(default)] pub jobs: Vec, } /// 单个定时任务配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JobConfig { pub name: String, /// 间隔时间,支持 "5m", "1h", "24h", "30s" 格式 pub interval: String, /// 对应的 Worker 名称 pub task: String, /// 传递给 Worker 的参数(JSON 格式) #[serde(default)] pub args: Option, /// 是否在启动时立即执行 #[serde(default)] pub run_on_start: bool, } impl Default for SchedulerConfig { fn default() -> Self { Self { jobs: Vec::new() } } } /// 服务器配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerConfig { #[serde(default = "default_host")] pub host: String, #[serde(default = "default_port")] pub port: u16, #[serde(default)] pub cors_origins: Vec, } /// 数据库配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DatabaseConfig { #[serde(default = "default_db_url")] pub url: String, } /// 认证配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthConfig { #[serde(default = "default_jwt_hours")] pub jwt_expiration_hours: i64, #[serde(default = "default_totp_issuer")] pub totp_issuer: String, /// Refresh Token 有效期 (小时), 默认 168 小时 = 7 天 #[serde(default = "default_refresh_hours")] pub refresh_token_hours: i64, } /// 中转服务配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RelayConfig { #[serde(default = "default_max_queue")] pub max_queue_size: usize, /// 每个 Provider 最大并发请求数 (预留,当前由 max_queue_size 控制) #[serde(default = "default_max_concurrent")] pub max_concurrent_per_provider: usize, /// 批量窗口间隔 (预留,用于请求合并优化) #[serde(default = "default_batch_window")] pub batch_window_ms: u64, #[serde(default = "default_retry_delay")] pub retry_delay_ms: u64, #[serde(default = "default_max_attempts")] pub max_attempts: u32, } fn default_host() -> String { "0.0.0.0".into() } fn default_port() -> u16 { 8080 } fn default_db_url() -> String { "postgres://localhost:5432/zclaw".into() } fn default_jwt_hours() -> i64 { 24 } fn default_totp_issuer() -> String { "ZCLAW SaaS".into() } fn default_refresh_hours() -> i64 { 168 } fn default_max_queue() -> usize { 1000 } fn default_max_concurrent() -> usize { 5 } fn default_batch_window() -> u64 { 50 } fn default_retry_delay() -> u64 { 1000 } fn default_max_attempts() -> u32 { 3 } /// 速率限制配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RateLimitConfig { /// 每分钟最大请求数 (滑动窗口) #[serde(default = "default_rpm")] pub requests_per_minute: u32, /// 突发允许的额外请求数 #[serde(default = "default_burst")] pub burst: u32, } fn default_rpm() -> u32 { 60 } fn default_burst() -> u32 { 10 } impl Default for RateLimitConfig { fn default() -> Self { Self { requests_per_minute: default_rpm(), burst: default_burst(), } } } impl Default for SaaSConfig { fn default() -> Self { Self { server: ServerConfig::default(), database: DatabaseConfig::default(), auth: AuthConfig::default(), relay: RelayConfig::default(), rate_limit: RateLimitConfig::default(), scheduler: SchedulerConfig::default(), } } } impl Default for ServerConfig { fn default() -> Self { Self { host: default_host(), port: default_port(), cors_origins: Vec::new(), } } } impl Default for DatabaseConfig { fn default() -> Self { Self { url: default_db_url() } } } impl Default for AuthConfig { fn default() -> Self { Self { jwt_expiration_hours: default_jwt_hours(), totp_issuer: default_totp_issuer(), refresh_token_hours: default_refresh_hours(), } } } impl Default for RelayConfig { fn default() -> Self { Self { max_queue_size: default_max_queue(), max_concurrent_per_provider: default_max_concurrent(), batch_window_ms: default_batch_window(), retry_delay_ms: default_retry_delay(), max_attempts: default_max_attempts(), } } } impl SaaSConfig { /// 加载配置文件,优先级: ZCLAW_SAAS_CONFIG > ZCLAW_ENV > ./saas-config.toml /// /// ZCLAW_ENV 环境选择: /// development → config/saas-development.toml /// production → config/saas-production.toml /// test → config/saas-test.toml /// /// ZCLAW_SAAS_CONFIG 指定精确路径(最高优先级) pub fn load() -> anyhow::Result { let config_path = if let Ok(path) = std::env::var("ZCLAW_SAAS_CONFIG") { PathBuf::from(path) } else if let Ok(env) = std::env::var("ZCLAW_ENV") { let filename = format!("config/saas-{}.toml", env); let path = PathBuf::from(&filename); if !path.exists() { anyhow::bail!( "ZCLAW_ENV={} 指定的配置文件 {} 不存在", env, filename ); } tracing::info!("Loading config for environment: {}", env); path } else { PathBuf::from("saas-config.toml") }; let mut config = if config_path.exists() { let content = std::fs::read_to_string(&config_path)?; toml::from_str(&content)? } else { tracing::warn!("Config file {:?} not found, using defaults", config_path); SaaSConfig::default() }; // 环境变量覆盖数据库 URL (避免在配置文件中存储密码) if let Ok(db_url) = std::env::var("ZCLAW_DATABASE_URL") { config.database.url = db_url; } Ok(config) } /// 获取 JWT 密钥 (从环境变量或生成临时值) /// 生产环境必须设置 ZCLAW_SAAS_JWT_SECRET pub fn jwt_secret(&self) -> anyhow::Result { match std::env::var("ZCLAW_SAAS_JWT_SECRET") { Ok(secret) => Ok(SecretString::from(secret)), Err(_) => { // 开发 fallback 密钥仅在 debug 构建中可用,不会进入 release #[cfg(debug_assertions)] { tracing::warn!("ZCLAW_SAAS_JWT_SECRET not set, using development default (INSECURE)"); Ok(SecretString::from("zclaw-dev-only-secret-do-not-use-in-prod".to_string())) } #[cfg(not(debug_assertions))] { anyhow::bail!( "ZCLAW_SAAS_JWT_SECRET 环境变量未设置。\ 请设置一个强随机密钥 (至少 32 字符)。" ) } } } } /// 获取 API Key 加密密钥 (复用 TOTP 加密密钥) pub fn api_key_encryption_key(&self) -> anyhow::Result<[u8; 32]> { self.totp_encryption_key() } /// 获取 TOTP 加密密钥 (AES-256-GCM, 32 字节) /// 从 ZCLAW_TOTP_ENCRYPTION_KEY 环境变量加载 (hex 编码的 64 字符) /// 开发环境使用默认值 (不安全) pub fn totp_encryption_key(&self) -> anyhow::Result<[u8; 32]> { match std::env::var("ZCLAW_TOTP_ENCRYPTION_KEY") { Ok(hex_key) => { if hex_key.len() != 64 { anyhow::bail!("ZCLAW_TOTP_ENCRYPTION_KEY 必须是 64 个十六进制字符 (32 字节)"); } let mut key = [0u8; 32]; for i in 0..32 { key[i] = u8::from_str_radix(&hex_key[i*2..i*2+2], 16) .map_err(|_| anyhow::anyhow!("ZCLAW_TOTP_ENCRYPTION_KEY 包含无效的十六进制字符"))?; } Ok(key) } Err(_) => { // 开发环境: 仅在 debug 构建中使用固定密钥 #[cfg(debug_assertions)] { tracing::warn!("ZCLAW_TOTP_ENCRYPTION_KEY not set, using development default (INSECURE)"); let mut key = [0u8; 32]; key.copy_from_slice(b"zclaw-dev-totp-encrypt-key-32b!x"); Ok(key) } #[cfg(not(debug_assertions))] { // 生产环境: 使用 JWT 密钥的 SHA-256 哈希作为加密密钥 tracing::warn!("ZCLAW_TOTP_ENCRYPTION_KEY not set, deriving from JWT secret"); let jwt = self.jwt_secret()?; let hash = sha2::Sha256::digest(jwt.expose_secret().as_bytes()); Ok(hash.into()) } } } } }