18c9354095
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
283 lines
8.9 KiB
Go
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
|
|
}
|