Files
ai-agent/internal/pkg/config/config.go
T
t 18c9354095 refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。

- 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。

- 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。

- 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
2026-08-28 22:23:13 +08:00

283 lines
8.9 KiB
Go

package config
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"github.com/spf13/viper"
)
const SettingsPrefix = "ai_agent_"
func FromSettings(values map[string]string) (*Config, error) {
cfg := defaultConfig()
setString(values, SettingsPrefix+"language", &cfg.Language)
setString(values, SettingsPrefix+"storage_default", (*string)(&cfg.Storage.Default))
if err := setInt64(values, SettingsPrefix+"storage_max_upload_size_mb", &cfg.Storage.MaxUploadSizeMB); err != nil {
return nil, err
}
setString(values, SettingsPrefix+"storage_local_root", &cfg.Storage.Local.Root)
setString(values, SettingsPrefix+"storage_local_base_url", &cfg.Storage.Local.BaseURL)
setString(values, SettingsPrefix+"storage_oss_endpoint", &cfg.Storage.OSS.Endpoint)
setString(values, SettingsPrefix+"storage_oss_bucket", &cfg.Storage.OSS.Bucket)
setString(values, SettingsPrefix+"storage_oss_access_key_id", &cfg.Storage.OSS.AccessKeyID)
setString(values, SettingsPrefix+"storage_oss_access_key_secret", &cfg.Storage.OSS.AccessKeySecret)
setString(values, SettingsPrefix+"storage_oss_base_url", &cfg.Storage.OSS.BaseURL)
if err := setBool(values, SettingsPrefix+"storage_oss_private", &cfg.Storage.OSS.Private); err != nil {
return nil, err
}
if err := setInt(values, SettingsPrefix+"storage_oss_signed_url_expire", &cfg.Storage.OSS.SignedURLExpire); err != nil {
return nil, err
}
setString(values, "ai_vector_path", &cfg.VectorDB.Path)
if err := setBool(values, SettingsPrefix+"wxwork_enabled", &cfg.WxWork.Enabled); err != nil {
return nil, err
}
setString(values, SettingsPrefix+"wxwork_corp_id", &cfg.WxWork.CorpID)
setString(values, SettingsPrefix+"wxwork_corp_secret", &cfg.WxWork.CorpSecret)
setString(values, SettingsPrefix+"wxwork_agent_id", &cfg.WxWork.AgentID)
setString(values, SettingsPrefix+"wxwork_rsa_private_key", &cfg.WxWork.RSAPrivateKey)
setString(values, SettingsPrefix+"wxwork_token", &cfg.WxWork.Token)
setString(values, SettingsPrefix+"wxwork_encoding_aes_key", &cfg.WxWork.EncodingAESKey)
if err := setBool(values, SettingsPrefix+"wxwork_notify_enabled", &cfg.WxWork.Notify.Enabled); err != nil {
return nil, err
}
if err := setJSON(values, SettingsPrefix+"wxwork_notify_to_users", &cfg.WxWork.Notify.ToUsers); err != nil {
return nil, err
}
if err := setBool(values, SettingsPrefix+"wxwork_notify_safe", &cfg.WxWork.Notify.Safe); err != nil {
return nil, err
}
if err := setBool(values, SettingsPrefix+"wxwork_notify_dedupe", &cfg.WxWork.Notify.EnableDuplicateCheck); err != nil {
return nil, err
}
if err := setInt(values, SettingsPrefix+"wxwork_notify_dedupe_interval", &cfg.WxWork.Notify.DuplicateCheckInterval); err != nil {
return nil, err
}
return cfg, nil
}
func defaultConfig() *Config {
return &Config{
Language: "zh-CN",
Server: ServerConfig{Port: 8080},
Storage: StorageConfig{
Default: enums.AssetProviderLocal, MaxUploadSizeMB: 20,
Local: LocalStorageConfig{Root: "data/storage", BaseURL: "/storage"},
OSS: OSSStorageConfig{SignedURLExpire: 600},
},
VectorDB: VectorDBConfig{Path: "data/agent/vectors.db"},
WxWork: WxWorkConfig{Notify: WxWorkNotifyConfig{
EnableDuplicateCheck: true, DuplicateCheckInterval: 1800,
}},
}
}
func setString(values map[string]string, key string, target *string) {
if value, ok := values[key]; ok {
*target = strings.TrimSpace(value)
}
}
func setInt(values map[string]string, key string, target *int) error {
value, ok := values[key]
if !ok || strings.TrimSpace(value) == "" {
return nil
}
parsed, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil {
return fmt.Errorf("invalid setting %s: %w", key, err)
}
*target = parsed
return nil
}
func setInt64(values map[string]string, key string, target *int64) error {
value, ok := values[key]
if !ok || strings.TrimSpace(value) == "" {
return nil
}
parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
if err != nil {
return fmt.Errorf("invalid setting %s: %w", key, err)
}
*target = parsed
return nil
}
func setBool(values map[string]string, key string, target *bool) error {
value, ok := values[key]
if !ok || strings.TrimSpace(value) == "" {
return nil
}
parsed, err := strconv.ParseBool(strings.TrimSpace(value))
if err != nil {
return fmt.Errorf("invalid setting %s: %w", key, err)
}
*target = parsed
return nil
}
func setJSON(values map[string]string, key string, target any) error {
value, ok := values[key]
if !ok || strings.TrimSpace(value) == "" {
return nil
}
if err := json.Unmarshal([]byte(value), target); err != nil {
return fmt.Errorf("invalid setting %s: %w", key, err)
}
return nil
}
type Config struct {
Language string `yaml:"language"`
Server ServerConfig `yaml:"server"`
DB DBConfig `yaml:"db"`
Logger LoggerConfig `yaml:"logger"`
Storage StorageConfig `yaml:"storage"`
VectorDB VectorDBConfig `yaml:"vectorDB"`
WxWork WxWorkConfig `yaml:"wxWork"`
}
func (c Config) LanguageOrDefault() string {
switch strings.ToLower(strings.TrimSpace(c.Language)) {
case "zh", "zh-cn", "zh_cn", "zh-hans":
return "zh-CN"
case "en", "en-us", "en_us":
return "en-US"
default:
return "zh-CN"
}
}
type WxWorkNotifyConfig struct {
Enabled bool `yaml:"enabled"`
ToUsers []int64 `yaml:"toUsers"`
Safe bool `yaml:"safe"`
EnableDuplicateCheck bool `yaml:"enableDuplicateCheck"`
DuplicateCheckInterval int `yaml:"duplicateCheckInterval"`
}
type ServerConfig struct {
Port int `yaml:"port"`
FrontendURL string `yaml:"frontendUrl"`
CORS CORSConfig `yaml:"cors"`
}
func (s ServerConfig) Address() string {
if s.Port <= 0 {
return ":8080"
}
return fmt.Sprintf(":%d", s.Port)
}
func (s ServerConfig) FrontendBaseURL() string {
if value := strings.TrimRight(strings.TrimSpace(s.FrontendURL), "/"); value != "" {
return value
}
return "http://127.0.0.1:3000"
}
type CORSConfig struct {
// AllowedOrigins 是允许浏览器跨域访问的 Origin 白名单,必须包含协议和域名。
// 留空表示不允许跨域请求;同源请求通常不会携带 Origin,不受影响。
AllowedOrigins []string `yaml:"allowedOrigins"`
}
type DBConfig struct {
Type string `yaml:"type"`
DSN string `yaml:"dsn"`
MaxIdleConns int `yaml:"maxIdleConns"`
MaxOpenConns int `yaml:"maxOpenConns"`
ConnMaxIdleTimeSeconds int `yaml:"connMaxIdleTimeSeconds"`
ConnMaxLifetimeSeconds int `yaml:"connMaxLifetimeSeconds"`
}
type LoggerConfig struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
AddSource bool `yaml:"addSource"`
}
type StorageConfig struct {
Default enums.AssetProvider `yaml:"default"`
MaxUploadSizeMB int64 `yaml:"maxUploadSizeMB"`
Local LocalStorageConfig `yaml:"local"`
OSS OSSStorageConfig `yaml:"oss"`
}
func (s StorageConfig) MaxUploadSizeBytes() int64 {
if s.MaxUploadSizeMB <= 0 {
return 5 << 20
}
return s.MaxUploadSizeMB << 20
}
func (s StorageConfig) MaxRequestBodySizeBytes() int64 {
limit := s.MaxUploadSizeBytes()
return limit + (1 << 20)
}
type LocalStorageConfig struct {
Root string `yaml:"root"`
BaseURL string `yaml:"baseUrl"`
}
type OSSStorageConfig struct {
Endpoint string `yaml:"endpoint"`
Bucket string `yaml:"bucket"`
AccessKeyID string `yaml:"accessKeyId"`
AccessKeySecret string `yaml:"accessKeySecret"`
BaseURL string `yaml:"baseUrl"`
Private bool `yaml:"private"`
SignedURLExpire int `yaml:"signedUrlExpireSeconds"`
}
type VectorDBConfig struct {
Path string `yaml:"path"`
}
// WxWorkConfig defines the WeCom application used for customer-service
// callbacks and notifications. Dashboard login is owned by be-system.
type WxWorkConfig struct {
// Enabled controls whether the WeCom SDK is initialized.
Enabled bool `yaml:"enabled"`
// CorpID 为企业微信公司 ID,例如 wwxxxxxxxxxxxxxxxx。
CorpID string `yaml:"corpId"`
// CorpSecret 为企业微信应用 Secret,用于换取 access_token。
CorpSecret string `yaml:"corpSecret"`
// AgentID 为企业微信自建应用 AgentID。
AgentID string `yaml:"agentId"`
// RSAPrivateKey 为企业微信回调解密私钥。
RSAPrivateKey string `yaml:"rsaPrivateKey"`
// Token 为企业微信回调 Token。
Token string `yaml:"token"`
// EncodingAESKey 为企业微信消息加解密密钥。
EncodingAESKey string `yaml:"encodingAESKey"`
// Notify 为企业微信应用消息通知配置。
Notify WxWorkNotifyConfig `yaml:"notify"`
}
func Load(path string) (*Config, error) {
v := viper.New()
v.SetConfigFile(path)
v.SetConfigType("yaml")
v.SetEnvPrefix("AGENT_DESK")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
if err := v.ReadInConfig(); err != nil {
return nil, err
}
cfg := &Config{}
if err := v.Unmarshal(cfg); err != nil {
return nil, err
}
return cfg, nil
}