Files
cursor-byok/server/src/config.rs
T
leokun d83e14af9a refactor: remove retry_count from ProviderConfig and enhance error handling in tool execution
- Removed the `retry_count` field from `ProviderConfig` as it is no longer needed.
- Introduced `argument_error` field in `ToolCall` to capture errors related to tool arguments.
- Updated various components to handle argument errors more gracefully, including in the `ToolDispatcher` and `ConversationOutput`.
- Enhanced tests to validate the new error handling and ensure proper functionality of tool calls.
2026-09-01 10:14:53 +08:00

171 lines
5.7 KiB
Rust

//! 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<PathBuf> {
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<PathBuf> {
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<u64>,
pub request_timeout: Duration,
pub allowed_body_fields: Option<std::collections::HashSet<String>>,
}
#[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<ConsoleSource>,
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<Self> {
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<Self> {
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<String> {
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<String> {
let data_dir = managed_data_dir()?;
database_url_for_dir(&data_dir)
}
fn database_url_for_dir(data_dir: &std::path::Path) -> Result<String> {
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)
);
}
}