From 85a3d1e27d5e9f8ad51d29df5686fb0bab105227 Mon Sep 17 00:00:00 2001 From: hhs <386998068@qq.com> Date: Wed, 10 Jun 2026 13:44:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20=E5=AE=9E=E7=8E=B0=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=202=20=E9=85=8D=E7=BD=AE=E5=8A=A0=E8=BD=BD=20?= =?UTF-8?q?=E2=80=94=20=E5=BA=94=E7=94=A8=E9=85=8D=E7=BD=AE=E4=B8=8E=20Age?= =?UTF-8?q?nt=20=E9=85=8D=E7=BD=AE=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 + go.sum | 3 + internal/config/application.go | 74 ++++++++++++++++++++ internal/config/loader.go | 121 +++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100644 go.sum diff --git a/go.mod b/go.mod index 975bea2..12c58d1 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module ai-agent-scaffold-go go 1.26.2 + +require gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4bc0337 --- /dev/null +++ b/go.sum @@ -0,0 +1,3 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/application.go b/internal/config/application.go index d912156..cc3d9e0 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -1 +1,75 @@ package config + +import ( + "fmt" + "os" + "time" + + "gopkg.in/yaml.v3" +) + +// Application 应用配置顶层结构 +type Application struct { + App AppSection `yaml:"app"` + Server ServerSection `yaml:"server"` + Agent AgentSection `yaml:"agent"` + LLM LLMSection `yaml:"llm"` +} + +// AppSection 应用基础信息 +type AppSection struct { + Name string `yaml:"name"` + Env string `yaml:"env"` +} + +// ServerSection 服务器配置 +type ServerSection struct { + Addr string `yaml:"addr"` +} + +// AgentSection Agent 配置路径列表 +type AgentSection struct { + ConfigPaths []string `yaml:"config-paths"` +} + +// LLMSection LLM 相关配置 +type LLMSection struct { + RequestTimeout string `yaml:"request-timeout"` +} + +const defaultLLMRequestTimeout = 5 * time.Minute + +// RequestTimeoutDuration 解析 LLM 请求超时时间 +func (s LLMSection) RequestTimeoutDuration() (time.Duration, error) { + if s.RequestTimeout == "" { + return defaultLLMRequestTimeout, nil + } + d, err := time.ParseDuration(s.RequestTimeout) + if err != nil { + return 0, fmt.Errorf("invalid llm.request-timeout %q: %w", s.RequestTimeout, err) + } + if d <= 0 { + return 0, fmt.Errorf("llm.request-timeout must be positive, got %q", s.RequestTimeout) + } + return d, nil +} + +// LoadApplication 从指定路径加载应用配置 +func LoadApplication(path string) (Application, error) { + data, err := os.ReadFile(path) + if err != nil { + return Application{}, fmt.Errorf("read application config %s: %w", path, err) + } + var app Application + if err := yaml.Unmarshal(data, &app); err != nil { + return Application{}, fmt.Errorf("parse application config: %w", err) + } + // 设置默认值 + if app.Server.Addr == "" { + app.Server.Addr = ":8091" + } + if app.App.Env == "" { + app.App.Env = "local" + } + return app, nil +} diff --git a/internal/config/loader.go b/internal/config/loader.go index d912156..2807699 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -1 +1,122 @@ package config + +import ( + "fmt" + "os" + "regexp" + "strings" + + "ai-agent-scaffold-go/internal/model" + "gopkg.in/yaml.v3" +) + +// agentRoot 对应 YAML 的 ai.agent.config.tables 结构 +type agentRoot struct { + AI struct { + Agent struct { + Config struct { + Tables map[string]model.AiAgentConfigTable `yaml:"tables"` + } `yaml:"config"` + } `yaml:"agent"` + } `yaml:"ai"` +} + +// LoadAgentTables 从字节数据加载 Agent 配置表 +func LoadAgentTables(data []byte) (map[string]model.AiAgentConfigTable, error) { + expanded := expandEnvPlaceholders(string(data)) + var root agentRoot + if err := yaml.Unmarshal([]byte(expanded), &root); err != nil { + return nil, fmt.Errorf("parse agent config: %w", err) + } + tables := root.AI.Agent.Config.Tables + if len(tables) == 0 { + return nil, fmt.Errorf("agent config tables are required") + } + for name, table := range tables { + normalizeDefaults(&table) + if err := validateTable(name, table); err != nil { + return nil, err + } + tables[name] = table + } + return tables, nil +} + +// LoadAgentTablesFile 从文件路径加载 Agent 配置表 +func LoadAgentTablesFile(path string) (map[string]model.AiAgentConfigTable, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read agent config %s: %w", path, err) + } + return LoadAgentTables(data) +} + +// envPlaceholderRE 匹配 ${VAR} 和 ${VAR:-default} 格式的环境变量占位符 +var envPlaceholderRE = regexp.MustCompile(`\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}`) + +// expandEnvPlaceholders 替换字符串中的环境变量占位符 +func expandEnvPlaceholders(input string) string { + return envPlaceholderRE.ReplaceAllStringFunc(input, func(match string) string { + groups := envPlaceholderRE.FindStringSubmatch(match) + name := groups[1] + if value, ok := os.LookupEnv(name); ok && value != "" { + return value + } + if len(groups) > 2 { + return groups[2] + } + return "" + }) +} + +// normalizeDefaults 填充配置默认值 +func normalizeDefaults(table *model.AiAgentConfigTable) { + if table.Module.AiAPI.CompletionsPath == "" { + table.Module.AiAPI.CompletionsPath = "v1/chat/completions" + } + for i := range table.Module.AgentWorkflows { + if table.Module.AgentWorkflows[i].MaxIterations == 0 { + table.Module.AgentWorkflows[i].MaxIterations = 3 + } + } +} + +// validateTable 校验配置表的必填字段 +func validateTable(name string, table model.AiAgentConfigTable) error { + prefix := "agent table " + name + required := map[string]string{ + "app-name": table.AppName, + "agent.agent-id": table.Agent.AgentID, + "module.ai-api.base-url": table.Module.AiAPI.BaseURL, + "module.ai-api.api-key": table.Module.AiAPI.APIKey, + "module.chat-model.model": table.Module.ChatModel.Model, + "module.runner.agent-name": table.Module.Runner.AgentName, + } + for field, value := range required { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("%s: %s is required", prefix, field) + } + } + if len(table.Module.Agents) == 0 { + return fmt.Errorf("%s: module.agents is required", prefix) + } + for i, agent := range table.Module.Agents { + if strings.TrimSpace(agent.Name) == "" { + return fmt.Errorf("%s: module.agents[%d].name is required", prefix, i) + } + if strings.TrimSpace(agent.Instruction) == "" { + return fmt.Errorf("%s: module.agents[%d].instruction is required", prefix, i) + } + } + for i, workflow := range table.Module.AgentWorkflows { + switch workflow.Type { + case model.WorkflowTypeLoop, model.WorkflowTypeParallel, model.WorkflowTypeSequential: + default: + return fmt.Errorf("%s: module.agent-workflows[%d].type is invalid: %s", prefix, i, workflow.Type) + } + if strings.TrimSpace(workflow.Name) == "" { + return fmt.Errorf("%s: module.agent-workflows[%d].name is required", prefix, i) + } + } + return nil +}