338 lines
12 KiB
Go
338 lines
12 KiB
Go
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 用 noop,V2 接 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_version,Ollama 的 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 引擎配置
|
||
//
|
||
// 字段分两组:
|
||
// - 引擎行为:MaxIterations(ReAct 最大轮数)/ Timeout(单次调用超时秒数)
|
||
// - 服务鉴权:SharedSecret / JWTSecret(middleware.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"` // 向量化 provider:noop / bge_m3 / aliyun / openai
|
||
EmbeddingAPIKey string `yaml:"embedding_api_key"` // embedding API Key(V2 接 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
|
||
}
|
||
}
|