From a9f98c5d392799096c4bd6904f39b66425d68cea Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Thu, 19 Jun 2025 18:38:43 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20chore(ratio-sync):=20im?= =?UTF-8?q?prove=20upstream=20ratio=20comparison=20&=20output=20cleanlines?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary 1. Consider “both unset” as identical • When both localValue and upstreamValue are nil, mark upstreamValue as "same" to avoid showing “Not set”. 2. Exclude fully-synced upstream channels from result • Scan `differences` to detect channels that contain at least one divergent value. • Remove channels whose every ratio is either `"same"` or `nil`, so the frontend only receives actionable discrepancies. Why These changes reduce visual noise in the Upstream Ratio Sync table, making it easier for admins to focus on models requiring attention. No functional regressions or breaking API changes are introduced. --- controller/ratio_sync.go | 48 +++++++------- web/src/i18n/locales/en.json | 24 ++++++- .../pages/Setting/Ratio/UpstreamRatioSync.js | 63 ++++++++----------- 3 files changed, 74 insertions(+), 61 deletions(-) diff --git a/controller/ratio_sync.go b/controller/ratio_sync.go index 490a2a74..368f92dd 100644 --- a/controller/ratio_sync.go +++ b/controller/ratio_sync.go @@ -29,7 +29,6 @@ type DifferenceItem struct { Upstreams map[string]interface{} `json:"upstreams"` // 上游值:具体值/"same"/null } -// SyncableChannel 可同步的渠道信息 type SyncableChannel struct { ID int `json:"id"` Name string `json:"name"` @@ -37,7 +36,6 @@ type SyncableChannel struct { Status int `json:"status"` } -// FetchUpstreamRatios 后端并发拉取上游倍率 func FetchUpstreamRatios(c *gin.Context) { var req dto.UpstreamRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -49,10 +47,8 @@ func FetchUpstreamRatios(c *gin.Context) { 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)) @@ -62,7 +58,7 @@ func FetchUpstreamRatios(c *gin.Context) { upstreams = append(upstreams, dto.UpstreamDTO{ Name: ch.Name, BaseURL: ch.GetBaseURL(), - Endpoint: "", // assume default endpoint + Endpoint: "", }) } } @@ -110,7 +106,6 @@ func FetchUpstreamRatios(c *gin.Context) { wg.Wait() close(ch) - // 本地倍率配置 localData := ratio_setting.GetExposedData() var testResults []dto.TestResult @@ -138,7 +133,6 @@ func FetchUpstreamRatios(c *gin.Context) { } } - // 构建差异化数据 differences := buildDifferences(localData, successfulChannels) c.JSON(http.StatusOK, gin.H{ @@ -150,7 +144,6 @@ func FetchUpstreamRatios(c *gin.Context) { }) } -// buildDifferences 构建差异化数据,只返回有意义的差异 func buildDifferences(localData map[string]any, successfulChannels []struct { name string data map[string]any @@ -158,10 +151,8 @@ func buildDifferences(localData map[string]any, successfulChannels []struct { 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 { @@ -172,7 +163,6 @@ func buildDifferences(localData map[string]any, successfulChannels []struct { } } - // 从上游数据收集模型名称 for _, channel := range successfulChannels { for _, ratioType := range ratioTypes { if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok { @@ -183,10 +173,8 @@ func buildDifferences(localData map[string]any, successfulChannels []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 { @@ -196,7 +184,6 @@ func buildDifferences(localData map[string]any, successfulChannels []struct { } } - // 收集上游值 upstreamValues := make(map[string]interface{}) hasUpstreamValue := false hasDifference := false @@ -209,7 +196,6 @@ func buildDifferences(localData map[string]any, successfulChannels []struct { upstreamValue = val hasUpstreamValue = true - // 检查是否与本地值不同 if localValue != nil && localValue != val { hasDifference = true } else if localValue == val { @@ -217,8 +203,10 @@ func buildDifferences(localData map[string]any, successfulChannels []struct { } } } + if upstreamValue == nil && localValue == nil { + upstreamValue = "same" + } - // 如果本地值为空但上游有值,这也是差异 if localValue == nil && upstreamValue != nil && upstreamValue != "same" { hasDifference = true } @@ -226,17 +214,13 @@ func buildDifferences(localData map[string]any, successfulChannels []struct { upstreamValues[channel.name] = upstreamValue } - // 应用过滤逻辑 shouldInclude := false if localValue != nil { - // 规则1: 本地值存在,至少有一个上游与本地值不同 if hasDifference { shouldInclude = true } - // 规则2: 本地值存在,但所有上游都未设置 - 不包含 } else { - // 规则3: 本地值不存在,至少有一个上游设置了值 if hasUpstreamValue { shouldInclude = true } @@ -254,10 +238,31 @@ func buildDifferences(localData map[string]any, successfulChannels []struct { } } + channelHasDiff := make(map[string]bool) + for _, ratioMap := range differences { + for _, item := range ratioMap { + for chName, val := range item.Upstreams { + if val != nil && val != "same" { + channelHasDiff[chName] = true + } + } + } + } + + for modelName, ratioMap := range differences { + for ratioType, item := range ratioMap { + for chName := range item.Upstreams { + if !channelHasDiff[chName] { + delete(item.Upstreams, chName) + } + } + differences[modelName][ratioType] = item + } + } + return differences } -// GetSyncableChannels 获取可用于倍率同步的渠道(base_url 不为空的渠道) func GetSyncableChannels(c *gin.Context) { channels, err := model.GetAllChannels(0, 0, true, false) if err != nil { @@ -270,7 +275,6 @@ func GetSyncableChannels(c *gin.Context) { var syncableChannels []dto.SyncableChannel for _, channel := range channels { - // 只返回 base_url 不为空的渠道 if channel.GetBaseURL() != "" { syncableChannels = append(syncableChannels, dto.SyncableChannel{ ID: channel.Id, diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index fc80f9c1..b8e1afd8 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1665,5 +1665,27 @@ "确定清除所有失效兑换码?": "Are you sure you want to clear all invalid redemption codes?", "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "This will delete all used, disabled, and expired redemption codes, this operation cannot be undone.", "选择过期时间(可选,留空为永久)": "Select expiration time (optional, leave blank for permanent)", - "请输入备注(仅管理员可见)": "Please enter a remark (only visible to administrators)" + "请输入备注(仅管理员可见)": "Please enter a remark (only visible to administrators)", + "上游倍率同步": "Upstream ratio synchronization", + "获取渠道失败:": "Failed to get channels: ", + "请至少选择一个渠道": "Please select at least one channel", + "获取倍率失败:": "Failed to get ratios: ", + "后端请求失败": "Backend request failed", + "部分渠道测试失败:": "Some channels failed to test: ", + "已与上游倍率完全一致,无需同步": "The upstream ratio is completely consistent, no synchronization is required", + "请求后端接口失败:": "Failed to request the backend interface: ", + "同步成功": "Synchronization successful", + "部分保存失败": "Some settings failed to save", + "保存失败": "Save failed", + "选择同步渠道": "Select synchronization channel", + "应用同步": "Apply synchronization", + "倍率类型": "Ratio type", + "当前值": "Current value", + "上游值": "Upstream value", + "差异": "Difference", + "搜索渠道名称或地址": "Search channel name or address", + "缓存倍率": "Cache ratio", + "暂无差异化倍率显示": "No differential ratio display", + "请先选择同步渠道": "Please select the synchronization channel first", + "与本地相同": "Same as local" } \ No newline at end of file diff --git a/web/src/pages/Setting/Ratio/UpstreamRatioSync.js b/web/src/pages/Setting/Ratio/UpstreamRatioSync.js index 2e12fd3b..aae6d9f3 100644 --- a/web/src/pages/Setting/Ratio/UpstreamRatioSync.js +++ b/web/src/pages/Setting/Ratio/UpstreamRatioSync.js @@ -11,7 +11,7 @@ import { RefreshCcw, CheckSquare, } from 'lucide-react'; -import { API, showError, showSuccess, showWarning } from '../../../helpers'; +import { API, showError, showSuccess, showWarning, stringToColor } from '../../../helpers'; import { DEFAULT_ENDPOINT } from '../../../constants'; import { useTranslation } from 'react-i18next'; import { @@ -35,14 +35,12 @@ export default function UpstreamRatioSync(props) { // 差异数据和测试结果 const [differences, setDifferences] = useState({}); - const [testResults, setTestResults] = useState([]); const [resolutions, setResolutions] = useState({}); // 分页相关状态 const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); - // 获取所有渠道 const fetchAllChannels = async () => { setLoading(true); try { @@ -51,18 +49,16 @@ export default function UpstreamRatioSync(props) { if (res.data.success) { const channels = res.data.data || []; - // 转换为Transfer组件所需格式 const transferData = channels.map(channel => ({ key: channel.id, label: channel.name, value: channel.id, - disabled: false, // 所有渠道都可以选择 + disabled: false, _originalData: channel, })); setAllChannels(transferData); - // 初始化端点配置 const initialEndpoints = {}; transferData.forEach(channel => { initialEndpoints[channel.key] = DEFAULT_ENDPOINT; @@ -78,7 +74,6 @@ export default function UpstreamRatioSync(props) { } }; - // 确认选择渠道 const confirmChannelSelection = () => { const selected = allChannels .filter(ch => selectedChannelIds.includes(ch.value)) @@ -93,7 +88,6 @@ export default function UpstreamRatioSync(props) { fetchRatiosFromChannels(selected); }; - // 从选定渠道获取倍率 const fetchRatiosFromChannels = async (channelList) => { setSyncLoading(true); @@ -113,17 +107,14 @@ export default function UpstreamRatioSync(props) { const { differences = {}, test_results = [] } = res.data.data; - // 显示测试结果 const errorResults = test_results.filter(r => r.status === 'error'); if (errorResults.length > 0) { showWarning(t('部分渠道测试失败:') + errorResults.map(r => `${r.name}: ${r.error}`).join(', ')); } setDifferences(differences); - setTestResults(test_results); setResolutions({}); - // 判断是否有差异 if (Object.keys(differences).length === 0) { showSuccess(t('已与上游倍率完全一致,无需同步')); } @@ -134,7 +125,6 @@ export default function UpstreamRatioSync(props) { } }; - // 解决冲突/选择值 const selectValue = (model, ratioType, value) => { setResolutions(prev => ({ ...prev, @@ -145,7 +135,6 @@ export default function UpstreamRatioSync(props) { })); }; - // 应用同步 const applySync = async () => { const currentRatios = { ModelRatio: JSON.parse(props.options.ModelRatio || '{}'), @@ -154,7 +143,6 @@ export default function UpstreamRatioSync(props) { ModelPrice: JSON.parse(props.options.ModelPrice || '{}'), }; - // 应用已选择的值 Object.entries(resolutions).forEach(([model, ratios]) => { Object.entries(ratios).forEach(([ratioType, value]) => { const optionKey = ratioType @@ -165,7 +153,6 @@ export default function UpstreamRatioSync(props) { }); }); - // 保存到后端 setLoading(true); try { const updates = Object.entries(currentRatios).map(([key, value]) => @@ -180,11 +167,26 @@ export default function UpstreamRatioSync(props) { if (results.every(res => res.data.success)) { showSuccess(t('同步成功')); props.refresh(); - // 清空状态 - setDifferences({}); - setTestResults([]); + + setDifferences(prevDifferences => { + const newDifferences = { ...prevDifferences }; + + Object.entries(resolutions).forEach(([model, ratios]) => { + Object.keys(ratios).forEach(ratioType => { + if (newDifferences[model] && newDifferences[model][ratioType]) { + delete newDifferences[model][ratioType]; + + if (Object.keys(newDifferences[model]).length === 0) { + delete newDifferences[model]; + } + } + }); + }); + + return newDifferences; + }); + setResolutions({}); - setSelectedChannelIds([]); } else { showError(t('部分保存失败')); } @@ -195,14 +197,12 @@ export default function UpstreamRatioSync(props) { } }; - // 计算当前页显示的数据 const getCurrentPageData = (dataSource) => { const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; return dataSource.slice(startIndex, endIndex); }; - // 渲染表格头部 const renderHeader = () => (
@@ -219,7 +219,6 @@ export default function UpstreamRatioSync(props) { {(() => { - // 检查是否有选择可应用的值 const hasSelections = Object.keys(resolutions).length > 0; return ( @@ -239,9 +238,7 @@ export default function UpstreamRatioSync(props) {
); - // 渲染差异表格 const renderDifferenceTable = () => { - // 构建数据源 const dataSource = useMemo(() => { const tmp = []; @@ -260,7 +257,6 @@ export default function UpstreamRatioSync(props) { return tmp; }, [differences]); - // 收集所有上游渠道名称 const upstreamNames = useMemo(() => { const set = new Set(); dataSource.forEach((row) => { @@ -274,13 +270,12 @@ export default function UpstreamRatioSync(props) { } darkModeImage={} - description={Object.keys(differences).length === 0 ? t('已与上游倍率完全一致') : t('请先选择同步渠道')} + description={Object.keys(differences).length === 0 ? t('暂无差异化倍率显示') : t('请先选择同步渠道')} style={{ padding: 30 }} /> ); } - // 列定义 const columns = [ { title: t('模型'), @@ -297,7 +292,7 @@ export default function UpstreamRatioSync(props) { cache_ratio: t('缓存倍率'), model_price: t('固定价格'), }; - return {typeMap[text] || text}; + return {typeMap[text] || text}; }, }, { @@ -309,16 +304,13 @@ export default function UpstreamRatioSync(props) { ), }, - // 动态上游列 ...upstreamNames.map((upName) => { - // 计算该渠道的全选状态 const channelStats = (() => { - let selectableCount = 0; // 可选择的项目数量 - let selectedCount = 0; // 已选择的项目数量 + let selectableCount = 0; + let selectedCount = 0; dataSource.forEach((row) => { const upstreamVal = row.upstreams?.[upName]; - // 只有具体数值的才是可选择的(不是null、undefined或"same") if (upstreamVal !== null && upstreamVal !== undefined && upstreamVal !== 'same') { selectableCount++; const isSelected = resolutions[row.model]?.[row.ratioType] === upstreamVal; @@ -337,7 +329,6 @@ export default function UpstreamRatioSync(props) { }; })(); - // 处理全选/取消全选 const handleBulkSelect = (checked) => { setResolutions((prev) => { const newRes = { ...prev }; @@ -346,11 +337,9 @@ export default function UpstreamRatioSync(props) { const upstreamVal = row.upstreams?.[upName]; if (upstreamVal !== null && upstreamVal !== undefined && upstreamVal !== 'same') { if (checked) { - // 选择该值 if (!newRes[row.model]) newRes[row.model] = {}; newRes[row.model][row.ratioType] = upstreamVal; } else { - // 取消选择该值 if (newRes[row.model]) { delete newRes[row.model][row.ratioType]; if (Object.keys(newRes[row.model]).length === 0) { @@ -389,7 +378,6 @@ export default function UpstreamRatioSync(props) { return {t('与本地相同')}; } - // 有具体值,可以选择 const isSelected = resolutions[record.model]?.[record.ratioType] === upstreamVal; return ( @@ -454,7 +442,6 @@ export default function UpstreamRatioSync(props) { ); }; - // 更新渠道端点 const updateChannelEndpoint = useCallback((channelId, endpoint) => { setChannelEndpoints(prev => ({ ...prev, [channelId]: endpoint })); }, []);