Commit c74a86d1 by Calcium-Ion Committed by GitHub

Merge pull request #1267 from t0ng7u/feature/upstream-ratio-sync

🔄 feat(ratio-sync): introduce upstream ratio synchronisation feature #1220
parents e8da9d7d cd4fc7a1
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"math/big" "math/big"
"math/rand" "math/rand"
"net" "net"
"net/url"
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
...@@ -284,3 +285,20 @@ func GetAudioDuration(ctx context.Context, filename string, ext string) (float64 ...@@ -284,3 +285,20 @@ func GetAudioDuration(ctx context.Context, filename string, ext string) (float64
} }
return strconv.ParseFloat(durationStr, 64) return strconv.ParseFloat(durationStr, 64)
} }
// BuildURL concatenates base and endpoint, returns the complete url string
func BuildURL(base string, endpoint string) string {
u, err := url.Parse(base)
if err != nil {
return base + endpoint
}
end := endpoint
if end == "" {
end = "/"
}
ref, err := url.Parse(end)
if err != nil {
return base + endpoint
}
return u.ResolveReference(ref).String()
}
package controller
import (
"net/http"
"one-api/setting/ratio_setting"
"github.com/gin-gonic/gin"
)
func GetRatioConfig(c *gin.Context) {
if !ratio_setting.IsExposeRatioEnabled() {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "倍率配置接口未启用",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": ratio_setting.GetExposedData(),
})
}
\ No newline at end of file
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 {
Name string `json:"name" binding:"required"`
BaseURL string `json:"base_url" binding:"required"`
Endpoint string `json:"endpoint"`
}
type UpstreamRequest struct {
ChannelIDs []int64 `json:"channel_ids"`
Timeout int `json:"timeout"`
}
// TestResult 上游测试连通性结果
type TestResult struct {
Name string `json:"name"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
}
// DifferenceItem 差异项
// Current 为本地值,可能为 nil
// Upstreams 为各渠道的上游值,具体数值 / "same" / nil
type DifferenceItem struct {
Current interface{} `json:"current"`
Upstreams map[string]interface{} `json:"upstreams"`
}
// SyncableChannel 可同步的渠道信息(base_url 不为空)
type SyncableChannel struct {
ID int `json:"id"`
Name string `json:"name"`
BaseURL string `json:"base_url"`
Status int `json:"status"`
}
\ No newline at end of file
...@@ -127,6 +127,7 @@ func InitOptionMap() { ...@@ -127,6 +127,7 @@ func InitOptionMap() {
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString() common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength) common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString() common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
common.OptionMap["ExposeRatioEnabled"] = strconv.FormatBool(ratio_setting.IsExposeRatioEnabled())
// 自动添加所有注册的模型配置 // 自动添加所有注册的模型配置
modelConfigs := config.GlobalConfig.ExportAllConfigs() modelConfigs := config.GlobalConfig.ExportAllConfigs()
...@@ -267,6 +268,8 @@ func updateOptionMap(key string, value string) (err error) { ...@@ -267,6 +268,8 @@ func updateOptionMap(key string, value string) (err error) {
setting.WorkerAllowHttpImageRequestEnabled = boolValue setting.WorkerAllowHttpImageRequestEnabled = boolValue
case "DefaultUseAutoGroup": case "DefaultUseAutoGroup":
setting.DefaultUseAutoGroup = boolValue setting.DefaultUseAutoGroup = boolValue
case "ExposeRatioEnabled":
ratio_setting.SetExposeRatioEnabled(boolValue)
} }
} }
switch key { switch key {
......
...@@ -36,6 +36,7 @@ func SetApiRouter(router *gin.Engine) { ...@@ -36,6 +36,7 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), controller.EmailBind) apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), controller.EmailBind)
apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), controller.TelegramLogin) apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), controller.TelegramLogin)
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), controller.TelegramBind) apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), controller.TelegramBind)
apiRouter.GET("/ratio_config", middleware.CriticalRateLimit(), controller.GetRatioConfig)
userRoute := apiRouter.Group("/user") userRoute := apiRouter.Group("/user")
{ {
...@@ -83,6 +84,12 @@ func SetApiRouter(router *gin.Engine) { ...@@ -83,6 +84,12 @@ func SetApiRouter(router *gin.Engine) {
optionRoute.POST("/rest_model_ratio", controller.ResetModelRatio) optionRoute.POST("/rest_model_ratio", controller.ResetModelRatio)
optionRoute.POST("/migrate_console_setting", controller.MigrateConsoleSetting) // 用于迁移检测的旧键,下个版本会删除 optionRoute.POST("/migrate_console_setting", controller.MigrateConsoleSetting) // 用于迁移检测的旧键,下个版本会删除
} }
ratioSyncRoute := apiRouter.Group("/ratio_sync")
ratioSyncRoute.Use(middleware.RootAuth())
{
ratioSyncRoute.GET("/channels", controller.GetSyncableChannels)
ratioSyncRoute.POST("/fetch", controller.FetchUpstreamRatios)
}
channelRoute := apiRouter.Group("/channel") channelRoute := apiRouter.Group("/channel")
channelRoute.Use(middleware.AdminAuth()) channelRoute.Use(middleware.AdminAuth())
{ {
......
...@@ -85,7 +85,11 @@ func UpdateCacheRatioByJSONString(jsonStr string) error { ...@@ -85,7 +85,11 @@ func UpdateCacheRatioByJSONString(jsonStr string) error {
cacheRatioMapMutex.Lock() cacheRatioMapMutex.Lock()
defer cacheRatioMapMutex.Unlock() defer cacheRatioMapMutex.Unlock()
cacheRatioMap = make(map[string]float64) cacheRatioMap = make(map[string]float64)
return json.Unmarshal([]byte(jsonStr), &cacheRatioMap) err := json.Unmarshal([]byte(jsonStr), &cacheRatioMap)
if err == nil {
InvalidateExposedDataCache()
}
return err
} }
// GetCacheRatio returns the cache ratio for a model // GetCacheRatio returns the cache ratio for a model
...@@ -106,3 +110,13 @@ func GetCreateCacheRatio(name string) (float64, bool) { ...@@ -106,3 +110,13 @@ func GetCreateCacheRatio(name string) (float64, bool) {
} }
return ratio, true return ratio, true
} }
func GetCacheRatioCopy() map[string]float64 {
cacheRatioMapMutex.RLock()
defer cacheRatioMapMutex.RUnlock()
copyMap := make(map[string]float64, len(cacheRatioMap))
for k, v := range cacheRatioMap {
copyMap[k] = v
}
return copyMap
}
package ratio_setting
import "sync/atomic"
var exposeRatioEnabled atomic.Bool
func init() {
exposeRatioEnabled.Store(false)
}
func SetExposeRatioEnabled(enabled bool) {
exposeRatioEnabled.Store(enabled)
}
func IsExposeRatioEnabled() bool {
return exposeRatioEnabled.Load()
}
\ No newline at end of file
package ratio_setting
import (
"sync"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
)
const exposedDataTTL = 30 * time.Second
type exposedCache struct {
data gin.H
expiresAt time.Time
}
var (
exposedData atomic.Value
rebuildMu sync.Mutex
)
func InvalidateExposedDataCache() {
exposedData.Store((*exposedCache)(nil))
}
func cloneGinH(src gin.H) gin.H {
dst := make(gin.H, len(src))
for k, v := range src {
dst[k] = v
}
return dst
}
func GetExposedData() gin.H {
if c, ok := exposedData.Load().(*exposedCache); ok && c != nil && time.Now().Before(c.expiresAt) {
return cloneGinH(c.data)
}
rebuildMu.Lock()
defer rebuildMu.Unlock()
if c, ok := exposedData.Load().(*exposedCache); ok && c != nil && time.Now().Before(c.expiresAt) {
return cloneGinH(c.data)
}
newData := gin.H{
"model_ratio": GetModelRatioCopy(),
"completion_ratio": GetCompletionRatioCopy(),
"cache_ratio": GetCacheRatioCopy(),
"model_price": GetModelPriceCopy(),
}
exposedData.Store(&exposedCache{
data: newData,
expiresAt: time.Now().Add(exposedDataTTL),
})
return cloneGinH(newData)
}
\ No newline at end of file
...@@ -317,7 +317,11 @@ func UpdateModelPriceByJSONString(jsonStr string) error { ...@@ -317,7 +317,11 @@ func UpdateModelPriceByJSONString(jsonStr string) error {
modelPriceMapMutex.Lock() modelPriceMapMutex.Lock()
defer modelPriceMapMutex.Unlock() defer modelPriceMapMutex.Unlock()
modelPriceMap = make(map[string]float64) modelPriceMap = make(map[string]float64)
return json.Unmarshal([]byte(jsonStr), &modelPriceMap) err := json.Unmarshal([]byte(jsonStr), &modelPriceMap)
if err == nil {
InvalidateExposedDataCache()
}
return err
} }
// GetModelPrice 返回模型的价格,如果模型不存在则返回-1,false // GetModelPrice 返回模型的价格,如果模型不存在则返回-1,false
...@@ -345,7 +349,11 @@ func UpdateModelRatioByJSONString(jsonStr string) error { ...@@ -345,7 +349,11 @@ func UpdateModelRatioByJSONString(jsonStr string) error {
modelRatioMapMutex.Lock() modelRatioMapMutex.Lock()
defer modelRatioMapMutex.Unlock() defer modelRatioMapMutex.Unlock()
modelRatioMap = make(map[string]float64) modelRatioMap = make(map[string]float64)
return json.Unmarshal([]byte(jsonStr), &modelRatioMap) err := json.Unmarshal([]byte(jsonStr), &modelRatioMap)
if err == nil {
InvalidateExposedDataCache()
}
return err
} }
// 处理带有思考预算的模型名称,方便统一定价 // 处理带有思考预算的模型名称,方便统一定价
...@@ -405,7 +413,11 @@ func UpdateCompletionRatioByJSONString(jsonStr string) error { ...@@ -405,7 +413,11 @@ func UpdateCompletionRatioByJSONString(jsonStr string) error {
CompletionRatioMutex.Lock() CompletionRatioMutex.Lock()
defer CompletionRatioMutex.Unlock() defer CompletionRatioMutex.Unlock()
CompletionRatio = make(map[string]float64) CompletionRatio = make(map[string]float64)
return json.Unmarshal([]byte(jsonStr), &CompletionRatio) err := json.Unmarshal([]byte(jsonStr), &CompletionRatio)
if err == nil {
InvalidateExposedDataCache()
}
return err
} }
func GetCompletionRatio(name string) float64 { func GetCompletionRatio(name string) float64 {
...@@ -609,3 +621,33 @@ func GetImageRatio(name string) (float64, bool) { ...@@ -609,3 +621,33 @@ func GetImageRatio(name string) (float64, bool) {
} }
return ratio, true return ratio, true
} }
func GetModelRatioCopy() map[string]float64 {
modelRatioMapMutex.RLock()
defer modelRatioMapMutex.RUnlock()
copyMap := make(map[string]float64, len(modelRatioMap))
for k, v := range modelRatioMap {
copyMap[k] = v
}
return copyMap
}
func GetModelPriceCopy() map[string]float64 {
modelPriceMapMutex.RLock()
defer modelPriceMapMutex.RUnlock()
copyMap := make(map[string]float64, len(modelPriceMap))
for k, v := range modelPriceMap {
copyMap[k] = v
}
return copyMap
}
func GetCompletionRatioCopy() map[string]float64 {
CompletionRatioMutex.RLock()
defer CompletionRatioMutex.RUnlock()
copyMap := make(map[string]float64, len(CompletionRatio))
for k, v := range CompletionRatio {
copyMap[k] = v
}
return copyMap
}
import React, { useState } from 'react';
import {
Modal,
Transfer,
Input,
Space,
Checkbox,
Avatar,
Highlight,
} from '@douyinfe/semi-ui';
import { IconClose } from '@douyinfe/semi-icons';
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,
visible,
onCancel,
onOk,
allChannels = [],
selectedChannelIds = [],
setSelectedChannelIds,
channelEndpoints,
updateChannelEndpoint,
}) {
const [searchText, setSearchText] = useState('');
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);
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>
</>
);
};
const renderSourceItem = (item) => {
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>
);
};
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 channelFilter = (input, item) => {
const searchLower = input.toLowerCase();
return item.label.toLowerCase().includes(searchLower) ||
(item._originalData?.base_url || '').toLowerCase().includes(searchLower);
};
return (
<Modal
visible={visible}
onCancel={onCancel}
onOk={onOk}
title={<span className="text-lg font-semibold">{t('选择同步渠道')}</span>}
width={1000}
>
<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('无搜索结果'),
}}
/>
</Space>
</Modal>
);
}
\ No newline at end of file
...@@ -6,6 +6,7 @@ import GroupRatioSettings from '../../pages/Setting/Ratio/GroupRatioSettings.js' ...@@ -6,6 +6,7 @@ import GroupRatioSettings from '../../pages/Setting/Ratio/GroupRatioSettings.js'
import ModelRatioSettings from '../../pages/Setting/Ratio/ModelRatioSettings.js'; import ModelRatioSettings from '../../pages/Setting/Ratio/ModelRatioSettings.js';
import ModelSettingsVisualEditor from '../../pages/Setting/Ratio/ModelSettingsVisualEditor.js'; import ModelSettingsVisualEditor from '../../pages/Setting/Ratio/ModelSettingsVisualEditor.js';
import ModelRatioNotSetEditor from '../../pages/Setting/Ratio/ModelRationNotSetEditor.js'; import ModelRatioNotSetEditor from '../../pages/Setting/Ratio/ModelRationNotSetEditor.js';
import UpstreamRatioSync from '../../pages/Setting/Ratio/UpstreamRatioSync.js';
import { API, showError } from '../../helpers'; import { API, showError } from '../../helpers';
...@@ -21,6 +22,7 @@ const RatioSetting = () => { ...@@ -21,6 +22,7 @@ const RatioSetting = () => {
GroupGroupRatio: '', GroupGroupRatio: '',
AutoGroups: '', AutoGroups: '',
DefaultUseAutoGroup: false, DefaultUseAutoGroup: false,
ExposeRatioEnabled: false,
UserUsableGroups: '', UserUsableGroups: '',
}); });
...@@ -48,7 +50,7 @@ const RatioSetting = () => { ...@@ -48,7 +50,7 @@ const RatioSetting = () => {
// 如果后端返回的不是合法 JSON,直接展示 // 如果后端返回的不是合法 JSON,直接展示
} }
} }
if (['DefaultUseAutoGroup'].includes(item.key)) { if (['DefaultUseAutoGroup', 'ExposeRatioEnabled'].includes(item.key)) {
newInputs[item.key] = item.value === 'true' ? true : false; newInputs[item.key] = item.value === 'true' ? true : false;
} else { } else {
newInputs[item.key] = item.value; newInputs[item.key] = item.value;
...@@ -78,10 +80,6 @@ const RatioSetting = () => { ...@@ -78,10 +80,6 @@ const RatioSetting = () => {
return ( return (
<Spin spinning={loading} size='large'> <Spin spinning={loading} size='large'>
{/* 分组倍率设置 */}
<Card style={{ marginTop: '10px' }}>
<GroupRatioSettings options={inputs} refresh={onRefresh} />
</Card>
{/* 模型倍率设置以及可视化编辑器 */} {/* 模型倍率设置以及可视化编辑器 */}
<Card style={{ marginTop: '10px' }}> <Card style={{ marginTop: '10px' }}>
<Tabs type='line'> <Tabs type='line'>
...@@ -100,8 +98,18 @@ const RatioSetting = () => { ...@@ -100,8 +98,18 @@ const RatioSetting = () => {
refresh={onRefresh} refresh={onRefresh}
/> />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab={t('上游倍率同步')} itemKey='upstream_sync'>
<UpstreamRatioSync
options={inputs}
refresh={onRefresh}
/>
</Tabs.TabPane>
</Tabs> </Tabs>
</Card> </Card>
{/* 分组倍率设置 */}
<Card style={{ marginTop: '10px' }}>
<GroupRatioSettings options={inputs} refresh={onRefresh} />
</Card>
</Spin> </Spin>
); );
}; };
......
export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend! export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend!
export const DEFAULT_ENDPOINT = '/api/ratio_config';
\ No newline at end of file
...@@ -1665,5 +1665,28 @@ ...@@ -1665,5 +1665,28 @@
"确定清除所有失效兑换码?": "Are you sure you want to clear all invalid redemption codes?", "确定清除所有失效兑换码?": "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.", "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "This will delete all used, disabled, and expired redemption codes, this operation cannot be undone.",
"选择过期时间(可选,留空为永久)": "Select expiration time (optional, leave blank for permanent)", "选择过期时间(可选,留空为永久)": "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 matching model found"
} }
\ No newline at end of file
...@@ -432,4 +432,72 @@ code { ...@@ -432,4 +432,72 @@ code {
.semi-table-tbody>.semi-table-row { .semi-table-tbody>.semi-table-row {
border-bottom: 1px solid rgba(0, 0, 0, 0.1); border-bottom: 1px solid rgba(0, 0, 0, 0.1);
} }
}
/* ==================== 同步倍率 - 渠道选择器 ==================== */
.components-transfer-source-item,
.components-transfer-selected-item {
display: flex;
align-items: center;
padding: 8px;
}
.semi-transfer-left-list,
.semi-transfer-right-list {
-ms-overflow-style: none;
scrollbar-width: none;
}
.semi-transfer-left-list::-webkit-scrollbar,
.semi-transfer-right-list::-webkit-scrollbar {
display: none;
}
.components-transfer-source-item .semi-checkbox,
.components-transfer-selected-item .semi-checkbox {
display: flex;
align-items: center;
width: 100%;
}
.components-transfer-source-item .semi-avatar,
.components-transfer-selected-item .semi-avatar {
margin-right: 12px;
flex-shrink: 0;
}
.components-transfer-source-item .info,
.components-transfer-selected-item .info {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
}
.components-transfer-source-item .name,
.components-transfer-selected-item .name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.components-transfer-source-item .email,
.components-transfer-selected-item .email {
font-size: 12px;
color: var(--semi-color-text-2);
display: flex;
align-items: center;
}
.components-transfer-selected-item .semi-icon-close {
margin-left: 8px;
cursor: pointer;
color: var(--semi-color-text-2);
}
.components-transfer-selected-item .semi-icon-close:hover {
color: var(--semi-color-text-0);
} }
\ No newline at end of file
...@@ -1112,7 +1112,6 @@ const Detail = (props) => { ...@@ -1112,7 +1112,6 @@ const Detail = (props) => {
</div> </div>
<Tabs <Tabs
type="button" type="button"
preventScroll={true}
activeKey={activeChartTab} activeKey={activeChartTab}
onChange={setActiveChartTab} onChange={setActiveChartTab}
> >
...@@ -1389,7 +1388,6 @@ const Detail = (props) => { ...@@ -1389,7 +1388,6 @@ const Detail = (props) => {
) : ( ) : (
<Tabs <Tabs
type="card" type="card"
preventScroll={true}
collapsible collapsible
activeKey={activeUptimeTab} activeKey={activeUptimeTab}
onChange={setActiveUptimeTab} onChange={setActiveUptimeTab}
......
...@@ -25,6 +25,7 @@ export default function ModelRatioSettings(props) { ...@@ -25,6 +25,7 @@ export default function ModelRatioSettings(props) {
ModelRatio: '', ModelRatio: '',
CacheRatio: '', CacheRatio: '',
CompletionRatio: '', CompletionRatio: '',
ExposeRatioEnabled: false,
}); });
const refForm = useRef(); const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs); const [inputsRow, setInputsRow] = useState(inputs);
...@@ -206,6 +207,17 @@ export default function ModelRatioSettings(props) { ...@@ -206,6 +207,17 @@ export default function ModelRatioSettings(props) {
/> />
</Col> </Col>
</Row> </Row>
<Row gutter={16}>
<Col span={16}>
<Form.Switch
label={t('暴露倍率接口')}
field={'ExposeRatioEnabled'}
onChange={(value) =>
setInputs({ ...inputs, ExposeRatioEnabled: value })
}
/>
</Col>
</Row>
</Form.Section> </Form.Section>
</Form> </Form>
<Space> <Space>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment