change v1

This commit is contained in:
lky-spec
2026-04-25 16:18:45 +08:00
parent 737f9bca95
commit c244e4cdf1
89 changed files with 17382 additions and 6198 deletions

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"nofx/crypto"
"nofx/logger"
"os"
"strings"
"time"
@@ -18,16 +19,16 @@ type AIModelStore struct {
// AIModel AI model configuration
type AIModel struct {
ID string `gorm:"primaryKey" json:"id"`
UserID string `gorm:"column:user_id;not null;default:default;index" json:"user_id"`
Name string `gorm:"not null" json:"name"`
Provider string `gorm:"not null" json:"provider"`
Enabled bool `gorm:"default:false" json:"enabled"`
ID string `gorm:"primaryKey" json:"id"`
UserID string `gorm:"column:user_id;not null;default:default;index" json:"user_id"`
Name string `gorm:"not null" json:"name"`
Provider string `gorm:"not null" json:"provider"`
Enabled bool `gorm:"default:false" json:"enabled"`
APIKey crypto.EncryptedString `gorm:"column:api_key;default:''" json:"apiKey"`
CustomAPIURL string `gorm:"column:custom_api_url;default:''" json:"customApiUrl"`
CustomModelName string `gorm:"column:custom_model_name;default:''" json:"customModelName"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CustomAPIURL string `gorm:"column:custom_api_url;default:''" json:"customApiUrl"`
CustomModelName string `gorm:"column:custom_model_name;default:''" json:"customModelName"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (AIModel) TableName() string { return "ai_models" }
@@ -145,32 +146,64 @@ func (s *AIModelStore) GetDefault(userID string) (*AIModel, error) {
}
func (s *AIModelStore) firstEnabledUsable(userID string) (*AIModel, error) {
var model AIModel
err := s.db.Where("user_id = ? AND enabled = ? AND api_key != ''", userID, true).
var models []AIModel
err := s.db.Where("user_id = ? AND enabled = ?", userID, true).
Order("updated_at DESC, id ASC").
First(&model).Error
Find(&models).Error
if err != nil {
return nil, err
}
return &model, nil
for i := range models {
if hasUsableAPIKey(models[i]) {
return &models[i], nil
}
}
return nil, gorm.ErrRecordNotFound
}
// GetAnyEnabled returns the first enabled AI model across all users.
// Used by single-user features (e.g. Telegram bot) that need any working LLM client.
func (s *AIModelStore) GetAnyEnabled() (*AIModel, error) {
var model AIModel
err := s.db.Where("enabled = ? AND api_key != ''", true).
var models []AIModel
err := s.db.Where("enabled = ?", true).
Order("updated_at DESC, id ASC").
First(&model).Error
Find(&models).Error
if err != nil {
return nil, err
}
return &model, nil
for i := range models {
if hasUsableAPIKey(models[i]) {
return &models[i], nil
}
}
return nil, gorm.ErrRecordNotFound
}
func hasUsableAPIKey(model AIModel) bool {
if strings.TrimSpace(string(model.APIKey)) != "" {
return true
}
envKeyByProvider := map[string]string{
"deepseek": "DEEPSEEK_API_KEY",
"openai": "OPENAI_API_KEY",
"claude": "ANTHROPIC_API_KEY",
"gemini": "GEMINI_API_KEY",
"grok": "XAI_API_KEY",
"kimi": "MOONSHOT_API_KEY",
"minimax": "MINIMAX_API_KEY",
"qwen": "DASHSCOPE_API_KEY",
}
envKey := envKeyByProvider[strings.ToLower(strings.TrimSpace(model.Provider))]
return envKey != "" && strings.TrimSpace(os.Getenv(envKey)) != ""
}
// Update updates AI model, creates if not exists
// IMPORTANT: If apiKey is empty string, the existing API key will be preserved (not overwritten)
func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error {
return s.UpdateWithName(userID, id, "", enabled, apiKey, customAPIURL, customModelName)
}
func (s *AIModelStore) UpdateWithName(userID, id, name string, enabled bool, apiKey, customAPIURL, customModelName string) error {
// Try exact ID match first
var existingModel AIModel
err := s.db.Where("user_id = ? AND id = ?", userID, id).First(&existingModel).Error
@@ -182,6 +215,9 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI
"custom_model_name": customModelName,
"updated_at": time.Now().UTC(),
}
if strings.TrimSpace(name) != "" {
updates["name"] = strings.TrimSpace(name)
}
// If apiKey is not empty, update it (encryption handled by crypto.EncryptedString)
if apiKey != "" {
updates["api_key"] = crypto.EncryptedString(apiKey)
@@ -200,6 +236,9 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI
"custom_model_name": customModelName,
"updated_at": time.Now().UTC(),
}
if strings.TrimSpace(name) != "" {
updates["name"] = strings.TrimSpace(name)
}
if apiKey != "" {
updates["api_key"] = crypto.EncryptedString(apiKey)
}
@@ -218,31 +257,35 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI
}
}
// Try to get name from existing model with same provider
// Try to get a sensible default name from an existing model with the same provider.
var refModel AIModel
var name string
defaultName := ""
if err := s.db.Where("provider = ?", provider).First(&refModel).Error; err == nil {
name = refModel.Name
defaultName = refModel.Name
} else {
if provider == "deepseek" {
name = "DeepSeek AI"
defaultName = "DeepSeek AI"
} else if provider == "qwen" {
name = "Qwen AI"
defaultName = "Qwen AI"
} else {
name = provider + " AI"
defaultName = provider + " AI"
}
}
finalName := strings.TrimSpace(name)
if finalName == "" {
finalName = strings.TrimSpace(defaultName)
}
newModelID := id
if id == provider {
newModelID = fmt.Sprintf("%s_%s", userID, provider)
}
logger.Infof("✓ Creating new AI model configuration: ID=%s, Provider=%s, Name=%s", newModelID, provider, name)
logger.Infof("✓ Creating new AI model configuration: ID=%s, Provider=%s, Name=%s", newModelID, provider, finalName)
newModel := &AIModel{
ID: newModelID,
UserID: userID,
Name: name,
Name: finalName,
Provider: provider,
Enabled: enabled,
APIKey: crypto.EncryptedString(apiKey),