Spaces:
Runtime error
Runtime error
| /* | |
| 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 <https://www.gnu.org/licenses/>. | |
| For commercial licensing, please contact support@quantumnous.com | |
| */ | |
| import React, { useEffect, useState, useRef } from 'react'; | |
| import { | |
| Banner, | |
| Button, | |
| Col, | |
| Form, | |
| Row, | |
| Spin, | |
| Progress, | |
| Descriptions, | |
| Tag, | |
| Popconfirm, | |
| Typography, | |
| } from '@douyinfe/semi-ui'; | |
| import { | |
| compareObjects, | |
| API, | |
| showError, | |
| showSuccess, | |
| showWarning, | |
| } from '../../../helpers'; | |
| import { useTranslation } from 'react-i18next'; | |
| const { Text } = Typography; | |
| // 格式化字节大小 | |
| function formatBytes(bytes, decimals = 2) { | |
| if (bytes === null || bytes === undefined || isNaN(bytes)) return '0 Bytes'; | |
| if (bytes === 0) return '0 Bytes'; | |
| if (bytes < 0) return '-' + formatBytes(-bytes, decimals); | |
| const k = 1024; | |
| const dm = decimals < 0 ? 0 : decimals; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| if (i < 0 || i >= sizes.length) return bytes + ' Bytes'; | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; | |
| } | |
| export default function SettingsPerformance(props) { | |
| const { t } = useTranslation(); | |
| const [loading, setLoading] = useState(false); | |
| const [statsLoading, setStatsLoading] = useState(false); | |
| const [stats, setStats] = useState(null); | |
| const [inputs, setInputs] = useState({ | |
| 'performance_setting.disk_cache_enabled': false, | |
| 'performance_setting.disk_cache_threshold_mb': 10, | |
| 'performance_setting.disk_cache_max_size_mb': 1024, | |
| 'performance_setting.disk_cache_path': '', | |
| 'performance_setting.monitor_enabled': false, | |
| 'performance_setting.monitor_cpu_threshold': 90, | |
| 'performance_setting.monitor_memory_threshold': 90, | |
| 'performance_setting.monitor_disk_threshold': 90, | |
| }); | |
| const refForm = useRef(); | |
| const [inputsRow, setInputsRow] = useState(inputs); | |
| function handleFieldChange(fieldName) { | |
| return (value) => { | |
| setInputs((inputs) => ({ ...inputs, [fieldName]: value })); | |
| }; | |
| } | |
| function onSubmit() { | |
| const updateArray = compareObjects(inputs, inputsRow); | |
| if (!updateArray.length) return showWarning(t('你似乎并没有修改什么')); | |
| const requestQueue = updateArray.map((item) => { | |
| let value = ''; | |
| if (typeof inputs[item.key] === 'boolean') { | |
| value = String(inputs[item.key]); | |
| } else { | |
| value = String(inputs[item.key]); | |
| } | |
| return API.put('/api/option/', { | |
| key: item.key, | |
| value, | |
| }); | |
| }); | |
| setLoading(true); | |
| Promise.all(requestQueue) | |
| .then((res) => { | |
| if (requestQueue.length === 1) { | |
| if (res.includes(undefined)) return; | |
| } else if (requestQueue.length > 1) { | |
| if (res.includes(undefined)) | |
| return showError(t('部分保存失败,请重试')); | |
| } | |
| showSuccess(t('保存成功')); | |
| props.refresh(); | |
| fetchStats(); | |
| }) | |
| .catch(() => { | |
| showError(t('保存失败,请重试')); | |
| }) | |
| .finally(() => { | |
| setLoading(false); | |
| }); | |
| } | |
| async function fetchStats() { | |
| setStatsLoading(true); | |
| try { | |
| const res = await API.get('/api/performance/stats'); | |
| if (res.data.success) { | |
| setStats(res.data.data); | |
| } | |
| } catch (error) { | |
| console.error('Failed to fetch performance stats:', error); | |
| } finally { | |
| setStatsLoading(false); | |
| } | |
| } | |
| async function clearDiskCache() { | |
| try { | |
| const res = await API.delete('/api/performance/disk_cache'); | |
| if (res.data.success) { | |
| showSuccess(t('磁盘缓存已清理')); | |
| fetchStats(); | |
| } else { | |
| showError(res.data.message || t('清理失败')); | |
| } | |
| } catch (error) { | |
| showError(t('清理失败')); | |
| } | |
| } | |
| async function resetStats() { | |
| try { | |
| const res = await API.post('/api/performance/reset_stats'); | |
| if (res.data.success) { | |
| showSuccess(t('统计已重置')); | |
| fetchStats(); | |
| } | |
| } catch (error) { | |
| showError(t('重置失败')); | |
| } | |
| } | |
| async function forceGC() { | |
| try { | |
| const res = await API.post('/api/performance/gc'); | |
| if (res.data.success) { | |
| showSuccess(t('GC 已执行')); | |
| fetchStats(); | |
| } | |
| } catch (error) { | |
| showError(t('GC 执行失败')); | |
| } | |
| } | |
| useEffect(() => { | |
| const currentInputs = {}; | |
| for (let key in props.options) { | |
| if (Object.keys(inputs).includes(key)) { | |
| if (typeof inputs[key] === 'boolean') { | |
| currentInputs[key] = | |
| props.options[key] === 'true' || props.options[key] === true; | |
| } else if (typeof inputs[key] === 'number') { | |
| currentInputs[key] = parseInt(props.options[key]) || inputs[key]; | |
| } else { | |
| currentInputs[key] = props.options[key]; | |
| } | |
| } | |
| } | |
| setInputs({ ...inputs, ...currentInputs }); | |
| setInputsRow({ ...inputs, ...currentInputs }); | |
| if (refForm.current) { | |
| refForm.current.setValues({ ...inputs, ...currentInputs }); | |
| } | |
| fetchStats(); | |
| }, [props.options]); | |
| const diskCacheUsagePercent = | |
| stats?.cache_stats?.disk_cache_max_bytes > 0 | |
| ? ( | |
| (stats.cache_stats.current_disk_usage_bytes / | |
| stats.cache_stats.disk_cache_max_bytes) * | |
| 100 | |
| ).toFixed(1) | |
| : 0; | |
| return ( | |
| <> | |
| <Spin spinning={loading}> | |
| <Form | |
| values={inputs} | |
| getFormApi={(formAPI) => (refForm.current = formAPI)} | |
| style={{ marginBottom: 15 }} | |
| > | |
| <Form.Section text={t('磁盘缓存设置(磁盘换内存)')}> | |
| <Banner | |
| type='info' | |
| description={t( | |
| '启用磁盘缓存后,大请求体将临时存储到磁盘而非内存,可显著降低内存占用,适用于处理包含大量图片/文件的请求。建议在 SSD 环境下使用。', | |
| )} | |
| style={{ marginBottom: 16 }} | |
| /> | |
| <Row gutter={16}> | |
| <Col xs={24} sm={12} md={8} lg={8} xl={8}> | |
| <Form.Switch | |
| field={'performance_setting.disk_cache_enabled'} | |
| label={t('启用磁盘缓存')} | |
| extraText={t('将大请求体临时存储到磁盘')} | |
| size='default' | |
| checkedText='|' | |
| uncheckedText='〇' | |
| onChange={handleFieldChange( | |
| 'performance_setting.disk_cache_enabled', | |
| )} | |
| /> | |
| </Col> | |
| <Col xs={24} sm={12} md={8} lg={8} xl={8}> | |
| <Form.InputNumber | |
| field={'performance_setting.disk_cache_threshold_mb'} | |
| label={t('磁盘缓存阈值 (MB)')} | |
| extraText={t('请求体超过此大小时使用磁盘缓存')} | |
| min={1} | |
| max={1024} | |
| onChange={handleFieldChange( | |
| 'performance_setting.disk_cache_threshold_mb', | |
| )} | |
| disabled={!inputs['performance_setting.disk_cache_enabled']} | |
| /> | |
| </Col> | |
| <Col xs={24} sm={12} md={8} lg={8} xl={8}> | |
| <Form.InputNumber | |
| field={'performance_setting.disk_cache_max_size_mb'} | |
| label={t('磁盘缓存最大总量 (MB)')} | |
| extraText={ | |
| stats?.disk_space_info?.total > 0 | |
| ? t('可用空间: {{free}} / 总空间: {{total}}', { | |
| free: formatBytes(stats.disk_space_info.free), | |
| total: formatBytes(stats.disk_space_info.total), | |
| }) | |
| : t('磁盘缓存占用的最大空间') | |
| } | |
| min={100} | |
| max={102400} | |
| onChange={handleFieldChange( | |
| 'performance_setting.disk_cache_max_size_mb', | |
| )} | |
| disabled={!inputs['performance_setting.disk_cache_enabled']} | |
| /> | |
| </Col> | |
| {/* 只在非容器环境显示缓存目录配置 */} | |
| {!stats?.config?.is_running_in_container && ( | |
| <Col xs={24} sm={12} md={8} lg={8} xl={8}> | |
| <Form.Input | |
| field={'performance_setting.disk_cache_path'} | |
| label={t('缓存目录')} | |
| extraText={t('留空使用系统临时目录')} | |
| placeholder={t('例如 /var/cache/new-api')} | |
| onChange={handleFieldChange( | |
| 'performance_setting.disk_cache_path', | |
| )} | |
| showClear | |
| disabled={!inputs['performance_setting.disk_cache_enabled']} | |
| /> | |
| </Col> | |
| )} | |
| </Row> | |
| </Form.Section> | |
| <Form.Section text={t('系统性能监控')}> | |
| <Banner | |
| type='info' | |
| description={t( | |
| '启用性能监控后,当系统资源使用率超过设定阈值时,将拒绝新的 Relay 请求 (/v1, /v1beta 等),以保护系统稳定性。', | |
| )} | |
| style={{ marginBottom: 16 }} | |
| /> | |
| <Row gutter={16}> | |
| <Col xs={24} sm={12} md={6} lg={6} xl={6}> | |
| <Form.Switch | |
| field={'performance_setting.monitor_enabled'} | |
| label={t('启用性能监控')} | |
| extraText={t('超过阈值时拒绝新请求')} | |
| size='default' | |
| checkedText='|' | |
| uncheckedText='〇' | |
| onChange={handleFieldChange( | |
| 'performance_setting.monitor_enabled', | |
| )} | |
| /> | |
| </Col> | |
| <Col xs={24} sm={12} md={6} lg={6} xl={6}> | |
| <Form.InputNumber | |
| field={'performance_setting.monitor_cpu_threshold'} | |
| label={t('CPU 阈值 (%)')} | |
| extraText={t('CPU 使用率超过此值时拒绝请求')} | |
| min={0} | |
| max={100} | |
| onChange={handleFieldChange( | |
| 'performance_setting.monitor_cpu_threshold', | |
| )} | |
| disabled={!inputs['performance_setting.monitor_enabled']} | |
| /> | |
| </Col> | |
| <Col xs={24} sm={12} md={6} lg={6} xl={6}> | |
| <Form.InputNumber | |
| field={'performance_setting.monitor_memory_threshold'} | |
| label={t('内存 阈值 (%)')} | |
| extraText={t('内存使用率超过此值时拒绝请求')} | |
| min={0} | |
| max={100} | |
| onChange={handleFieldChange( | |
| 'performance_setting.monitor_memory_threshold', | |
| )} | |
| disabled={!inputs['performance_setting.monitor_enabled']} | |
| /> | |
| </Col> | |
| <Col xs={24} sm={12} md={6} lg={6} xl={6}> | |
| <Form.InputNumber | |
| field={'performance_setting.monitor_disk_threshold'} | |
| label={t('磁盘 阈值 (%)')} | |
| extraText={t('磁盘使用率超过此值时拒绝请求')} | |
| min={0} | |
| max={100} | |
| onChange={handleFieldChange( | |
| 'performance_setting.monitor_disk_threshold', | |
| )} | |
| disabled={!inputs['performance_setting.monitor_enabled']} | |
| /> | |
| </Col> | |
| </Row> | |
| <Row> | |
| <Button size='default' onClick={onSubmit}> | |
| {t('保存性能设置')} | |
| </Button> | |
| </Row> | |
| </Form.Section> | |
| </Form> | |
| </Spin> | |
| {/* 性能统计 */} | |
| <Spin spinning={statsLoading}> | |
| <Form.Section text={t('性能监控')}> | |
| <Row gutter={16} style={{ marginBottom: 16 }}> | |
| <Col span={24}> | |
| <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}> | |
| <Button onClick={fetchStats}>{t('刷新统计')}</Button> | |
| <Popconfirm | |
| title={t('确认清理不活跃的磁盘缓存?')} | |
| content={t('这将删除超过 10 分钟未使用的临时缓存文件')} | |
| onConfirm={clearDiskCache} | |
| > | |
| <Button type='warning'>{t('清理不活跃缓存')}</Button> | |
| </Popconfirm> | |
| <Button onClick={resetStats}>{t('重置统计')}</Button> | |
| <Button onClick={forceGC}>{t('执行 GC')}</Button> | |
| </div> | |
| </Col> | |
| </Row> | |
| {stats && ( | |
| <> | |
| {/* 缓存使用情况 */} | |
| <Row | |
| gutter={16} | |
| style={{ | |
| marginBottom: 16, | |
| display: 'flex', | |
| alignItems: 'stretch', | |
| }} | |
| > | |
| <Col xs={24} md={12} style={{ display: 'flex' }}> | |
| <div | |
| style={{ | |
| padding: 16, | |
| background: 'var(--semi-color-fill-0)', | |
| borderRadius: 8, | |
| flex: 1, | |
| display: 'flex', | |
| flexDirection: 'column', | |
| }} | |
| > | |
| <Text strong style={{ marginBottom: 8, display: 'block' }}> | |
| {t('请求体磁盘缓存')} | |
| </Text> | |
| <Progress | |
| percent={parseFloat(diskCacheUsagePercent)} | |
| showInfo | |
| style={{ marginBottom: 8 }} | |
| stroke={ | |
| parseFloat(diskCacheUsagePercent) > 80 | |
| ? 'var(--semi-color-danger)' | |
| : 'var(--semi-color-primary)' | |
| } | |
| /> | |
| <div | |
| style={{ | |
| display: 'flex', | |
| justifyContent: 'space-between', | |
| marginBottom: 8, | |
| }} | |
| > | |
| <Text type='tertiary'> | |
| {formatBytes( | |
| stats.cache_stats.current_disk_usage_bytes, | |
| )}{' '} | |
| / {formatBytes(stats.cache_stats.disk_cache_max_bytes)} | |
| </Text> | |
| <Text type='tertiary'> | |
| {t('活跃文件')}: {stats.cache_stats.active_disk_files} | |
| </Text> | |
| </div> | |
| <div style={{ marginTop: 'auto' }}> | |
| <Tag color='blue'> | |
| {t('磁盘命中')}: {stats.cache_stats.disk_cache_hits} | |
| </Tag> | |
| </div> | |
| </div> | |
| </Col> | |
| <Col xs={24} md={12} style={{ display: 'flex' }}> | |
| <div | |
| style={{ | |
| padding: 16, | |
| background: 'var(--semi-color-fill-0)', | |
| borderRadius: 8, | |
| flex: 1, | |
| display: 'flex', | |
| flexDirection: 'column', | |
| }} | |
| > | |
| <Text strong style={{ marginBottom: 8, display: 'block' }}> | |
| {t('请求体内存缓存')} | |
| </Text> | |
| <div | |
| style={{ | |
| display: 'flex', | |
| justifyContent: 'space-between', | |
| marginBottom: 8, | |
| }} | |
| > | |
| <Text> | |
| {t('当前缓存大小')}:{' '} | |
| {formatBytes( | |
| stats.cache_stats.current_memory_usage_bytes, | |
| )} | |
| </Text> | |
| <Text> | |
| {t('活跃缓存数')}:{' '} | |
| {stats.cache_stats.active_memory_buffers} | |
| </Text> | |
| </div> | |
| <div style={{ marginTop: 'auto' }}> | |
| <Tag color='green'> | |
| {t('内存命中')}: {stats.cache_stats.memory_cache_hits} | |
| </Tag> | |
| </div> | |
| </div> | |
| </Col> | |
| </Row> | |
| {/* 缓存目录磁盘空间 */} | |
| {stats.disk_space_info?.total > 0 && ( | |
| <Row gutter={16} style={{ marginBottom: 16 }}> | |
| <Col span={24}> | |
| <div | |
| style={{ | |
| padding: 16, | |
| background: 'var(--semi-color-fill-0)', | |
| borderRadius: 8, | |
| }} | |
| > | |
| <Text | |
| strong | |
| style={{ marginBottom: 8, display: 'block' }} | |
| > | |
| {t('缓存目录磁盘空间')} | |
| </Text> | |
| <Progress | |
| percent={parseFloat( | |
| stats.disk_space_info.used_percent.toFixed(1), | |
| )} | |
| showInfo | |
| style={{ marginBottom: 8 }} | |
| stroke={ | |
| stats.disk_space_info.used_percent > 90 | |
| ? 'var(--semi-color-danger)' | |
| : stats.disk_space_info.used_percent > 70 | |
| ? 'var(--semi-color-warning)' | |
| : 'var(--semi-color-primary)' | |
| } | |
| /> | |
| <div | |
| style={{ | |
| display: 'flex', | |
| justifyContent: 'space-between', | |
| flexWrap: 'wrap', | |
| gap: 8, | |
| }} | |
| > | |
| <Text type='tertiary'> | |
| {t('已用')}: {formatBytes(stats.disk_space_info.used)} | |
| </Text> | |
| <Text type='tertiary'> | |
| {t('可用')}: {formatBytes(stats.disk_space_info.free)} | |
| </Text> | |
| <Text type='tertiary'> | |
| {t('总计')}:{' '} | |
| {formatBytes(stats.disk_space_info.total)} | |
| </Text> | |
| </div> | |
| {stats.disk_space_info.free < | |
| inputs['performance_setting.disk_cache_max_size_mb'] * | |
| 1024 * | |
| 1024 && ( | |
| <Banner | |
| type='warning' | |
| description={t('磁盘可用空间小于缓存最大总量设置')} | |
| style={{ marginTop: 8 }} | |
| /> | |
| )} | |
| </div> | |
| </Col> | |
| </Row> | |
| )} | |
| {/* 系统内存统计 */} | |
| <Row gutter={16}> | |
| <Col span={24}> | |
| <Descriptions | |
| data={[ | |
| { | |
| key: t('已分配内存'), | |
| value: formatBytes(stats.memory_stats.alloc), | |
| }, | |
| { | |
| key: t('总分配内存'), | |
| value: formatBytes(stats.memory_stats.total_alloc), | |
| }, | |
| { | |
| key: t('系统内存'), | |
| value: formatBytes(stats.memory_stats.sys), | |
| }, | |
| { key: t('GC 次数'), value: stats.memory_stats.num_gc }, | |
| { | |
| key: t('Goroutine 数'), | |
| value: stats.memory_stats.num_goroutine, | |
| }, | |
| { | |
| key: t('缓存目录'), | |
| value: stats.disk_cache_info.path, | |
| }, | |
| { | |
| key: t('目录文件数'), | |
| value: stats.disk_cache_info.file_count, | |
| }, | |
| { | |
| key: t('目录总大小'), | |
| value: formatBytes(stats.disk_cache_info.total_size), | |
| }, | |
| ]} | |
| /> | |
| </Col> | |
| </Row> | |
| </> | |
| )} | |
| </Form.Section> | |
| </Spin> | |
| </> | |
| ); | |
| } | |