// Package config 为 Orca 框架提供配置管理功能。 // // 配置从 TOML 文件加载,默认路径为 ~/.orca/config.toml。 // 所有模型参数和 Agent 身份描述均通过配置文件管理, // 不再从环境变量读取。 package config import ( "fmt" "os" "path/filepath" "time" "github.com/BurntSushi/toml" ) const ( // ProviderOllama 选择本地 Ollama LLM 后端。 ProviderOllama = "ollama" // ProviderDeepSeek 选择云端 DeepSeek LLM 后端。 ProviderDeepSeek = "deepseek" ) // Config 保存 Orca 框架的所有配置信息。 type Config struct { Provider string `toml:"provider"` Ollama OllamaConfig `toml:"ollama"` DeepSeek DeepSeekConfig `toml:"deepseek"` Sandbox SandboxConfig `toml:"sandbox"` Session SessionConfig `toml:"session"` Agent AgentConfig `toml:"agent"` } // OllamaConfig 保存 Ollama LLM 后端的设置。 type OllamaConfig struct { BaseURL string `toml:"base_url"` Model string `toml:"model"` Timeout time.Duration `toml:"timeout"` } // DeepSeekConfig 保存 DeepSeek LLM 后端的设置。 type DeepSeekConfig struct { BaseURL string `toml:"base_url"` Model string `toml:"model"` APIKey string `toml:"api_key"` Timeout time.Duration `toml:"timeout"` } // SandboxConfig 保存命令执行沙箱的设置。 type SandboxConfig struct { Timeout time.Duration `toml:"timeout"` MaxMemory int64 `toml:"max_memory"` WorkingDir string `toml:"working_dir"` } // SessionConfig 保存对话会话存储的设置。 type SessionConfig struct { StorageDir string `toml:"storage_dir"` MaxHistory int `toml:"max_history"` } // AgentConfig 保存 Agent 身份和行为的配置。 type AgentConfig struct { // Role 是 Agent 的角色标识,如 "assistant", "coder", "reviewer" 等。 Role string `toml:"role"` // SystemPrompt 是 Agent 的系统提示词(直接内联配置)。 // 如果 PromptFile 也配置了,PromptFile 优先级更高。 SystemPrompt string `toml:"system_prompt"` // PromptFile 是外部提示词文件的路径(相对于 ~/.orca/ 或绝对路径)。 // 如果文件存在,其内容会作为 system prompt 使用。 PromptFile string `toml:"prompt_file"` } // DefaultConfig 返回默认配置。 // 注意:模型相关默认值均为空字符串,要求用户必须在 config.toml 中配置。 func DefaultConfig() *Config { home, _ := os.UserHomeDir() return &Config{ Provider: ProviderDeepSeek, Ollama: OllamaConfig{ BaseURL: "http://localhost:11434", Model: "", Timeout: 120 * time.Second, }, DeepSeek: DeepSeekConfig{ BaseURL: "https://api.deepseek.com/v1", Model: "", APIKey: "", Timeout: 120 * time.Second, }, Sandbox: SandboxConfig{ Timeout: 30 * time.Second, MaxMemory: 512 * 1024 * 1024, WorkingDir: filepath.Join(home, ".orca", "sandbox"), }, Session: SessionConfig{ StorageDir: filepath.Join(home, ".orca", "sessions"), MaxHistory: 100, }, Agent: AgentConfig{ Role: "assistant", SystemPrompt: "", PromptFile: "", }, } } // LoadConfigFromFile 从指定路径加载 TOML 配置文件。 // 如果文件不存在,返回默认配置。 func LoadConfigFromFile(path string) (*Config, error) { cfg := DefaultConfig() if _, err := os.Stat(path); os.IsNotExist(err) { return cfg, nil } if _, err := toml.DecodeFile(path, cfg); err != nil { return nil, fmt.Errorf("config: failed to load %q: %w", path, err) } return cfg, nil } // LoadConfig 从默认路径加载配置。 // 默认路径优先级: // 1. ./config.toml(当前工作目录) // 2. ~/.orca/config.toml func LoadConfig() (*Config, error) { // 尝试当前目录 if _, err := os.Stat("config.toml"); err == nil { return LoadConfigFromFile("config.toml") } // 尝试用户主目录 home, err := os.UserHomeDir() if err != nil { return DefaultConfig(), nil } defaultPath := filepath.Join(home, ".orca", "config.toml") return LoadConfigFromFile(defaultPath) } // GetSystemPrompt 返回 Agent 的系统提示词。 // 优先级:PromptFile > SystemPrompt > 空字符串。 func (c *Config) GetSystemPrompt() string { // 如果配置了外部文件,优先读取文件 if c.Agent.PromptFile != "" { path := c.Agent.PromptFile // 如果是相对路径,基于 ~/.orca/ 解析 if !filepath.IsAbs(path) { home, _ := os.UserHomeDir() path = filepath.Join(home, ".orca", path) } if data, err := os.ReadFile(path); err == nil { return string(data) } } // 返回内联配置的 prompt return c.Agent.SystemPrompt } // GetPromptsDir 返回提示词文件目录。 func GetPromptsDir() string { home, _ := os.UserHomeDir() return filepath.Join(home, ".orca", "prompts") } func (c *Config) IsValid() error { if c.Provider != ProviderOllama && c.Provider != ProviderDeepSeek { return errConfig("provider must be 'ollama' or 'deepseek'") } if c.Provider == ProviderOllama { if c.Ollama.BaseURL == "" { return errConfig("ollama.base_url must not be empty") } if c.Ollama.Model == "" { return errConfig("ollama.model must not be empty") } if c.Ollama.Timeout <= 0 { return errConfig("ollama.timeout must be positive") } } if c.Provider == ProviderDeepSeek { if c.DeepSeek.BaseURL == "" { return errConfig("deepseek.base_url must not be empty") } if c.DeepSeek.Model == "" { return errConfig("deepseek.model must not be empty") } if c.DeepSeek.APIKey == "" { return errConfig("deepseek.api_key must not be empty") } if c.DeepSeek.Timeout <= 0 { return errConfig("deepseek.timeout must be positive") } } if c.Sandbox.Timeout <= 0 { return errConfig("sandbox.timeout must be positive") } if c.Sandbox.MaxMemory <= 0 { return errConfig("sandbox.max_memory must be positive") } if c.Session.MaxHistory <= 0 { return errConfig("session.max_history must be positive") } return nil } func errConfig(msg string) error { return &ConfigError{Message: msg} } type ConfigError struct { Message string } func (e *ConfigError) Error() string { return "config: " + e.Message }