@@ -37,13 +37,19 @@ import {
AlertTriangle ,
CheckCircle ,
} from 'lucide-react' ;
import { API , showError , showSuccess , showWarning , stringToColor } from '../../../helpers' ;
import {
API ,
showError ,
showSuccess ,
showWarning ,
stringToColor ,
} from '../../../helpers' ;
import { useIsMobile } from '../../../hooks/common/useIsMobile' ;
import { DEFAULT _ENDPOINT } from '../../../constants' ;
import { useTranslation } from 'react-i18next' ;
import {
IllustrationNoResult ,
IllustrationNoResultDark
IllustrationNoResultDark ,
} from '@douyinfe/semi-illustrations' ;
import ChannelSelectorModal from '../../../components/settings/ChannelSelectorModal' ;
@@ -72,7 +78,12 @@ function ConflictConfirmModal({ t, visible, items, onOk, onCancel }) {
onOk = { onOk }
size = { isMobile ? 'full-width' : 'large' }
>
< Table columns = { columns } dataSource = { items } pagination = { false } size = "small" / >
< Table
columns = { columns }
dataSource = { items }
pagination = { false }
size = 'small'
/ >
< / Modal >
) ;
}
@@ -126,7 +137,7 @@ export default function UpstreamRatioSync(props) {
if ( res . data . success ) {
const channels = res . data . data || [ ] ;
const transferData = channels . map ( channel => ( {
const transferData = channels . map ( ( channel ) => ( {
key : channel . id ,
label : channel . name ,
value : channel . id ,
@@ -137,9 +148,9 @@ export default function UpstreamRatioSync(props) {
setAllChannels ( transferData ) ;
// 合并已有 endpoints, 避免每次打开弹窗都重置
setChannelEndpoints ( prev => {
setChannelEndpoints ( ( prev ) => {
const merged = { ... prev } ;
transferData . forEach ( channel => {
transferData . forEach ( ( channel ) => {
if ( ! merged [ channel . key ] ) {
merged [ channel . key ] = DEFAULT _ENDPOINT ;
}
@@ -158,8 +169,8 @@ export default function UpstreamRatioSync(props) {
const confirmChannelSelection = ( ) => {
const selected = allChannels
. filter ( ch => selectedChannelIds . includes ( ch . value ) )
. map ( ch => ch . _originalData ) ;
. filter ( ( ch ) => selectedChannelIds . includes ( ch . value ) )
. map ( ( ch ) => ch . _originalData ) ;
if ( selected . length === 0 ) {
showWarning ( t ( '请至少选择一个渠道' ) ) ;
@@ -173,7 +184,7 @@ export default function UpstreamRatioSync(props) {
const fetchRatiosFromChannels = async ( channelList ) => {
setSyncLoading ( true ) ;
const upstreams = channelList . map ( ch => ( {
const upstreams = channelList . map ( ( ch ) => ( {
id : ch . id ,
name : ch . name ,
base _url : ch . base _url ,
@@ -196,9 +207,12 @@ export default function UpstreamRatioSync(props) {
const { differences = { } , test _results = [ ] } = res . data . data ;
const errorResults = test _results . filter ( r => r . status === 'error' ) ;
const errorResults = test _results . filter ( ( r ) => r . status === 'error' ) ;
if ( errorResults . length > 0 ) {
showWarning ( t ( '部分渠道测试失败:' ) + errorResults . map ( r => ` ${ r . name } : ${ r . error } ` ) . join ( ', ' ) ) ;
showWarning (
t ( '部分渠道测试失败:' ) +
errorResults . map ( ( r ) => ` ${ r . name } : ${ r . error } ` ) . join ( ', ' ) ,
) ;
}
setDifferences ( differences ) ;
@@ -219,26 +233,29 @@ export default function UpstreamRatioSync(props) {
return ratioType === 'model_price' ? 'price' : 'ratio' ;
}
const selectValue = useCallback ( ( model , ratioType , value ) => {
const category = getBillingCategory ( ratioType ) ;
const selectValue = useCallback (
( model , ratioType , value ) => {
const category = getBillingCategory ( ratioType ) ;
setResolutions ( prev => {
const newModelRes = { ... ( prev [ model ] || { } ) } ;
setResolutions ( ( prev ) => {
const newModelRes = { ... ( prev [ model ] || { } ) } ;
Object . keys ( newModelRes ) . forEach ( ( rt ) => {
if ( getBillingCategory ( rt ) !== category ) {
delete newModelRes [ rt ] ;
}
Object . keys ( newModelRes ) . forEach ( ( rt ) => {
if ( getBillingCategory ( rt ) !== category ) {
delete newModelRes [ rt ] ;
}
} ) ;
newModelRes [ ratioType ] = value ;
return {
... prev ,
[ model ] : newModelRes ,
} ;
} ) ;
newModelRes [ ratioType ] = value ;
return {
... prev ,
[ model ] : newModelRes ,
} ;
} ) ;
} , [ setResolutions ] ) ;
} ,
[ setResolutions ] ,
) ;
const applySync = async ( ) => {
const currentRatios = {
@@ -252,9 +269,12 @@ export default function UpstreamRatioSync(props) {
const getLocalBillingCategory = ( model ) => {
if ( currentRatios . ModelPrice [ model ] !== undefined ) return 'price' ;
if ( currentRatios . ModelRatio [ model ] !== undefined ||
if (
currentRatios . ModelRatio [ model ] !== undefined ||
currentRatios . CompletionRatio [ model ] !== undefined ||
currentRatios . CacheRatio [ model ] !== undefined ) return 'ratio' ;
currentRatios . CacheRatio [ model ] !== undefined
)
return 'ratio' ;
return null ;
} ;
@@ -272,9 +292,10 @@ export default function UpstreamRatioSync(props) {
const newCat = 'model_price' in ratios ? 'price' : 'ratio' ;
if ( localCat && localCat !== newCat ) {
const currentDesc = localCat === 'price'
? ` ${ t ( '固定价格' ) } : ${ currentRatios . ModelPrice [ model ] } `
: ` ${ t ( '模型倍率 ' ) } : ${ currentRatios . ModelRatio [ model ] ? ? '-' } \ n ${ t ( '补全倍率' ) } : ${ currentRatios . CompletionRatio [ model ] ? ? '-' } ` ;
const currentDesc =
localCat === 'price'
? ` ${ t ( '固定价格 ' ) } : ${ currentRatios . ModelPrice [ model ] } `
: ` ${ t ( '模型倍率' ) } : ${ currentRatios . ModelRatio [ model ] ? ? '-' } \ n ${ t ( '补全倍率' ) } : ${ currentRatios . CompletionRatio [ model ] ? ? '-' } ` ;
let newDesc = '' ;
if ( newCat === 'price' ) {
@@ -308,80 +329,83 @@ export default function UpstreamRatioSync(props) {
await performSync ( currentRatios ) ;
} ;
const performSync = useCallback ( async ( currentRatios ) => {
const finalRatios = {
ModelRatio : { ... currentRatios . Mode lRatio } ,
Completion Ratio: { ... currentRatios . Completion Ratio } ,
Cache Ratio: { ... currentRatios . Cache Ratio } ,
ModelPrice : { ... currentRatios . ModelPrice } ,
} ;
const performSync = useCallback (
async ( currentRatios ) => {
const fina lRatios = {
Model Ratio : { ... currentRatios . Model Ratio } ,
Completion Ratio : { ... currentRatios . Completion Ratio } ,
CacheRatio : { ... currentRatios . CacheRatio } ,
ModelPrice : { ... currentRatios . ModelPrice } ,
} ;
Object . entries ( resolutions ) . forEach ( ( [ model , ratios ] ) => {
const selectedTypes = Object . keys ( ratios ) ;
const hasPrice = selectedTypes . includes ( 'model_price' ) ;
const hasRatio = selectedTypes . some ( rt => rt !== 'model_price' ) ;
Object . entries ( resolutions ) . forEach ( ( [ model , ratios ] ) => {
const selectedTypes = Object . keys ( ratios ) ;
const hasPrice = selectedTypes . includes ( 'model_price' ) ;
const hasRatio = selectedTypes . some ( ( rt ) => rt !== 'model_price' ) ;
if ( hasPrice ) {
delete finalRatios . ModelRatio [ model ] ;
delete finalRatios . CompletionRatio [ model ] ;
delete finalRatios . CacheRatio [ model ] ;
}
if ( hasRatio ) {
delete finalRatios . ModelPrice [ model ] ;
}
if ( hasPrice ) {
delete finalRatios . ModelRatio [ model ] ;
delete finalRatios . CompletionRatio [ model ] ;
delete finalRatios . CacheRatio [ model ] ;
}
if ( hasRatio ) {
delete finalRatios . ModelPrice [ model ] ;
}
Object . entries ( ratios ) . forEach ( ( [ ratioType , value ] ) => {
const optionKey = ratioType
. split ( '_' )
. map ( word => word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 ) )
. join ( '' ) ;
finalRatios [ optionKey ] [ model ] = parseFloat ( value ) ;
Object . entries ( ratios ) . forEach ( ( [ ratioType , value ] ) => {
const optionKey = ratioType
. split ( '_' )
. map ( ( word ) => word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 ) )
. join ( '' ) ;
finalRatios [ optionKey ] [ model ] = parseFloat ( value ) ;
} ) ;
} ) ;
} ) ;
setLoading ( true ) ;
try {
const updates = Object . entries ( finalRatios ) . map ( ( [ key , value ] ) =>
API . put ( '/api/option/' , {
key ,
value : JSON . stringify ( value , null , 2 ) ,
} )
) ;
setLoading ( true ) ;
try {
const updates = Object . entries ( finalRatios ) . map ( ( [ key , value ] ) =>
API . put ( '/api/option/' , {
key ,
value : JSON . stringify ( value , null , 2 ) ,
} ) ,
) ;
const results = await Promise . all ( updates ) ;
const results = await Promise . all ( updates ) ;
if ( results . every ( res => res . data . success ) ) {
showSuccess ( t ( '同步成功' ) ) ;
props . refresh ( ) ;
if ( results . every ( ( res ) => res . data . success ) ) {
showSuccess ( t ( '同步成功' ) ) ;
props . refresh ( ) ;
setDifferences ( prevDifferences => {
const newDifferences = { ... prevDifferences } ;
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 ] ;
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 ] ;
if ( Object . keys ( newDifferences [ model ] ) . length === 0 ) {
delete newDifferences [ model ] ;
}
}
}
} ) ;
} ) ;
return newDifferences ;
} ) ;
return newDifferences ;
} ) ;
setResolutions ( { } ) ;
} else {
showError ( t ( '部分 保存失败' ) ) ;
setResolutions ( { } ) ;
} else {
showError ( t ( '部分保存失败' ) ) ;
}
} catch ( error ) {
showError ( t ( '保存失败' ) ) ;
} finally {
setLoading ( false ) ;
}
} catch ( error ) {
showError ( t ( '保存失败' ) ) ;
} finally {
setLoading ( false ) ;
}
} , [ resolutions , props . options , props . refresh ] ) ;
} ,
[ resolutions , props . options , props . refresh ] ,
) ;
const getCurrentPageData = ( dataSource ) => {
const startIndex = ( currentPage - 1 ) * pageSize ;
@@ -390,12 +414,12 @@ 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 flex-col md:flex-row gap-2 w-full md:w-auto order-2 md:order-1" >
< 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 flex-col md:flex-row gap-2 w-full md:w-auto order-2 md:order-1' >
< Button
icon = { < RefreshCcw size = { 14 } / > }
className = " w-full md:w-auto mt-2"
className = ' w-full md:w-auto mt-2'
onClick = { ( ) => {
setModalVisible ( true ) ;
if ( allChannels . length === 0 ) {
@@ -415,20 +439,20 @@ export default function UpstreamRatioSync(props) {
type = 'secondary'
onClick = { applySync }
disabled = { ! hasSelections }
className = " w-full md:w-auto mt-2"
className = ' w-full md:w-auto mt-2'
>
{ t ( '应用同步' ) }
< / Button >
) ;
} ) ( ) }
< div className = " flex flex-col sm:flex-row gap-2 w-full md:w-auto mt-2" >
< 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 = " w-full sm:w-64"
className = ' w-full sm:w-64'
showClear
/ >
@@ -436,14 +460,16 @@ export default function UpstreamRatioSync(props) {
placeholder = { t ( '按倍率类型筛选' ) }
value = { ratioTypeFilter }
onChange = { setRatioTypeFilter }
className = " w-full sm:w-48"
className = ' 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.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 >
@@ -457,7 +483,11 @@ export default function UpstreamRatioSync(props) {
Object . entries ( differences ) . forEach ( ( [ model , ratioTypes ] ) => {
const hasPrice = 'model_price' in ratioTypes ;
const hasOtherRatio = [ 'model_ratio' , 'completion_ratio' , 'cache_ratio' ] . some ( rt => rt in ratioTypes ) ;
const hasOtherRatio = [
'model_ratio' ,
'completion_ratio' ,
'cache_ratio' ,
] . some ( ( rt ) => rt in ratioTypes ) ;
const billingConflict = hasPrice && hasOtherRatio ;
Object . entries ( ratioTypes ) . forEach ( ( [ ratioType , diff ] ) => {
@@ -481,12 +511,13 @@ export default function UpstreamRatioSync(props) {
return dataSource ;
}
return dataSource . filter ( item => {
const matchesKeyword = ! searchKeyword . trim ( ) ||
return dataSource . filter ( ( item ) => {
const matchesKeyword =
! searchKeyword . trim ( ) ||
item . model . toLowerCase ( ) . includes ( searchKeyword . toLowerCase ( ) . trim ( ) ) ;
const matchesRatioType = ! ratioTypeFilter ||
item . ratioType === ratioTypeFilter ;
const matchesRatioType =
! ratioTypeFilter || item . ratioType === ratioTypeFilter ;
return matchesKeyword && matchesRatioType ;
} ) ;
@@ -504,13 +535,17 @@ export default function UpstreamRatioSync(props) {
return (
< Empty
image = { < IllustrationNoResult style = { { width : 150 , height : 150 } } / > }
darkModeImage = { < IllustrationNoResultDark style = { { width : 150 , height : 150 } } / > }
darkModeImage = {
< IllustrationNoResultDark style = { { width : 150 , height : 150 } } / >
}
description = {
searchKeyword . trim ( )
? t ( '未找到匹配的模型' )
: ( Object . keys ( differences ) . length === 0 ?
( hasSynced ? t ( '暂无差异化倍率显示' ) : t ( '请先选择同步渠道' ) )
: t ( '请先选择同步渠道' ) )
: Object . keys ( differences ) . length === 0
? hasSynced
? t ( '暂无差异化倍率显示' )
: t ( '请先选择同步渠道' )
: t ( '请先选择同步渠道' )
}
style = { { padding : 30 } }
/ >
@@ -533,13 +568,22 @@ export default function UpstreamRatioSync(props) {
cache _ratio : t ( '缓存倍率' ) ,
model _price : t ( '固定价格' ) ,
} ;
const baseTag = < Tag color = { stringToColor ( text ) } shape = "circle" > { typeMap [ text ] || text } < / Tag > ;
const baseTag = (
< Tag color = { stringToColor ( text ) } shape = 'circle' >
{ typeMap [ text ] || text }
< / Tag >
) ;
if ( record ? . billingConflict ) {
return (
< div className = " flex items-center gap-1" >
< div className = ' flex items-center gap-1' >
{ baseTag }
< Tooltip position = "top" content = { t ( '该模型存在固定价格与倍率计费方式冲突,请确认选择' ) } >
< AlertTriangle size = { 14 } className = "text-yellow-500" / >
< Tooltip
position = 'top'
content = { t (
'该模型存在固定价格与倍率计费方式冲突,请确认选择' ,
) }
>
< AlertTriangle size = { 14 } className = 'text-yellow-500' / >
< / Tooltip >
< / div >
) ;
@@ -551,12 +595,19 @@ export default function UpstreamRatioSync(props) {
title : t ( '置信度' ) ,
dataIndex : 'confidence' ,
render : ( _ , record ) => {
const allConfident = Object . values ( record . confidence || { } ) . every ( v => v !== false ) ;
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 } / > } >
< Tag
color = 'green'
shape = 'circle'
type = 'light'
prefixIcon = { < CheckCircle size = { 14 } / > }
>
{ t ( '可信' ) }
< / Tag >
< / Tooltip >
@@ -568,8 +619,15 @@ export default function UpstreamRatioSync(props) {
. join ( ', ' ) ;
return (
< Tooltip content = { t ( '以下上游数据可能不可信:' ) + untrustedSources } >
< Tag color = "yellow" shape = "circle" type = "light" prefixIcon = { < AlertTriangle size = { 14 } / > } >
< Tooltip
content = { t ( '以下上游数据可能不可信:' ) + untrustedSources }
>
< Tag
color = 'yellow'
shape = 'circle'
type = 'light'
prefixIcon = { < AlertTriangle size = { 14 } / > }
>
{ t ( '谨慎' ) }
< / Tag >
< / Tooltip >
@@ -581,7 +639,10 @@ export default function UpstreamRatioSync(props) {
title : t ( '当前值' ) ,
dataIndex : 'current' ,
render : ( text ) => (
< Tag color = { text !== null && text !== undefined ? 'blue' : 'default' } shape = "circle" >
< Tag
color = { text !== null && text !== undefined ? 'blue' : 'default' }
shape = 'circle'
>
{ text !== null && text !== undefined ? text : t ( '未设置' ) }
< / Tag >
) ,
@@ -593,9 +654,14 @@ export default function UpstreamRatioSync(props) {
filteredDataSource . forEach ( ( row ) => {
const upstreamVal = row . upstreams ? . [ upName ] ;
if ( upstreamVal !== null && upstreamVal !== undefined && upstreamVal !== 'same' ) {
if (
upstreamVal !== null &&
upstreamVal !== undefined &&
upstreamVal !== 'same'
) {
selectableCount ++ ;
const isSelected = resolutions [ row . model ] ? . [ row . ratioType ] === upstreamVal ;
const isSelected =
resolutions [ row . model ] ? . [ row . ratioType ] === upstreamVal ;
if ( isSelected ) {
selectedCount ++ ;
}
@@ -605,9 +671,11 @@ export default function UpstreamRatioSync(props) {
return {
selectableCount ,
selectedCount ,
allSelected : selectableCount > 0 && selectedCount === selectableCount ,
partiallySelected : selected Count > 0 && selectedCount < selectableCount ,
hasSelectableItems : selectableCount > 0
allSelected :
selectabl eCount > 0 && selectedCount === selectableCount ,
partiallySelected :
selectedCount > 0 && selectedCount < selectableCount ,
hasSelectableItems : selectableCount > 0 ,
} ;
} ) ( ) ;
@@ -615,7 +683,11 @@ export default function UpstreamRatioSync(props) {
if ( checked ) {
filteredDataSource . forEach ( ( row ) => {
const upstreamVal = row . upstreams ? . [ upName ] ;
if ( upstreamVal !== null && upstreamVal !== undefined && upstreamVal !== 'same' ) {
if (
upstreamVal !== null &&
upstreamVal !== undefined &&
upstreamVal !== 'same'
) {
selectValue ( row . model , row . ratioType , upstreamVal ) ;
}
} ) ;
@@ -653,17 +725,26 @@ export default function UpstreamRatioSync(props) {
const isConfident = record . confidence ? . [ upName ] !== false ;
if ( upstreamVal === null || upstreamVal === undefined ) {
return < Tag color = "default" shape = "circle" > { t ( '未设置' ) } < / Tag > ;
return (
< Tag color = 'default' shape = 'circle' >
{ t ( '未设置' ) }
< / Tag >
) ;
}
if ( upstreamVal === 'same' ) {
return < Tag color = "blue" shape = "circle" > { t ( '与本地相同' ) } < / Tag > ;
return (
< Tag color = 'blue' shape = 'circle' >
{ t ( '与本地相同' ) }
< / Tag >
) ;
}
const isSelected = resolutions [ record . model ] ? . [ record . ratioType ] === upstreamVal ;
const isSelected =
resolutions [ record . model ] ? . [ record . ratioType ] === upstreamVal ;
return (
< div className = " flex items-center gap-2" >
< div className = ' flex items-center gap-2' >
< Checkbox
checked = { isSelected }
onChange = { ( e ) => {
@@ -687,8 +768,11 @@ export default function UpstreamRatioSync(props) {
{ upstreamVal }
< / Checkbox >
{ ! isConfident && (
< Tooltip position = 'left' content = { t ( '该数据可能不可信,请谨慎使用' ) } >
< AlertTriangle size = { 16 } className = "text-yellow-500" / >
< Tooltip
position = 'left'
content = { t ( '该数据可能不可信,请谨慎使用' ) }
>
< AlertTriangle size = { 16 } className = 'text-yellow-500' / >
< / Tooltip >
) }
< / div >
@@ -716,7 +800,7 @@ export default function UpstreamRatioSync(props) {
onShowSizeChange : ( current , size ) => {
setCurrentPage ( 1 ) ;
setPageSize ( size ) ;
}
} ,
} }
scroll = { { x : 'max-content' } }
size = 'middle'
@@ -726,7 +810,7 @@ export default function UpstreamRatioSync(props) {
} ;
const updateChannelEndpoint = useCallback ( ( channelId , endpoint ) => {
setChannelEndpoints ( prev => ( { ... prev , [ channelId ] : endpoint } ) ) ;
setChannelEndpoints ( ( prev ) => ( { ... prev , [ channelId ] : endpoint } ) ) ;
} , [ ] ) ;
const handleModalClose = ( ) => {
@@ -773,4 +857,4 @@ export default function UpstreamRatioSync(props) {
/ >
< / >
) ;
}
}