Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 0 additions & 87 deletions internal/config/builtin_providers.go

This file was deleted.

117 changes: 0 additions & 117 deletions internal/config/builtin_providers_test.go

This file was deleted.

185 changes: 185 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package config

import (
"errors"
"fmt"
"os"
"path/filepath"
goruntime "runtime"
"strings"
)

const (
DefaultWorkdir = "."
DefaultMaxLoops = 8
DefaultToolTimeoutSec = 20
)

type Config struct {
Providers []ProviderConfig `yaml:"-"`
SelectedProvider string `yaml:"selected_provider"`
CurrentModel string `yaml:"current_model"`
Workdir string `yaml:"-"`
Shell string `yaml:"shell"`
MaxLoops int `yaml:"max_loops,omitempty"`
ToolTimeoutSec int `yaml:"tool_timeout_sec,omitempty"`
Context ContextConfig `yaml:"context,omitempty"`
Tools ToolsConfig `yaml:"tools,omitempty"`
Memo MemoConfig `yaml:"memo,omitempty"`
}

// StaticDefaults 返回 config 层负责的静态默认值骨架,不包含 provider 装配和选择状态修复。
func StaticDefaults() *Config {
return &Config{
Workdir: DefaultWorkdir,
Shell: defaultShell(),
MaxLoops: DefaultMaxLoops,
ToolTimeoutSec: DefaultToolTimeoutSec,
Context: defaultContextConfig(),
Tools: ToolsConfig{
WebFetch: defaultWebFetchConfig(),
MCP: defaultMCPConfig(),
},
Memo: defaultMemoConfig(),
}
}

func (c *Config) Clone() Config {
if c == nil {
return *StaticDefaults()
}

clone := *c
clone.Providers = cloneProviders(c.Providers)
clone.Context = c.Context.Clone()
clone.Tools = c.Tools.Clone()
clone.Memo = c.Memo.Clone()
return clone
}

// applyStaticDefaults 仅补齐静态配置默认值
func (c *Config) applyStaticDefaults(defaults Config) {
if c == nil {
return
}

if strings.TrimSpace(c.Workdir) == "" {
c.Workdir = defaults.Workdir
}
if strings.TrimSpace(c.Shell) == "" {
c.Shell = defaults.Shell
}
if c.MaxLoops <= 0 {
c.MaxLoops = defaults.MaxLoops
}
if c.ToolTimeoutSec <= 0 {
c.ToolTimeoutSec = defaults.ToolTimeoutSec
}
c.Context.ApplyDefaults(defaults.Context)
c.Tools.ApplyDefaults(defaults.Tools)
c.Memo.ApplyDefaults(defaults.Memo)

c.Workdir = normalizeWorkdir(c.Workdir)
}

// ValidateSnapshot 校验配置快照本身是否结构完整
func (c *Config) ValidateSnapshot() error {
if c == nil {
return errors.New("config: config is nil")
}
if len(c.Providers) == 0 {
return errors.New("config: providers is empty")
}

seen := make(map[string]struct{}, len(c.Providers))
seenEndpoints := make(map[string]string, len(c.Providers))
for i, provider := range c.Providers {
if err := provider.Validate(); err != nil {
return fmt.Errorf("config: provider[%d]: %w", i, err)
}

key := normalizeProviderName(provider.Name)
if _, exists := seen[key]; exists {
return fmt.Errorf("config: duplicate provider name %q", provider.Name)
}
seen[key] = struct{}{}

identity, err := provider.Identity()
if err != nil {
return fmt.Errorf("config: provider[%d]: %w", i, err)
}
if existingName, exists := seenEndpoints[identity.Key()]; exists {
return fmt.Errorf(
"config: duplicate provider endpoint %q for providers %q and %q",
identity.String(),
existingName,
provider.Name,
)
}
seenEndpoints[identity.Key()] = provider.Name
}

if strings.TrimSpace(c.Workdir) == "" {
return errors.New("config: workdir is empty")
}
if !filepath.IsAbs(c.Workdir) {
return fmt.Errorf("config: workdir must be absolute, got %q", c.Workdir)
}
if err := c.Tools.Validate(); err != nil {
return fmt.Errorf("config: tools: %w", err)
}
if err := c.Context.Validate(); err != nil {
return fmt.Errorf("config: context: %w", err)
}
if err := c.Memo.Validate(); err != nil {
return fmt.Errorf("config: memo: %w", err)
}

return nil
}

func (c *Config) ProviderByName(name string) (ProviderConfig, error) {
if c == nil {
return ProviderConfig{}, errors.New("config: config is nil")
}

target := normalizeProviderName(name)
for _, provider := range c.Providers {
if normalizeProviderName(provider.Name) == target {
return provider, nil
}
}

return ProviderConfig{}, fmt.Errorf("config: provider %q not found", name)
}

func normalizeWorkdir(workdir string) string {
workdir = strings.TrimSpace(workdir)
if workdir == "" {
return ""
}

if workdir == "." {
if wd, err := os.Getwd(); err == nil {
return wd
}
return workdir
}

if filepath.IsAbs(workdir) {
return filepath.Clean(workdir)
}

if wd, err := os.Getwd(); err == nil {
return filepath.Clean(filepath.Join(wd, workdir))
}

return filepath.Clean(workdir)
}

func defaultShell() string {
if goruntime.GOOS == "windows" {
return "powershell"
}
return "bash"
}
Loading
Loading