package controller import ( "errors" "fmt" "net/http" "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/ratio_setting" "github.com/gin-gonic/gin" ) // filterChannelPricingMapByVisibleChannels 仅保留可见渠道的渠道倍率配置。 func filterChannelPricingMapByVisibleChannels(source map[string]map[string]float64, visibleChannelIDs map[int]struct{}) map[string]map[string]float64 { filtered := make(map[string]map[string]float64, len(source)) for channelID, modelRatio := range source { id, err := model.ParseSupplierChannelIDFilter(channelID) if err != nil { continue } if _, ok := visibleChannelIDs[id]; !ok { continue } filtered[channelID] = modelRatio } return filtered } func filterChannelTierPricingMapByVisibleChannels(source map[string]map[string]ratio_setting.TierSegments, visibleChannelIDs map[int]struct{}) map[string]map[string]ratio_setting.TierSegments { filtered := make(map[string]map[string]ratio_setting.TierSegments, len(source)) for channelID, modelRatio := range source { id, err := model.ParseSupplierChannelIDFilter(channelID) if err != nil { continue } if _, ok := visibleChannelIDs[id]; !ok { continue } filtered[channelID] = modelRatio } return filtered } // getPricingVisibleChannelsForUser 返回定价/模型广场可见的渠道列表及 channel_* Option 过滤用的 ID 集合。 // 当前策略:所有角色(包含已审核供应商)均可见全部渠道,与普通用户保持一致。 func getPricingVisibleChannelsForUser(c *gin.Context) ([]model.ChannelSimplePricingItem, map[int]struct{}, error) { channels, err := model.ListChannelsForPricing() if err != nil { return nil, nil, err } visibleChannelIDs := make(map[int]struct{}, len(channels)) for _, item := range channels { visibleChannelIDs[item.ChannelID] = struct{}{} } return channels, visibleChannelIDs, nil } // shouldBlurPricing 检查 HeaderNavModules 配置中是否有任一模块开启了 blurPricing。 func shouldBlurPricing() bool { common.OptionMapRWMutex.RLock() raw := common.OptionMap["HeaderNavModules"] common.OptionMapRWMutex.RUnlock() if raw == "" { return false } var modules map[string]any if err := common.Unmarshal([]byte(raw), &modules); err != nil { return false } for _, key := range []string{"home", "pricing"} { switch v := modules[key].(type) { case map[string]any: if bp, ok := v["blurPricing"]; ok { if b, ok := bp.(bool); ok && b { return true } } } } return false } // sanitizePricingData 将定价数据中的价格和供应商信息置零/清空。 func sanitizePricingData(data []model.PricingAPIItem) { for i := range data { data[i].ModelRatio = 0 data[i].ModelPrice = 0 data[i].CompletionRatio = nil data[i].CacheRatio = nil data[i].CreateCacheRatio = nil data[i].ImageRatio = nil data[i].AudioRatio = nil data[i].AudioCompletionRatio = nil data[i].VideoRatio = nil data[i].VideoCompletionRatio = nil data[i].VideoPrice = nil data[i].VideoFlatClipHint = nil data[i].ImagePerImageHint = nil for j := range data[i].ChannelList { data[i].ChannelList[j].ModelPrice = 0 data[i].ChannelList[j].ModelRatio = 0 data[i].ChannelList[j].CompletionRatio = 0 data[i].ChannelList[j].CacheRatio = 0 data[i].ChannelList[j].CreateCacheRatio = 0 data[i].ChannelList[j].ModelTierRatio = nil data[i].ChannelList[j].CompletionTierRatio = nil data[i].ChannelList[j].CacheTierRatio = nil data[i].ChannelList[j].CreateCacheTierRatio = nil data[i].ChannelList[j].PriceDiscountPercent = 0 data[i].ChannelList[j].SupplierAlias = "" data[i].ChannelList[j].CompanyLogoURL = "" data[i].ChannelList[j].SupplierType = "" } for j := range data[i].SupplierList { data[i].SupplierList[j].SupplierAlias = "" data[i].SupplierList[j].CompanyLogoURL = "" data[i].SupplierList[j].SupplierType = "" } } } func buildPricingAPIData() []model.PricingAPIItem { pricing := model.GetPricing() filtered := make([]model.Pricing, 0, len(pricing)) for _, p := range pricing { if ratio_setting.ModelHasConfiguredPricing(p.ModelName) { filtered = append(filtered, p) } } channels, err := model.ListChannelsForPricing() if err != nil { channels = nil } visibleChannelIDs := make(map[int]struct{}, len(channels)) for _, item := range channels { visibleChannelIDs[item.ChannelID] = struct{}{} } channelPricingMeta, err := model.ListChannelPricingMeta() if err != nil { channelPricingMeta = nil } return model.BuildPricingAPIItems(filtered, visibleChannelIDs, channelPricingMeta, true) } // CollectPricingShowableModelNames 返回 /pricing 接口前端可展示的模型名集合(与 GetPricing 同源条件)。 // 判定条件与 /pricing 完全一致: // 1. 模型已配置定价(ratio_setting.ModelHasConfiguredPricing)。 // 2. 至少存在一个 (model, 可见渠道) 满足 model.BuildPricingAPIItems 的单测门禁 // (ManualDisplayResponseTime>0 或 LastTestSuccess && LastResponseTime>0;该渠道若已有任何成功单测,则本模型也需通过模糊匹配)。 // // 用于操练场等需要"配好定价 + 测试连通性通过"判定与定价页保持一致的位置,避免两端各自实现的判定门槛漂移导致少展示。 func CollectPricingShowableModelNames() map[string]bool { pricing := model.GetPricing() filtered := make([]model.Pricing, 0, len(pricing)) for _, p := range pricing { if ratio_setting.ModelHasConfiguredPricing(p.ModelName) { filtered = append(filtered, p) } } visibleChannelIDs := make(map[int]struct{}) if channels, err := model.ListChannelsForPricing(); err == nil { for _, item := range channels { visibleChannelIDs[item.ChannelID] = struct{}{} } } metas, err := model.ListChannelPricingMeta() if err != nil { metas = nil } items := model.BuildPricingAPIItems(filtered, visibleChannelIDs, metas, false) out := make(map[string]bool, len(items)) for i := range items { name := strings.TrimSpace(items[i].ModelName) if name == "" { continue } out[name] = true } return out } func validateAdminIssuedToken(rawToken string) error { tokenKey := strings.TrimSpace(rawToken) if strings.HasPrefix(strings.ToLower(tokenKey), "bearer ") { tokenKey = strings.TrimSpace(tokenKey[7:]) } tokenKey = strings.TrimPrefix(tokenKey, "sk-") token, err := model.ValidateUserToken(tokenKey) if err != nil { return err } if token == nil || !model.IsAdmin(token.UserId) { return errors.New("令牌不是管理员签发") } return nil } func PriceSync(c *gin.Context) { var req struct { Token string `json:"token"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "请求参数格式错误"}) return } if err := validateAdminIssuedToken(req.Token); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "token 验证失败"}) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": buildPricingAPIData(), }) } // GetPricing 返回前端定价展示数据。 func GetPricing(c *gin.Context) { pricing := model.GetPricing() filtered := make([]model.Pricing, 0, len(pricing)) for _, p := range pricing { if ratio_setting.ModelHasConfiguredPricing(p.ModelName) { filtered = append(filtered, p) } } _, visibleChannelIDs, err := getPricingVisibleChannelsForUser(c) if err != nil { visibleChannelIDs = map[int]struct{}{} } userId, exists := c.Get("id") usableGroup := map[string]string{} groupRatio := map[string]float64{} groupModelPrice := map[string]map[string]float64{} groupModelRatio := map[string]map[string]float64{} channelModelPrice := map[string]map[string]float64{} channelModelRatio := map[string]map[string]float64{} channelCompletionRatio := map[string]map[string]float64{} channelCacheRatio := map[string]map[string]float64{} channelCreateCacheRatio := map[string]map[string]float64{} channelImageRatio := map[string]map[string]float64{} channelAudioRatio := map[string]map[string]float64{} channelAudioCompletionRatio := map[string]map[string]float64{} channelVideoRatio := map[string]map[string]float64{} channelVideoCompletionRatio := map[string]map[string]float64{} channelVideoPrice := map[string]map[string]float64{} channelImagePrice := map[string]map[string]float64{} channelModelTierRatio := map[string]map[string]ratio_setting.TierSegments{} channelCompletionTierRatio := map[string]map[string]ratio_setting.TierSegments{} channelCacheTierRatio := map[string]map[string]ratio_setting.TierSegments{} channelCreateCacheTierRatio := map[string]map[string]ratio_setting.TierSegments{} supplierModelPrice := map[string]map[string]float64{} supplierModelRatio := map[string]map[string]float64{} for s, f := range ratio_setting.GetGroupRatioCopy() { groupRatio[s] = f } var group string if exists { user, err := model.GetUserCache(userId.(int)) if err == nil { group = user.Group for g := range groupRatio { ratio, ok := ratio_setting.GetGroupGroupRatio(group, g) if ok { groupRatio[g] = ratio } } } } usableGroup = service.GetUserUsableGroups(group) // check groupRatio contains usableGroup for group := range ratio_setting.GetGroupRatioCopy() { if _, ok := usableGroup[group]; !ok { delete(groupRatio, group) } } for group, modelPrice := range ratio_setting.GetGroupModelPriceCopy() { if _, ok := usableGroup[group]; ok { groupModelPrice[group] = modelPrice } } for group, modelRatioByGroup := range ratio_setting.GetGroupModelRatioCopy() { if _, ok := usableGroup[group]; ok { groupModelRatio[group] = modelRatioByGroup } } for channelID, modelPrice := range ratio_setting.GetChannelModelPriceCopy() { channelModelPrice[channelID] = modelPrice } for channelID, modelRatio := range ratio_setting.GetChannelModelRatioCopy() { channelModelRatio[channelID] = modelRatio } for channelID, modelRatio := range ratio_setting.GetChannelCompletionRatioCopy() { channelCompletionRatio[channelID] = modelRatio } for channelID, modelRatio := range ratio_setting.GetChannelCacheRatioCopy() { channelCacheRatio[channelID] = modelRatio } for channelID, modelRatio := range ratio_setting.GetChannelCreateCacheRatioCopy() { channelCreateCacheRatio[channelID] = modelRatio } for channelID, modelRatio := range ratio_setting.GetChannelImageRatioCopy() { channelImageRatio[channelID] = modelRatio } for channelID, modelRatio := range ratio_setting.GetChannelAudioRatioCopy() { channelAudioRatio[channelID] = modelRatio } for channelID, modelRatio := range ratio_setting.GetChannelAudioCompletionRatioCopy() { channelAudioCompletionRatio[channelID] = modelRatio } for channelID, modelRatio := range ratio_setting.GetChannelVideoRatioCopy() { channelVideoRatio[channelID] = modelRatio } for channelID, modelRatio := range ratio_setting.GetChannelVideoCompletionRatioCopy() { channelVideoCompletionRatio[channelID] = modelRatio } for channelID, modelPrice := range ratio_setting.GetChannelVideoPriceCopy() { channelVideoPrice[channelID] = modelPrice } for channelID, modelPrice := range ratio_setting.GetChannelImagePriceCopy() { channelImagePrice[channelID] = modelPrice } for channelID, tierRatio := range ratio_setting.GetChannelModelTierRatioCopy() { channelModelTierRatio[channelID] = tierRatio } for channelID, tierRatio := range ratio_setting.GetChannelCompletionTierRatioCopy() { channelCompletionTierRatio[channelID] = tierRatio } for channelID, tierRatio := range ratio_setting.GetChannelCacheTierRatioCopy() { channelCacheTierRatio[channelID] = tierRatio } for channelID, tierRatio := range ratio_setting.GetChannelCreateCacheTierRatioCopy() { channelCreateCacheTierRatio[channelID] = tierRatio } channelModelPrice = filterChannelPricingMapByVisibleChannels(channelModelPrice, visibleChannelIDs) channelModelRatio = filterChannelPricingMapByVisibleChannels(channelModelRatio, visibleChannelIDs) channelCompletionRatio = filterChannelPricingMapByVisibleChannels(channelCompletionRatio, visibleChannelIDs) channelCacheRatio = filterChannelPricingMapByVisibleChannels(channelCacheRatio, visibleChannelIDs) channelCreateCacheRatio = filterChannelPricingMapByVisibleChannels(channelCreateCacheRatio, visibleChannelIDs) channelImageRatio = filterChannelPricingMapByVisibleChannels(channelImageRatio, visibleChannelIDs) channelAudioRatio = filterChannelPricingMapByVisibleChannels(channelAudioRatio, visibleChannelIDs) channelAudioCompletionRatio = filterChannelPricingMapByVisibleChannels(channelAudioCompletionRatio, visibleChannelIDs) channelVideoRatio = filterChannelPricingMapByVisibleChannels(channelVideoRatio, visibleChannelIDs) channelVideoCompletionRatio = filterChannelPricingMapByVisibleChannels(channelVideoCompletionRatio, visibleChannelIDs) channelVideoPrice = filterChannelPricingMapByVisibleChannels(channelVideoPrice, visibleChannelIDs) channelImagePrice = filterChannelPricingMapByVisibleChannels(channelImagePrice, visibleChannelIDs) channelModelTierRatio = filterChannelTierPricingMapByVisibleChannels(channelModelTierRatio, visibleChannelIDs) channelCompletionTierRatio = filterChannelTierPricingMapByVisibleChannels(channelCompletionTierRatio, visibleChannelIDs) channelCacheTierRatio = filterChannelTierPricingMapByVisibleChannels(channelCacheTierRatio, visibleChannelIDs) channelCreateCacheTierRatio = filterChannelTierPricingMapByVisibleChannels(channelCreateCacheTierRatio, visibleChannelIDs) for supplierID, modelPrice := range ratio_setting.GetSupplierModelPriceCopy() { supplierModelPrice[supplierID] = modelPrice } for supplierID, modelRatio := range ratio_setting.GetSupplierModelRatioCopy() { supplierModelRatio[supplierID] = modelRatio } channelPricingMeta, err := model.ListChannelPricingMeta() if err != nil { channelPricingMeta = nil } pricingData := model.BuildPricingAPIItems(filtered, visibleChannelIDs, channelPricingMeta, false) if exists && common.IsDistributorProfitShareMode() { if uid, ok := userId.(int); ok && uid > 0 { model.ApplyInviteeMarkupToPricingAPIForUser(uid, pricingData) } } // 读取热度统计周期配置 common.OptionMapRWMutex.RLock() heatStatPeriod := common.OptionMap["HeatStatPeriod"] common.OptionMapRWMutex.RUnlock() if heatStatPeriod == "" { heatStatPeriod = model.HeatStatPeriod7d } // 查询模型和渠道的请求统计数据 modelStats, _ := model.GetModelRequestStatsByPeriod(heatStatPeriod) // 将 visibleChannelIDs (map) 转换为 slice visibleIDSlice := make([]int, 0, len(visibleChannelIDs)) for id := range visibleChannelIDs { visibleIDSlice = append(visibleIDSlice, id) } channelStats, _ := model.GetChannelModelRequestStatsByPeriod(visibleIDSlice, heatStatPeriod) // 构建查询映射 modelStatsMap := make(map[string]model.ModelRequestStats) for _, s := range modelStats { modelStatsMap[s.ModelName] = s } channelStatsMap := make(map[string]model.ChannelModelRequestStats) for _, s := range channelStats { key := fmt.Sprintf("%d:%s", s.ChannelID, s.ModelName) channelStatsMap[key] = s } // 预加载所有模型的权重配置 modelConfigs := make(map[string]model.Model) var allModels []model.Model model.DB.Find(&allModels) for _, m := range allModels { modelConfigs[m.ModelName] = m } // 预加载所有渠道-模型热力配置 channelModelHeats, _ := model.GetAllChannelModelHeats() channelHeatMap := make(map[string]model.ChannelModelHeat) for _, heat := range channelModelHeats { key := fmt.Sprintf("%d:%s", heat.ChannelID, heat.ModelName) channelHeatMap[key] = heat } // 整合统计数据到 pricingData for i := range pricingData { item := &pricingData[i] modelName := item.ModelName // 整合渠道数据 for j := range item.ChannelList { ch := &item.ChannelList[j] key := fmt.Sprintf("%d:%s", ch.ChannelID, modelName) var modelWeight float64 = 1 // 获取渠道-模型热力配置(新表) if heat, ok := channelHeatMap[key]; ok { ch.SortWeight = heat.ChannelSortWeight ch.ManualBaseReqCount = heat.ManualBaseReqCount modelWeight = heat.ModelSortWeight if modelWeight <= 0 { modelWeight = 1 } } else { // 默认配置 ch.SortWeight = 1 ch.ManualBaseReqCount = 0 } // 获取渠道-模型自动统计数据 if cs, ok := channelStatsMap[key]; ok { ch.AutoReqCount = cs.RequestCount7d } else { ch.AutoReqCount = 0 } // 计算渠道最终调用次数和热度得分 ch.FinalReqCount = ch.ManualBaseReqCount + ch.AutoReqCount // 热度分 = 最终调用次数 × 渠道权重 × 模型权重 // 防止权重为0导致热度分为0 if ch.SortWeight <= 0 { ch.SortWeight = 1 } ch.ChannelHeatScore = float64(ch.FinalReqCount) * ch.SortWeight * modelWeight } } blurPricing := false if !exists && shouldBlurPricing() { blurPricing = true sanitizePricingData(pricingData) groupRatio = map[string]float64{} groupModelPrice = map[string]map[string]float64{} groupModelRatio = map[string]map[string]float64{} channelModelPrice = map[string]map[string]float64{} channelModelRatio = map[string]map[string]float64{} channelCompletionRatio = map[string]map[string]float64{} channelCacheRatio = map[string]map[string]float64{} channelCreateCacheRatio = map[string]map[string]float64{} channelImageRatio = map[string]map[string]float64{} channelAudioRatio = map[string]map[string]float64{} channelAudioCompletionRatio = map[string]map[string]float64{} channelVideoRatio = map[string]map[string]float64{} channelVideoCompletionRatio = map[string]map[string]float64{} channelVideoPrice = map[string]map[string]float64{} channelImagePrice = map[string]map[string]float64{} channelModelTierRatio = map[string]map[string]ratio_setting.TierSegments{} channelCompletionTierRatio = map[string]map[string]ratio_setting.TierSegments{} channelCacheTierRatio = map[string]map[string]ratio_setting.TierSegments{} channelCreateCacheTierRatio = map[string]map[string]ratio_setting.TierSegments{} supplierModelPrice = map[string]map[string]float64{} supplierModelRatio = map[string]map[string]float64{} } c.JSON(200, gin.H{ "success": true, "data": pricingData, "blur_pricing": blurPricing, "heat_stat_period": heatStatPeriod, "vendors": model.GetVendors(), // "channels": channels, "group_ratio": groupRatio, "group_model_price": groupModelPrice, "group_model_ratio": groupModelRatio, "channel_model_price": channelModelPrice, "channel_model_ratio": channelModelRatio, "channel_completion_ratio": channelCompletionRatio, "channel_cache_ratio": channelCacheRatio, "channel_create_cache_ratio": channelCreateCacheRatio, "channel_image_ratio": channelImageRatio, "channel_image_price": channelImagePrice, "channel_audio_ratio": channelAudioRatio, "channel_audio_completion_ratio": channelAudioCompletionRatio, "channel_video_ratio": channelVideoRatio, "channel_video_completion_ratio": channelVideoCompletionRatio, "channel_video_price": channelVideoPrice, "channel_model_tier_ratio": channelModelTierRatio, "channel_completion_tier_ratio": channelCompletionTierRatio, "channel_cache_tier_ratio": channelCacheTierRatio, "channel_create_cache_tier_ratio": channelCreateCacheTierRatio, "supplier_model_price": supplierModelPrice, "supplier_model_ratio": supplierModelRatio, "usable_group": usableGroup, "supported_endpoint": model.GetSupportedEndpointMap(), "auto_groups": service.GetUserAutoGroup(group), "pricing_version": "b58e1c9a3f7d4e2a8c0b1d6e9f4a2c7d8e0f1b2a3", }) } func ResetModelRatio(c *gin.Context) { defaultStr := ratio_setting.DefaultModelRatio2JSONString() err := model.UpdateOption("ModelRatio", defaultStr) if err != nil { c.JSON(200, gin.H{ "success": false, "message": err.Error(), }) return } err = ratio_setting.UpdateModelRatioByJSONString(defaultStr) if err != nil { c.JSON(200, gin.H{ "success": false, "message": err.Error(), }) return } c.JSON(200, gin.H{ "success": true, "message": "重置模型倍率成功", }) } func GetVendors(c *gin.Context) { vendors := model.GetVendors() c.JSON(200, gin.H{ "success": true, "data": vendors, }) }