//! Loads and validates process-level server configuration. use std::{env, fs, net::SocketAddr, path::PathBuf, time::Duration}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use crate::{Error, Result}; const DATA_DIR_NAME: &str = ".cursor-byok-v3"; const DATABASE_FILE_NAME: &str = "cursor-byok.db"; const V0049_DATA_DIR_NAME: &str = ".cursor-local-assistant-v2"; const V0049_CONFIG_FILE_NAME: &str = "config.yaml"; const DEFAULT_PROVIDER_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 60); const DEFAULT_PROVIDER_STREAM_IDLE_TIMEOUT: Duration = Duration::from_secs(30 * 60); pub fn managed_data_dir() -> Result { let home_dir = dirs::home_dir() .ok_or_else(|| Error::Config("cannot resolve user home directory".into()))?; let data_dir = home_dir.join(DATA_DIR_NAME); fs::create_dir_all(&data_dir)?; #[cfg(unix)] fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o700))?; Ok(data_dir) } pub fn v0049_config_path() -> Result { let home_dir = dirs::home_dir() .ok_or_else(|| Error::Config("cannot resolve user home directory".into()))?; Ok(home_dir .join(V0049_DATA_DIR_NAME) .join(V0049_CONFIG_FILE_NAME)) } #[derive(Clone, Debug, PartialEq, Eq)] pub enum ProviderKind { OpenAiChat, OpenAiResponses, Anthropic, } #[derive(Clone)] pub struct ProviderConfig { pub kind: ProviderKind, pub request_url: String, pub api_key: String, pub custom_headers: reqwest::header::HeaderMap, pub max_output_tokens: Option, pub request_timeout: Duration, pub allowed_body_fields: Option>, } #[derive(Clone)] pub struct Config { pub listen_addr: SocketAddr, pub database_url: String, pub provider_request_timeout: Duration, pub provider_stream_idle_timeout: Duration, pub console: Option, pub use_persisted_ports: bool, /// 面向用户的应用版本;桌面壳会覆盖为自身版本,用于插件 minAppVersion 门控。 pub app_version: String, } #[derive(Clone)] pub enum ConsoleSource { Directory(PathBuf), Proxy(url::Url), } impl Config { pub fn from_env() -> Result { let listen_addr = env::var("CURSOR_LISTEN_ADDR") .unwrap_or_else(|_| "127.0.0.1:3000".into()) .parse() .map_err(|error| Error::Config(format!("invalid CURSOR_LISTEN_ADDR: {error}")))?; let request_timeout = match env::var("CURSOR_PROVIDER_TIMEOUT_SECONDS") { Ok(value) => Duration::from_secs(value.parse().map_err(|error| { Error::Config(format!("invalid CURSOR_PROVIDER_TIMEOUT_SECONDS: {error}")) })?), Err(env::VarError::NotPresent) => DEFAULT_PROVIDER_REQUEST_TIMEOUT, Err(error) => { return Err(Error::Config(format!( "invalid CURSOR_PROVIDER_TIMEOUT_SECONDS: {error}" ))) } }; let console_dir = env::var_os("CURSOR_CONSOLE_DIR").map(PathBuf::from); let console_proxy = env::var("CURSOR_CONSOLE_PROXY") .ok() .map(|value| { value.parse().map_err(|error| { Error::Config(format!("invalid CURSOR_CONSOLE_PROXY: {error}")) }) }) .transpose()?; let console = match (console_dir, console_proxy) { (Some(_), Some(_)) => { return Err(Error::Config( "CURSOR_CONSOLE_DIR and CURSOR_CONSOLE_PROXY cannot both be set".into(), )) } (Some(directory), None) => Some(ConsoleSource::Directory(directory)), (None, Some(proxy)) => Some(ConsoleSource::Proxy(proxy)), (None, None) => None, }; Ok(Self { listen_addr, database_url: database_url_from_env()?, provider_request_timeout: request_timeout, provider_stream_idle_timeout: DEFAULT_PROVIDER_STREAM_IDLE_TIMEOUT, console, use_persisted_ports: false, app_version: env!("CARGO_PKG_VERSION").into(), }) } pub fn desktop() -> Result { Ok(Self { listen_addr: "127.0.0.1:0" .parse() .expect("desktop listen address is static"), database_url: default_database_url()?, provider_request_timeout: DEFAULT_PROVIDER_REQUEST_TIMEOUT, provider_stream_idle_timeout: DEFAULT_PROVIDER_STREAM_IDLE_TIMEOUT, console: None, use_persisted_ports: true, app_version: env!("CARGO_PKG_VERSION").into(), }) } } fn database_url_from_env() -> Result { match env::var("CURSOR_DATABASE_URL") { Ok(database_url) => Ok(database_url), Err(env::VarError::NotPresent) => default_database_url(), Err(error) => Err(Error::Config(format!( "invalid CURSOR_DATABASE_URL: {error}" ))), } } fn default_database_url() -> Result { let data_dir = managed_data_dir()?; database_url_for_dir(&data_dir) } fn database_url_for_dir(data_dir: &std::path::Path) -> Result { let database_path = data_dir.join(DATABASE_FILE_NAME); let database_path = database_path .to_str() .ok_or_else(|| Error::Config("database path is not valid UTF-8".into()))?; Ok(format!("sqlite://{database_path}")) } #[cfg(test)] mod tests { use super::*; #[test] fn provider_timeout_defaults_match_runtime_boundaries() { assert_eq!( DEFAULT_PROVIDER_STREAM_IDLE_TIMEOUT, Duration::from_secs(30 * 60) ); assert_eq!( DEFAULT_PROVIDER_REQUEST_TIMEOUT, Duration::from_secs(60 * 60) ); } }