feat: add workbench AI integration
This commit is contained in:
@@ -19,6 +19,7 @@ type Config struct {
|
||||
Kafka KafkaConfig `json:"kafka"`
|
||||
Auth AuthConfig `json:"auth"`
|
||||
Manager ManagerConfig `json:"manager"`
|
||||
AI AIConfig `json:"ai"`
|
||||
ReadTimeout time.Duration `json:"-"`
|
||||
WriteTimeout time.Duration `json:"-"`
|
||||
}
|
||||
@@ -70,6 +71,22 @@ type ManagerConfig struct {
|
||||
TimeoutMS int `json:"timeout_ms"`
|
||||
}
|
||||
|
||||
type AIConfig struct {
|
||||
Provider string `json:"provider"`
|
||||
DeepSeek DeepSeekConfig `json:"deepseek"`
|
||||
DefaultModel string `json:"default_model"`
|
||||
ComplexModel string `json:"complex_model"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
MaxOutputTokens int `json:"max_output_tokens"`
|
||||
}
|
||||
|
||||
type DeepSeekConfig struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKey string `json:"api_key"`
|
||||
TimeoutMS int `json:"timeout_ms"`
|
||||
MaxRetries int `json:"max_retries"`
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
cfg := defaultConfig()
|
||||
if path := strings.TrimSpace(os.Getenv("IM_WORKBENCH_CONFIG")); path != "" {
|
||||
@@ -92,7 +109,7 @@ func defaultConfig() *Config {
|
||||
return &Config{
|
||||
Listen: ":6061",
|
||||
MySQL: MySQLConfig{
|
||||
DSN: "root:root@tcp(127.0.0.1:3306)/meeting_im?parseTime=true&charset=utf8mb4,utf8&loc=Local",
|
||||
DSN: "root:root@tcp(127.0.0.1:3306)/workbench?parseTime=true&charset=utf8mb4,utf8&loc=Local",
|
||||
MaxOpenConns: 15,
|
||||
MaxIdleConns: 5,
|
||||
},
|
||||
@@ -116,6 +133,18 @@ func defaultConfig() *Config {
|
||||
UIDKeyKeys: []string{"uid_encryption_key", "UID_ENCRYPTION_KEY", "uid_key"},
|
||||
TimeoutMS: 3000,
|
||||
},
|
||||
AI: AIConfig{
|
||||
Provider: "deepseek",
|
||||
DefaultModel: "deepseek-flash",
|
||||
ComplexModel: "deepseek-v4-pro",
|
||||
Temperature: 0.2,
|
||||
MaxOutputTokens: 4096,
|
||||
DeepSeek: DeepSeekConfig{
|
||||
BaseURL: "https://api.deepseek.com",
|
||||
TimeoutMS: 60000,
|
||||
MaxRetries: 2,
|
||||
},
|
||||
},
|
||||
ReadTimeout: 8 * time.Second,
|
||||
WriteTimeout: 8 * time.Second,
|
||||
}
|
||||
@@ -144,6 +173,15 @@ func applyEnv(cfg *Config) {
|
||||
setString(&cfg.Manager.ServiceName, "IM_WORKBENCH_MANAGER_SERVICE_NAME")
|
||||
setString(&cfg.Manager.Secret, "IM_WORKBENCH_MANAGER_SECRET")
|
||||
setString(&cfg.Manager.UIDKeyGroup, "IM_WORKBENCH_MANAGER_UID_KEY_GROUP")
|
||||
setString(&cfg.AI.Provider, "IM_WORKBENCH_AI_PROVIDER")
|
||||
setString(&cfg.AI.DeepSeek.BaseURL, "DEEPSEEK_BASE_URL")
|
||||
setString(&cfg.AI.DeepSeek.APIKey, "DEEPSEEK_API_KEY")
|
||||
setString(&cfg.AI.DefaultModel, "DEEPSEEK_DEFAULT_MODEL")
|
||||
setString(&cfg.AI.ComplexModel, "DEEPSEEK_COMPLEX_MODEL")
|
||||
setInt(&cfg.AI.DeepSeek.TimeoutMS, "DEEPSEEK_TIMEOUT_MS")
|
||||
setInt(&cfg.AI.DeepSeek.MaxRetries, "DEEPSEEK_MAX_RETRIES")
|
||||
setInt(&cfg.AI.MaxOutputTokens, "DEEPSEEK_MAX_OUTPUT_TOKENS")
|
||||
setFloat(&cfg.AI.Temperature, "DEEPSEEK_TEMPERATURE")
|
||||
if strings.TrimSpace(cfg.Auth.InternalSecret) == "" {
|
||||
setString(&cfg.Auth.InternalSecret, "IM_INTERNAL_API_SECRET")
|
||||
}
|
||||
@@ -200,7 +238,7 @@ func (c *Config) validate() error {
|
||||
return fmt.Errorf("mysql dsn is invalid: %w", err)
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(parsedDSN.DBName), "tinode") {
|
||||
return errors.New("mysql database tinode is deprecated; use meeting_im")
|
||||
return errors.New("mysql database tinode is deprecated; use workbench")
|
||||
}
|
||||
if strings.TrimSpace(c.Redis.Addr) == "" && !c.Auth.SkipSessionInDev {
|
||||
return errors.New("redis addr is required")
|
||||
@@ -211,6 +249,27 @@ func (c *Config) validate() error {
|
||||
if !strings.Contains(c.Auth.SessionKeyPattern, "%s") {
|
||||
return fmt.Errorf("session key pattern must include %%s")
|
||||
}
|
||||
if strings.TrimSpace(c.AI.Provider) == "" {
|
||||
c.AI.Provider = "deepseek"
|
||||
}
|
||||
if strings.TrimSpace(c.AI.DeepSeek.BaseURL) == "" {
|
||||
c.AI.DeepSeek.BaseURL = "https://api.deepseek.com"
|
||||
}
|
||||
if c.AI.DeepSeek.TimeoutMS <= 0 {
|
||||
c.AI.DeepSeek.TimeoutMS = 60000
|
||||
}
|
||||
if strings.TrimSpace(c.AI.DefaultModel) == "" {
|
||||
c.AI.DefaultModel = "deepseek-flash"
|
||||
}
|
||||
if strings.TrimSpace(c.AI.ComplexModel) == "" {
|
||||
c.AI.ComplexModel = c.AI.DefaultModel
|
||||
}
|
||||
if c.AI.Temperature == 0 {
|
||||
c.AI.Temperature = 0.2
|
||||
}
|
||||
if c.AI.MaxOutputTokens <= 0 {
|
||||
c.AI.MaxOutputTokens = 4096
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -237,6 +296,14 @@ func setBool(target *bool, key string) {
|
||||
}
|
||||
}
|
||||
|
||||
func setFloat(target *float64, key string) {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
if parsed, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
*target = parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func splitCSV(value string) []string {
|
||||
parts := strings.Split(value, ",")
|
||||
items := make([]string, 0, len(parts))
|
||||
|
||||
@@ -15,13 +15,13 @@ func TestValidateRejectsDeprecatedTinodeDatabase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfigUsesMeetingIMDatabase(t *testing.T) {
|
||||
func TestDefaultConfigUsesWorkbenchDatabase(t *testing.T) {
|
||||
cfg := defaultConfig()
|
||||
if err := cfg.validate(); err != nil {
|
||||
t.Fatalf("default config validation failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(cfg.MySQL.DSN, "/meeting_im?") {
|
||||
t.Fatalf("default mysql dsn = %q, want meeting_im database", cfg.MySQL.DSN)
|
||||
if !strings.Contains(cfg.MySQL.DSN, "/workbench?") {
|
||||
t.Fatalf("default mysql dsn = %q, want workbench database", cfg.MySQL.DSN)
|
||||
}
|
||||
if cfg.MySQL.MaxOpenConns != 15 || cfg.MySQL.MaxIdleConns != 5 {
|
||||
t.Fatalf("default mysql pool = %d/%d, want 15/5", cfg.MySQL.MaxOpenConns, cfg.MySQL.MaxIdleConns)
|
||||
|
||||
Reference in New Issue
Block a user