package agent import ( "encoding/json" "os" "path/filepath" "strings" "sync" ) // 支持的接口类型。 const ( ProviderOpenAI = "openai" ProviderAnthropic = "anthropic" ) // 协作 worker 管理模式。 const ( // CoopModeDocker 通过 Docker 容器管理 worker(需本机/远程 Docker daemon)。 CoopModeDocker = "docker" // CoopModeLocal 通过本地子进程管理 worker(无需 Docker,托管沙箱等场景)。 CoopModeLocal = "local" ) // LLM 配置环境变量名。环境变量优先级最高,覆盖 api-config.json 用户配置与内置默认值。 // 适用于托管沙箱等场景:平台注入环境变量,Agent 启动即生效,无需通过 Web 设置页配置。 const ( EnvLLMAPIKey = "LLM_API_KEY" EnvLLMBaseURL = "LLM_BASE_URL" EnvLLMModel = "LLM_MODEL" EnvLLMProvider = "LLM_PROVIDER" EnvLLMEngine = "LLM_ENGINE" EnvCoopMode = "COOP_MODE" ) // APIConfig 是用户可配置的 LLM API 连接信息。 type APIConfig struct { // APIKey 是模型服务商 API Key。 APIKey string `json:"api_key,omitempty"` // BaseURL 是模型接口地址。 BaseURL string `json:"base_url,omitempty"` // Model 是模型名称。 Model string `json:"model,omitempty"` // Provider 是接口类型:openai(OpenAI 兼容)或 anthropic(Anthropic Messages API)。 Provider string `json:"provider,omitempty"` // Engine 是默认协作引擎:pi(默认)/ pigo / claude。 // 仅影响 run_coop 未显式指定 engine 时的默认值,见 tools_coop.go。 Engine string `json:"engine,omitempty"` // CoopMode 是协作 worker 管理模式:local(默认,本地子进程)/ docker(Docker 容器)。 // local 模式无需 Docker,适用于托管沙箱等无 Docker 环境;docker 模式需本机/远程 Docker daemon。 CoopMode string `json:"coop_mode,omitempty"` } // APIConfigStore 持久化用户的 LLM API 配置,未配置的字段回退到默认值(环境变量)。 type APIConfigStore struct { mu sync.RWMutex path string config APIConfig defaults APIConfig } func NewAPIConfigStore(dataDir string, defaults APIConfig) (*APIConfigStore, error) { store := &APIConfigStore{ path: filepath.Join(dataDir, "api-config.json"), defaults: defaults, } store.load() return store, nil } func (s *APIConfigStore) load() { data, err := os.ReadFile(s.path) if err != nil { return } if len(data) == 0 { return } var config APIConfig if err := json.Unmarshal(data, &config); err != nil { return } s.config = config } func (s *APIConfigStore) persist() error { data, err := json.MarshalIndent(s.config, "", " ") if err != nil { return err } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, data, 0o600); err != nil { return err } return os.Rename(tmp, s.path) } // APIKey 返回生效的 API Key。 // 优先级:环境变量 LLM_API_KEY > api-config.json > 内置默认值。 func (s *APIConfigStore) APIKey() string { if v := strings.TrimSpace(os.Getenv(EnvLLMAPIKey)); v != "" { return v } s.mu.RLock() defer s.mu.RUnlock() if s.config.APIKey != "" { return s.config.APIKey } return s.defaults.APIKey } // BaseURL 返回生效的接口地址。 // 优先级:环境变量 LLM_BASE_URL > api-config.json > 内置默认值。 func (s *APIConfigStore) BaseURL() string { if v := strings.TrimSpace(os.Getenv(EnvLLMBaseURL)); v != "" { return v } s.mu.RLock() defer s.mu.RUnlock() if s.config.BaseURL != "" { return s.config.BaseURL } return s.defaults.BaseURL } // Model 返回生效的模型名称。 // 优先级:环境变量 LLM_MODEL > api-config.json > 内置默认值。 func (s *APIConfigStore) Model() string { if v := strings.TrimSpace(os.Getenv(EnvLLMModel)); v != "" { return v } s.mu.RLock() defer s.mu.RUnlock() if s.config.Model != "" { return s.config.Model } return s.defaults.Model } // IsAPIKeyConfigured 报告是否已配置 API Key(含环境变量、用户配置与内置默认值)。 // LLM 客户端实际会按 APIKey() 的优先级(环境变量 > api-config.json > 默认值)取用, // 因此只要最终能拿到非空 Key 就视为已配置,避免误判导致无法对话。 func (s *APIConfigStore) IsAPIKeyConfigured() bool { if v := strings.TrimSpace(os.Getenv(EnvLLMAPIKey)); v != "" { return true } s.mu.RLock() defer s.mu.RUnlock() return s.config.APIKey != "" || s.defaults.APIKey != "" } // Provider 返回生效的接口类型。 // 优先级:环境变量 LLM_PROVIDER > api-config.json > 内置默认值 > openai。 func (s *APIConfigStore) Provider() string { if v := strings.TrimSpace(os.Getenv(EnvLLMProvider)); v != "" { if v == ProviderOpenAI || v == ProviderAnthropic { return v } } s.mu.RLock() defer s.mu.RUnlock() if s.config.Provider != "" { return s.config.Provider } if s.defaults.Provider != "" { return s.defaults.Provider } return ProviderOpenAI } // Engine 返回生效的默认协作引擎:pi(默认)/ pigo / claude。 // 优先级:环境变量 LLM_ENGINE > api-config.json > 内置默认值 > pi。 // 仅在 run_coop 未显式指定 engine 时使用。 func (s *APIConfigStore) Engine() string { if v := strings.TrimSpace(os.Getenv(EnvLLMEngine)); v != "" { switch v { case "pi", "pigo", "claude": return v } } s.mu.RLock() defer s.mu.RUnlock() switch s.config.Engine { case "pi", "pigo", "claude": return s.config.Engine } if s.defaults.Engine != "" { return s.defaults.Engine } return "pi" } // CoopMode 返回生效的协作 worker 管理模式:local(默认)/ docker。 // 优先级:环境变量 COOP_MODE > api-config.json > 内置默认值 > local。 // local 模式通过本地子进程运行 worker(无需 Docker),docker 模式通过 Docker 容器运行。 func (s *APIConfigStore) CoopMode() string { if v := strings.TrimSpace(os.Getenv(EnvCoopMode)); v != "" { switch v { case CoopModeDocker, CoopModeLocal: return v } } s.mu.RLock() defer s.mu.RUnlock() switch s.config.CoopMode { case CoopModeDocker, CoopModeLocal: return s.config.CoopMode } if s.defaults.CoopMode != "" { return s.defaults.CoopMode } return CoopModeLocal } // Update 更新用户配置。每个参数为 nil 表示不修改该字段; // 非 nil(含空字符串)表示设置或清除该字段(空串=清除,回退默认)。 func (s *APIConfigStore) Update(apiKey, baseURL, model, provider, engine, coopMode *string) error { s.mu.Lock() defer s.mu.Unlock() if apiKey != nil { s.config.APIKey = *apiKey } if baseURL != nil { s.config.BaseURL = *baseURL } if model != nil { s.config.Model = *model } if provider != nil { value := strings.TrimSpace(*provider) if value != ProviderOpenAI && value != ProviderAnthropic { value = "" } s.config.Provider = value } if engine != nil { value := strings.TrimSpace(*engine) switch value { case "pi", "pigo", "claude": // 合法值 default: value = "" } s.config.Engine = value } if coopMode != nil { value := strings.TrimSpace(*coopMode) switch value { case CoopModeDocker, CoopModeLocal: // 合法值 default: value = "" } s.config.CoopMode = value } return s.persist() }