586 lines
20 KiB
Go
586 lines
20 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
cryptorand "crypto/rand"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"yingce/backend/internal/model"
|
|
)
|
|
|
|
func (s *Service) invalidateRouteCatalog() {
|
|
s.routeCatalogRefreshMu.Lock()
|
|
s.routeCatalogMu.Lock()
|
|
s.routeCatalog = nil
|
|
s.routeCatalogVersion++
|
|
s.routeCatalogRetryAt = time.Time{}
|
|
s.routeCatalogRefreshError = nil
|
|
s.routeCatalogMu.Unlock()
|
|
s.routeCatalogRefreshMu.Unlock()
|
|
if s.coordinator != nil {
|
|
ctx, cancel := context.WithTimeout(context.Background(), runtimeCoordinationTimeout)
|
|
defer cancel()
|
|
if err := s.coordinator.BumpRouteCatalogVersion(ctx); err != nil {
|
|
log.Printf("logical model route catalog distributed invalidation failed: %v", err)
|
|
}
|
|
}
|
|
s.initReadCaches()
|
|
s.routeVersionReadCache.Clear()
|
|
}
|
|
|
|
func (s *Service) routeCatalogSnapshot() (*routeCatalogSnapshot, error) {
|
|
now := time.Now()
|
|
version := s.currentRouteCatalogVersion()
|
|
s.routeCatalogMu.RLock()
|
|
snapshot := s.routeCatalog
|
|
if snapshot != nil && now.Sub(snapshot.LoadedAt) < s.routeCatalogTTL && snapshot.CatalogVersion == version {
|
|
s.routeCatalogMu.RUnlock()
|
|
return snapshot, nil
|
|
}
|
|
s.routeCatalogMu.RUnlock()
|
|
|
|
s.routeCatalogRefreshMu.Lock()
|
|
defer s.routeCatalogRefreshMu.Unlock()
|
|
now = time.Now()
|
|
version = s.currentRouteCatalogVersion()
|
|
s.routeCatalogMu.RLock()
|
|
snapshot = s.routeCatalog
|
|
if snapshot != nil && now.Sub(snapshot.LoadedAt) < s.routeCatalogTTL && snapshot.CatalogVersion == version {
|
|
s.routeCatalogMu.RUnlock()
|
|
return snapshot, nil
|
|
}
|
|
s.routeCatalogMu.RUnlock()
|
|
|
|
// 刷新锁只能防止并行回源,失败后还需要冷却,否则等待者会逐个重打数据库。
|
|
if s.routeCatalogRefreshError != nil && now.Before(s.routeCatalogRetryAt) {
|
|
if snapshot != nil && snapshot.CatalogVersion == version && now.Sub(snapshot.LoadedAt) <= s.routeCatalogMaxStale {
|
|
return snapshot, nil
|
|
}
|
|
return nil, s.routeCatalogRefreshError
|
|
}
|
|
loaded, err := s.loadRouteCatalog()
|
|
if err != nil {
|
|
s.routeCatalogRefreshError = err
|
|
s.routeCatalogRetryAt = time.Now().Add(2 * time.Second)
|
|
log.Printf("logical model route catalog refresh failed; retry cooled down: %v", err)
|
|
// 已有快照过期时允许短暂继续服务,数据库首次加载失败则明确失败。
|
|
if snapshot != nil && snapshot.CatalogVersion == version && now.Sub(snapshot.LoadedAt) <= s.routeCatalogMaxStale {
|
|
return snapshot, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
s.routeCatalogRefreshError = nil
|
|
s.routeCatalogRetryAt = time.Time{}
|
|
s.routeCatalogMu.Lock()
|
|
s.routeCatalog = loaded
|
|
s.routeCatalogMu.Unlock()
|
|
return loaded, nil
|
|
}
|
|
|
|
func (s *Service) currentRouteCatalogVersion() int64 {
|
|
s.routeCatalogMu.RLock()
|
|
localVersion := s.routeCatalogVersion
|
|
s.routeCatalogMu.RUnlock()
|
|
if s.coordinator == nil || !s.coordinator.HasRedis() {
|
|
return localVersion
|
|
}
|
|
s.initReadCaches()
|
|
ctx, cancel := context.WithTimeout(context.Background(), runtimeCoordinationTimeout)
|
|
defer cancel()
|
|
version, err := s.routeVersionReadCache.Get(ctx, routeCatalogVersionKey, func(ctx context.Context) (int64, int, error) {
|
|
value, err := s.coordinator.RouteCatalogVersion(ctx)
|
|
if err != nil {
|
|
log.Printf("logical model route catalog distributed version check failed: %v", err)
|
|
}
|
|
return value, 256, err
|
|
})
|
|
if err != nil {
|
|
return localVersion
|
|
}
|
|
if version > localVersion {
|
|
return version
|
|
}
|
|
return localVersion
|
|
}
|
|
|
|
func (s *Service) loadRouteCatalog() (*routeCatalogSnapshot, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
repo := s.repo.WithContext(ctx)
|
|
items, err := repo.LogicalModels(false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snapshot := &routeCatalogSnapshot{LoadedAt: time.Now(), CatalogVersion: s.currentRouteCatalogVersion(), Models: make(map[string]cachedLogicalModel), Ordered: make([]string, 0, len(items))}
|
|
graphs, err := repo.LogicalModelGraphs(items, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
systemChannelIDs := make([]string, 0)
|
|
for _, graph := range graphs {
|
|
if graph == nil {
|
|
continue
|
|
}
|
|
for _, channelModel := range graph.ChannelModels {
|
|
systemChannelIDs = append(systemChannelIDs, channelModel.ChannelID)
|
|
}
|
|
}
|
|
systemChannels, err := repo.SystemChannelsByIDs(systemChannelIDs, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
enabledSystemChannels := make(map[string]bool, len(systemChannels))
|
|
for _, channel := range systemChannels {
|
|
enabledSystemChannels[channel.ID] = true
|
|
}
|
|
for _, item := range items {
|
|
graph := graphs[item.ID]
|
|
if graph == nil || graph.Revision == nil {
|
|
log.Printf("logical model omitted from route catalog id=%s: graph unavailable", item.ID)
|
|
continue
|
|
}
|
|
productSpec, decodeErr := DecodeCapabilitySpec(graph.Revision.CapabilitySpecJSON)
|
|
if decodeErr != nil {
|
|
log.Printf("logical model omitted from route catalog id=%s: invalid product capability: %v", item.ID, decodeErr)
|
|
continue
|
|
}
|
|
channelModelByID := make(map[string]model.ChannelModel, len(graph.ChannelModels))
|
|
for _, channelModel := range graph.ChannelModels {
|
|
channelModelByID[channelModel.ID] = channelModel
|
|
}
|
|
cached := cachedLogicalModel{Model: item, Revision: *graph.Revision, ProductSpec: productSpec, Defaults: map[string]any{}}
|
|
for _, route := range graph.Routes {
|
|
channelModel, ok := channelModelByID[route.ChannelModelID]
|
|
if !ok || !channelModel.Enabled || !enabledSystemChannels[channelModel.ChannelID] {
|
|
continue
|
|
}
|
|
if item.PricePolicy == "unified" && item.BillingMode == "token" && !supportsTokenBilling(item.Capability, channelModel.Protocol) {
|
|
continue
|
|
}
|
|
if item.PricePolicy == "channel" && !channelModelHasActivePriceTier(channelModel) {
|
|
continue
|
|
}
|
|
capabilitySpec, specErr := channelModelCapabilitySpec(channelModel, s.protocolRegistry())
|
|
if specErr != nil {
|
|
log.Printf("logical route omitted from catalog route_id=%s channel_model_id=%s: invalid capability: %v", route.ID, channelModel.ID, specErr)
|
|
continue
|
|
}
|
|
cached.Routes = append(cached.Routes, cachedLogicalRoute{Route: route, CapabilitySpec: capabilitySpec, ChannelModel: channelModel})
|
|
}
|
|
productSpec = capabilitySpecWithRoutePresets(productSpec, enabledLogicalRouteSpecs(cached.Routes))
|
|
defaults, defaultsErr := decodeLogicalDefaults(graph.Revision.DefaultOptionsJSON, productSpec)
|
|
if defaultsErr != nil {
|
|
log.Printf("logical model omitted from route catalog id=%s: invalid defaults: %v", item.ID, defaultsErr)
|
|
continue
|
|
}
|
|
cached.ProductSpec = productSpec
|
|
cached.Defaults = defaults
|
|
snapshot.Models[item.ID] = cached
|
|
snapshot.Ordered = append(snapshot.Ordered, item.ID)
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
// ResolveLogicalModel 将创作意图解析为一次可执行的路由快照。
|
|
// 解析同时约束能力合同、启用状态、价格档和渠道协议;调用方不得在解析完成后自行替换其中任一供应链字段,
|
|
// 否则会出现“目录显示可用、任务实际走另一条线路”的配置漂移。
|
|
func (s *Service) ResolveLogicalModel(logicalModelID string, intent ModelRequestIntent) (*RoutedModel, error) {
|
|
snapshot, err := s.routeCatalogSnapshot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cached, ok := snapshot.Models[strings.TrimSpace(logicalModelID)]
|
|
if !ok {
|
|
return nil, BadAuthRequest("所选模型不可用")
|
|
}
|
|
intent = resolveModelRequestIntent(cached.ProductSpec, intent, cached.Defaults)
|
|
if parameters := cached.ProductSpec.ImageParameters; parameters != nil {
|
|
if _, err := resolveImageParameterValues(parameters, intent.Options); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if match := MatchCapability(cached.ProductSpec, intent); !match.Matched {
|
|
return nil, modelCapabilityMismatchError(match)
|
|
}
|
|
eligible := s.eligibleLogicalRoutes(cached.Routes, intent, nil, cached.Model.PricePolicy == "channel")
|
|
if len(eligible) == 0 {
|
|
return nil, BadAuthRequest("当前模型暂时无法满足这组输入和参数")
|
|
}
|
|
selected := weightedRoute(eligible)
|
|
var priceTier *model.ChannelModelPriceTier
|
|
if cached.Model.PricePolicy == "channel" {
|
|
priceTier = channelModelPriceTierForIntent(selected.ChannelModel, intent, selected.CapabilitySpec)
|
|
if priceTier == nil {
|
|
return nil, BadAuthRequest("当前模型尚未配置所选规格的价格")
|
|
}
|
|
}
|
|
return &RoutedModel{LogicalModel: cached.Model, Revision: cached.Revision, Route: selected.Route, ChannelModel: selected.ChannelModel, CapabilitySpec: selected.CapabilitySpec, PriceTier: priceTier, Defaults: cached.Defaults, Intent: intent}, nil
|
|
}
|
|
|
|
func (s *Service) eligibleLogicalRoutes(routes []cachedLogicalRoute, intent ModelRequestIntent, tried map[string]bool, requirePriceTier bool) []cachedLogicalRoute {
|
|
eligible := make([]cachedLogicalRoute, 0, len(routes))
|
|
maxPriority := math.MinInt
|
|
for _, route := range routes {
|
|
if !route.Route.Enabled || route.Route.Weight <= 0 || tried[route.Route.ID] || s.logicalRouteBlocked(route) {
|
|
continue
|
|
}
|
|
if match := MatchCapability(route.CapabilitySpec, intent); !match.Matched {
|
|
continue
|
|
}
|
|
if requirePriceTier && channelModelPriceTierForIntent(route.ChannelModel, intent, route.CapabilitySpec) == nil {
|
|
continue
|
|
}
|
|
if route.Route.Priority > maxPriority {
|
|
eligible = eligible[:0]
|
|
maxPriority = route.Route.Priority
|
|
}
|
|
if route.Route.Priority == maxPriority {
|
|
eligible = append(eligible, route)
|
|
}
|
|
}
|
|
return eligible
|
|
}
|
|
|
|
func channelModelHasActivePriceTier(channelModel model.ChannelModel) bool {
|
|
for _, tier := range channelModel.PriceTiers {
|
|
if tier.Enabled && tier.PriceConfigured {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// channelModelPriceTierForIntent 使用“精确规格优先、通配规格兜底”的规则。SKU 选择器与
|
|
// 运行意图使用同一组规范键,因而图片质量/画幅、视频分辨率/时长和生成操作都能独立定价。
|
|
func channelModelPriceTierForIntent(channelModel model.ChannelModel, intent ModelRequestIntent, specs ...CapabilitySpec) *model.ChannelModelPriceTier {
|
|
selector := skuSelectorForIntent(intent)
|
|
parameters := imagePriceParametersForChannel(channelModel, specs...)
|
|
if parameters != nil {
|
|
selector = imagePriceSelectorForParameters(parameters, intent)
|
|
}
|
|
bestScore := -1
|
|
var best *model.ChannelModelPriceTier
|
|
for index := range channelModel.PriceTiers {
|
|
tier := &channelModel.PriceTiers[index]
|
|
if !tier.Enabled || !tier.PriceConfigured {
|
|
continue
|
|
}
|
|
tierSelector := skuSelectorForTier(*tier)
|
|
if parameters != nil {
|
|
tierSelector = model.DecodeSKUSelector(tier.SelectorJSON)
|
|
if validateImageParameterPriceSelector(parameters, tierSelector) != nil {
|
|
continue
|
|
}
|
|
}
|
|
matched, score := matchSKUSelector(tierSelector, selector)
|
|
if !matched {
|
|
continue
|
|
}
|
|
if score > bestScore {
|
|
best, bestScore = tier, score
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
// channelModelHasCompatiblePriceTier is only for catalog discovery: an option
|
|
// omitted from the intent is still a choice, while every supplied selector
|
|
// value must be compatible with at least one active, configured tier.
|
|
func channelModelHasCompatiblePriceTier(channelModel model.ChannelModel, intent ModelRequestIntent, specs ...CapabilitySpec) bool {
|
|
requested := skuSelectorForIntent(intent)
|
|
parameters := imagePriceParametersForChannel(channelModel, specs...)
|
|
if parameters != nil {
|
|
requested = imagePriceSelectorForParameters(parameters, intent)
|
|
}
|
|
for _, tier := range channelModel.PriceTiers {
|
|
if !tier.Enabled || !tier.PriceConfigured {
|
|
continue
|
|
}
|
|
compatible := true
|
|
tierSelector := skuSelectorForTier(tier)
|
|
if parameters != nil {
|
|
tierSelector = model.DecodeSKUSelector(tier.SelectorJSON)
|
|
if validateImageParameterPriceSelector(parameters, tierSelector) != nil {
|
|
continue
|
|
}
|
|
}
|
|
for key, expected := range tierSelector {
|
|
expected = strings.TrimSpace(expected)
|
|
if expected == "" || expected == "*" {
|
|
continue
|
|
}
|
|
if actual, provided := requested[key]; provided && actual != expected {
|
|
compatible = false
|
|
break
|
|
}
|
|
}
|
|
if compatible {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func skuSelectorForIntent(intent ModelRequestIntent) map[string]string {
|
|
selector := map[string]string{}
|
|
if operation := strings.ToLower(strings.TrimSpace(intent.Operation)); operation != "" {
|
|
selector["operation"] = operation
|
|
}
|
|
switch normalizeCapability(intent.Capability) {
|
|
case "video":
|
|
// 价格档按实际参考素材归类。供应商执行仍可使用 reference_to_video、extend
|
|
// 等细分操作;计价时视频参考优先归为视频生视频,其余图片参考无论数量
|
|
// 都归为图生视频。
|
|
if intent.Inputs["video"] > 0 {
|
|
selector["operation"] = "video_to_video"
|
|
} else if intent.Inputs["image"] > 0 {
|
|
selector["operation"] = "image_to_video"
|
|
}
|
|
if count := intent.Inputs["image"]; count > 0 {
|
|
selector["imageCount"] = strconv.Itoa(count)
|
|
}
|
|
if value := normalizeChannelModelTierResolution(fmt.Sprint(intent.Options["vquality"])); value != "*" {
|
|
selector["vquality"] = value
|
|
}
|
|
if seconds, err := strconv.Atoi(strings.TrimSpace(fmt.Sprint(intent.Options["videoSeconds"]))); err == nil && seconds > 0 {
|
|
selector["videoSeconds"] = strconv.Itoa(seconds)
|
|
}
|
|
if audio := normalizedScalar(intent.Options["videoGenerateAudio"]); audio == "true" || audio == "false" {
|
|
selector["videoGenerateAudio"] = audio
|
|
}
|
|
case "image":
|
|
for name, value := range intent.Options {
|
|
if value != nil {
|
|
selector[name] = fmt.Sprint(value)
|
|
}
|
|
}
|
|
if intent.Inputs["image"] > 0 {
|
|
selector["operation"] = "image_to_image"
|
|
} else {
|
|
selector["operation"] = "text_to_image"
|
|
}
|
|
rawQuality, _ := intent.Options["quality"].(string)
|
|
rawSize, _ := intent.Options["size"].(string)
|
|
quality := normalizeImagePriceQuality(rawQuality, "")
|
|
if quality != "" {
|
|
selector["quality"] = quality
|
|
if isImageResolutionTier(quality) {
|
|
selector["resolution"] = quality
|
|
}
|
|
}
|
|
if resolution := imageResolutionFromSize(rawSize); resolution != "" {
|
|
selector["resolution"] = resolution
|
|
// 保留存量 quality=1k/2k/4k 价格档的请求选择器。
|
|
if quality == "" || quality == "auto" || quality == "any" {
|
|
selector["quality"] = resolution
|
|
}
|
|
}
|
|
if quality := selector["quality"]; quality == "auto" || quality == "any" {
|
|
delete(selector, "quality")
|
|
}
|
|
if value := strings.ToLower(strings.TrimSpace(rawSize)); value != "" && value != "auto" && value != "any" {
|
|
selector["size"] = value
|
|
}
|
|
}
|
|
return selector
|
|
}
|
|
|
|
func normalizeImagePriceQuality(rawQuality string, rawSize string) string {
|
|
quality := strings.ToLower(strings.TrimSpace(rawQuality))
|
|
if quality != "" && quality != "auto" && quality != "any" {
|
|
return quality
|
|
}
|
|
parts := strings.Split(strings.ToLower(strings.TrimSpace(rawSize)), "x")
|
|
if len(parts) != 2 {
|
|
return ""
|
|
}
|
|
width, widthErr := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64)
|
|
height, heightErr := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64)
|
|
if widthErr != nil || heightErr != nil || width <= 0 || height <= 0 || width > (1<<32)/height {
|
|
return ""
|
|
}
|
|
pixels := width * height
|
|
switch {
|
|
case pixels <= 2_000_000:
|
|
return "1k"
|
|
case pixels <= 4_300_000:
|
|
return "2k"
|
|
case pixels <= 8_294_400:
|
|
return "4k"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func imageResolutionFromSize(rawSize string) string {
|
|
parts := strings.Split(strings.ToLower(strings.TrimSpace(rawSize)), "x")
|
|
if len(parts) != 2 {
|
|
return ""
|
|
}
|
|
width, widthErr := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64)
|
|
height, heightErr := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64)
|
|
if widthErr != nil || heightErr != nil || width <= 0 || height <= 0 || width > (1<<32)/height {
|
|
return ""
|
|
}
|
|
pixels := width * height
|
|
switch {
|
|
case pixels <= 2_000_000:
|
|
return "1k"
|
|
case pixels <= 4_300_000:
|
|
return "2k"
|
|
case pixels <= 8_294_400:
|
|
return "4k"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func isImageResolutionTier(value string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case "1k", "2k", "4k":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func skuSelectorForTier(tier model.ChannelModelPriceTier) map[string]string {
|
|
selector := model.DecodeSKUSelector(tier.SelectorJSON)
|
|
if quality := strings.ToLower(strings.TrimSpace(selector["quality"])); isImageResolutionTier(quality) && strings.TrimSpace(selector["resolution"]) == "" {
|
|
selector["resolution"] = quality
|
|
delete(selector, "quality")
|
|
}
|
|
if len(selector) == 0 {
|
|
if resolution := normalizeChannelModelTierResolution(tier.Resolution); resolution != "*" {
|
|
selector["vquality"] = resolution
|
|
}
|
|
if tier.VideoSeconds > 0 {
|
|
selector["videoSeconds"] = strconv.Itoa(tier.VideoSeconds)
|
|
}
|
|
}
|
|
return selector
|
|
}
|
|
|
|
func matchSKUSelector(tier map[string]string, requested map[string]string) (bool, int) {
|
|
score := 0
|
|
for key, expected := range tier {
|
|
expected = strings.TrimSpace(expected)
|
|
if expected == "" || expected == "*" {
|
|
continue
|
|
}
|
|
if requested[key] != expected {
|
|
return false, 0
|
|
}
|
|
score++
|
|
}
|
|
return true, score
|
|
}
|
|
|
|
func (s *Service) logicalRouteBlocked(route cachedLogicalRoute) bool {
|
|
now := time.Now()
|
|
keys := []string{"channel:" + route.ChannelModel.ChannelID, "channel-model:" + route.ChannelModel.ID, "route:" + route.Route.ID}
|
|
// 这里会删除过期项,必须使用写锁;不要改成 RLock。
|
|
s.routeHealthMu.Lock()
|
|
for _, key := range keys {
|
|
until, exists := s.routeHealthBlocked[key]
|
|
if !exists {
|
|
continue
|
|
}
|
|
if !until.After(now) {
|
|
delete(s.routeHealthBlocked, key)
|
|
continue
|
|
}
|
|
s.routeHealthMu.Unlock()
|
|
return true
|
|
}
|
|
s.routeHealthMu.Unlock()
|
|
|
|
if s.coordinator == nil || !s.coordinator.HasRedis() {
|
|
return false
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
|
defer cancel()
|
|
for _, key := range keys {
|
|
until, err := s.coordinator.RouteBlockedUntil(ctx, key)
|
|
if err != nil {
|
|
log.Printf("logical route distributed health check failed key=%s: %v", key, err)
|
|
return false
|
|
}
|
|
if until.After(now) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func mergeIntentDefaults(options map[string]any, defaults map[string]any) map[string]any {
|
|
result := make(map[string]any, len(defaults)+len(options))
|
|
for key, value := range defaults {
|
|
result[key] = value
|
|
}
|
|
for key, value := range options {
|
|
result[key] = value
|
|
}
|
|
return result
|
|
}
|
|
|
|
func weightedRoute(routes []cachedLogicalRoute) cachedLogicalRoute {
|
|
if len(routes) == 1 {
|
|
return routes[0]
|
|
}
|
|
var total int64
|
|
for _, route := range routes {
|
|
total += int64(route.Route.Weight)
|
|
}
|
|
if total <= 0 {
|
|
return routes[0]
|
|
}
|
|
var raw [8]byte
|
|
if _, err := cryptorand.Read(raw[:]); err != nil {
|
|
return routes[0]
|
|
}
|
|
totalUnsigned := uint64(total)
|
|
// 丢弃不能整除 total 的低概率尾部,避免取模造成轻微权重偏差。
|
|
threshold := -totalUnsigned % totalUnsigned
|
|
randomValue := binary.LittleEndian.Uint64(raw[:])
|
|
for randomValue < threshold {
|
|
if _, err := cryptorand.Read(raw[:]); err != nil {
|
|
return routes[0]
|
|
}
|
|
randomValue = binary.LittleEndian.Uint64(raw[:])
|
|
}
|
|
pick := int64(randomValue % totalUnsigned)
|
|
for _, route := range routes {
|
|
if pick < int64(route.Route.Weight) {
|
|
return route
|
|
}
|
|
pick -= int64(route.Route.Weight)
|
|
}
|
|
return routes[len(routes)-1]
|
|
}
|
|
|
|
func (s *Service) sortedRouteDiagnostics(routes []cachedLogicalRoute, intent ModelRequestIntent) []RouteSimulationCandidate {
|
|
result := make([]RouteSimulationCandidate, 0, len(routes))
|
|
poolPriority := math.MinInt
|
|
for _, route := range routes {
|
|
match := MatchCapability(route.CapabilitySpec, intent)
|
|
blocked := s.logicalRouteBlocked(route)
|
|
if route.Route.Enabled && route.Route.Weight > 0 && match.Matched && !blocked && route.Route.Priority > poolPriority {
|
|
poolPriority = route.Route.Priority
|
|
}
|
|
result = append(result, RouteSimulationCandidate{RouteID: route.Route.ID, ChannelModelID: route.ChannelModel.ID, ChannelModelKey: route.ChannelModel.ModelKey, ChannelModelName: route.ChannelModel.DisplayName, Priority: route.Route.Priority, Weight: route.Route.Weight, Enabled: route.Route.Enabled, Matched: match.Matched, Blocked: blocked, Reasons: match.Reasons})
|
|
}
|
|
for index := range result {
|
|
result[index].InPool = result[index].Enabled && result[index].Weight > 0 && result[index].Matched && !result[index].Blocked && result[index].Priority == poolPriority
|
|
}
|
|
sort.SliceStable(result, func(i, j int) bool { return result[i].Priority > result[j].Priority })
|
|
return result
|
|
}
|