Explorar o código

✨ feat(ratio-sync): support /api/pricing parsing, confidence verification & UI enhancements

Backend
- controller/ratio_sync.go
  • Parse /api/pricing response and convert to ratio / price maps.
  • Introduce confidence heuristic (model_ratio = 37.5 && completion_ratio = 1) to flag unreliable data.
  • Include confidence map when building differences and filter “same”/empty entries.
- dto/ratio_sync.go
  • Add `ID` to UpstreamDTO, `upstreams` to UpstreamRequest, and `Confidence` to DifferenceItem.

Frontend
- ChannelSelectorModal.js
  • Re-implement with table layout, pagination, search, endpoint-type selector and mobile support.
- UpstreamRatioSync.js
  • Send full upstream objects, add ratio-type filter, confidence badges/tooltips, retain endpoints.
  • Leverage ChannelSelectorModal’s pagination reset.
- ChannelsTable.js – fix tag color for disabled status.
- en.json – add translations for new UI labels.

Motivation
These changes let users sync model ratios / prices from different upstream endpoints and visually identify potentially unreliable data, improving operational safety and flexibility.
t0ng7u hai 8 meses
pai
achega
b43423bffc

+ 163 - 11
controller/ratio_sync.go

@@ -3,6 +3,7 @@ package controller
 import (
     "context"
     "encoding/json"
+    "fmt"
     "net/http"
     "strings"
     "sync"
@@ -43,7 +44,17 @@ func FetchUpstreamRatios(c *gin.Context) {
 
     var upstreams []dto.UpstreamDTO
 
-    if len(req.ChannelIDs) > 0 {
+    if len(req.Upstreams) > 0 {
+        for _, u := range req.Upstreams {
+            if strings.HasPrefix(u.BaseURL, "http") {
+                if u.Endpoint == "" {
+                    u.Endpoint = defaultEndpoint
+                }
+                u.BaseURL = strings.TrimRight(u.BaseURL, "/")
+                upstreams = append(upstreams, u)
+            }
+        }
+    } else if len(req.ChannelIDs) > 0 {
         intIds := make([]int, 0, len(req.ChannelIDs))
         for _, id64 := range req.ChannelIDs {
             intIds = append(intIds, int(id64))
@@ -57,6 +68,7 @@ func FetchUpstreamRatios(c *gin.Context) {
         for _, ch := range dbChannels {
             if base := ch.GetBaseURL(); strings.HasPrefix(base, "http") {
                 upstreams = append(upstreams, dto.UpstreamDTO{
+                    ID:       ch.Id,
                     Name:     ch.Name,
                     BaseURL:  strings.TrimRight(base, "/"),
                     Endpoint: "",
@@ -93,43 +105,125 @@ func FetchUpstreamRatios(c *gin.Context) {
             }
             fullURL := chItem.BaseURL + endpoint
 
+            uniqueName := chItem.Name
+            if chItem.ID != 0 {
+                uniqueName = fmt.Sprintf("%s(%d)", chItem.Name, chItem.ID)
+            }
+
             ctx, cancel := context.WithTimeout(c.Request.Context(), time.Duration(req.Timeout)*time.Second)
             defer cancel()
 
             httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
             if err != nil {
                 common.LogWarn(c.Request.Context(), "build request failed: "+err.Error())
-                ch <- upstreamResult{Name: chItem.Name, Err: err.Error()}
+                ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
                 return
             }
 
             resp, err := client.Do(httpReq)
             if err != nil {
                 common.LogWarn(c.Request.Context(), "http error on "+chItem.Name+": "+err.Error())
-                ch <- upstreamResult{Name: chItem.Name, Err: err.Error()}
+                ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
                 return
             }
             defer resp.Body.Close()
             if resp.StatusCode != http.StatusOK {
                 common.LogWarn(c.Request.Context(), "non-200 from "+chItem.Name+": "+resp.Status)
-                ch <- upstreamResult{Name: chItem.Name, Err: resp.Status}
+                ch <- upstreamResult{Name: uniqueName, Err: resp.Status}
                 return
             }
+            // 兼容两种上游接口格式:
+            //  type1: /api/ratio_config -> data 为 map[string]any,包含 model_ratio/completion_ratio/cache_ratio/model_price
+            //  type2: /api/pricing      -> data 为 []Pricing 列表,需要转换为与 type1 相同的 map 格式
             var body struct {
-                Success bool                   `json:"success"`
-                Data    map[string]any         `json:"data"`
-                Message string                 `json:"message"`
+                Success bool            `json:"success"`
+                Data    json.RawMessage `json:"data"`
+                Message string          `json:"message"`
             }
+
             if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
                 common.LogWarn(c.Request.Context(), "json decode failed from "+chItem.Name+": "+err.Error())
-                ch <- upstreamResult{Name: chItem.Name, Err: err.Error()}
+                ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
                 return
             }
+
             if !body.Success {
-                ch <- upstreamResult{Name: chItem.Name, Err: body.Message}
+                ch <- upstreamResult{Name: uniqueName, Err: body.Message}
                 return
             }
-            ch <- upstreamResult{Name: chItem.Name, Data: body.Data}
+
+            // 尝试按 type1 解析
+            var type1Data map[string]any
+            if err := json.Unmarshal(body.Data, &type1Data); err == nil {
+                // 如果包含至少一个 ratioTypes 字段,则认为是 type1
+                isType1 := false
+                for _, rt := range ratioTypes {
+                    if _, ok := type1Data[rt]; ok {
+                        isType1 = true
+                        break
+                    }
+                }
+                if isType1 {
+                    ch <- upstreamResult{Name: uniqueName, Data: type1Data}
+                    return
+                }
+            }
+
+            // 如果不是 type1,则尝试按 type2 (/api/pricing) 解析
+            var pricingItems []struct {
+                ModelName       string  `json:"model_name"`
+                QuotaType       int     `json:"quota_type"`
+                ModelRatio      float64 `json:"model_ratio"`
+                ModelPrice      float64 `json:"model_price"`
+                CompletionRatio float64 `json:"completion_ratio"`
+            }
+            if err := json.Unmarshal(body.Data, &pricingItems); err != nil {
+                common.LogWarn(c.Request.Context(), "unrecognized data format from "+chItem.Name+": "+err.Error())
+                ch <- upstreamResult{Name: uniqueName, Err: "无法解析上游返回数据"}
+                return
+            }
+
+            modelRatioMap := make(map[string]float64)
+            completionRatioMap := make(map[string]float64)
+            modelPriceMap := make(map[string]float64)
+
+            for _, item := range pricingItems {
+                if item.QuotaType == 1 {
+                    modelPriceMap[item.ModelName] = item.ModelPrice
+                } else {
+                    modelRatioMap[item.ModelName] = item.ModelRatio
+                    // completionRatio 可能为 0,此时也直接赋值,保持与上游一致
+                    completionRatioMap[item.ModelName] = item.CompletionRatio
+                }
+            }
+
+            converted := make(map[string]any)
+
+            if len(modelRatioMap) > 0 {
+                ratioAny := make(map[string]any, len(modelRatioMap))
+                for k, v := range modelRatioMap {
+                    ratioAny[k] = v
+                }
+                converted["model_ratio"] = ratioAny
+            }
+
+            if len(completionRatioMap) > 0 {
+                compAny := make(map[string]any, len(completionRatioMap))
+                for k, v := range completionRatioMap {
+                    compAny[k] = v
+                }
+                converted["completion_ratio"] = compAny
+            }
+
+            if len(modelPriceMap) > 0 {
+                priceAny := make(map[string]any, len(modelPriceMap))
+                for k, v := range modelPriceMap {
+                    priceAny[k] = v
+                }
+                converted["model_price"] = priceAny
+            }
+
+            ch <- upstreamResult{Name: uniqueName, Data: converted}
         }(chn)
     }
 
@@ -202,6 +296,43 @@ func buildDifferences(localData map[string]any, successfulChannels []struct {
         }
     }
 
+    confidenceMap := make(map[string]map[string]bool)
+    
+    // 预处理阶段:检查pricing接口的可信度
+    for _, channel := range successfulChannels {
+        confidenceMap[channel.name] = make(map[string]bool)
+        
+        modelRatios, hasModelRatio := channel.data["model_ratio"].(map[string]any)
+        completionRatios, hasCompletionRatio := channel.data["completion_ratio"].(map[string]any)
+        
+        if hasModelRatio && hasCompletionRatio {
+            // 遍历所有模型,检查是否满足不可信条件
+            for modelName := range allModels {
+                // 默认为可信
+                confidenceMap[channel.name][modelName] = true
+                
+                // 检查是否满足不可信条件:model_ratio为37.5且completion_ratio为1
+                if modelRatioVal, ok := modelRatios[modelName]; ok {
+                    if completionRatioVal, ok := completionRatios[modelName]; ok {
+                        // 转换为float64进行比较
+                        if modelRatioFloat, ok := modelRatioVal.(float64); ok {
+                            if completionRatioFloat, ok := completionRatioVal.(float64); ok {
+                                if modelRatioFloat == 37.5 && completionRatioFloat == 1.0 {
+                                    confidenceMap[channel.name][modelName] = false
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            // 如果不是从pricing接口获取的数据,则全部标记为可信
+            for modelName := range allModels {
+                confidenceMap[channel.name][modelName] = true
+            }
+        }
+    }
+
     for modelName := range allModels {
         for _, ratioType := range ratioTypes {
             var localValue interface{} = nil
@@ -214,6 +345,7 @@ func buildDifferences(localData map[string]any, successfulChannels []struct {
             }
 
             upstreamValues := make(map[string]interface{})
+            confidenceValues := make(map[string]bool)
             hasUpstreamValue := false
             hasDifference := false
 
@@ -241,6 +373,8 @@ func buildDifferences(localData map[string]any, successfulChannels []struct {
                 }
                 
                 upstreamValues[channel.name] = upstreamValue
+                
+                confidenceValues[channel.name] = confidenceMap[channel.name][modelName]
             }
 
             shouldInclude := false
@@ -262,6 +396,7 @@ func buildDifferences(localData map[string]any, successfulChannels []struct {
                 differences[modelName][ratioType] = dto.DifferenceItem{
                     Current:   localValue,
                     Upstreams: upstreamValues,
+                    Confidence: confidenceValues,
                 }
             }
         }
@@ -283,9 +418,26 @@ func buildDifferences(localData map[string]any, successfulChannels []struct {
             for chName := range item.Upstreams {
                 if !channelHasDiff[chName] {
                     delete(item.Upstreams, chName)
+                    delete(item.Confidence, chName)
+                }
+            }
+
+            allSame := true
+            for _, v := range item.Upstreams {
+                if v != "same" {
+                    allSame = false
+                    break
                 }
             }
-            differences[modelName][ratioType] = item
+            if len(item.Upstreams) == 0 || allSame {
+                delete(ratioMap, ratioType)
+            } else {
+                differences[modelName][ratioType] = item
+            }
+        }
+
+        if len(ratioMap) == 0 {
+            delete(differences, modelName)
         }
     }
 

+ 3 - 14
dto/ratio_sync.go

@@ -1,18 +1,7 @@
 package dto
 
-// UpstreamDTO 提交到后端同步倍率的上游渠道信息
-// Endpoint 可以为空,后端会默认使用 /api/ratio_config
-// BaseURL 必须以 http/https 开头,不要以 / 结尾
-// 例如: https://api.example.com
-// Endpoint: /api/ratio_config
-// 提交示例:
-// {
-//   "name": "openai",
-//   "base_url": "https://api.openai.com",
-//   "endpoint": "/ratio_config"
-// }
-
 type UpstreamDTO struct {
+    ID       int    `json:"id,omitempty"`
     Name     string `json:"name" binding:"required"`
     BaseURL  string `json:"base_url" binding:"required"`
     Endpoint string `json:"endpoint"`
@@ -20,6 +9,7 @@ type UpstreamDTO struct {
 
 type UpstreamRequest struct {
     ChannelIDs []int64 `json:"channel_ids"`
+    Upstreams   []UpstreamDTO `json:"upstreams"`
     Timeout    int     `json:"timeout"`
 }
 
@@ -37,10 +27,9 @@ type TestResult struct {
 type DifferenceItem struct {
     Current   interface{}            `json:"current"`
     Upstreams map[string]interface{} `json:"upstreams"`
+    Confidence map[string]bool       `json:"confidence"`
 }
 
-// SyncableChannel 可同步的渠道信息(base_url 不为空)
-
 type SyncableChannel struct {
     ID      int    `json:"id"`
     Name    string `json:"name"`

+ 196 - 103
web/src/components/settings/ChannelSelectorModal.js

@@ -1,115 +1,183 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
+import { isMobile } from '../../helpers';
 import {
   Modal,
-  Transfer,
+  Table,
   Input,
   Space,
-  Checkbox,
-  Avatar,
   Highlight,
+  Select,
+  Tag,
 } from '@douyinfe/semi-ui';
-import { IconClose } from '@douyinfe/semi-icons';
+import { IconSearch } from '@douyinfe/semi-icons';
+import { CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
 
-const CHANNEL_STATUS_CONFIG = {
-  1: { color: 'green', text: '启用' },
-  2: { color: 'red', text: '禁用' },
-  3: { color: 'amber', text: '自禁' },
-  default: { color: 'grey', text: '未知' }
-};
-
-const getChannelStatusConfig = (status) => {
-  return CHANNEL_STATUS_CONFIG[status] || CHANNEL_STATUS_CONFIG.default;
-};
-
-export default function ChannelSelectorModal({
-  t,
+const ChannelSelectorModal = forwardRef(({
   visible,
   onCancel,
   onOk,
-  allChannels = [],
-  selectedChannelIds = [],
+  allChannels,
+  selectedChannelIds,
   setSelectedChannelIds,
   channelEndpoints,
   updateChannelEndpoint,
-}) {
+  t,
+}, ref) => {
   const [searchText, setSearchText] = useState('');
+  const [currentPage, setCurrentPage] = useState(1);
+  const [pageSize, setPageSize] = useState(10);
 
-  const ChannelInfo = ({ item, showEndpoint = false, isSelected = false }) => {
-    const channelId = item.key || item.value;
-    const currentEndpoint = channelEndpoints[channelId];
-    const baseUrl = item._originalData?.base_url || '';
-    const status = item._originalData?.status || 0;
-    const statusConfig = getChannelStatusConfig(status);
+  const [filteredData, setFilteredData] = useState([]);
 
-    return (
-      <>
-        <Avatar color={statusConfig.color} size="small">
-          {statusConfig.text}
-        </Avatar>
-        <div className="info">
-          <div className="name">
-            {isSelected ? (
-              item.label
-            ) : (
-              <Highlight sourceString={item.label} searchWords={[searchText]} />
-            )}
-          </div>
-          <div className="email" style={showEndpoint ? { display: 'flex', alignItems: 'center', gap: '4px' } : {}}>
-            <span className="text-xs text-gray-500 truncate max-w-[200px]" title={baseUrl}>
-              {isSelected ? (
-                baseUrl
-              ) : (
-                <Highlight sourceString={baseUrl} searchWords={[searchText]} />
-              )}
-            </span>
-            {showEndpoint && (
-              <Input
-                size="small"
-                value={currentEndpoint}
-                onChange={(value) => updateChannelEndpoint(channelId, value)}
-                placeholder="/api/ratio_config"
-                className="flex-1 text-xs"
-                style={{ fontSize: '12px' }}
-              />
-            )}
-            {isSelected && !showEndpoint && (
-              <span className="text-xs text-gray-700 font-mono bg-gray-100 px-2 py-1 rounded ml-2">
-                {currentEndpoint}
-              </span>
-            )}
-          </div>
-        </div>
-      </>
-    );
+  useImperativeHandle(ref, () => ({
+    resetPagination: () => {
+      setCurrentPage(1);
+      setSearchText('');
+    },
+  }));
+
+  useEffect(() => {
+    if (!allChannels) return;
+
+    const searchLower = searchText.trim().toLowerCase();
+    const matched = searchLower
+      ? allChannels.filter((item) => {
+        const name = (item.label || '').toLowerCase();
+        const baseUrl = (item._originalData?.base_url || '').toLowerCase();
+        return name.includes(searchLower) || baseUrl.includes(searchLower);
+      })
+      : allChannels;
+
+    setFilteredData(matched);
+  }, [allChannels, searchText]);
+
+  const total = filteredData.length;
+
+  const paginatedData = filteredData.slice(
+    (currentPage - 1) * pageSize,
+    currentPage * pageSize,
+  );
+
+  const updateEndpoint = (channelId, endpoint) => {
+    if (typeof updateChannelEndpoint === 'function') {
+      updateChannelEndpoint(channelId, endpoint);
+    }
   };
 
-  const renderSourceItem = (item) => {
+  const renderEndpointCell = (text, record) => {
+    const channelId = record.key || record.value;
+    const currentEndpoint = channelEndpoints[channelId] || '';
+
+    const getEndpointType = (ep) => {
+      if (ep === '/api/ratio_config') return 'ratio_config';
+      if (ep === '/api/pricing') return 'pricing';
+      return 'custom';
+    };
+
+    const currentType = getEndpointType(currentEndpoint);
+
+    const handleTypeChange = (val) => {
+      if (val === 'ratio_config') {
+        updateEndpoint(channelId, '/api/ratio_config');
+      } else if (val === 'pricing') {
+        updateEndpoint(channelId, '/api/pricing');
+      } else {
+        if (currentType !== 'custom') {
+          updateEndpoint(channelId, '');
+        }
+      }
+    };
+
     return (
-      <div className="components-transfer-source-item" key={item.key}>
-        <Checkbox
-          onChange={item.onChange}
-          checked={item.checked}
-          style={{ height: 52, alignItems: 'center' }}
-        >
-          <ChannelInfo item={item} showEndpoint={true} />
-        </Checkbox>
+      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+        <Select
+          size="small"
+          value={currentType}
+          onChange={handleTypeChange}
+          style={{ width: 120 }}
+          optionList={[
+            { label: 'ratio_config', value: 'ratio_config' },
+            { label: 'pricing', value: 'pricing' },
+            { label: 'custom', value: 'custom' },
+          ]}
+        />
+        {currentType === 'custom' && (
+          <Input
+            size="small"
+            value={currentEndpoint}
+            onChange={(val) => updateEndpoint(channelId, val)}
+            placeholder="/your/endpoint"
+            style={{ width: 160, fontSize: 12 }}
+          />
+        )}
       </div>
     );
   };
 
-  const renderSelectedItem = (item) => {
-    return (
-      <div className="components-transfer-selected-item" key={item.key}>
-        <ChannelInfo item={item} isSelected={true} />
-        <IconClose style={{ cursor: 'pointer' }} onClick={item.onRemove} />
-      </div>
-    );
+  const renderStatusCell = (status) => {
+    switch (status) {
+      case 1:
+        return (
+          <Tag size='large' color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
+            {t('已启用')}
+          </Tag>
+        );
+      case 2:
+        return (
+          <Tag size='large' color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
+            {t('已禁用')}
+          </Tag>
+        );
+      case 3:
+        return (
+          <Tag size='large' color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
+            {t('自动禁用')}
+          </Tag>
+        );
+      default:
+        return (
+          <Tag size='large' color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
+            {t('未知状态')}
+          </Tag>
+        );
+    }
   };
 
-  const channelFilter = (input, item) => {
-    const searchLower = input.toLowerCase();
-    return item.label.toLowerCase().includes(searchLower) ||
-      (item._originalData?.base_url || '').toLowerCase().includes(searchLower);
+  const renderNameCell = (text) => (
+    <Highlight sourceString={text} searchWords={[searchText]} />
+  );
+
+  const renderBaseUrlCell = (text) => (
+    <Highlight sourceString={text} searchWords={[searchText]} />
+  );
+
+  const columns = [
+    {
+      title: t('名称'),
+      dataIndex: 'label',
+      render: renderNameCell,
+    },
+    {
+      title: t('源地址'),
+      dataIndex: '_originalData.base_url',
+      render: (_, record) => renderBaseUrlCell(record._originalData?.base_url || ''),
+    },
+    {
+      title: t('状态'),
+      dataIndex: '_originalData.status',
+      render: (_, record) => renderStatusCell(record._originalData?.status || 0),
+    },
+    {
+      title: t('同步接口'),
+      dataIndex: 'endpoint',
+      fixed: 'right',
+      render: renderEndpointCell,
+    },
+  ];
+
+  const rowSelection = {
+    selectedRowKeys: selectedChannelIds,
+    onChange: (keys) => setSelectedChannelIds(keys),
   };
 
   return (
@@ -118,26 +186,51 @@ export default function ChannelSelectorModal({
       onCancel={onCancel}
       onOk={onOk}
       title={<span className="text-lg font-semibold">{t('选择同步渠道')}</span>}
-      width={1000}
+      size={isMobile() ? 'full-width' : 'large'}
+      keepDOM
+      lazyRender={false}
     >
       <Space vertical style={{ width: '100%' }}>
-        <Transfer
-          style={{ width: '100%' }}
-          dataSource={allChannels}
-          value={selectedChannelIds}
-          onChange={setSelectedChannelIds}
-          renderSourceItem={renderSourceItem}
-          renderSelectedItem={renderSelectedItem}
-          filter={channelFilter}
-          inputProps={{ placeholder: t('搜索渠道名称或地址') }}
-          onSearch={setSearchText}
-          emptyContent={{
-            left: t('暂无渠道'),
-            right: t('暂无选择'),
-            search: t('无搜索结果'),
+        <Input
+          prefix={<IconSearch size={14} />}
+          placeholder={t('搜索渠道名称或地址')}
+          value={searchText}
+          onChange={setSearchText}
+          showClear
+          className="!rounded-full"
+        />
+
+        <Table
+          columns={columns}
+          dataSource={paginatedData}
+          rowKey="key"
+          rowSelection={rowSelection}
+          pagination={{
+            currentPage: currentPage,
+            pageSize: pageSize,
+            total: total,
+            showSizeChanger: true,
+            showQuickJumper: true,
+            pageSizeOptions: ['10', '20', '50', '100'],
+            formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
+              start: page.currentStart,
+              end: page.currentEnd,
+              total: total,
+            }),
+            onChange: (page, size) => {
+              setCurrentPage(page);
+              setPageSize(size);
+            },
+            onShowSizeChange: (curr, size) => {
+              setCurrentPage(1);
+              setPageSize(size);
+            },
           }}
+          size="small"
         />
       </Space>
     </Modal>
   );
-} 
+});
+
+export default ChannelSelectorModal; 

+ 1 - 1
web/src/components/table/ChannelsTable.js

@@ -114,7 +114,7 @@ const ChannelsTable = () => {
         );
       case 2:
         return (
-          <Tag size='large' color='yellow' shape='circle' prefixIcon={<XCircle size={14} />}>
+          <Tag size='large' color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
             {t('已禁用')}
           </Tag>
         );

+ 10 - 1
web/src/i18n/locales/en.json

@@ -1701,5 +1701,14 @@
   "充值分组倍率": "Recharge group ratio",
   "充值方式设置": "Recharge method settings",
   "更新支付设置": "Update payment settings",
-  "通知": "Notice"
+  "通知": "Notice",
+  "源地址": "Source address",
+  "同步接口": "Synchronization interface",
+  "置信度": "Confidence",
+  "谨慎": "Cautious",
+  "该数据可能不可信,请谨慎使用": "This data may not be reliable, please use with caution",
+  "可信": "Reliable",
+  "所有上游数据均可信": "All upstream data is reliable",
+  "以下上游数据可能不可信:": "The following upstream data may not be reliable: ",
+  "按倍率类型筛选": "Filter by ratio type"
 }

+ 134 - 43
web/src/pages/Setting/Ratio/UpstreamRatioSync.js

@@ -7,11 +7,15 @@ import {
   Checkbox,
   Form,
   Input,
+  Tooltip,
+  Select,
 } from '@douyinfe/semi-ui';
 import { IconSearch } from '@douyinfe/semi-icons';
 import {
   RefreshCcw,
   CheckSquare,
+  AlertTriangle,
+  CheckCircle,
 } from 'lucide-react';
 import { API, showError, showSuccess, showWarning, stringToColor } from '../../../helpers';
 import { DEFAULT_ENDPOINT } from '../../../constants';
@@ -49,6 +53,11 @@ export default function UpstreamRatioSync(props) {
   // 搜索相关状态
   const [searchKeyword, setSearchKeyword] = useState('');
 
+  // 倍率类型过滤
+  const [ratioTypeFilter, setRatioTypeFilter] = useState('');
+
+  const channelSelectorRef = React.useRef(null);
+
   const fetchAllChannels = async () => {
     setLoading(true);
     try {
@@ -67,11 +76,16 @@ export default function UpstreamRatioSync(props) {
 
         setAllChannels(transferData);
 
-        const initialEndpoints = {};
-        transferData.forEach(channel => {
-          initialEndpoints[channel.key] = DEFAULT_ENDPOINT;
+        // 合并已有 endpoints,避免每次打开弹窗都重置
+        setChannelEndpoints(prev => {
+          const merged = { ...prev };
+          transferData.forEach(channel => {
+            if (!merged[channel.key]) {
+              merged[channel.key] = DEFAULT_ENDPOINT;
+            }
+          });
+          return merged;
         });
-        setChannelEndpoints(initialEndpoints);
       } else {
         showError(res.data.message);
       }
@@ -99,8 +113,15 @@ export default function UpstreamRatioSync(props) {
   const fetchRatiosFromChannels = async (channelList) => {
     setSyncLoading(true);
 
+    const upstreams = channelList.map(ch => ({
+      id: ch.id,
+      name: ch.name,
+      base_url: ch.base_url,
+      endpoint: channelEndpoints[ch.id] || DEFAULT_ENDPOINT,
+    }));
+
     const payload = {
-      channel_ids: channelList.map(ch => parseInt(ch.id)),
+      upstreams: upstreams,
       timeout: 10,
     };
 
@@ -215,13 +236,15 @@ export default function UpstreamRatioSync(props) {
   const renderHeader = () => (
     <div className="flex flex-col w-full">
       <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
-        <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
+        <div className="flex flex-col md:flex-row gap-2 w-full md:w-auto order-2 md:order-1">
           <Button
             icon={<RefreshCcw size={14} />}
             className="!rounded-full w-full md:w-auto mt-2"
             onClick={() => {
               setModalVisible(true);
-              fetchAllChannels();
+              if (allChannels.length === 0) {
+                fetchAllChannels();
+              }
             }}
           >
             {t('选择同步渠道')}
@@ -243,14 +266,30 @@ export default function UpstreamRatioSync(props) {
             );
           })()}
 
-          <Input
-            prefix={<IconSearch size={14} />}
-            placeholder={t('搜索模型名称')}
-            value={searchKeyword}
-            onChange={setSearchKeyword}
-            className="!rounded-full w-full md:w-64 mt-2"
-            showClear
-          />
+          <div className="flex flex-col sm:flex-row gap-2 w-full md:w-auto mt-2">
+            <Input
+              prefix={<IconSearch size={14} />}
+              placeholder={t('搜索模型名称')}
+              value={searchKeyword}
+              onChange={setSearchKeyword}
+              className="!rounded-full w-full sm:w-64"
+              showClear
+            />
+
+            <Select
+              placeholder={t('按倍率类型筛选')}
+              value={ratioTypeFilter}
+              onChange={setRatioTypeFilter}
+              className="!rounded-full w-full sm:w-48"
+              showClear
+              onClear={() => setRatioTypeFilter('')}
+            >
+              <Select.Option value="model_ratio">{t('模型倍率')}</Select.Option>
+              <Select.Option value="completion_ratio">{t('补全倍率')}</Select.Option>
+              <Select.Option value="cache_ratio">{t('缓存倍率')}</Select.Option>
+              <Select.Option value="model_price">{t('固定价格')}</Select.Option>
+            </Select>
+          </div>
         </div>
       </div>
     </div>
@@ -268,6 +307,7 @@ export default function UpstreamRatioSync(props) {
             ratioType,
             current: diff.current,
             upstreams: diff.upstreams,
+            confidence: diff.confidence || {},
           });
         });
       });
@@ -276,15 +316,20 @@ export default function UpstreamRatioSync(props) {
     }, [differences]);
 
     const filteredDataSource = useMemo(() => {
-      if (!searchKeyword.trim()) {
+      if (!searchKeyword.trim() && !ratioTypeFilter) {
         return dataSource;
       }
 
-      const keyword = searchKeyword.toLowerCase().trim();
-      return dataSource.filter(item =>
-        item.model.toLowerCase().includes(keyword)
-      );
-    }, [dataSource, searchKeyword]);
+      return dataSource.filter(item => {
+        const matchesKeyword = !searchKeyword.trim() ||
+          item.model.toLowerCase().includes(searchKeyword.toLowerCase().trim());
+
+        const matchesRatioType = !ratioTypeFilter ||
+          item.ratioType === ratioTypeFilter;
+
+        return matchesKeyword && matchesRatioType;
+      });
+    }, [dataSource, searchKeyword, ratioTypeFilter]);
 
     const upstreamNames = useMemo(() => {
       const set = new Set();
@@ -330,6 +375,36 @@ export default function UpstreamRatioSync(props) {
           return <Tag color={stringToColor(text)} shape="circle">{typeMap[text] || text}</Tag>;
         },
       },
+      {
+        title: t('置信度'),
+        dataIndex: 'confidence',
+        render: (_, record) => {
+          const allConfident = Object.values(record.confidence || {}).every(v => v !== false);
+
+          if (allConfident) {
+            return (
+              <Tooltip content={t('所有上游数据均可信')}>
+                <Tag color="green" shape="circle" type="light" prefixIcon={<CheckCircle size={14} />}>
+                  {t('可信')}
+                </Tag>
+              </Tooltip>
+            );
+          } else {
+            const untrustedSources = Object.entries(record.confidence || {})
+              .filter(([_, isConfident]) => isConfident === false)
+              .map(([name]) => name)
+              .join(', ');
+
+            return (
+              <Tooltip content={t('以下上游数据可能不可信:') + untrustedSources}>
+                <Tag color="yellow" shape="circle" type="light" prefixIcon={<AlertTriangle size={14} />}>
+                  {t('谨慎')}
+                </Tag>
+              </Tooltip>
+            );
+          }
+        },
+      },
       {
         title: t('当前值'),
         dataIndex: 'current',
@@ -404,6 +479,7 @@ export default function UpstreamRatioSync(props) {
           dataIndex: upName,
           render: (_, record) => {
             const upstreamVal = record.upstreams?.[upName];
+            const isConfident = record.confidence?.[upName] !== false;
 
             if (upstreamVal === null || upstreamVal === undefined) {
               return <Tag color="default" shape="circle">{t('未设置')}</Tag>;
@@ -416,28 +492,35 @@ export default function UpstreamRatioSync(props) {
             const isSelected = resolutions[record.model]?.[record.ratioType] === upstreamVal;
 
             return (
-              <Checkbox
-                checked={isSelected}
-                onChange={(e) => {
-                  const isChecked = e.target.checked;
-                  if (isChecked) {
-                    selectValue(record.model, record.ratioType, upstreamVal);
-                  } else {
-                    setResolutions((prev) => {
-                      const newRes = { ...prev };
-                      if (newRes[record.model]) {
-                        delete newRes[record.model][record.ratioType];
-                        if (Object.keys(newRes[record.model]).length === 0) {
-                          delete newRes[record.model];
+              <div className="flex items-center gap-2">
+                <Checkbox
+                  checked={isSelected}
+                  onChange={(e) => {
+                    const isChecked = e.target.checked;
+                    if (isChecked) {
+                      selectValue(record.model, record.ratioType, upstreamVal);
+                    } else {
+                      setResolutions((prev) => {
+                        const newRes = { ...prev };
+                        if (newRes[record.model]) {
+                          delete newRes[record.model][record.ratioType];
+                          if (Object.keys(newRes[record.model]).length === 0) {
+                            delete newRes[record.model];
+                          }
                         }
-                      }
-                      return newRes;
-                    });
-                  }
-                }}
-              >
-                {upstreamVal}
-              </Checkbox>
+                        return newRes;
+                      });
+                    }
+                  }}
+                >
+                  {upstreamVal}
+                </Checkbox>
+                {!isConfident && (
+                  <Tooltip position='left' content={t('该数据可能不可信,请谨慎使用')}>
+                    <AlertTriangle size={16} className="text-yellow-500" />
+                  </Tooltip>
+                )}
+              </div>
             );
           },
         };
@@ -481,6 +564,13 @@ export default function UpstreamRatioSync(props) {
     setChannelEndpoints(prev => ({ ...prev, [channelId]: endpoint }));
   }, []);
 
+  const handleModalClose = () => {
+    setModalVisible(false);
+    if (channelSelectorRef.current) {
+      channelSelectorRef.current.resetPagination();
+    }
+  };
+
   return (
     <>
       <Form.Section text={renderHeader()}>
@@ -488,9 +578,10 @@ export default function UpstreamRatioSync(props) {
       </Form.Section>
 
       <ChannelSelectorModal
+        ref={channelSelectorRef}
         t={t}
         visible={modalVisible}
-        onCancel={() => setModalVisible(false)}
+        onCancel={handleModalClose}
         onOk={confirmChannelSelection}
         allChannels={allChannels}
         selectedChannelIds={selectedChannelIds}