- Add bubbletea/lipgloss/glamour dependencies for TUI - Create internal/tui package with EventWriter, styles, and bubbletea Model - Support streaming output display in conversation window - Add right panel with statistics and active agent status - Implement multi-agent collaboration with sub-agents - Add AgentCallTool for delegating tasks to sub-agents - Support parallel tool execution with goroutines - Auto-discover sub-agents from ~/.orca/prompts/ directory - Fix orchestrator routing based on msg.To field - Add non-blocking event writer with timeout to prevent blocking
226 lines
6.0 KiB
Go
226 lines
6.0 KiB
Go
// 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
|
||
}
|