Skip to main content

skilllite_core/config/
loader.rs

1//! 统一环境变量加载逻辑
2//!
3//! 集中维护 fallback 链,避免在业务代码中重复 `or_else` 调用。
4
5use std::env;
6
7/// 废弃变量 → 推荐变量映射(用于检测并提示迁移)
8const DEPRECATED_PAIRS: &[(&str, &str)] = &[
9    ("SKILLBOX_AUDIT_LOG", "SKILLLITE_AUDIT_LOG"),
10    ("SKILLBOX_QUIET", "SKILLLITE_QUIET"),
11    ("SKILLBOX_CACHE_DIR", "SKILLLITE_CACHE_DIR"),
12    ("AGENTSKILL_CACHE_DIR", "SKILLLITE_CACHE_DIR"),
13    ("SKILLBOX_LOG_LEVEL", "SKILLLITE_LOG_LEVEL"),
14    ("SKILLBOX_LOG_JSON", "SKILLLITE_LOG_JSON"),
15    ("SKILLBOX_SANDBOX_LEVEL", "SKILLLITE_SANDBOX_LEVEL"),
16    ("SKILLBOX_MAX_MEMORY_MB", "SKILLLITE_MAX_MEMORY_MB"),
17    ("SKILLBOX_TIMEOUT_SECS", "SKILLLITE_TIMEOUT_SECS"),
18    ("SKILLBOX_AUTO_APPROVE", "SKILLLITE_AUTO_APPROVE"),
19    ("SKILLBOX_NO_SANDBOX", "SKILLLITE_NO_SANDBOX"),
20    ("SKILLBOX_ALLOW_PLAYWRIGHT", "SKILLLITE_ALLOW_PLAYWRIGHT"),
21    ("SKILLBOX_SCRIPT_ARGS", "SKILLLITE_SCRIPT_ARGS"),
22];
23
24/// 检测废弃变量:若使用了废弃变量且未设置推荐变量,打印一次迁移提示
25fn warn_deprecated_env_vars() {
26    use std::sync::Once;
27    static WARNED: Once = Once::new();
28    WARNED.call_once(|| {
29        let mut hints = Vec::new();
30        for (deprecated, recommended) in DEPRECATED_PAIRS {
31            if env::var(deprecated).is_ok() && env::var(recommended).is_err() {
32                hints.push(format!("{} → {}", deprecated, recommended));
33            }
34        }
35        if !hints.is_empty() {
36            tracing::warn!(
37                "[DEPRECATED] 以下环境变量已废弃,建议迁移:\n   {}\n   详见 docs/zh/ENV_REFERENCE.md",
38                hints.join("\n   ")
39            );
40        }
41    });
42}
43
44/// 解析 .env 文件内容为 key-value 对(不修改进程环境)。
45/// 与 load_dotenv / load_dotenv_from_dir 使用相同的解析规则。
46fn parse_dotenv_content(content: &str) -> Vec<(String, String)> {
47    let mut vars = Vec::new();
48    for line in content.lines() {
49        let line = line.trim();
50        if line.is_empty() || line.starts_with('#') {
51            continue;
52        }
53        if let Some(eq_pos) = line.find('=') {
54            let key = line[..eq_pos].trim().to_string();
55            let mut value = line[eq_pos + 1..].trim();
56            if let Some(hash_pos) = value.find('#') {
57                let before_hash = value[..hash_pos].trim_end();
58                if !before_hash.contains('"') && !before_hash.contains('\'') {
59                    value = before_hash;
60                }
61            }
62            if (value.starts_with('"') && value.ends_with('"'))
63                || (value.starts_with('\'') && value.ends_with('\''))
64            {
65                value = &value[1..value.len() - 1];
66            }
67            if !key.is_empty() {
68                vars.push((key, value.to_string()));
69            }
70        }
71    }
72    vars
73}
74
75/// 从指定目录解析 .env,返回 key-value 对(不修改进程环境)。
76/// 用于子进程等需要将 .env 作为 env 传入的场景。
77pub fn parse_dotenv_from_dir(dir: &std::path::Path) -> Vec<(String, String)> {
78    let path = dir.join(".env");
79    if let Ok(content) = std::fs::read_to_string(&path) {
80        parse_dotenv_content(&content)
81    } else {
82        vec![]
83    }
84}
85
86/// 从 start 目录向上查找 .env,最多查找 max_levels 层,返回首次找到的解析结果。
87/// 用于 assistant 等需要从工作区向上查找 .env 的场景。
88pub fn parse_dotenv_walking_up(
89    start: &std::path::Path,
90    max_levels: usize,
91) -> Vec<(String, String)> {
92    let mut dir = start.canonicalize().unwrap_or_else(|_| start.to_path_buf());
93    for _ in 0..max_levels {
94        let vars = parse_dotenv_from_dir(&dir);
95        if !vars.is_empty() {
96            return vars;
97        }
98        if !dir.pop() {
99            break;
100        }
101    }
102    vec![]
103}
104
105/// Load .env from a specific directory (does not overwrite existing vars).
106/// Used by swarm to load from project root when started from a different cwd.
107pub fn load_dotenv_from_dir(dir: &std::path::Path) {
108    for (key, value) in parse_dotenv_from_dir(dir) {
109        if env::var(&key).is_err() {
110            #[allow(unsafe_code)]
111            unsafe {
112                env::set_var(&key, &value);
113            }
114        }
115    }
116}
117
118/// 加载当前目录下的 `.env` 到环境变量(不覆盖已存在的变量)
119pub fn load_dotenv() {
120    use std::sync::Once;
121    static INIT: Once = Once::new();
122    INIT.call_once(|| {
123        let path = env::current_dir()
124            .map(|d| d.join(".env"))
125            .unwrap_or_else(|_| std::path::PathBuf::from(".env"));
126        if let Ok(content) = std::fs::read_to_string(&path) {
127            for (key, value) in parse_dotenv_content(&content) {
128                if env::var(&key).is_err() {
129                    #[allow(unsafe_code)]
130                    unsafe {
131                        env::set_var(&key, &value);
132                    }
133                }
134            }
135        }
136        warn_deprecated_env_vars();
137    });
138}
139
140/// 从主变量或别名链读取环境变量,失败时使用默认值
141pub fn env_or<F>(primary: &str, aliases: &[&str], default: F) -> String
142where
143    F: FnOnce() -> String,
144{
145    env::var(primary)
146        .ok()
147        .or_else(|| aliases.iter().find_map(|a| env::var(a).ok()))
148        .filter(|s| !s.is_empty())
149        .unwrap_or_else(default)
150}
151
152/// 从主变量或别名链读取,返回 Option(空值视为未设置)
153pub fn env_optional(primary: &str, aliases: &[&str]) -> Option<String> {
154    env::var(primary)
155        .ok()
156        .or_else(|| aliases.iter().find_map(|a| env::var(a).ok()))
157        .and_then(|s| {
158            let s = s.trim().to_string();
159            if s.is_empty() {
160                None
161            } else {
162                Some(s)
163            }
164        })
165}
166
167/// 解析布尔型环境变量:1/true/yes 为 true,0/false/no 为 false
168pub fn env_bool(primary: &str, aliases: &[&str], default: bool) -> bool {
169    let v = env::var(primary)
170        .ok()
171        .or_else(|| aliases.iter().find_map(|a| env::var(a).ok()));
172    match v.as_deref() {
173        Some(s) => !matches!(
174            s.trim().to_lowercase().as_str(),
175            "0" | "false" | "no" | "off"
176        ),
177        None => default,
178    }
179}
180
181/// P0 可观测 vs P1 可阻断:返回是否在「可阻断」模式。false(默认)= 仅展示状态不阻断;true = 阻断 HashChanged/SignatureInvalid/TrustDeny
182pub fn supply_chain_block_enabled() -> bool {
183    use crate::config::env_keys::observability;
184    env_bool(observability::SKILLLITE_SUPPLY_CHAIN_BLOCK, &[], false)
185}
186
187/// 检查环境变量是否存在(任意主变量或别名)
188#[allow(dead_code)] // 供后续迁移使用
189pub fn env_is_set(primary: &str, aliases: &[&str]) -> bool {
190    env::var(primary).is_ok() || aliases.iter().any(|a| env::var(a).is_ok())
191}
192
193// ─── 集中式 env::set_var / remove_var 包装 ─────────────────────────────────
194//
195// 所有对 `std::env::set_var` / `remove_var` 的调用都应通过下面的函数进行,
196// 业务代码不再直接出现 `unsafe { env::set_var(...) }`。
197//
198// SAFETY 约定:调用方需确保在多线程启动前(tokio runtime 创建前)调用。
199
200/// 设置单个环境变量(unsafe 集中在此处)
201#[allow(unsafe_code)]
202pub fn set_env_var(key: &str, value: &str) {
203    unsafe { env::set_var(key, value) };
204}
205
206/// 移除单个环境变量
207#[allow(unsafe_code)]
208pub fn remove_env_var(key: &str) {
209    unsafe { env::remove_var(key) };
210}
211
212/// 初始化 LLM 环境变量(api_base / api_key / model)
213///
214/// quickstart 等入口在 tokio runtime 启动前调用。
215pub fn init_llm_env(api_base: &str, api_key: &str, model: &str) {
216    set_env_var("OPENAI_API_BASE", api_base);
217    set_env_var("OPENAI_API_KEY", api_key);
218    set_env_var("SKILLLITE_MODEL", model);
219}
220
221/// 初始化 daemon/stdio 模式的静默环境变量
222pub fn init_daemon_env() {
223    set_env_var("SKILLLITE_AUTO_APPROVE", "1");
224    set_env_var("SKILLLITE_QUIET", "1");
225}
226
227/// 确保 `SKILLLITE_OUTPUT_DIR` 有值,若未设置则使用 `~/.skilllite/chat/output/`。
228///
229/// 同时创建目录(若不存在)。chat 和 agent-rpc 入口共用。
230pub fn ensure_default_output_dir() {
231    let paths = super::PathsConfig::from_env();
232    if paths.output_dir.is_none() {
233        let chat_output = crate::paths::chat_root().join("output");
234        let s = chat_output.to_string_lossy().to_string();
235        set_env_var("SKILLLITE_OUTPUT_DIR", &s);
236        if !chat_output.exists() {
237            let _ = std::fs::create_dir_all(&chat_output);
238        }
239    } else if let Some(ref output_dir) = paths.output_dir {
240        let p = std::path::PathBuf::from(output_dir);
241        if !p.exists() {
242            let _ = std::fs::create_dir_all(&p);
243        }
244    }
245}
246
247/// RAII guard:drop 时通过 [`remove_env_var`] 清除指定环境变量。
248///
249/// 用于 exec_script 等需要临时设置再还原的场景。
250pub struct ScopedEnvGuard(pub &'static str);
251
252impl Drop for ScopedEnvGuard {
253    fn drop(&mut self) {
254        remove_env_var(self.0);
255    }
256}