|
|
@@ -0,0 +1,983 @@
|
|
|
+package controller
|
|
|
+
|
|
|
+import (
|
|
|
+ "fmt"
|
|
|
+ "net/http"
|
|
|
+ "slices"
|
|
|
+ "strings"
|
|
|
+ "sync"
|
|
|
+ "sync/atomic"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/QuantumNous/new-api/common"
|
|
|
+ "github.com/QuantumNous/new-api/constant"
|
|
|
+ "github.com/QuantumNous/new-api/dto"
|
|
|
+ "github.com/QuantumNous/new-api/model"
|
|
|
+ "github.com/QuantumNous/new-api/relay/channel/gemini"
|
|
|
+ "github.com/QuantumNous/new-api/relay/channel/ollama"
|
|
|
+ "github.com/QuantumNous/new-api/service"
|
|
|
+
|
|
|
+ "github.com/gin-gonic/gin"
|
|
|
+ "github.com/samber/lo"
|
|
|
+)
|
|
|
+
|
|
|
+const (
|
|
|
+ channelUpstreamModelUpdateTaskDefaultIntervalMinutes = 30
|
|
|
+ channelUpstreamModelUpdateTaskBatchSize = 100
|
|
|
+ channelUpstreamModelUpdateMinCheckIntervalSeconds = 300
|
|
|
+ channelUpstreamModelUpdateNotifySuppressWindowSeconds = 86400
|
|
|
+ channelUpstreamModelUpdateNotifyMaxChannelDetails = 8
|
|
|
+ channelUpstreamModelUpdateNotifyMaxModelDetails = 12
|
|
|
+ channelUpstreamModelUpdateNotifyMaxFailedChannelIDs = 10
|
|
|
+)
|
|
|
+
|
|
|
+var (
|
|
|
+ channelUpstreamModelUpdateTaskOnce sync.Once
|
|
|
+ channelUpstreamModelUpdateTaskRunning atomic.Bool
|
|
|
+ channelUpstreamModelUpdateNotifyState = struct {
|
|
|
+ sync.Mutex
|
|
|
+ lastNotifiedAt int64
|
|
|
+ lastChangedChannels int
|
|
|
+ lastFailedChannels int
|
|
|
+ }{}
|
|
|
+)
|
|
|
+
|
|
|
+type applyChannelUpstreamModelUpdatesRequest struct {
|
|
|
+ ID int `json:"id"`
|
|
|
+ AddModels []string `json:"add_models"`
|
|
|
+ RemoveModels []string `json:"remove_models"`
|
|
|
+ IgnoreModels []string `json:"ignore_models"`
|
|
|
+}
|
|
|
+
|
|
|
+type applyAllChannelUpstreamModelUpdatesResult struct {
|
|
|
+ ChannelID int `json:"channel_id"`
|
|
|
+ ChannelName string `json:"channel_name"`
|
|
|
+ AddedModels []string `json:"added_models"`
|
|
|
+ RemovedModels []string `json:"removed_models"`
|
|
|
+ RemainingModels []string `json:"remaining_models"`
|
|
|
+ RemainingRemoveModels []string `json:"remaining_remove_models"`
|
|
|
+}
|
|
|
+
|
|
|
+type detectChannelUpstreamModelUpdatesResult struct {
|
|
|
+ ChannelID int `json:"channel_id"`
|
|
|
+ ChannelName string `json:"channel_name"`
|
|
|
+ AddModels []string `json:"add_models"`
|
|
|
+ RemoveModels []string `json:"remove_models"`
|
|
|
+ LastCheckTime int64 `json:"last_check_time"`
|
|
|
+ AutoAddedModels int `json:"auto_added_models"`
|
|
|
+}
|
|
|
+
|
|
|
+type upstreamModelUpdateChannelSummary struct {
|
|
|
+ ChannelName string
|
|
|
+ AddCount int
|
|
|
+ RemoveCount int
|
|
|
+}
|
|
|
+
|
|
|
+func normalizeModelNames(models []string) []string {
|
|
|
+ return lo.Uniq(lo.FilterMap(models, func(model string, _ int) (string, bool) {
|
|
|
+ trimmed := strings.TrimSpace(model)
|
|
|
+ return trimmed, trimmed != ""
|
|
|
+ }))
|
|
|
+}
|
|
|
+
|
|
|
+func mergeModelNames(base []string, appended []string) []string {
|
|
|
+ merged := normalizeModelNames(base)
|
|
|
+ seen := make(map[string]struct{}, len(merged))
|
|
|
+ for _, model := range merged {
|
|
|
+ seen[model] = struct{}{}
|
|
|
+ }
|
|
|
+ for _, model := range normalizeModelNames(appended) {
|
|
|
+ if _, ok := seen[model]; ok {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ seen[model] = struct{}{}
|
|
|
+ merged = append(merged, model)
|
|
|
+ }
|
|
|
+ return merged
|
|
|
+}
|
|
|
+
|
|
|
+func subtractModelNames(base []string, removed []string) []string {
|
|
|
+ removeSet := make(map[string]struct{}, len(removed))
|
|
|
+ for _, model := range normalizeModelNames(removed) {
|
|
|
+ removeSet[model] = struct{}{}
|
|
|
+ }
|
|
|
+ return lo.Filter(normalizeModelNames(base), func(model string, _ int) bool {
|
|
|
+ _, ok := removeSet[model]
|
|
|
+ return !ok
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func intersectModelNames(base []string, allowed []string) []string {
|
|
|
+ allowedSet := make(map[string]struct{}, len(allowed))
|
|
|
+ for _, model := range normalizeModelNames(allowed) {
|
|
|
+ allowedSet[model] = struct{}{}
|
|
|
+ }
|
|
|
+ return lo.Filter(normalizeModelNames(base), func(model string, _ int) bool {
|
|
|
+ _, ok := allowedSet[model]
|
|
|
+ return ok
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func applySelectedModelChanges(originModels []string, addModels []string, removeModels []string) []string {
|
|
|
+ // Add wins when the same model appears in both selected lists.
|
|
|
+ normalizedAdd := normalizeModelNames(addModels)
|
|
|
+ normalizedRemove := subtractModelNames(normalizeModelNames(removeModels), normalizedAdd)
|
|
|
+ return subtractModelNames(mergeModelNames(originModels, normalizedAdd), normalizedRemove)
|
|
|
+}
|
|
|
+
|
|
|
+func normalizeChannelModelMapping(channel *model.Channel) map[string]string {
|
|
|
+ if channel == nil || channel.ModelMapping == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ rawMapping := strings.TrimSpace(*channel.ModelMapping)
|
|
|
+ if rawMapping == "" || rawMapping == "{}" {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ parsed := make(map[string]string)
|
|
|
+ if err := common.UnmarshalJsonStr(rawMapping, &parsed); err != nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ normalized := make(map[string]string, len(parsed))
|
|
|
+ for source, target := range parsed {
|
|
|
+ normalizedSource := strings.TrimSpace(source)
|
|
|
+ normalizedTarget := strings.TrimSpace(target)
|
|
|
+ if normalizedSource == "" || normalizedTarget == "" {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ normalized[normalizedSource] = normalizedTarget
|
|
|
+ }
|
|
|
+ if len(normalized) == 0 {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ return normalized
|
|
|
+}
|
|
|
+
|
|
|
+func collectPendingUpstreamModelChangesFromModels(
|
|
|
+ localModels []string,
|
|
|
+ upstreamModels []string,
|
|
|
+ ignoredModels []string,
|
|
|
+ modelMapping map[string]string,
|
|
|
+) (pendingAddModels []string, pendingRemoveModels []string) {
|
|
|
+ localSet := make(map[string]struct{})
|
|
|
+ localModels = normalizeModelNames(localModels)
|
|
|
+ upstreamModels = normalizeModelNames(upstreamModels)
|
|
|
+ for _, modelName := range localModels {
|
|
|
+ localSet[modelName] = struct{}{}
|
|
|
+ }
|
|
|
+ upstreamSet := make(map[string]struct{}, len(upstreamModels))
|
|
|
+ for _, modelName := range upstreamModels {
|
|
|
+ upstreamSet[modelName] = struct{}{}
|
|
|
+ }
|
|
|
+
|
|
|
+ ignoredSet := make(map[string]struct{})
|
|
|
+ for _, modelName := range normalizeModelNames(ignoredModels) {
|
|
|
+ ignoredSet[modelName] = struct{}{}
|
|
|
+ }
|
|
|
+
|
|
|
+ redirectSourceSet := make(map[string]struct{}, len(modelMapping))
|
|
|
+ redirectTargetSet := make(map[string]struct{}, len(modelMapping))
|
|
|
+ for source, target := range modelMapping {
|
|
|
+ redirectSourceSet[source] = struct{}{}
|
|
|
+ redirectTargetSet[target] = struct{}{}
|
|
|
+ }
|
|
|
+
|
|
|
+ coveredUpstreamSet := make(map[string]struct{}, len(localSet)+len(redirectTargetSet))
|
|
|
+ for modelName := range localSet {
|
|
|
+ coveredUpstreamSet[modelName] = struct{}{}
|
|
|
+ }
|
|
|
+ for modelName := range redirectTargetSet {
|
|
|
+ coveredUpstreamSet[modelName] = struct{}{}
|
|
|
+ }
|
|
|
+
|
|
|
+ pendingAdd := lo.Filter(upstreamModels, func(modelName string, _ int) bool {
|
|
|
+ if _, ok := coveredUpstreamSet[modelName]; ok {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if _, ok := ignoredSet[modelName]; ok {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ })
|
|
|
+ pendingRemove := lo.Filter(localModels, func(modelName string, _ int) bool {
|
|
|
+ // Redirect source models are virtual aliases and should not be removed
|
|
|
+ // only because they are absent from upstream model list.
|
|
|
+ if _, ok := redirectSourceSet[modelName]; ok {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ _, ok := upstreamSet[modelName]
|
|
|
+ return !ok
|
|
|
+ })
|
|
|
+ return normalizeModelNames(pendingAdd), normalizeModelNames(pendingRemove)
|
|
|
+}
|
|
|
+
|
|
|
+func collectPendingUpstreamModelChanges(channel *model.Channel, settings dto.ChannelOtherSettings) (pendingAddModels []string, pendingRemoveModels []string, err error) {
|
|
|
+ upstreamModels, err := fetchChannelUpstreamModelIDs(channel)
|
|
|
+ if err != nil {
|
|
|
+ return nil, nil, err
|
|
|
+ }
|
|
|
+ pendingAddModels, pendingRemoveModels = collectPendingUpstreamModelChangesFromModels(
|
|
|
+ channel.GetModels(),
|
|
|
+ upstreamModels,
|
|
|
+ settings.UpstreamModelUpdateIgnoredModels,
|
|
|
+ normalizeChannelModelMapping(channel),
|
|
|
+ )
|
|
|
+ return pendingAddModels, pendingRemoveModels, nil
|
|
|
+}
|
|
|
+
|
|
|
+func getUpstreamModelUpdateMinCheckIntervalSeconds() int64 {
|
|
|
+ interval := int64(common.GetEnvOrDefault(
|
|
|
+ "CHANNEL_UPSTREAM_MODEL_UPDATE_MIN_CHECK_INTERVAL_SECONDS",
|
|
|
+ channelUpstreamModelUpdateMinCheckIntervalSeconds,
|
|
|
+ ))
|
|
|
+ if interval < 0 {
|
|
|
+ return channelUpstreamModelUpdateMinCheckIntervalSeconds
|
|
|
+ }
|
|
|
+ return interval
|
|
|
+}
|
|
|
+
|
|
|
+func fetchChannelUpstreamModelIDs(channel *model.Channel) ([]string, error) {
|
|
|
+ baseURL := constant.ChannelBaseURLs[channel.Type]
|
|
|
+ if channel.GetBaseURL() != "" {
|
|
|
+ baseURL = channel.GetBaseURL()
|
|
|
+ }
|
|
|
+
|
|
|
+ if channel.Type == constant.ChannelTypeOllama {
|
|
|
+ key := strings.TrimSpace(strings.Split(channel.Key, "\n")[0])
|
|
|
+ models, err := ollama.FetchOllamaModels(baseURL, key)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return normalizeModelNames(lo.Map(models, func(item ollama.OllamaModel, _ int) string {
|
|
|
+ return item.Name
|
|
|
+ })), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ if channel.Type == constant.ChannelTypeGemini {
|
|
|
+ key, _, apiErr := channel.GetNextEnabledKey()
|
|
|
+ if apiErr != nil {
|
|
|
+ return nil, fmt.Errorf("获取渠道密钥失败: %w", apiErr)
|
|
|
+ }
|
|
|
+ key = strings.TrimSpace(key)
|
|
|
+ models, err := gemini.FetchGeminiModels(baseURL, key, channel.GetSetting().Proxy)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return normalizeModelNames(models), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ var url string
|
|
|
+ switch channel.Type {
|
|
|
+ case constant.ChannelTypeAli:
|
|
|
+ url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL)
|
|
|
+ case constant.ChannelTypeZhipu_v4:
|
|
|
+ if plan, ok := constant.ChannelSpecialBases[baseURL]; ok && plan.OpenAIBaseURL != "" {
|
|
|
+ url = fmt.Sprintf("%s/models", plan.OpenAIBaseURL)
|
|
|
+ } else {
|
|
|
+ url = fmt.Sprintf("%s/api/paas/v4/models", baseURL)
|
|
|
+ }
|
|
|
+ case constant.ChannelTypeVolcEngine:
|
|
|
+ if plan, ok := constant.ChannelSpecialBases[baseURL]; ok && plan.OpenAIBaseURL != "" {
|
|
|
+ url = fmt.Sprintf("%s/v1/models", plan.OpenAIBaseURL)
|
|
|
+ } else {
|
|
|
+ url = fmt.Sprintf("%s/v1/models", baseURL)
|
|
|
+ }
|
|
|
+ case constant.ChannelTypeMoonshot:
|
|
|
+ if plan, ok := constant.ChannelSpecialBases[baseURL]; ok && plan.OpenAIBaseURL != "" {
|
|
|
+ url = fmt.Sprintf("%s/models", plan.OpenAIBaseURL)
|
|
|
+ } else {
|
|
|
+ url = fmt.Sprintf("%s/v1/models", baseURL)
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ url = fmt.Sprintf("%s/v1/models", baseURL)
|
|
|
+ }
|
|
|
+
|
|
|
+ key, _, apiErr := channel.GetNextEnabledKey()
|
|
|
+ if apiErr != nil {
|
|
|
+ return nil, fmt.Errorf("获取渠道密钥失败: %w", apiErr)
|
|
|
+ }
|
|
|
+ key = strings.TrimSpace(key)
|
|
|
+
|
|
|
+ headers, err := buildFetchModelsHeaders(channel, key)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ body, err := GetResponseBody(http.MethodGet, url, channel, headers)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ var result OpenAIModelsResponse
|
|
|
+ if err := common.Unmarshal(body, &result); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ ids := lo.Map(result.Data, func(item OpenAIModel, _ int) string {
|
|
|
+ if channel.Type == constant.ChannelTypeGemini {
|
|
|
+ return strings.TrimPrefix(item.ID, "models/")
|
|
|
+ }
|
|
|
+ return item.ID
|
|
|
+ })
|
|
|
+
|
|
|
+ return normalizeModelNames(ids), nil
|
|
|
+}
|
|
|
+
|
|
|
+func updateChannelUpstreamModelSettings(channel *model.Channel, settings dto.ChannelOtherSettings, updateModels bool) error {
|
|
|
+ channel.SetOtherSettings(settings)
|
|
|
+ updates := map[string]interface{}{
|
|
|
+ "settings": channel.OtherSettings,
|
|
|
+ }
|
|
|
+ if updateModels {
|
|
|
+ updates["models"] = channel.Models
|
|
|
+ }
|
|
|
+ return model.DB.Model(&model.Channel{}).Where("id = ?", channel.Id).Updates(updates).Error
|
|
|
+}
|
|
|
+
|
|
|
+func checkAndPersistChannelUpstreamModelUpdates(
|
|
|
+ channel *model.Channel,
|
|
|
+ settings *dto.ChannelOtherSettings,
|
|
|
+ force bool,
|
|
|
+ allowAutoApply bool,
|
|
|
+) (modelsChanged bool, autoAdded int, err error) {
|
|
|
+ now := common.GetTimestamp()
|
|
|
+ if !force {
|
|
|
+ minInterval := getUpstreamModelUpdateMinCheckIntervalSeconds()
|
|
|
+ if settings.UpstreamModelUpdateLastCheckTime > 0 &&
|
|
|
+ now-settings.UpstreamModelUpdateLastCheckTime < minInterval {
|
|
|
+ return false, 0, nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ pendingAddModels, pendingRemoveModels, fetchErr := collectPendingUpstreamModelChanges(channel, *settings)
|
|
|
+ settings.UpstreamModelUpdateLastCheckTime = now
|
|
|
+ if fetchErr != nil {
|
|
|
+ if err = updateChannelUpstreamModelSettings(channel, *settings, false); err != nil {
|
|
|
+ return false, 0, err
|
|
|
+ }
|
|
|
+ return false, 0, fetchErr
|
|
|
+ }
|
|
|
+
|
|
|
+ if allowAutoApply && settings.UpstreamModelUpdateAutoSyncEnabled && len(pendingAddModels) > 0 {
|
|
|
+ originModels := normalizeModelNames(channel.GetModels())
|
|
|
+ mergedModels := mergeModelNames(originModels, pendingAddModels)
|
|
|
+ if len(mergedModels) > len(originModels) {
|
|
|
+ channel.Models = strings.Join(mergedModels, ",")
|
|
|
+ autoAdded = len(mergedModels) - len(originModels)
|
|
|
+ modelsChanged = true
|
|
|
+ }
|
|
|
+ settings.UpstreamModelUpdateLastDetectedModels = []string{}
|
|
|
+ } else {
|
|
|
+ settings.UpstreamModelUpdateLastDetectedModels = pendingAddModels
|
|
|
+ }
|
|
|
+ settings.UpstreamModelUpdateLastRemovedModels = pendingRemoveModels
|
|
|
+
|
|
|
+ if err = updateChannelUpstreamModelSettings(channel, *settings, modelsChanged); err != nil {
|
|
|
+ return false, autoAdded, err
|
|
|
+ }
|
|
|
+ if modelsChanged {
|
|
|
+ if err = channel.UpdateAbilities(nil); err != nil {
|
|
|
+ return true, autoAdded, err
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return modelsChanged, autoAdded, nil
|
|
|
+}
|
|
|
+
|
|
|
+func refreshChannelRuntimeCache() {
|
|
|
+ if common.MemoryCacheEnabled {
|
|
|
+ func() {
|
|
|
+ defer func() {
|
|
|
+ if r := recover(); r != nil {
|
|
|
+ common.SysLog(fmt.Sprintf("InitChannelCache panic: %v", r))
|
|
|
+ }
|
|
|
+ }()
|
|
|
+ model.InitChannelCache()
|
|
|
+ }()
|
|
|
+ }
|
|
|
+ service.ResetProxyClientCache()
|
|
|
+}
|
|
|
+
|
|
|
+func shouldSendUpstreamModelUpdateNotification(now int64, changedChannels int, failedChannels int) bool {
|
|
|
+ if changedChannels <= 0 && failedChannels <= 0 {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ channelUpstreamModelUpdateNotifyState.Lock()
|
|
|
+ defer channelUpstreamModelUpdateNotifyState.Unlock()
|
|
|
+
|
|
|
+ if channelUpstreamModelUpdateNotifyState.lastNotifiedAt > 0 &&
|
|
|
+ now-channelUpstreamModelUpdateNotifyState.lastNotifiedAt < channelUpstreamModelUpdateNotifySuppressWindowSeconds &&
|
|
|
+ channelUpstreamModelUpdateNotifyState.lastChangedChannels == changedChannels &&
|
|
|
+ channelUpstreamModelUpdateNotifyState.lastFailedChannels == failedChannels {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ channelUpstreamModelUpdateNotifyState.lastNotifiedAt = now
|
|
|
+ channelUpstreamModelUpdateNotifyState.lastChangedChannels = changedChannels
|
|
|
+ channelUpstreamModelUpdateNotifyState.lastFailedChannels = failedChannels
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+func buildUpstreamModelUpdateTaskNotificationContent(
|
|
|
+ checkedChannels int,
|
|
|
+ changedChannels int,
|
|
|
+ detectedAddModels int,
|
|
|
+ detectedRemoveModels int,
|
|
|
+ autoAddedModels int,
|
|
|
+ failedChannelIDs []int,
|
|
|
+ channelSummaries []upstreamModelUpdateChannelSummary,
|
|
|
+ addModelSamples []string,
|
|
|
+ removeModelSamples []string,
|
|
|
+) string {
|
|
|
+ var builder strings.Builder
|
|
|
+ failedChannels := len(failedChannelIDs)
|
|
|
+ builder.WriteString(fmt.Sprintf(
|
|
|
+ "上游模型巡检摘要:检测渠道 %d 个,发现变更 %d 个,新增 %d 个,删除 %d 个,自动同步新增 %d 个,失败 %d 个。",
|
|
|
+ checkedChannels,
|
|
|
+ changedChannels,
|
|
|
+ detectedAddModels,
|
|
|
+ detectedRemoveModels,
|
|
|
+ autoAddedModels,
|
|
|
+ failedChannels,
|
|
|
+ ))
|
|
|
+
|
|
|
+ if len(channelSummaries) > 0 {
|
|
|
+ displayCount := min(len(channelSummaries), channelUpstreamModelUpdateNotifyMaxChannelDetails)
|
|
|
+ builder.WriteString(fmt.Sprintf("\n\n变更渠道明细(展示 %d/%d):", displayCount, len(channelSummaries)))
|
|
|
+ for _, summary := range channelSummaries[:displayCount] {
|
|
|
+ builder.WriteString(fmt.Sprintf("\n- %s (+%d / -%d)", summary.ChannelName, summary.AddCount, summary.RemoveCount))
|
|
|
+ }
|
|
|
+ if len(channelSummaries) > displayCount {
|
|
|
+ builder.WriteString(fmt.Sprintf("\n- 其余 %d 个渠道已省略", len(channelSummaries)-displayCount))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ normalizedAddModelSamples := normalizeModelNames(addModelSamples)
|
|
|
+ if len(normalizedAddModelSamples) > 0 {
|
|
|
+ displayCount := min(len(normalizedAddModelSamples), channelUpstreamModelUpdateNotifyMaxModelDetails)
|
|
|
+ builder.WriteString(fmt.Sprintf("\n\n新增模型示例(展示 %d/%d):%s",
|
|
|
+ displayCount,
|
|
|
+ len(normalizedAddModelSamples),
|
|
|
+ strings.Join(normalizedAddModelSamples[:displayCount], ", "),
|
|
|
+ ))
|
|
|
+ if len(normalizedAddModelSamples) > displayCount {
|
|
|
+ builder.WriteString(fmt.Sprintf("(其余 %d 个已省略)", len(normalizedAddModelSamples)-displayCount))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ normalizedRemoveModelSamples := normalizeModelNames(removeModelSamples)
|
|
|
+ if len(normalizedRemoveModelSamples) > 0 {
|
|
|
+ displayCount := min(len(normalizedRemoveModelSamples), channelUpstreamModelUpdateNotifyMaxModelDetails)
|
|
|
+ builder.WriteString(fmt.Sprintf("\n\n删除模型示例(展示 %d/%d):%s",
|
|
|
+ displayCount,
|
|
|
+ len(normalizedRemoveModelSamples),
|
|
|
+ strings.Join(normalizedRemoveModelSamples[:displayCount], ", "),
|
|
|
+ ))
|
|
|
+ if len(normalizedRemoveModelSamples) > displayCount {
|
|
|
+ builder.WriteString(fmt.Sprintf("(其余 %d 个已省略)", len(normalizedRemoveModelSamples)-displayCount))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if failedChannels > 0 {
|
|
|
+ displayCount := min(failedChannels, channelUpstreamModelUpdateNotifyMaxFailedChannelIDs)
|
|
|
+ displayIDs := lo.Map(failedChannelIDs[:displayCount], func(channelID int, _ int) string {
|
|
|
+ return fmt.Sprintf("%d", channelID)
|
|
|
+ })
|
|
|
+ builder.WriteString(fmt.Sprintf(
|
|
|
+ "\n\n失败渠道 ID(展示 %d/%d):%s",
|
|
|
+ displayCount,
|
|
|
+ failedChannels,
|
|
|
+ strings.Join(displayIDs, ", "),
|
|
|
+ ))
|
|
|
+ if failedChannels > displayCount {
|
|
|
+ builder.WriteString(fmt.Sprintf("(其余 %d 个已省略)", failedChannels-displayCount))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return builder.String()
|
|
|
+}
|
|
|
+
|
|
|
+func runChannelUpstreamModelUpdateTaskOnce() {
|
|
|
+ if !channelUpstreamModelUpdateTaskRunning.CompareAndSwap(false, true) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer channelUpstreamModelUpdateTaskRunning.Store(false)
|
|
|
+
|
|
|
+ checkedChannels := 0
|
|
|
+ failedChannels := 0
|
|
|
+ failedChannelIDs := make([]int, 0)
|
|
|
+ changedChannels := 0
|
|
|
+ detectedAddModels := 0
|
|
|
+ detectedRemoveModels := 0
|
|
|
+ autoAddedModels := 0
|
|
|
+ channelSummaries := make([]upstreamModelUpdateChannelSummary, 0)
|
|
|
+ addModelSamples := make([]string, 0)
|
|
|
+ removeModelSamples := make([]string, 0)
|
|
|
+ refreshNeeded := false
|
|
|
+
|
|
|
+ lastID := 0
|
|
|
+ for {
|
|
|
+ var channels []*model.Channel
|
|
|
+ query := model.DB.
|
|
|
+ Select("id", "name", "type", "key", "status", "base_url", "models", "settings", "setting", "other", "group", "priority", "weight", "tag", "channel_info", "header_override").
|
|
|
+ Where("status = ?", common.ChannelStatusEnabled).
|
|
|
+ Order("id asc").
|
|
|
+ Limit(channelUpstreamModelUpdateTaskBatchSize)
|
|
|
+ if lastID > 0 {
|
|
|
+ query = query.Where("id > ?", lastID)
|
|
|
+ }
|
|
|
+ err := query.Find(&channels).Error
|
|
|
+ if err != nil {
|
|
|
+ common.SysLog(fmt.Sprintf("upstream model update task query failed: %v", err))
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if len(channels) == 0 {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ lastID = channels[len(channels)-1].Id
|
|
|
+
|
|
|
+ for _, channel := range channels {
|
|
|
+ if channel == nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ settings := channel.GetOtherSettings()
|
|
|
+ if !settings.UpstreamModelUpdateCheckEnabled {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ checkedChannels++
|
|
|
+ modelsChanged, autoAdded, err := checkAndPersistChannelUpstreamModelUpdates(channel, &settings, false, true)
|
|
|
+ if err != nil {
|
|
|
+ failedChannels++
|
|
|
+ failedChannelIDs = append(failedChannelIDs, channel.Id)
|
|
|
+ common.SysLog(fmt.Sprintf("upstream model update check failed: channel_id=%d channel_name=%s err=%v", channel.Id, channel.Name, err))
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ currentAddModels := normalizeModelNames(settings.UpstreamModelUpdateLastDetectedModels)
|
|
|
+ currentRemoveModels := normalizeModelNames(settings.UpstreamModelUpdateLastRemovedModels)
|
|
|
+ currentAddCount := len(currentAddModels) + autoAdded
|
|
|
+ currentRemoveCount := len(currentRemoveModels)
|
|
|
+ detectedAddModels += currentAddCount
|
|
|
+ detectedRemoveModels += currentRemoveCount
|
|
|
+ if currentAddCount > 0 || currentRemoveCount > 0 {
|
|
|
+ changedChannels++
|
|
|
+ channelSummaries = append(channelSummaries, upstreamModelUpdateChannelSummary{
|
|
|
+ ChannelName: channel.Name,
|
|
|
+ AddCount: currentAddCount,
|
|
|
+ RemoveCount: currentRemoveCount,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ addModelSamples = mergeModelNames(addModelSamples, currentAddModels)
|
|
|
+ removeModelSamples = mergeModelNames(removeModelSamples, currentRemoveModels)
|
|
|
+ if modelsChanged {
|
|
|
+ refreshNeeded = true
|
|
|
+ }
|
|
|
+ autoAddedModels += autoAdded
|
|
|
+
|
|
|
+ if common.RequestInterval > 0 {
|
|
|
+ time.Sleep(common.RequestInterval)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(channels) < channelUpstreamModelUpdateTaskBatchSize {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if refreshNeeded {
|
|
|
+ refreshChannelRuntimeCache()
|
|
|
+ }
|
|
|
+
|
|
|
+ if checkedChannels > 0 || common.DebugEnabled {
|
|
|
+ common.SysLog(fmt.Sprintf(
|
|
|
+ "upstream model update task done: checked_channels=%d changed_channels=%d detected_add_models=%d detected_remove_models=%d failed_channels=%d auto_added_models=%d",
|
|
|
+ checkedChannels,
|
|
|
+ changedChannels,
|
|
|
+ detectedAddModels,
|
|
|
+ detectedRemoveModels,
|
|
|
+ failedChannels,
|
|
|
+ autoAddedModels,
|
|
|
+ ))
|
|
|
+ }
|
|
|
+ if changedChannels > 0 || failedChannels > 0 {
|
|
|
+ now := common.GetTimestamp()
|
|
|
+ if !shouldSendUpstreamModelUpdateNotification(now, changedChannels, failedChannels) {
|
|
|
+ common.SysLog(fmt.Sprintf(
|
|
|
+ "upstream model update notification skipped in 24h window: changed_channels=%d failed_channels=%d",
|
|
|
+ changedChannels,
|
|
|
+ failedChannels,
|
|
|
+ ))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ service.NotifyUpstreamModelUpdateWatchers(
|
|
|
+ "上游模型巡检通知",
|
|
|
+ buildUpstreamModelUpdateTaskNotificationContent(
|
|
|
+ checkedChannels,
|
|
|
+ changedChannels,
|
|
|
+ detectedAddModels,
|
|
|
+ detectedRemoveModels,
|
|
|
+ autoAddedModels,
|
|
|
+ failedChannelIDs,
|
|
|
+ channelSummaries,
|
|
|
+ addModelSamples,
|
|
|
+ removeModelSamples,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func StartChannelUpstreamModelUpdateTask() {
|
|
|
+ channelUpstreamModelUpdateTaskOnce.Do(func() {
|
|
|
+ if !common.IsMasterNode {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if !common.GetEnvOrDefaultBool("CHANNEL_UPSTREAM_MODEL_UPDATE_TASK_ENABLED", true) {
|
|
|
+ common.SysLog("upstream model update task disabled by CHANNEL_UPSTREAM_MODEL_UPDATE_TASK_ENABLED")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ intervalMinutes := common.GetEnvOrDefault(
|
|
|
+ "CHANNEL_UPSTREAM_MODEL_UPDATE_TASK_INTERVAL_MINUTES",
|
|
|
+ channelUpstreamModelUpdateTaskDefaultIntervalMinutes,
|
|
|
+ )
|
|
|
+ if intervalMinutes < 1 {
|
|
|
+ intervalMinutes = channelUpstreamModelUpdateTaskDefaultIntervalMinutes
|
|
|
+ }
|
|
|
+ interval := time.Duration(intervalMinutes) * time.Minute
|
|
|
+
|
|
|
+ go func() {
|
|
|
+ common.SysLog(fmt.Sprintf("upstream model update task started: interval=%s", interval))
|
|
|
+ runChannelUpstreamModelUpdateTaskOnce()
|
|
|
+ ticker := time.NewTicker(interval)
|
|
|
+ defer ticker.Stop()
|
|
|
+ for range ticker.C {
|
|
|
+ runChannelUpstreamModelUpdateTaskOnce()
|
|
|
+ }
|
|
|
+ }()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func ApplyChannelUpstreamModelUpdates(c *gin.Context) {
|
|
|
+ var req applyChannelUpstreamModelUpdatesRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if req.ID <= 0 {
|
|
|
+ c.JSON(http.StatusOK, gin.H{
|
|
|
+ "success": false,
|
|
|
+ "message": "invalid channel id",
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ channel, err := model.GetChannelById(req.ID, true)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ beforeSettings := channel.GetOtherSettings()
|
|
|
+ ignoredModels := intersectModelNames(req.IgnoreModels, beforeSettings.UpstreamModelUpdateLastDetectedModels)
|
|
|
+
|
|
|
+ addedModels, removedModels, remainingModels, remainingRemoveModels, modelsChanged, err := applyChannelUpstreamModelUpdates(
|
|
|
+ channel,
|
|
|
+ req.AddModels,
|
|
|
+ req.IgnoreModels,
|
|
|
+ req.RemoveModels,
|
|
|
+ )
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if modelsChanged {
|
|
|
+ refreshChannelRuntimeCache()
|
|
|
+ }
|
|
|
+
|
|
|
+ c.JSON(http.StatusOK, gin.H{
|
|
|
+ "success": true,
|
|
|
+ "message": "",
|
|
|
+ "data": gin.H{
|
|
|
+ "id": channel.Id,
|
|
|
+ "added_models": addedModels,
|
|
|
+ "removed_models": removedModels,
|
|
|
+ "ignored_models": ignoredModels,
|
|
|
+ "remaining_models": remainingModels,
|
|
|
+ "remaining_remove_models": remainingRemoveModels,
|
|
|
+ "models": channel.Models,
|
|
|
+ "settings": channel.OtherSettings,
|
|
|
+ },
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func DetectChannelUpstreamModelUpdates(c *gin.Context) {
|
|
|
+ var req applyChannelUpstreamModelUpdatesRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if req.ID <= 0 {
|
|
|
+ c.JSON(http.StatusOK, gin.H{
|
|
|
+ "success": false,
|
|
|
+ "message": "invalid channel id",
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ channel, err := model.GetChannelById(req.ID, true)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ settings := channel.GetOtherSettings()
|
|
|
+ if !settings.UpstreamModelUpdateCheckEnabled {
|
|
|
+ c.JSON(http.StatusOK, gin.H{
|
|
|
+ "success": false,
|
|
|
+ "message": "该渠道未开启上游模型更新检测",
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ modelsChanged, autoAdded, err := checkAndPersistChannelUpstreamModelUpdates(channel, &settings, true, false)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if modelsChanged {
|
|
|
+ refreshChannelRuntimeCache()
|
|
|
+ }
|
|
|
+
|
|
|
+ c.JSON(http.StatusOK, gin.H{
|
|
|
+ "success": true,
|
|
|
+ "message": "",
|
|
|
+ "data": detectChannelUpstreamModelUpdatesResult{
|
|
|
+ ChannelID: channel.Id,
|
|
|
+ ChannelName: channel.Name,
|
|
|
+ AddModels: normalizeModelNames(settings.UpstreamModelUpdateLastDetectedModels),
|
|
|
+ RemoveModels: normalizeModelNames(settings.UpstreamModelUpdateLastRemovedModels),
|
|
|
+ LastCheckTime: settings.UpstreamModelUpdateLastCheckTime,
|
|
|
+ AutoAddedModels: autoAdded,
|
|
|
+ },
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func applyChannelUpstreamModelUpdates(
|
|
|
+ channel *model.Channel,
|
|
|
+ addModelsInput []string,
|
|
|
+ ignoreModelsInput []string,
|
|
|
+ removeModelsInput []string,
|
|
|
+) (
|
|
|
+ addedModels []string,
|
|
|
+ removedModels []string,
|
|
|
+ remainingModels []string,
|
|
|
+ remainingRemoveModels []string,
|
|
|
+ modelsChanged bool,
|
|
|
+ err error,
|
|
|
+) {
|
|
|
+ settings := channel.GetOtherSettings()
|
|
|
+ pendingAddModels := normalizeModelNames(settings.UpstreamModelUpdateLastDetectedModels)
|
|
|
+ pendingRemoveModels := normalizeModelNames(settings.UpstreamModelUpdateLastRemovedModels)
|
|
|
+ addModels := intersectModelNames(addModelsInput, pendingAddModels)
|
|
|
+ ignoreModels := intersectModelNames(ignoreModelsInput, pendingAddModels)
|
|
|
+ removeModels := intersectModelNames(removeModelsInput, pendingRemoveModels)
|
|
|
+ removeModels = subtractModelNames(removeModels, addModels)
|
|
|
+
|
|
|
+ originModels := normalizeModelNames(channel.GetModels())
|
|
|
+ nextModels := applySelectedModelChanges(originModels, addModels, removeModels)
|
|
|
+ modelsChanged = !slices.Equal(originModels, nextModels)
|
|
|
+ if modelsChanged {
|
|
|
+ channel.Models = strings.Join(nextModels, ",")
|
|
|
+ }
|
|
|
+
|
|
|
+ settings.UpstreamModelUpdateIgnoredModels = mergeModelNames(settings.UpstreamModelUpdateIgnoredModels, ignoreModels)
|
|
|
+ if len(addModels) > 0 {
|
|
|
+ settings.UpstreamModelUpdateIgnoredModels = subtractModelNames(settings.UpstreamModelUpdateIgnoredModels, addModels)
|
|
|
+ }
|
|
|
+ remainingModels = subtractModelNames(pendingAddModels, append(addModels, ignoreModels...))
|
|
|
+ remainingRemoveModels = subtractModelNames(pendingRemoveModels, removeModels)
|
|
|
+ settings.UpstreamModelUpdateLastDetectedModels = remainingModels
|
|
|
+ settings.UpstreamModelUpdateLastRemovedModels = remainingRemoveModels
|
|
|
+ settings.UpstreamModelUpdateLastCheckTime = common.GetTimestamp()
|
|
|
+
|
|
|
+ if err := updateChannelUpstreamModelSettings(channel, settings, modelsChanged); err != nil {
|
|
|
+ return nil, nil, nil, nil, false, err
|
|
|
+ }
|
|
|
+
|
|
|
+ if modelsChanged {
|
|
|
+ if err := channel.UpdateAbilities(nil); err != nil {
|
|
|
+ return addModels, removeModels, remainingModels, remainingRemoveModels, true, err
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return addModels, removeModels, remainingModels, remainingRemoveModels, modelsChanged, nil
|
|
|
+}
|
|
|
+
|
|
|
+func collectPendingApplyUpstreamModelChanges(settings dto.ChannelOtherSettings) (pendingAddModels []string, pendingRemoveModels []string) {
|
|
|
+ return normalizeModelNames(settings.UpstreamModelUpdateLastDetectedModels), normalizeModelNames(settings.UpstreamModelUpdateLastRemovedModels)
|
|
|
+}
|
|
|
+
|
|
|
+func findEnabledChannelsAfterID(lastID int, batchSize int) ([]*model.Channel, error) {
|
|
|
+ var channels []*model.Channel
|
|
|
+ query := model.DB.
|
|
|
+ Select("id", "name", "type", "key", "status", "base_url", "models", "settings", "setting", "other", "group", "priority", "weight", "tag", "channel_info", "header_override").
|
|
|
+ Where("status = ?", common.ChannelStatusEnabled).
|
|
|
+ Order("id asc").
|
|
|
+ Limit(batchSize)
|
|
|
+ if lastID > 0 {
|
|
|
+ query = query.Where("id > ?", lastID)
|
|
|
+ }
|
|
|
+ return channels, query.Find(&channels).Error
|
|
|
+}
|
|
|
+
|
|
|
+func ApplyAllChannelUpstreamModelUpdates(c *gin.Context) {
|
|
|
+ results := make([]applyAllChannelUpstreamModelUpdatesResult, 0)
|
|
|
+ failed := make([]int, 0)
|
|
|
+ refreshNeeded := false
|
|
|
+ addedModelCount := 0
|
|
|
+ removedModelCount := 0
|
|
|
+
|
|
|
+ lastID := 0
|
|
|
+ for {
|
|
|
+ channels, err := findEnabledChannelsAfterID(lastID, channelUpstreamModelUpdateTaskBatchSize)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if len(channels) == 0 {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ lastID = channels[len(channels)-1].Id
|
|
|
+
|
|
|
+ for _, channel := range channels {
|
|
|
+ if channel == nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ settings := channel.GetOtherSettings()
|
|
|
+ if !settings.UpstreamModelUpdateCheckEnabled {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ pendingAddModels, pendingRemoveModels := collectPendingApplyUpstreamModelChanges(settings)
|
|
|
+ if len(pendingAddModels) == 0 && len(pendingRemoveModels) == 0 {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ addedModels, removedModels, remainingModels, remainingRemoveModels, modelsChanged, err := applyChannelUpstreamModelUpdates(
|
|
|
+ channel,
|
|
|
+ pendingAddModels,
|
|
|
+ nil,
|
|
|
+ pendingRemoveModels,
|
|
|
+ )
|
|
|
+ if err != nil {
|
|
|
+ failed = append(failed, channel.Id)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if modelsChanged {
|
|
|
+ refreshNeeded = true
|
|
|
+ }
|
|
|
+ addedModelCount += len(addedModels)
|
|
|
+ removedModelCount += len(removedModels)
|
|
|
+ results = append(results, applyAllChannelUpstreamModelUpdatesResult{
|
|
|
+ ChannelID: channel.Id,
|
|
|
+ ChannelName: channel.Name,
|
|
|
+ AddedModels: addedModels,
|
|
|
+ RemovedModels: removedModels,
|
|
|
+ RemainingModels: remainingModels,
|
|
|
+ RemainingRemoveModels: remainingRemoveModels,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(channels) < channelUpstreamModelUpdateTaskBatchSize {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if refreshNeeded {
|
|
|
+ refreshChannelRuntimeCache()
|
|
|
+ }
|
|
|
+
|
|
|
+ c.JSON(http.StatusOK, gin.H{
|
|
|
+ "success": true,
|
|
|
+ "message": "",
|
|
|
+ "data": gin.H{
|
|
|
+ "processed_channels": len(results),
|
|
|
+ "added_models": addedModelCount,
|
|
|
+ "removed_models": removedModelCount,
|
|
|
+ "failed_channel_ids": failed,
|
|
|
+ "results": results,
|
|
|
+ },
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func DetectAllChannelUpstreamModelUpdates(c *gin.Context) {
|
|
|
+ results := make([]detectChannelUpstreamModelUpdatesResult, 0)
|
|
|
+ failed := make([]int, 0)
|
|
|
+ detectedAddCount := 0
|
|
|
+ detectedRemoveCount := 0
|
|
|
+ refreshNeeded := false
|
|
|
+
|
|
|
+ lastID := 0
|
|
|
+ for {
|
|
|
+ channels, err := findEnabledChannelsAfterID(lastID, channelUpstreamModelUpdateTaskBatchSize)
|
|
|
+ if err != nil {
|
|
|
+ common.ApiError(c, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if len(channels) == 0 {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ lastID = channels[len(channels)-1].Id
|
|
|
+
|
|
|
+ for _, channel := range channels {
|
|
|
+ if channel == nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ settings := channel.GetOtherSettings()
|
|
|
+ if !settings.UpstreamModelUpdateCheckEnabled {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ modelsChanged, autoAdded, err := checkAndPersistChannelUpstreamModelUpdates(channel, &settings, true, false)
|
|
|
+ if err != nil {
|
|
|
+ failed = append(failed, channel.Id)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if modelsChanged {
|
|
|
+ refreshNeeded = true
|
|
|
+ }
|
|
|
+
|
|
|
+ addModels := normalizeModelNames(settings.UpstreamModelUpdateLastDetectedModels)
|
|
|
+ removeModels := normalizeModelNames(settings.UpstreamModelUpdateLastRemovedModels)
|
|
|
+ detectedAddCount += len(addModels)
|
|
|
+ detectedRemoveCount += len(removeModels)
|
|
|
+ results = append(results, detectChannelUpstreamModelUpdatesResult{
|
|
|
+ ChannelID: channel.Id,
|
|
|
+ ChannelName: channel.Name,
|
|
|
+ AddModels: addModels,
|
|
|
+ RemoveModels: removeModels,
|
|
|
+ LastCheckTime: settings.UpstreamModelUpdateLastCheckTime,
|
|
|
+ AutoAddedModels: autoAdded,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(channels) < channelUpstreamModelUpdateTaskBatchSize {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if refreshNeeded {
|
|
|
+ refreshChannelRuntimeCache()
|
|
|
+ }
|
|
|
+
|
|
|
+ c.JSON(http.StatusOK, gin.H{
|
|
|
+ "success": true,
|
|
|
+ "message": "",
|
|
|
+ "data": gin.H{
|
|
|
+ "processed_channels": len(results),
|
|
|
+ "failed_channel_ids": failed,
|
|
|
+ "detected_add_models": detectedAddCount,
|
|
|
+ "detected_remove_models": detectedRemoveCount,
|
|
|
+ "channel_detected_results": results,
|
|
|
+ },
|
|
|
+ })
|
|
|
+}
|