// Package dao 提供 transcribe 模块对外部 LLM 配置中心的只读访问 package dao import ( "context" "errors" "fmt" "time" "github.com/echochat/backend/app/transcribe/model/llm" "github.com/echochat/backend/pkg/db" ) // ErrLLMSourceDisabled LLM 配置库未启用 var ErrLLMSourceDisabled = errors.New("LLM 配置中心未启用 (llm_source.enabled=false)") // ErrNoActiveSTT 没有可用的 STT 模型/Key var ErrNoActiveSTT = errors.New("未找到可用的 STT 模型或 API Key") // LLMConfigDAO 外部 LLM 配置只读 DAO(MySQL) // // 选择策略说明(PickActiveSTT): // 1. 在 t_llm_model 中找 model_type=4 (ASR) 且 status=1 deleted=0 ORDER BY sort ASC 的第一条 // 2. 用其 provider_id 在 t_llm_provider 中找对应 provider,要求 status=1 deleted=0 // 3. 在 t_llm_key 中按 provider_id 选 status=1 deleted=0 且 (expire_time IS NULL OR expire_time > NOW()) // 且 (daily_limit=0 OR today_count < daily_limit) ORDER BY weight DESC, today_count ASC LIMIT 1 // // 任何一步落空都返回 ErrNoActiveSTT,避免上游用半成品配置发起调用。 type LLMConfigDAO struct { source *db.LLMSourceDB } // NewLLMConfigDAO 创建实例 func NewLLMConfigDAO(source *db.LLMSourceDB) *LLMConfigDAO { return &LLMConfigDAO{source: source} } // IsEnabled 暴露给上层做"是否能转写"的快速判断,避免每次都 catch error func (d *LLMConfigDAO) IsEnabled() bool { return d != nil && d.source != nil && d.source.IsEnabled() } // STTConfig 三表合一的运行时配置快照,用于本次转写调用 type STTConfig struct { Provider llm.Provider Model llm.Model Key llm.Key } // PickActiveSTT 按选择策略挑出一组可用的 (provider, model, key) // // 注意:不做事务,因为这是只读 + LLM 配置中心通常更新频率极低, // 偶发的"挑出后 key 立刻被禁用"由调用层捕获 401/403 后重试解决。 func (d *LLMConfigDAO) PickActiveSTT(ctx context.Context) (*STTConfig, error) { if !d.IsEnabled() { return nil, ErrLLMSourceDisabled } gdb := d.source.DB() // 1) 选 ASR 模型 var model llm.Model err := gdb.WithContext(ctx). Where("deleted = 0 AND status = ? AND model_type = ?", llm.StatusEnabled, llm.ModelTypeASR). Order("sort ASC, id ASC"). First(&model).Error if err != nil { return nil, fmt.Errorf("%w: %v", ErrNoActiveSTT, err) } // 2) 模型对应的 provider 必须也启用 var provider llm.Provider err = gdb.WithContext(ctx). Where("id = ? AND deleted = 0 AND status = ?", model.ProviderID, llm.StatusEnabled). First(&provider).Error if err != nil { return nil, fmt.Errorf("%w: provider not active for model %s", ErrNoActiveSTT, model.ModelCode) } // 3) 选 key now := time.Now() var key llm.Key err = gdb.WithContext(ctx). Where("deleted = 0 AND status = ? AND provider_id = ?", llm.StatusEnabled, provider.ID). Where("expire_time IS NULL OR expire_time > ?", now). Where("daily_limit = 0 OR today_count < daily_limit"). Order("weight DESC, today_count ASC, id ASC"). First(&key).Error if err != nil { return nil, fmt.Errorf("%w: no usable key for provider %s", ErrNoActiveSTT, provider.ProviderCode) } return &STTConfig{Provider: provider, Model: model, Key: key}, nil } // ListASRModels 列出全部启用的 ASR 模型(admin 可视化用,可不接 UI,先备好接口) func (d *LLMConfigDAO) ListASRModels(ctx context.Context) ([]llm.Model, error) { if !d.IsEnabled() { return nil, ErrLLMSourceDisabled } var list []llm.Model err := d.source.DB().WithContext(ctx). Where("deleted = 0 AND status = ? AND model_type = ?", llm.StatusEnabled, llm.ModelTypeASR). Order("sort ASC, id ASC"). Find(&list).Error return list, err }