Files
EchoChat/backend/go-service/app/transcribe/dao/llm_config_dao.go
2026-05-18 21:23:00 +08:00

107 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 配置只读 DAOMySQL
//
// 选择策略说明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
}