259 lines
7.1 KiB
Go
259 lines
7.1 KiB
Go
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()
|
||
}
|