Add visual status indicators and improve user experience for the upstream ratio sync channel selector modal. Features: - Add status-based avatar indicators for channels (enabled/disabled/auto-disabled) - Implement search functionality with text highlighting - Add endpoint configuration input for each channel - Optimize component structure with reusable ChannelInfo component UI Improvements: - Custom styling for transfer component items - Hide scrollbars for cleaner appearance in transfer lists - Responsive layout adjustments for channel information display - Color-coded avatars: green (enabled), red (disabled), amber (auto-disabled), grey (unknown) Code Quality: - Extract channel status configuration to constants - Create reusable ChannelInfo component to reduce code duplication - Implement proper search filtering for both channel names and URLs - Add consistent styling classes for transfer demo components Files modified: - web/src/components/settings/ChannelSelectorModal.js - web/src/pages/Setting/Ratio/UpstreamRatioSync.js - web/src/index.css This enhancement provides better visual feedback for channel status and improves the overall user experience when selecting channels for ratio synchronization.
289 lines
9.2 KiB
Go
289 lines
9.2 KiB
Go
package controller
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"one-api/model"
|
||
"one-api/setting/ratio_setting"
|
||
"one-api/dto"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
type upstreamResult struct {
|
||
Name string `json:"name"`
|
||
Data map[string]any `json:"data,omitempty"`
|
||
Err string `json:"err,omitempty"`
|
||
}
|
||
|
||
type TestResult struct {
|
||
Name string `json:"name"`
|
||
Status string `json:"status"`
|
||
Error string `json:"error,omitempty"`
|
||
}
|
||
|
||
type DifferenceItem struct {
|
||
Current interface{} `json:"current"` // 当前本地值,可能为null
|
||
Upstreams map[string]interface{} `json:"upstreams"` // 上游值:具体值/"same"/null
|
||
}
|
||
|
||
// SyncableChannel 可同步的渠道信息
|
||
type SyncableChannel struct {
|
||
ID int `json:"id"`
|
||
Name string `json:"name"`
|
||
BaseURL string `json:"base_url"`
|
||
Status int `json:"status"`
|
||
}
|
||
|
||
// FetchUpstreamRatios 后端并发拉取上游倍率
|
||
func FetchUpstreamRatios(c *gin.Context) {
|
||
var req dto.UpstreamRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||
return
|
||
}
|
||
|
||
if req.Timeout <= 0 {
|
||
req.Timeout = 10
|
||
}
|
||
|
||
// build upstream list from ids
|
||
var upstreams []dto.UpstreamDTO
|
||
if len(req.ChannelIDs) > 0 {
|
||
// convert []int64 -> []int for model function
|
||
intIds := make([]int, 0, len(req.ChannelIDs))
|
||
for _, id64 := range req.ChannelIDs {
|
||
intIds = append(intIds, int(id64))
|
||
}
|
||
dbChannels, _ := model.GetChannelsByIds(intIds)
|
||
for _, ch := range dbChannels {
|
||
upstreams = append(upstreams, dto.UpstreamDTO{
|
||
Name: ch.Name,
|
||
BaseURL: ch.GetBaseURL(),
|
||
Endpoint: "", // assume default endpoint
|
||
})
|
||
}
|
||
}
|
||
|
||
var wg sync.WaitGroup
|
||
ch := make(chan upstreamResult, len(upstreams))
|
||
|
||
for _, chn := range upstreams {
|
||
wg.Add(1)
|
||
go func(chItem dto.UpstreamDTO) {
|
||
defer wg.Done()
|
||
endpoint := chItem.Endpoint
|
||
if endpoint == "" {
|
||
endpoint = "/api/ratio_config"
|
||
}
|
||
url := chItem.BaseURL + endpoint
|
||
client := http.Client{Timeout: time.Duration(req.Timeout) * time.Second}
|
||
resp, err := client.Get(url)
|
||
if err != nil {
|
||
ch <- upstreamResult{Name: chItem.Name, Err: err.Error()}
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK {
|
||
ch <- upstreamResult{Name: chItem.Name, Err: resp.Status}
|
||
return
|
||
}
|
||
var body struct {
|
||
Success bool `json:"success"`
|
||
Data map[string]any `json:"data"`
|
||
Message string `json:"message"`
|
||
}
|
||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||
ch <- upstreamResult{Name: chItem.Name, Err: err.Error()}
|
||
return
|
||
}
|
||
if !body.Success {
|
||
ch <- upstreamResult{Name: chItem.Name, Err: body.Message}
|
||
return
|
||
}
|
||
ch <- upstreamResult{Name: chItem.Name, Data: body.Data}
|
||
}(chn)
|
||
}
|
||
|
||
wg.Wait()
|
||
close(ch)
|
||
|
||
// 本地倍率配置
|
||
localData := ratio_setting.GetExposedData()
|
||
|
||
var testResults []dto.TestResult
|
||
var successfulChannels []struct {
|
||
name string
|
||
data map[string]any
|
||
}
|
||
|
||
for r := range ch {
|
||
if r.Err != "" {
|
||
testResults = append(testResults, dto.TestResult{
|
||
Name: r.Name,
|
||
Status: "error",
|
||
Error: r.Err,
|
||
})
|
||
} else {
|
||
testResults = append(testResults, dto.TestResult{
|
||
Name: r.Name,
|
||
Status: "success",
|
||
})
|
||
successfulChannels = append(successfulChannels, struct {
|
||
name string
|
||
data map[string]any
|
||
}{name: r.Name, data: r.Data})
|
||
}
|
||
}
|
||
|
||
// 构建差异化数据
|
||
differences := buildDifferences(localData, successfulChannels)
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": gin.H{
|
||
"differences": differences,
|
||
"test_results": testResults,
|
||
},
|
||
})
|
||
}
|
||
|
||
// buildDifferences 构建差异化数据,只返回有意义的差异
|
||
func buildDifferences(localData map[string]any, successfulChannels []struct {
|
||
name string
|
||
data map[string]any
|
||
}) map[string]map[string]dto.DifferenceItem {
|
||
differences := make(map[string]map[string]dto.DifferenceItem)
|
||
ratioTypes := []string{"model_ratio", "completion_ratio", "cache_ratio", "model_price"}
|
||
|
||
// 收集所有模型名称
|
||
allModels := make(map[string]struct{})
|
||
|
||
// 从本地数据收集模型名称
|
||
for _, ratioType := range ratioTypes {
|
||
if localRatioAny, ok := localData[ratioType]; ok {
|
||
if localRatio, ok := localRatioAny.(map[string]float64); ok {
|
||
for modelName := range localRatio {
|
||
allModels[modelName] = struct{}{}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 从上游数据收集模型名称
|
||
for _, channel := range successfulChannels {
|
||
for _, ratioType := range ratioTypes {
|
||
if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
|
||
for modelName := range upstreamRatio {
|
||
allModels[modelName] = struct{}{}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 对每个模型和每个比率类型进行分析
|
||
for modelName := range allModels {
|
||
for _, ratioType := range ratioTypes {
|
||
// 获取本地值
|
||
var localValue interface{} = nil
|
||
if localRatioAny, ok := localData[ratioType]; ok {
|
||
if localRatio, ok := localRatioAny.(map[string]float64); ok {
|
||
if val, exists := localRatio[modelName]; exists {
|
||
localValue = val
|
||
}
|
||
}
|
||
}
|
||
|
||
// 收集上游值
|
||
upstreamValues := make(map[string]interface{})
|
||
hasUpstreamValue := false
|
||
hasDifference := false
|
||
|
||
for _, channel := range successfulChannels {
|
||
var upstreamValue interface{} = nil
|
||
|
||
if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
|
||
if val, exists := upstreamRatio[modelName]; exists {
|
||
upstreamValue = val
|
||
hasUpstreamValue = true
|
||
|
||
// 检查是否与本地值不同
|
||
if localValue != nil && localValue != val {
|
||
hasDifference = true
|
||
} else if localValue == val {
|
||
upstreamValue = "same"
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果本地值为空但上游有值,这也是差异
|
||
if localValue == nil && upstreamValue != nil && upstreamValue != "same" {
|
||
hasDifference = true
|
||
}
|
||
|
||
upstreamValues[channel.name] = upstreamValue
|
||
}
|
||
|
||
// 应用过滤逻辑
|
||
shouldInclude := false
|
||
|
||
if localValue != nil {
|
||
// 规则1: 本地值存在,至少有一个上游与本地值不同
|
||
if hasDifference {
|
||
shouldInclude = true
|
||
}
|
||
// 规则2: 本地值存在,但所有上游都未设置 - 不包含
|
||
} else {
|
||
// 规则3: 本地值不存在,至少有一个上游设置了值
|
||
if hasUpstreamValue {
|
||
shouldInclude = true
|
||
}
|
||
}
|
||
|
||
if shouldInclude {
|
||
if differences[modelName] == nil {
|
||
differences[modelName] = make(map[string]dto.DifferenceItem)
|
||
}
|
||
differences[modelName][ratioType] = dto.DifferenceItem{
|
||
Current: localValue,
|
||
Upstreams: upstreamValues,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return differences
|
||
}
|
||
|
||
// GetSyncableChannels 获取可用于倍率同步的渠道(base_url 不为空的渠道)
|
||
func GetSyncableChannels(c *gin.Context) {
|
||
channels, err := model.GetAllChannels(0, 0, true, false)
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": false,
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
var syncableChannels []dto.SyncableChannel
|
||
for _, channel := range channels {
|
||
// 只返回 base_url 不为空的渠道
|
||
if channel.GetBaseURL() != "" {
|
||
syncableChannels = append(syncableChannels, dto.SyncableChannel{
|
||
ID: channel.Id,
|
||
Name: channel.Name,
|
||
BaseURL: channel.GetBaseURL(),
|
||
Status: channel.Status,
|
||
})
|
||
}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": "",
|
||
"data": syncableChannels,
|
||
})
|
||
} |