Skip to main content

skilllite_core/config/
schema.rs

1//! 按领域分组的配置结构体
2//!
3//! 从环境变量加载,统一 fallback 逻辑。
4
5use super::env_keys::{llm, observability as obv_keys, sandbox as sb_keys};
6use super::loader::{env_bool, env_optional, env_or};
7use std::path::PathBuf;
8
9/// LLM API 配置
10#[derive(Debug, Clone)]
11pub struct LlmConfig {
12    pub api_base: String,
13    pub api_key: String,
14    pub model: String,
15}
16
17impl LlmConfig {
18    /// 从环境变量加载,空值使用默认(会自动加载 .env)
19    pub fn from_env() -> Self {
20        super::loader::load_dotenv();
21        Self {
22            api_base: env_or(llm::API_BASE, llm::API_BASE_ALIASES, || {
23                "https://api.openai.com/v1".to_string()
24            }),
25            api_key: env_or(llm::API_KEY, llm::API_KEY_ALIASES, String::new),
26            model: env_or(llm::MODEL, llm::MODEL_ALIASES, || "gpt-4o".to_string()),
27        }
28    }
29
30    /// 从环境变量加载,若 api_key 或 api_base 为空则返回 None
31    pub fn try_from_env() -> Option<Self> {
32        let cfg = Self::from_env();
33        if cfg.api_key.trim().is_empty() || cfg.api_base.trim().is_empty() {
34            None
35        } else {
36            Some(cfg)
37        }
38    }
39
40    /// 默认 model(当未显式设置时,按 api_base 推断)
41    pub fn default_model_for_base(api_base: &str) -> &'static str {
42        if api_base.contains("localhost:11434") || api_base.contains("127.0.0.1:11434") {
43            "qwen2.5:7b"
44        } else if api_base.contains("api.openai.com") {
45            "gpt-4o"
46        } else if api_base.contains("api.deepseek.com") {
47            "deepseek-chat"
48        } else if api_base.contains("dashscope.aliyuncs.com") {
49            "qwen-plus"
50        } else if api_base.contains("minimax") {
51            "MiniMax-M2.5"
52        } else {
53            "gpt-4o"
54        }
55    }
56}
57
58/// 工作区与输出路径配置
59#[derive(Debug, Clone)]
60pub struct PathsConfig {
61    pub workspace: String,
62    pub output_dir: Option<String>,
63    pub skills_repo: String,
64    /// 沙箱内 skill 路径的根目录,用于 path validation
65    pub skills_root: Option<String>,
66    pub data_dir: PathBuf,
67}
68
69impl PathsConfig {
70    pub fn from_env() -> Self {
71        let default_data_dir = crate::paths::data_root();
72        super::loader::load_dotenv();
73        let workspace =
74            super::loader::env_optional(super::env_keys::paths::SKILLLITE_WORKSPACE, &[])
75                .unwrap_or_else(|| {
76                    std::env::current_dir()
77                        .unwrap_or_else(|_| PathBuf::from("."))
78                        .to_string_lossy()
79                        .to_string()
80                });
81
82        let output_dir =
83            super::loader::env_optional(super::env_keys::paths::SKILLLITE_OUTPUT_DIR, &[]);
84
85        let skills_repo =
86            super::loader::env_or(super::env_keys::paths::SKILLLITE_SKILLS_REPO, &[], || {
87                "EXboys/skilllite".to_string()
88            });
89
90        let skills_root =
91            super::loader::env_optional(super::env_keys::paths::SKILLBOX_SKILLS_ROOT, &[]);
92
93        Self {
94            workspace,
95            output_dir,
96            skills_repo,
97            skills_root,
98            data_dir: default_data_dir,
99        }
100    }
101}
102
103/// Agent 功能开关
104#[derive(Debug, Clone)]
105pub struct AgentFeatureFlags {
106    pub enable_memory: bool,
107    pub enable_task_planning: bool,
108    /// 启用 Memory 向量检索(需 memory_vector feature + embedding API)
109    pub enable_memory_vector: bool,
110}
111
112impl AgentFeatureFlags {
113    pub fn from_env() -> Self {
114        Self {
115            enable_memory: env_bool("SKILLLITE_ENABLE_MEMORY", &[], true),
116            enable_task_planning: env_bool("SKILLLITE_ENABLE_TASK_PLANNING", &[], true),
117            enable_memory_vector: env_bool("SKILLLITE_ENABLE_MEMORY_VECTOR", &[], false),
118        }
119    }
120}
121
122/// Embedding API 配置(用于 memory vector 检索)
123#[derive(Debug, Clone)]
124#[allow(dead_code)] // used when memory_vector feature is enabled
125pub struct EmbeddingConfig {
126    pub model: String,
127    pub dimension: usize,
128    pub api_base: String,
129    pub api_key: String,
130}
131
132impl EmbeddingConfig {
133    pub fn from_env() -> Self {
134        super::loader::load_dotenv();
135        // 支持独立的 embedding API 配置
136        let api_base = super::loader::env_or(
137            "SKILLLITE_EMBEDDING_BASE_URL",
138            &["EMBEDDING_BASE_URL"],
139            || {
140                super::loader::env_or(llm::API_BASE, llm::API_BASE_ALIASES, || {
141                    "https://api.openai.com/v1".to_string()
142                })
143            },
144        );
145        let api_key = super::loader::env_or(
146            "SKILLLITE_EMBEDDING_API_KEY",
147            &["EMBEDDING_API_KEY"],
148            || super::loader::env_or(llm::API_KEY, llm::API_KEY_ALIASES, || "".to_string()),
149        );
150        let (default_model, default_dim) = Self::default_for_base(&api_base);
151        let model =
152            super::loader::env_or("SKILLLITE_EMBEDDING_MODEL", &["EMBEDDING_MODEL"], || {
153                default_model.to_string()
154            });
155        let dimension = super::loader::env_optional("SKILLLITE_EMBEDDING_DIMENSION", &[])
156            .and_then(|s| s.parse::<usize>().ok())
157            .unwrap_or(default_dim);
158        Self {
159            model,
160            dimension,
161            api_base,
162            api_key,
163        }
164    }
165
166    /// 按 api_base 推断默认 embedding 模型和维度
167    fn default_for_base(api_base: &str) -> (&'static str, usize) {
168        let base_lower = api_base.to_lowercase();
169        if base_lower.contains("dashscope.aliyuncs.com") {
170            // 通义千问 / Qwen
171            ("text-embedding-v3", 1024)
172        } else if base_lower.contains("api.deepseek.com") {
173            ("deepseek-embedding", 1536)
174        } else if base_lower.contains("generativelanguage.googleapis.com") {
175            // Google Gemini API (OpenAI 兼容端点)
176            ("gemini-embedding-001", 3072)
177        } else if base_lower.contains("localhost:11434") || base_lower.contains("127.0.0.1:11434") {
178            // Ollama
179            ("nomic-embed-text", 768)
180        } else if base_lower.contains("minimax") {
181            // MiniMax embedding
182            ("text-embedding-01", 1536)
183        } else {
184            ("text-embedding-3-small", 1536)
185        }
186    }
187}
188
189/// 可观测性配置:quiet、log_level、log_json、audit_log、security_events_log
190#[derive(Debug, Clone)]
191pub struct ObservabilityConfig {
192    pub quiet: bool,
193    pub log_level: String,
194    pub log_json: bool,
195    pub audit_log: Option<String>,
196    pub security_events_log: Option<String>,
197}
198
199impl ObservabilityConfig {
200    pub fn from_env() -> &'static Self {
201        use std::sync::OnceLock;
202        static CACHE: OnceLock<ObservabilityConfig> = OnceLock::new();
203        CACHE.get_or_init(|| {
204            super::loader::load_dotenv();
205            let quiet = env_bool(obv_keys::SKILLLITE_QUIET, obv_keys::QUIET_ALIASES, false);
206            let log_level = env_or(
207                obv_keys::SKILLLITE_LOG_LEVEL,
208                obv_keys::LOG_LEVEL_ALIASES,
209                || "skilllite=info,skilllite_swarm=info".to_string(),
210            );
211            let log_json = env_bool(
212                obv_keys::SKILLLITE_LOG_JSON,
213                obv_keys::LOG_JSON_ALIASES,
214                false,
215            );
216            let audit_disabled = env_bool(obv_keys::SKILLLITE_AUDIT_DISABLED, &[], false);
217            let audit_log = if audit_disabled {
218                None
219            } else {
220                env_optional(obv_keys::SKILLLITE_AUDIT_LOG, obv_keys::AUDIT_LOG_ALIASES).or_else(
221                    || {
222                        Some(
223                            crate::paths::data_root()
224                                .join("audit")
225                                .to_string_lossy()
226                                .into_owned(),
227                        )
228                    },
229                )
230            };
231            let security_events_log = env_optional(obv_keys::SKILLLITE_SECURITY_EVENTS_LOG, &[]);
232            Self {
233                quiet,
234                log_level,
235                log_json,
236                audit_log,
237                security_events_log,
238            }
239        })
240    }
241}
242
243/// 沙箱环境配置:级别、资源限制、开关等
244///
245/// 所有沙箱相关环境变量统一由此读取,兼容 `SKILLLITE_*` 与 `SKILLBOX_*`。
246/// runner、linux、macos、windows、policy 等应通过本配置访问,不再直接使用 env_compat。
247#[derive(Debug, Clone)]
248pub struct SandboxEnvConfig {
249    /// 沙箱级别 1/2/3,默认 3
250    pub sandbox_level: u8,
251    /// 最大内存 MB,默认 256
252    pub max_memory_mb: u64,
253    /// 执行超时秒数,默认 30
254    pub timeout_secs: u64,
255    /// 是否自动批准 L3 安全提示
256    pub auto_approve: bool,
257    /// 是否禁用沙箱(等同于 level 1)
258    pub no_sandbox: bool,
259    /// 是否允许 Playwright(放宽沙箱)
260    pub allow_playwright: bool,
261    /// 透传给脚本的额外参数(SKILLLITE_SCRIPT_ARGS / SKILLBOX_SCRIPT_ARGS)
262    pub script_args: Option<String>,
263}
264
265impl SandboxEnvConfig {
266    pub fn from_env() -> Self {
267        super::loader::load_dotenv();
268        let sandbox_level = env_or(
269            sb_keys::SKILLLITE_SANDBOX_LEVEL,
270            sb_keys::SANDBOX_LEVEL_ALIASES,
271            || "3".to_string(),
272        )
273        .parse::<u8>()
274        .ok()
275        .and_then(|n| if (1..=3).contains(&n) { Some(n) } else { None })
276        .unwrap_or(3);
277
278        let max_memory_mb = env_or(
279            sb_keys::SKILLLITE_MAX_MEMORY_MB,
280            sb_keys::MAX_MEMORY_MB_ALIASES,
281            || "256".to_string(),
282        )
283        .parse::<u64>()
284        .ok()
285        .unwrap_or(256);
286
287        let timeout_secs = env_or(
288            sb_keys::SKILLLITE_TIMEOUT_SECS,
289            sb_keys::TIMEOUT_SECS_ALIASES,
290            || "30".to_string(),
291        )
292        .parse::<u64>()
293        .ok()
294        .unwrap_or(30);
295
296        let auto_approve = env_bool(
297            sb_keys::SKILLLITE_AUTO_APPROVE,
298            sb_keys::AUTO_APPROVE_ALIASES,
299            false,
300        );
301        let no_sandbox = env_bool(
302            sb_keys::SKILLLITE_NO_SANDBOX,
303            sb_keys::NO_SANDBOX_ALIASES,
304            false,
305        );
306        let allow_playwright = env_bool(
307            sb_keys::SKILLLITE_ALLOW_PLAYWRIGHT,
308            sb_keys::ALLOW_PLAYWRIGHT_ALIASES,
309            false,
310        );
311        let script_args =
312            env_optional(sb_keys::SKILLLITE_SCRIPT_ARGS, sb_keys::SCRIPT_ARGS_ALIASES);
313
314        Self {
315            sandbox_level,
316            max_memory_mb,
317            timeout_secs,
318            auto_approve,
319            no_sandbox,
320            allow_playwright,
321            script_args,
322        }
323    }
324}
325
326/// 缓存目录配置
327#[derive(Debug, Clone)]
328pub struct CacheConfig;
329
330impl CacheConfig {
331    pub fn cache_dir() -> Option<String> {
332        super::loader::load_dotenv();
333        env_optional(
334            super::env_keys::cache::SKILLLITE_CACHE_DIR,
335            super::env_keys::cache::CACHE_DIR_ALIASES,
336        )
337    }
338}