use std::path::{Path, PathBuf};
use super::agent_roles;
use super::builder::{ConfigBuilder, override_opt_string_non_empty};
use super::source::parse_config_file_roles;
use super::user_config_xdg::{
cwd_has_local_user_config, resolve_default_user_config_toml_after_seed,
};
pub(super) fn merge_user_config_layers(
config_path: Option<&str>,
b: &mut ConfigBuilder,
) -> Result<Vec<PathBuf>, String> {
let xdg_owned = match config_path.map(str::trim).filter(|s| !s.is_empty()) {
Some(_) => None,
None if cwd_has_local_user_config() => None,
None => {
resolve_default_user_config_toml_after_seed().map(|p| p.to_string_lossy().into_owned())
}
};
let effective_path = config_path
.map(str::trim)
.filter(|s| !s.is_empty())
.or(xdg_owned.as_deref());
let config_paths: Vec<&str> = match effective_path {
Some(p) => vec![p],
None => vec!["config.toml", ".agent_demo.toml"],
};
let mut system_prompt_search_bases: Vec<PathBuf> = Vec::new();
merge_from_primary_user_files(
&config_paths,
effective_path,
b,
&mut system_prompt_search_bases,
)?;
merge_agent_roles_sidecar(effective_path, b)?;
Ok(system_prompt_search_bases)
}
fn merge_from_primary_user_files(
config_paths: &[&str],
config_path: Option<&str>,
b: &mut ConfigBuilder,
system_prompt_search_bases: &mut Vec<PathBuf>,
) -> Result<(), String> {
for path in config_paths {
if Path::new(path).exists() {
apply_one_existing_user_config(path, b, system_prompt_search_bases)?;
if config_path.is_some() {
break;
}
} else if config_path.is_some() {
return Err(format!("配置文件 \"{path}\" 不存在"));
}
}
Ok(())
}
fn apply_one_existing_user_config(
path: &str,
b: &mut ConfigBuilder,
system_prompt_search_bases: &mut Vec<PathBuf>,
) -> Result<(), String> {
system_prompt_search_bases.push(directory_containing_config_file(path));
let s =
std::fs::read_to_string(path).map_err(|e| format!("无法读取配置文件 \"{path}\": {e}"))?;
let (agent_opt, role_rows, tr_opt, sched_rows) = parse_config_file_roles(&s)
.map_err(|e| format!("配置文件 \"{path}\" TOML 解析失败: {e}"))?;
if let Some(agent) = agent_opt {
b.apply_section(agent);
}
b.merge_agent_role_rows(&role_rows);
b.merge_scheduled_agent_task_rows(&sched_rows);
if let Some(tr) = tr_opt {
b.apply_tool_registry(tr);
}
Ok(())
}
fn merge_agent_roles_sidecar(
config_path: Option<&str>,
b: &mut ConfigBuilder,
) -> Result<(), String> {
let sidecar_path = resolve_agent_roles_sidecar_path(config_path);
let Some(sc) = sidecar_path.filter(|p| p.exists()) else {
return Ok(());
};
let s = std::fs::read_to_string(&sc)
.map_err(|e| format!("无法读取角色配置文件 \"{}\": {}", sc.display(), e))?;
let mut default_slot: Option<String> = None;
agent_roles::merge_agent_roles_file_into_builder(
&s,
&mut default_slot,
&mut b.agent_role_entries,
)?;
override_opt_string_non_empty(&mut b.roles_prompts.default_agent_role_id, default_slot);
Ok(())
}
fn resolve_agent_roles_sidecar_path(config_path: Option<&str>) -> Option<PathBuf> {
if let Some(p) = config_path
.as_ref()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
{
Path::new(p)
.parent()
.map(|dir| dir.join("agent_roles.toml"))
} else {
Some(Path::new("config/agent_roles.toml").to_path_buf())
}
}
fn directory_containing_config_file(config_path: &str) -> PathBuf {
let p = Path::new(config_path);
match p.parent() {
None => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
Some(parent) if parent.as_os_str().is_empty() => {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
Some(parent) => parent.to_path_buf(),
}
}
#[cfg(test)]
mod discovery_tests {
use super::*;
use crate::cm_config::builder::ConfigBuilder;
use crate::cm_config::xdg::{ENV_CONFIG_DIR, test_env_lock};
fn recover_cwd_if_needed() {
if std::env::current_dir().is_err() {
let fallback = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let _ = std::env::set_current_dir(&fallback);
}
}
#[test]
fn cwd_local_config_beats_xdg() {
let _guard = test_env_lock();
recover_cwd_if_needed();
let tmp = tempfile::tempdir().expect("tempdir");
let xdg = tmp.path().join("xdg");
let cwd = tmp.path().join("cwd");
std::fs::create_dir_all(&xdg).unwrap();
std::fs::create_dir_all(&cwd).unwrap();
std::fs::write(xdg.join("config.toml"), b"[agent]\nmodel = \"from-xdg\"\n").unwrap();
std::fs::write(cwd.join("config.toml"), b"[agent]\nmodel = \"from-cwd\"\n").unwrap();
let prev = std::env::current_dir().unwrap_or_else(|_| cwd.clone());
unsafe {
std::env::set_var(ENV_CONFIG_DIR, &xdg);
}
std::env::set_current_dir(&cwd).unwrap();
let mut b = ConfigBuilder::default();
let bases = merge_user_config_layers(None, &mut b).unwrap();
let _ = std::env::set_current_dir(&prev);
unsafe {
std::env::remove_var(ENV_CONFIG_DIR);
}
assert_eq!(b.llm.model, "from-cwd");
assert_eq!(bases, vec![cwd]);
}
#[test]
fn xdg_used_when_no_cwd_local_and_config_dir_set() {
let _guard = test_env_lock();
recover_cwd_if_needed();
let tmp = tempfile::tempdir().expect("tempdir");
let xdg = tmp.path().join("xdg");
let cwd = tmp.path().join("cwd");
std::fs::create_dir_all(&xdg).unwrap();
std::fs::create_dir_all(&cwd).unwrap();
std::fs::write(xdg.join("config.toml"), b"[agent]\nmodel = \"from-xdg\"\n").unwrap();
let prev = std::env::current_dir().unwrap_or_else(|_| cwd.clone());
unsafe {
std::env::set_var(ENV_CONFIG_DIR, &xdg);
}
std::env::set_current_dir(&cwd).unwrap();
let mut b = ConfigBuilder::default();
let bases = merge_user_config_layers(None, &mut b).unwrap();
let _ = std::env::set_current_dir(&prev);
unsafe {
std::env::remove_var(ENV_CONFIG_DIR);
}
assert_eq!(b.llm.model, "from-xdg");
assert_eq!(bases, vec![xdg]);
}
}