Files
2026-08-14 23:41:57 +08:00

259 lines
7.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 是接口类型:openaiOpenAI 兼容)或 anthropicAnthropic 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()
}