package model import ( "errors" "strings" "github.com/QuantumNous/new-api/common" ) // ModelTestResult 记录“某渠道 + 某模型”的最近一次测试结果与累计统计。 // 约定:数据库表名为 model_test_results,布尔列 last_test_success 表示该 (channel_id, model_name) 行「最近一次单测是否成功」;与 Upsert、AutoMigrate 一致。 // 主键 (channel_id, model_name) 下每个渠道每个模型名至多一行。 type ModelTestResult struct { // ChannelId 渠道 ID(联合主键之一)。 ChannelId int `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index:idx_mtr_channel_model,priority:1;comment:渠道ID(联合主键)"` // ModelName 模型名称(联合主键之一;GORM 默认列 model_name)。 ModelName string `json:"model_name" gorm:"primaryKey;autoIncrement:false;type:varchar(255);index:idx_mtr_channel_model,priority:2;comment:模型名称(联合主键)"` // LastTestSuccess 最新一次渠道单测是否成功。MySQL 通常映射为 TINYINT(1),库内存 0/1(1=成功、0=失败),查询操练场用 Pluck+WHERE(=1) 与整型比较一致。 LastTestSuccess bool `json:"last_test_success" gorm:"default:false;comment:最近一次测试是否成功"` // LastTestTime 最近一次测试时间(Unix 秒级时间戳)。 LastTestTime int64 `json:"last_test_time" gorm:"bigint;default:0;comment:最近一次测试时间(Unix秒)"` // LastResponseTime 最近一次测试响应耗时(毫秒)。 LastResponseTime int `json:"last_response_time" gorm:"default:0;comment:最近一次测试响应耗时(毫秒)"` // LastTestMessage 最近一次测试错误信息;成功时通常为空字符串。 LastTestMessage string `json:"last_test_message" gorm:"type:text;comment:最近一次测试错误信息"` // TestCountSuccess 累计成功次数。 TestCountSuccess int `json:"test_count_success" gorm:"default:0;comment:累计测试成功次数"` // TestCountFail 累计失败次数。 TestCountFail int `json:"test_count_fail" gorm:"default:0;comment:累计测试失败次数"` // ManualDisplayResponseTime 运营手动覆盖的「展示用」响应时间(毫秒);0 表示不覆盖展示耗时(仍用 LastResponseTime 与下列 ManualStabilityGrade 规则)。 ManualDisplayResponseTime int `json:"manual_display_response_time" gorm:"default:0;comment:运营展示用响应耗时(毫秒) 0=不覆盖"` // ManualStabilityGrade 运营手动覆盖的稳定性等级 1-5,0 表示不覆盖(展示仍可按 LastResponseTime 分档);与 ManualDisplayResponseTime 可同时使用,以手动为准参与 UI。 ManualStabilityGrade int `json:"manual_stability_grade" gorm:"default:0;comment:运营展示用稳定性等级1-5 0=不覆盖"` } // TableName 显式表名,避免 GORM 命名与迁移/手工表名不一致导致查询为空。 func (ModelTestResult) TableName() string { return "model_test_results" } // UpsertModelTestResult 按 (channel_id, model_name) 更新模型测试结果;不存在则插入。 func UpsertModelTestResult(channelId int, modelName string, success bool, responseTime int64, message string) error { modelName = strings.TrimSpace(modelName) if channelId <= 0 || modelName == "" { return nil } now := common.GetTimestamp() result := &ModelTestResult{ ChannelId: channelId, ModelName: modelName, LastTestSuccess: success, LastTestTime: now, LastResponseTime: int(responseTime), LastTestMessage: message, } if success { result.TestCountSuccess = 1 } else { result.TestCountFail = 1 } update := map[string]interface{}{ "last_test_success": success, "last_test_time": now, "last_response_time": int(responseTime), "last_test_message": message, } if success { update["test_count_success"] = DB.Raw("test_count_success + 1") } else { update["test_count_fail"] = DB.Raw("test_count_fail + 1") } return DB.Where("channel_id = ? AND model_name = ?", channelId, modelName).Assign(update).FirstOrCreate(result).Error } // mtrResultTableNames 以正式表名 model_test_results 为首;少数旧环境若仅有 model_test_result 会第二顺位尝试读。 var mtrResultTableNames = []string{"model_test_results", "model_test_result"} // pluckMTRLastSuccessModelNames 在 SQL 的 WHERE 中筛出最近一次成功,只 Pluck model_name。MySQL 下成功存为 1,必须用 1 比较,否则与库内整型/BOOL 对拍失败会导致全空。 // 对 Find 只取少数字段时 bool 解包异常,这里不用 Find,只用 Pluck+字符串列。 func pluckMTRLastSuccessModelNames(t string) ([]string, error) { var names []string if common.UsingPostgreSQL { if err := DB.Table(t).Select("model_name").Where("last_test_success = ?", true).Pluck("model_name", &names).Error; err != nil { return nil, err } return names, nil } // MySQL 常见 TINYINT(1)/BIT:成功为 1。SQLite 等亦多为 0/1,与 ? 传 1 对拍。 if common.UsingMySQL || common.UsingSQLite { if err := DB.Table(t).Select("model_name").Where("last_test_success = ?", 1).Pluck("model_name", &names).Error; err != nil { return nil, err } return names, nil } if err := DB.Table(t).Select("model_name").Where("last_test_success = ?", 1).Pluck("model_name", &names).Error; err != nil { return nil, err } if len(names) == 0 { var names2 []string if err2 := DB.Table(t).Select("model_name").Where("last_test_success = ?", true).Pluck("model_name", &names2).Error; err2 == nil { return names2, nil } } return names, nil } // loadMTRAllLastSuccessModelNames 合并多表名尝试后的、Trim 去重后的 model_name 列表(均为最近一次为成功的行)。 func loadMTRAllLastSuccessModelNames() ([]string, error) { if DB == nil { return nil, nil } mg := DB.Migrator() seen := make(map[string]struct{}) out := make([]string, 0, 32) for _, t := range mtrResultTableNames { if !mg.HasTable(t) { continue } names, err := pluckMTRLastSuccessModelNames(t) if err != nil { return nil, err } for i := range names { k := strings.TrimSpace(names[i]) if k == "" { continue } if _, ok := seen[k]; ok { continue } seen[k] = struct{}{} out = append(out, k) } } return out, nil } // lastPathSeg 取路径中最后一段(以 / 分隔,常见于 供应商/模型 与短名 对照)。 func lastPathSeg(s string) string { s = strings.TrimSpace(s) if s == "" { return s } if i := strings.LastIndex(s, "/"); i >= 0 && i+1 < len(s) { return strings.TrimSpace(s[i+1:]) } return s } // stripGeminiModelsPrefix 若形如 models/xxx(Gemini API 常带此前缀),与后台短名对拍时去掉再比。 func stripGeminiModelsPrefix(s string) string { s = strings.TrimSpace(s) low := strings.ToLower(s) if strings.HasPrefix(low, "models/") { if len(s) < len("models/")+1 { return s } return s[len("models/"):] } return s } // mtrNameMatchesForPlayground 判断 model_test_results 中记录的名称与 models.model_name 是否可视为同一条目(全串 Trim+大小写、models/ 前缀、路径最后一段对拍)。 func mtrNameMatchesForPlayground(mtrName, modelMetaName string) bool { a := strings.TrimSpace(mtrName) b := strings.TrimSpace(modelMetaName) if a == "" || b == "" { return false } a, b = stripGeminiModelsPrefix(a), stripGeminiModelsPrefix(b) if a == "" || b == "" { return false } if strings.EqualFold(a, b) { return true } aLast, bLast := lastPathSeg(a), lastPathSeg(b) if strings.EqualFold(aLast, b) || strings.EqualFold(bLast, a) { return true } if aLast != a && bLast != b && strings.EqualFold(aLast, bLast) { return true } return false } // GetPlaygroundTestSuccessByModelNames 对来自 models 元数据的一批 model_name,标出是否在 model_test_results 中存在可对应的「最近一次成功」条(多策略对名)。 func GetPlaygroundTestSuccessByModelNames(candidates []string) (map[string]bool, error) { out := make(map[string]bool, len(candidates)) if len(candidates) == 0 { return out, nil } mtrList, err := loadMTRAllLastSuccessModelNames() if err != nil { return nil, err } if len(mtrList) == 0 { for _, c := range candidates { out[c] = false } return out, nil } for _, c := range candidates { ok := false for i := range mtrList { if mtrNameMatchesForPlayground(mtrList[i], c) { ok = true break } } // 同一 model_name 在 candidate 中重复时结果相同,以最后一次覆盖即可 out[c] = ok } return out, nil } // GetLatestSuccessfulModelNames 返回「在任意 (channel,model) 上最近一次测试成功」的 model_name 去重集合(键为 Trim 后;供其它逻辑复用)。 func GetLatestSuccessfulModelNames() (map[string]bool, error) { list, err := loadMTRAllLastSuccessModelNames() if err != nil { return nil, err } result := make(map[string]bool, len(list)) for i := range list { result[list[i]] = true } return result, nil } type channelModelTestRow struct { ChannelId int `gorm:"column:channel_id"` ModelName string `gorm:"column:model_name"` } func loadMTRPricingSuccessRows(table string) ([]channelModelTestRow, error) { var rows []channelModelTestRow q := DB.Table(table).Select("channel_id", "model_name") if common.UsingPostgreSQL { if err := q.Where("last_test_success = ?", true).Find(&rows).Error; err != nil { return nil, err } return rows, nil } if err := q.Where("last_test_success = ?", 1).Find(&rows).Error; err != nil { return nil, err } return rows, nil } // LoadChannelPricingTestSuccessIndex 返回 channel_id -> 该渠道下最近一次单测成功的模型名列表(去重,保留插入顺序)。 // 会依次尝试 model_test_results / model_test_result 等与 Upsert 一致的表名。 func LoadChannelPricingTestSuccessIndex() (map[int][]string, error) { if DB == nil { return map[int][]string{}, nil } mg := DB.Migrator() out := make(map[int][]string) seen := make(map[int]map[string]struct{}) for _, t := range mtrResultTableNames { if !mg.HasTable(t) { continue } rows, err := loadMTRPricingSuccessRows(t) if err != nil { return nil, err } for i := range rows { cid := rows[i].ChannelId name := strings.TrimSpace(rows[i].ModelName) if cid <= 0 || name == "" { continue } if seen[cid] == nil { seen[cid] = make(map[string]struct{}) } if _, ok := seen[cid][name]; ok { continue } seen[cid][name] = struct{}{} out[cid] = append(out[cid], name) } } return out, nil } // ChannelPricingRowMatchesLastTestSuccess 判断 (channelID, pricingModelName) 是否在单测结果表中存在可匹配的成功记录。 func ChannelPricingRowMatchesLastTestSuccess(byChannel map[int][]string, channelID int, pricingModelName string) bool { if byChannel == nil || channelID <= 0 { return false } names, ok := byChannel[channelID] if !ok || len(names) == 0 { return false } for i := range names { if mtrNameMatchesForPlayground(names[i], pricingModelName) { return true } } return false } // GetModelTestResultsByModelNameAndChannelIDs 按定价/元数据中的 model_name 与渠道 ID 列表查询 model_test_results(行键与 Upsert 写入的 model_name 一致)。 // channelIds 为空时返回空切片,不查库。 func GetModelTestResultsByModelNameAndChannelIDs(modelName string, channelIds []int) ([]ModelTestResult, error) { modelName = strings.TrimSpace(modelName) if modelName == "" || len(channelIds) == 0 { return nil, nil } var out []ModelTestResult err := DB.Model(&ModelTestResult{}). Where("model_name = ? AND channel_id IN ?", modelName, channelIds). Find(&out).Error if err != nil { return nil, err } return out, nil } // SetModelTestResultManualDisplay 更新 (channel_id, model_name) 的展示用运营字段;manualMs、manualGrade 均为 0 表示取消覆盖。 // manualGrade 允许 0-5:0=不展示等级覆盖;1-5 为有效等级。manualMs 为毫秒,>0 表示用该值参与模型广场侧响应时间/颜色展示。 func SetModelTestResultManualDisplay(channelId int, modelName string, manualMs int, manualGrade int) error { modelName = strings.TrimSpace(modelName) if channelId <= 0 || modelName == "" { return errors.New("invalid channel_id or model_name") } if manualMs < 0 { manualMs = 0 } if manualGrade < 0 { manualGrade = 0 } if manualGrade > 5 { manualGrade = 5 } updates := map[string]interface{}{ "manual_display_response_time": manualMs, "manual_stability_grade": manualGrade, } // 使用 Assign + FirstOrCreate 做幂等写入,避免「字段值未变化导致 RowsAffected=0」时误判为不存在而重复插入主键。 return DB.Where("channel_id = ? AND model_name = ?", channelId, modelName). Assign(updates). FirstOrCreate(&ModelTestResult{ ChannelId: channelId, ModelName: modelName, ManualDisplayResponseTime: manualMs, ManualStabilityGrade: manualGrade, }).Error } // GetModelTestResultsByChannelIDAndModelNames 渠道测试弹窗用:单渠道 + 多模型名,一次查出已有单测/运营行。 func GetModelTestResultsByChannelIDAndModelNames(channelId int, modelNames []string) ([]ModelTestResult, error) { if channelId <= 0 || len(modelNames) == 0 { return nil, nil } var out []ModelTestResult err := DB.Model(&ModelTestResult{}). Where("channel_id = ? AND model_name IN ?", channelId, modelNames). Find(&out).Error if err != nil { return nil, err } return out, nil } // GetAllModelTestResultsByChannelID 返回某渠道在 model_test_results 中的全部行(弹窗内避免 URL 携带超长 model_names)。 func GetAllModelTestResultsByChannelID(channelId int) ([]ModelTestResult, error) { if channelId <= 0 { return nil, nil } var out []ModelTestResult err := DB.Model(&ModelTestResult{}).Where("channel_id = ?", channelId).Find(&out).Error if err != nil { return nil, err } return out, nil }