Files
xk-ai-agent/internal/config/config.go
2026-08-14 21:50:48 +08:00

338 lines
12 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 config
import (
"os"
"gopkg.in/yaml.v3"
)
// ========================================================================
// 配置模块 —— 支持多模型、多场景路由
// ========================================================================
// 核心设计:
// - LLM 配置从"单一模型"升级为"模型池"
// - 每个模型有独立名称、供应商、API Key、BaseURL、模型名
// - 通过 Routes 映射"业务场景"到"具体模型"
// - 通过 FallbackChains 配置降级链
//
// 配置优先级:环境变量 > config.yaml > 代码默认值
// ========================================================================
// Config 全局配置结构体
type Config struct {
Server ServerConfig `yaml:"server"` // HTTP 服务配置
MaxKB MaxKBConfig `yaml:"maxkb"` // MaxKB 知识库配置
LLM LLMConfig `yaml:"llm"` // 多模型 LLM 配置
DB DBConfig `yaml:"db"` // 数据库配置
Agent AgentConfig `yaml:"agent"` // Agent 引擎配置
KB KBConfigYAML `yaml:"kb"` // 本地知识库配置V1 用 noopV2 接 BGE-M3
Panel PanelConfig `yaml:"panel"` // 独立管理前端(/admin SPA登录配置
}
// PanelConfig 独立管理前端的登录凭据配置
//
// 为什么单独一组而不复用 KB.AdminPassword
// - 旧面板走口令头X-KB-Admin-Password新前端走「账号+密码+验证码 → JWT」
// 两套体系并存互不影响,凭据分开配置便于独立轮换
// - 验证码是固定值(内网面板防脚本误触即可,不做图形码)
//
// 默认值在 Load() 里写死兜底,生产环境建议在 config.yaml 覆盖
type PanelConfig struct {
Username string `yaml:"username"` // 登录账号
Password string `yaml:"password"` // 登录密码
Captcha string `yaml:"captcha"` // 固定验证码
}
// ServerConfig HTTP 服务器配置
type ServerConfig struct {
Port string `yaml:"port"` // 监听端口
}
// MaxKBConfig MaxKB 知识库平台配置
type MaxKBConfig struct {
BaseURL string `yaml:"base_url"` // MaxKB 服务地址
APIKey string `yaml:"api_key"` // 应用 API Key
AppID string `yaml:"app_id"` // 知识库应用 ID
}
// ========================================================================
// LLM 多模型配置(核心升级点)
// ========================================================================
// LLMConfig 大语言模型总配置
//
// 示例 YAML
//
// llm:
// default_provider: "deepseek" # 默认供应商
// models:
// deepseek:
// provider: "deepseek"
// api_key: "sk-xxx"
// base_url: "https://api.deepseek.com"
// model: "deepseek-chat"
// timeout: 120
// openai:
// provider: "openai"
// api_key: "sk-xxx"
// base_url: "https://api.openai.com/v1"
// model: "gpt-4o"
// embedding_model: "text-embedding-3-small"
// timeout: 120
// ollama:
// provider: "ollama"
// base_url: "http://localhost:11434"
// model: "qwen2.5:72b"
// timeout: 300
// qwen:
// provider: "qwen"
// api_key: "sk-xxx"
// base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1"
// model: "qwen-max"
// timeout: 120
// routes:
// emr-generator: "deepseek" # 病历生成用 DeepSeek
// prescription: "openai" # 处方校验用 GPT-4o
// knowledge-qa: "qwen" # 知识问答用通义千问
// embedding: "openai" # 向量化用 OpenAI
// fallback: "ollama" # 降级兜底用本地模型
// fallback_chains:
// emr-generator: ["deepseek", "qwen", "ollama"]
// prescription: ["openai", "deepseek"]
type LLMConfig struct {
DefaultProvider string `yaml:"default_provider"` // 默认供应商名
Models map[string]LLMConfigEx `yaml:"models"` // 模型池(名称→配置)
Routes map[string]string `yaml:"routes"` // 场景→模型路由
FallbackChains map[string][]string `yaml:"fallback_chains"` // 降级链
}
// LLMConfigEx 单个模型的扩展配置
//
// 在基础配置上增加了 Extra 字段,用于存放各供应商特有的参数
// (如 Azure 的 deployment/api_versionOllama 的 num_ctx 等)
type LLMConfigEx struct {
Provider string `yaml:"provider"` // 供应商类型deepseek/openai/azure/ollama/qwen
APIKey string `yaml:"api_key"` // API 密钥
BaseURL string `yaml:"base_url"` // API 地址
Model string `yaml:"model"` // 模型名称
EmbeddingModel string `yaml:"embedding_model"` // Embedding 模型(可选)
Timeout int `yaml:"timeout"` // 超时(秒)
Extra map[string]string `yaml:"extra"` // 供应商特有参数
}
// DBConfig 数据库配置
type DBConfig struct {
DSN string `yaml:"dsn"` // z_xk 库连接串
EncryptKey string `yaml:"encrypt_key"` // AES 加密主密钥(与 PHP .env ENCRYPT_KEY 同源)
}
// AgentConfig Agent 引擎配置
//
// 字段分两组:
// - 引擎行为MaxIterationsReAct 最大轮数)/ Timeout单次调用超时秒数
// - 服务鉴权SharedSecret / JWTSecretmiddleware.Auth 读取)
//
// 鉴权双轨制(与 PHP 后台 ai_agent_secret 对接):
// - SharedSecret简单字符串密钥运维在 PHP 后台填一个值即可
// PHP TcmAgentClient 把同样的字符串塞进 Authorization: Bearer xxx
// Go 端 middleware.Auth 拿到后直接 == 比对,无需签发 JWT
// - JWTSecret严格 HS256 签名校验,适合需要解析 user_id/role 的场景
// (如小程序直连 Agent留空则禁用 JWT 路径,仅比对 SharedSecret
//
// middleware.Auth 校验顺序JWT 先试 → SharedSecret 比对 → 默认密钥(开发模式)
type AgentConfig struct {
MaxIterations int `yaml:"max_iterations"` // Agent 最大推理轮数
Timeout int `yaml:"timeout"` // 单次调用超时(秒)
SharedSecret string `yaml:"shared_secret"` // 共享密钥(与 PHP 后台 ai_agent_secret 对应;空则放行无 Token 请求)
JWTSecret string `yaml:"jwt_secret"` // JWT 签名密钥(留空则禁用 JWT 路径,仅启用 SharedSecret
}
// KBConfigYAML 本地知识库的 YAML 配置(兜底用)
//
// 注意V1 实际开关在 xk_system_config 表里agentcfg 包加载),
// 这里只是兜底——DB 不可用或 key 缺失时回落到这些值。
//
// V2 接入 BGE-M3 时建议在 yaml 里配 api_key避免明文入 DB
type KBConfigYAML struct {
EmbeddingProvider string `yaml:"embedding_provider"` // 向量化 providernoop / bge_m3 / aliyun / openai
EmbeddingAPIKey string `yaml:"embedding_api_key"` // embedding API KeyV2 接 BGE-M3 时填)
EmbeddingBaseURL string `yaml:"embedding_base_url"` // embedding 服务地址(如本地部署的 BGE-M3 HTTP 服务)
EmbeddingModel string `yaml:"embedding_model"` // embedding 模型名(如 bge-m3
AdminPassword string `yaml:"admin_password"` // /kb/view 后台访问口令HTTP Basic Auth 密码)
}
// ========================================================================
// 接口实现(供 agent.Runner 的 InitRunner 使用)
// ========================================================================
//
// Runner 通过接口(而非具体类型)获取配置,实现解耦。
// 这样 testing 时可以用 mock 配置替换。
// GetMaxKB 返回 MaxKB 配置(实现 MaxKBConfigGetter 接口)
func (c *Config) GetMaxKB() MaxKBConfigGetter {
return &maxKBWrapper{c.MaxKB}
}
// GetAgent 返回 Agent 配置(实现 AgentConfigGetter 接口)
func (c *Config) GetAgent() AgentConfigGetter {
return &agentCfgWrapper{c.Agent}
}
// MaxKBConfigGetter MaxKB 配置读取接口
type MaxKBConfigGetter interface {
GetBaseURL() string
GetAPIKey() string
GetAppID() string
}
// AgentConfigGetter Agent 配置读取接口
type AgentConfigGetter interface {
GetMaxIterations() int
GetTimeout() int
}
// maxKBWrapper 包装 MaxKBConfig 实现接口
type maxKBWrapper struct{ cfg MaxKBConfig }
func (w *maxKBWrapper) GetBaseURL() string { return w.cfg.BaseURL }
func (w *maxKBWrapper) GetAPIKey() string { return w.cfg.APIKey }
func (w *maxKBWrapper) GetAppID() string { return w.cfg.AppID }
// agentCfgWrapper 包装 AgentConfig 实现接口
type agentCfgWrapper struct{ cfg AgentConfig }
func (w *agentCfgWrapper) GetMaxIterations() int { return w.cfg.MaxIterations }
func (w *agentCfgWrapper) GetTimeout() int { return w.cfg.Timeout }
// ========================================================================
// 配置加载
// ========================================================================
// Load 加载配置文件
//
// 加载顺序(后者覆盖前者):
// 1. 代码内置默认值
// 2. manifest/config/config.yaml 文件
// 3. 环境变量(容器化部署时常用)
//
// 环境变量映射:
// SERVER_PORT → server.port
// MAXKB_API_KEY → maxkb.api_key
// LLM_API_KEY → 所有模型的 api_key通用覆盖
// DEEPSEEK_API_KEY → 仅覆盖 deepseek 模型
// OPENAI_API_KEY → 仅覆盖 openai 模型
// QWEN_API_KEY → 仅覆盖 qwen 模型
// AZURE_API_KEY → 仅覆盖 azure 模型
// OLLAMA_URL → 仅覆盖 ollama 的 base_url
func Load() *Config {
cfg := &Config{
// 默认值
Server: ServerConfig{Port: "8080"},
Agent: AgentConfig{MaxIterations: 10, Timeout: 120},
// 管理前端登录默认凭据(生产环境在 config.yaml 的 panel 段覆盖)
Panel: PanelConfig{Username: "liqi", Password: "qiqi991012", Captcha: "999999"},
LLM: LLMConfig{
DefaultProvider: "deepseek",
Models: map[string]LLMConfigEx{
"deepseek": {
Provider: "deepseek",
BaseURL: "https://api.deepseek.com",
Model: "deepseek-chat",
Timeout: 120,
},
},
Routes: map[string]string{
// PHP TcmAgentClient 透传的业务场景名(必须能命中,否则会走默认 provider
"medical_record": "deepseek", // 病历生成PHP 业务侧叫 medical_record
"prescription": "deepseek", // 处方生成
// Go 内部兼容名Go 自己的 /api/v1/emr/generate 等端点用 emr-generator
"emr-generator": "deepseek",
"knowledge-qa": "deepseek",
"embedding": "deepseek",
},
},
}
// 尝试读取 YAML 配置文件
data, err := os.ReadFile("manifest/config/config.yaml")
if err == nil {
yaml.Unmarshal(data, cfg)
}
// 环境变量覆盖(优先级最高)
applyEnvOverrides(cfg)
return cfg
}
// applyEnvOverrides 用环境变量覆盖配置
func applyEnvOverrides(cfg *Config) {
// Server
if port := os.Getenv("SERVER_PORT"); port != "" {
cfg.Server.Port = port
}
// MaxKB
if key := os.Getenv("MAXKB_API_KEY"); key != "" {
cfg.MaxKB.APIKey = key
}
if url := os.Getenv("MAXKB_BASE_URL"); url != "" {
cfg.MaxKB.BaseURL = url
}
// 通用 LLM Key覆盖所有模型
if key := os.Getenv("LLM_API_KEY"); key != "" {
// 注意Go 中 map 元素不可寻址,必须先取出副本改完再写回
for name, m := range cfg.LLM.Models {
m.APIKey = key
cfg.LLM.Models[name] = m
}
}
// 按供应商分别覆盖
envMap := map[string]string{
"DEEPSEEK_API_KEY": "deepseek",
"OPENAI_API_KEY": "openai",
"AZURE_API_KEY": "azure",
"QWEN_API_KEY": "qwen",
}
urlMap := map[string]string{
"OLLAMA_URL": "ollama",
}
for envKey, modelName := range envMap {
if val := os.Getenv(envKey); val != "" {
if m, ok := cfg.LLM.Models[modelName]; ok {
m.APIKey = val
cfg.LLM.Models[modelName] = m
}
}
}
for envKey, modelName := range urlMap {
if val := os.Getenv(envKey); val != "" {
if m, ok := cfg.LLM.Models[modelName]; ok {
m.BaseURL = val
cfg.LLM.Models[modelName] = m
}
}
}
// 数据库
if dsn := os.Getenv("DB_DSN"); dsn != "" {
cfg.DB.DSN = dsn
}
// AES 主密钥(与 PHP .env ENCRYPT_KEY 同源,用于解密 xk_ai_api_key.api_key
if key := os.Getenv("ENCRYPT_KEY"); key != "" {
cfg.DB.EncryptKey = key
}
// Agent 鉴权(与 PHP 后台 ai_agent_secret 对接)
// 优先级:环境变量 > config.yaml > 空(开发模式放行无 Token 请求)
if secret := os.Getenv("AGENT_SHARED_SECRET"); secret != "" {
cfg.Agent.SharedSecret = secret
}
if jwtSecret := os.Getenv("AGENT_JWT_SECRET"); jwtSecret != "" {
cfg.Agent.JWTSecret = jwtSecret
}
}