/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useEffect, useState, useRef } from 'react'; import { Table, Button, Input, Modal, Form, Space, Typography, RadioGroup, Radio, Notification, } from '@douyinfe/semi-ui'; import { IconDelete, IconPlus, IconSearch, IconSave, IconEdit, IconBolt, } from '@douyinfe/semi-icons'; import { API, showError, showSuccess } from '../../../helpers'; import { useTranslation } from 'react-i18next'; export default function ModelRatioNotSetEditor(props) { const { t } = useTranslation(); const [models, setModels] = useState([]); const [visible, setVisible] = useState(false); const [batchVisible, setBatchVisible] = useState(false); const [currentModel, setCurrentModel] = useState(null); const [searchText, setSearchText] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [loading, setLoading] = useState(false); const [enabledModels, setEnabledModels] = useState([]); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [batchFillType, setBatchFillType] = useState('ratio'); const [batchFillValue, setBatchFillValue] = useState(''); const [batchRatioValue, setBatchRatioValue] = useState(''); const [batchCompletionRatioValue, setBatchCompletionRatioValue] = useState(''); const addFormRef = useRef(null); const editFormRef = useRef(null); const [editVisible, setEditVisible] = useState(false); const [editRecord, setEditRecord] = useState(null); const [pricingMode, setPricingMode] = useState('per-token'); const [pricingSubMode, setPricingSubMode] = useState('ratio'); const { Text } = Typography; // 定义可选的每页显示条数 const pageSizeOptions = [10, 20, 50, 100]; const getAllEnabledModels = async () => { try { const res = await API.get('/api/channel/models_enabled'); const { success, message, data } = res.data; if (success) { setEnabledModels(data); } else { showError(message); } } catch (error) { console.error(t('获取启用模型失败:'), error); showError(t('获取启用模型失败')); } }; useEffect(() => { // 获取所有启用的模型 getAllEnabledModels(); }, []); useEffect(() => { try { const modelPrice = JSON.parse(props.options.ModelPrice || '{}'); const modelRatio = JSON.parse(props.options.ModelRatio || '{}'); const completionRatio = JSON.parse(props.options.CompletionRatio || '{}'); const cacheRatio = JSON.parse(props.options.CacheRatio || '{}'); const createCacheRatio = JSON.parse(props.options.CreateCacheRatio || '{}'); const imageRatio = JSON.parse(props.options.ImageRatio || '{}'); const audioRatio = JSON.parse(props.options.AudioRatio || '{}'); const audioCompletionRatio = JSON.parse(props.options.AudioCompletionRatio || '{}'); // 找出所有未设置价格和倍率的模型 const unsetModels = enabledModels.filter((modelName) => { const hasPrice = modelPrice[modelName] !== undefined; const hasRatio = modelRatio[modelName] !== undefined; // 如果模型没有价格或者没有倍率设置,则显示 return !hasPrice && !hasRatio; }); // 创建模型数据 const modelData = unsetModels.map((name) => ({ name, price: modelPrice[name] || '', ratio: modelRatio[name] || '', completionRatio: completionRatio[name] || '', cacheRatio: cacheRatio[name] ?? '', cacheCreationRatio: createCacheRatio[name] ?? '', imageRatio: imageRatio[name] ?? '', audioRatio: audioRatio[name] ?? '', audioCompletionRatio: audioCompletionRatio[name] ?? '', })); setModels(modelData); // 清空选择 setSelectedRowKeys([]); } catch (error) { console.error(t('JSON解析错误:'), error); } }, [props.options, enabledModels]); // 首先声明分页相关的工具函数 const getPagedData = (data, currentPage, pageSize) => { const start = (currentPage - 1) * pageSize; const end = start + pageSize; return data.slice(start, end); }; // 处理页面大小变化 const handlePageSizeChange = (size) => { setPageSize(size); // 重新计算当前页,避免数据丢失 const totalPages = Math.ceil(filteredModels.length / size); if (currentPage > totalPages) { setCurrentPage(totalPages || 1); } }; // 在 return 语句之前,先处理过滤和分页逻辑 const filteredModels = models.filter((model) => searchText ? model.name.includes(searchText) : true, ); // 然后基于过滤后的数据计算分页数据 const pagedData = getPagedData(filteredModels, currentPage, pageSize); const SubmitData = async () => { setLoading(true); const output = { ModelPrice: JSON.parse(props.options.ModelPrice || '{}'), ModelRatio: JSON.parse(props.options.ModelRatio || '{}'), CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}'), CacheRatio: JSON.parse(props.options.CacheRatio || '{}'), CreateCacheRatio: JSON.parse(props.options.CreateCacheRatio || '{}'), ImageRatio: JSON.parse(props.options.ImageRatio || '{}'), AudioRatio: JSON.parse(props.options.AudioRatio || '{}'), AudioCompletionRatio: JSON.parse(props.options.AudioCompletionRatio || '{}'), }; try { // 数据转换 - 只处理已修改的模型 models.forEach((model) => { // 只有当用户设置了值时才更新 if (model.price !== '') { // 如果价格不为空,则转换为浮点数,忽略倍率参数 output.ModelPrice[model.name] = parseFloat(model.price); } else { if (model.ratio !== '') output.ModelRatio[model.name] = parseFloat(model.ratio); if (model.completionRatio !== '') output.CompletionRatio[model.name] = parseFloat( model.completionRatio, ); } // 高级倍率字段(0 是有效值) if (model.cacheRatio !== '' && model.cacheRatio != null) output.CacheRatio[model.name] = parseFloat(model.cacheRatio); if (model.cacheCreationRatio !== '' && model.cacheCreationRatio != null) output.CreateCacheRatio[model.name] = parseFloat(model.cacheCreationRatio); if (model.imageRatio !== '' && model.imageRatio != null) output.ImageRatio[model.name] = parseFloat(model.imageRatio); if (model.audioRatio !== '' && model.audioRatio != null) output.AudioRatio[model.name] = parseFloat(model.audioRatio); if (model.audioCompletionRatio !== '' && model.audioCompletionRatio != null) output.AudioCompletionRatio[model.name] = parseFloat(model.audioCompletionRatio); }); // 准备API请求数组 const finalOutput = { ModelPrice: JSON.stringify(output.ModelPrice, null, 2), ModelRatio: JSON.stringify(output.ModelRatio, null, 2), CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2), CacheRatio: JSON.stringify(output.CacheRatio, null, 2), CreateCacheRatio: JSON.stringify(output.CreateCacheRatio, null, 2), ImageRatio: JSON.stringify(output.ImageRatio, null, 2), AudioRatio: JSON.stringify(output.AudioRatio, null, 2), AudioCompletionRatio: JSON.stringify(output.AudioCompletionRatio, null, 2), }; const requestQueue = Object.entries(finalOutput).map(([key, value]) => { return API.put('/api/option/', { key, value, }); }); // 批量处理请求 const results = await Promise.all(requestQueue); // 验证结果 if (requestQueue.length === 1) { if (results.includes(undefined)) return; } else if (requestQueue.length > 1) { if (results.includes(undefined)) { return showError(t('部分保存失败,请重试')); } } // 检查每个请求的结果 for (const res of results) { if (!res.data.success) { return showError(res.data.message); } } showSuccess(t('保存成功')); props.refresh(); // 重新获取未设置的模型 getAllEnabledModels(); } catch (error) { console.error(t('保存失败:'), error); showError(t('保存失败,请重试')); } finally { setLoading(false); } }; const columns = [ { title: t('模型名称'), dataIndex: 'name', key: 'name', }, { title: t('模型固定价格'), dataIndex: 'price', key: 'price', render: (text, record) => ( updateModel(record.name, 'price', value)} /> ), }, { title: t('模型倍率'), dataIndex: 'ratio', key: 'ratio', render: (text, record) => ( updateModel(record.name, 'ratio', value)} /> ), }, { title: t('补全倍率'), dataIndex: 'completionRatio', key: 'completionRatio', render: (text, record) => ( updateModel(record.name, 'completionRatio', value) } /> ), }, { title: t('操作'), key: 'action', render: (_, record) => ( } onClick={() => openEditModal(record)} /> ), }, ]; const updateModel = (name, field, value) => { if (value !== '' && isNaN(value)) { showError(t('请输入数字')); return; } setModels((prev) => prev.map((model) => model.name === name ? { ...model, [field]: value } : model, ), ); }; const addModel = (values) => { // 检查模型名称是否存在, 如果存在则拒绝添加 if (models.some((model) => model.name === values.name)) { showError(t('模型名称已存在')); return; } // 从表单读取高级倍率字段 let advancedValues = {}; if (addFormRef.current) { const formValues = addFormRef.current.getValues(); advancedValues = { cacheRatio: formValues.cacheRatio != null ? formValues.cacheRatio : '', cacheCreationRatio: formValues.cacheCreationRatio != null ? formValues.cacheCreationRatio : '', imageRatio: formValues.imageRatio != null ? formValues.imageRatio : '', audioRatio: formValues.audioRatio != null ? formValues.audioRatio : '', audioCompletionRatio: formValues.audioCompletionRatio != null ? formValues.audioCompletionRatio : '', }; } setModels((prev) => [ { name: values.name, price: values.price || '', ratio: values.ratio || '', completionRatio: values.completionRatio || '', cacheRatio: advancedValues.cacheRatio ?? '', cacheCreationRatio: advancedValues.cacheCreationRatio ?? '', imageRatio: advancedValues.imageRatio ?? '', audioRatio: advancedValues.audioRatio ?? '', audioCompletionRatio: advancedValues.audioCompletionRatio ?? '', }, ...prev, ]); setVisible(false); showSuccess(t('添加成功')); }; // 批量填充功能 const handleBatchFill = () => { if (selectedRowKeys.length === 0) { showError(t('请先选择需要批量设置的模型')); return; } if (batchFillType === 'bothRatio') { if (batchRatioValue === '' || batchCompletionRatioValue === '') { showError(t('请输入模型倍率和补全倍率')); return; } if (isNaN(batchRatioValue) || isNaN(batchCompletionRatioValue)) { showError(t('请输入有效的数字')); return; } } else { if (batchFillValue === '') { showError(t('请输入填充值')); return; } if (isNaN(batchFillValue)) { showError(t('请输入有效的数字')); return; } } // 根据选择的类型批量更新模型 setModels((prev) => prev.map((model) => { if (selectedRowKeys.includes(model.name)) { if (batchFillType === 'price') { return { ...model, price: batchFillValue, ratio: '', completionRatio: '', }; } else if (batchFillType === 'ratio') { return { ...model, price: '', ratio: batchFillValue, }; } else if (batchFillType === 'completionRatio') { return { ...model, price: '', completionRatio: batchFillValue, }; } else if (batchFillType === 'bothRatio') { return { ...model, price: '', ratio: batchRatioValue, completionRatio: batchCompletionRatioValue, }; } } return model; }), ); setBatchVisible(false); Notification.success({ title: t('批量设置成功'), content: t('已为 {{count}} 个模型设置{{type}}', { count: selectedRowKeys.length, type: batchFillType === 'price' ? t('固定价格') : batchFillType === 'ratio' ? t('模型倍率') : batchFillType === 'completionRatio' ? t('补全倍率') : t('模型倍率和补全倍率'), }), duration: 3, }); }; const handleBatchTypeChange = (value) => { setBatchFillType(value); // 切换类型时清空对应的值 if (value !== 'bothRatio') { setBatchFillValue(''); } else { setBatchRatioValue(''); setBatchCompletionRatioValue(''); } }; const calculateTokenPriceFromRatio = (ratio) => { return ratio * 2; }; const openEditModal = (record) => { const initialPricingMode = record.price !== '' ? 'per-request' : 'per-token'; const initialPricingSubMode = 'ratio'; setPricingMode(initialPricingMode); setPricingSubMode(initialPricingSubMode); const modelCopy = { ...record }; // 计算tokenPrice(从ratio反推) if (record.ratio) { modelCopy.tokenPrice = calculateTokenPriceFromRatio( parseFloat(record.ratio), ).toString(); if (record.completionRatio) { modelCopy.completionTokenPrice = ( parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio) ).toString(); } } setEditRecord(modelCopy); setEditVisible(true); // 使用 setTimeout 确保表单渲染后再设置值 setTimeout(() => { if (editFormRef.current) { const formValues = {}; if (initialPricingMode === 'per-request') { formValues.priceInput = modelCopy.price; } else if (initialPricingMode === 'per-token') { formValues.ratioInput = modelCopy.ratio; formValues.completionRatioInput = modelCopy.completionRatio; formValues.modelTokenPrice = modelCopy.tokenPrice; formValues.completionTokenPrice = modelCopy.completionTokenPrice; } formValues.cacheRatio = modelCopy.cacheRatio; formValues.cacheCreationRatio = modelCopy.cacheCreationRatio; formValues.imageRatio = modelCopy.imageRatio; formValues.audioRatio = modelCopy.audioRatio; formValues.audioCompletionRatio = modelCopy.audioCompletionRatio; editFormRef.current.setValues(formValues); } }, 0); }; const rowSelection = { selectedRowKeys, onChange: (selectedKeys) => { setSelectedRowKeys(selectedKeys); }, }; return ( <> } onClick={() => setVisible(true)}> {t('添加模型')} } type='secondary' onClick={() => setBatchVisible(true)} disabled={selectedRowKeys.length === 0} > {t('批量设置')} ({selectedRowKeys.length}) } onClick={SubmitData} loading={loading} > {t('应用更改')} } placeholder={t('搜索模型名称')} value={searchText} onChange={(value) => { setSearchText(value); setCurrentPage(1); }} style={{ width: 200 }} /> {t('此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除')} setCurrentPage(page), onPageSizeChange: handlePageSizeChange, pageSizeOptions: pageSizeOptions, showTotal: true, showSizeChanger: true, }} empty={ {t('没有未设置的模型')} } /> {/* 添加模型弹窗 */} setVisible(false)} onOk={() => { currentModel && addModel(currentModel); }} > (addFormRef.current = api)}> setCurrentModel((prev) => ({ ...prev, name: value })) } /> {t('定价模式')}: {currentModel?.priceMode ? t('固定价格') : t('倍率模式')} > } onChange={(checked) => { setCurrentModel((prev) => ({ ...prev, price: '', ratio: '', completionRatio: '', priceMode: checked, })); }} /> {currentModel?.priceMode ? ( setCurrentModel((prev) => ({ ...prev, price: value })) } /> ) : ( <> setCurrentModel((prev) => ({ ...prev, ratio: value })) } /> setCurrentModel((prev) => ({ ...prev, completionRatio: value, })) } /> > )} {/* 批量设置弹窗 */} setBatchVisible(false)} onOk={handleBatchFill} width={500} > handleBatchTypeChange('price')} > {t('固定价格')} handleBatchTypeChange('ratio')} > {t('模型倍率')} handleBatchTypeChange('completionRatio')} > {t('补全倍率')} handleBatchTypeChange('bothRatio')} > {t('模型倍率和补全倍率同时设置')} {batchFillType === 'bothRatio' ? ( <> setBatchRatioValue(value)} /> setBatchCompletionRatioValue(value)} /> > ) : ( setBatchFillValue(value)} /> )} {t('将为选中的 ')} {selectedRowKeys.length}{' '} {t(' 个模型设置相同的值')} {t('当前设置类型: ')}{' '} {batchFillType === 'price' ? t('固定价格') : batchFillType === 'ratio' ? t('模型倍率') : batchFillType === 'completionRatio' ? t('补全倍率') : t('模型倍率和补全倍率')} {/* 编辑模型弹窗 */} { setEditVisible(false); setEditRecord(null); setPricingMode('per-token'); setPricingSubMode('ratio'); }} onOk={() => { if (editRecord && editFormRef.current) { const valuesToSave = { ...editRecord }; // 处理按价格设置模式 if ( pricingMode === 'per-token' && pricingSubMode === 'token-price' && editRecord.tokenPrice ) { const tokenPrice = parseFloat(editRecord.tokenPrice); valuesToSave.ratio = (tokenPrice / 2).toString(); if (editRecord.completionTokenPrice) { const completionPrice = parseFloat( editRecord.completionTokenPrice, ); const modelPrice = parseFloat(editRecord.tokenPrice); if (modelPrice > 0) { valuesToSave.completionRatio = ( completionPrice / modelPrice ).toString(); } } } // 根据定价模式清理字段 if (pricingMode === 'per-token') { valuesToSave.price = ''; } else { valuesToSave.ratio = ''; valuesToSave.completionRatio = ''; } // 从表单读取高级倍率字段 const formValues = editFormRef.current.getValues(); valuesToSave.cacheRatio = formValues.cacheRatio != null ? formValues.cacheRatio : ''; valuesToSave.cacheCreationRatio = formValues.cacheCreationRatio != null ? formValues.cacheCreationRatio : ''; valuesToSave.imageRatio = formValues.imageRatio != null ? formValues.imageRatio : ''; valuesToSave.audioRatio = formValues.audioRatio != null ? formValues.audioRatio : ''; valuesToSave.audioCompletionRatio = formValues.audioCompletionRatio != null ? formValues.audioCompletionRatio : ''; setModels((prev) => prev.map((model) => { if (model.name !== editRecord.name) return model; return { ...model, price: valuesToSave.price, ratio: valuesToSave.ratio, completionRatio: valuesToSave.completionRatio, cacheRatio: valuesToSave.cacheRatio, cacheCreationRatio: valuesToSave.cacheCreationRatio, imageRatio: valuesToSave.imageRatio, audioRatio: valuesToSave.audioRatio, audioCompletionRatio: valuesToSave.audioCompletionRatio, }; }), ); setEditVisible(false); setEditRecord(null); showSuccess(t('更新成功')); } }} > (editFormRef.current = api)} > { setPricingMode(e.target.value); }} > {t('按量计费')} {t('按次计费')} {pricingMode === 'per-token' && ( <> { setPricingSubMode(e.target.value); }} > {t('按倍率设置')} {t('按价格设置')} {pricingSubMode === 'ratio' && ( <> setEditRecord((prev) => prev ? { ...prev, ratio: value } : prev, ) } initValue={editRecord?.ratio || ''} /> setEditRecord((prev) => prev ? { ...prev, completionRatio: value } : prev, ) } initValue={editRecord?.completionRatio || ''} /> > )} {pricingSubMode === 'token-price' && ( <> { let updated = { ...(editRecord || {}), tokenPrice: value, ratio: 0, }; if (!isNaN(value) && value !== '') { const tokenPrice = parseFloat(value); updated.ratio = (tokenPrice / 2).toString(); } setEditRecord(updated); }} initValue={editRecord?.tokenPrice || ''} /> { let updated = { ...(editRecord || {}), completionTokenPrice: value, completionRatio: 0, }; if ( !isNaN(value) && value !== '' && editRecord?.tokenPrice ) { const completionPrice = parseFloat(value); const modelPrice = parseFloat( editRecord.tokenPrice, ); if (modelPrice > 0) { updated.completionRatio = ( completionPrice / modelPrice ).toString(); } } setEditRecord(updated); }} initValue={editRecord?.completionTokenPrice || ''} /> > )} > )} {pricingMode === 'per-request' && ( setEditRecord((prev) => prev ? { ...prev, price: value } : prev, ) } initValue={editRecord?.price || ''} /> )} > ); }