mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-19 08:58:37 +00:00
feat: disk request body cache (#2780)
* feat: 引入通用 HTTP BodyStorage/DiskCache 缓存配置与管理 - 新增 common/body_storage.go 提供 HTTP 请求体存储抽象和文件缓存能力 - 增加 common/disk_cache_config.go 支持全局磁盘缓存配置 - main.go 挂载缓存初始化流程 - 新增和补充 controller/performance.go (及 unix/windows) 用于缓存性能监控接口 - middleware/body_cleanup.go 自动清理缓存文件 - router 挂载相关接口 - 前端 settings 页面新增性能监控设置 PerformanceSetting - 优化缓存开关状态和模块热插拔能力 - 其他相关文件同步适配缓存扩展 * fix: 修复 BodyStorage 并发安全和错误处理问题 - 修复 diskStorage.Close() 竞态条件,先获取锁再执行 CAS - 为 memoryStorage 添加互斥锁和 closed 状态检查 - 修复 CreateBodyStorageFromReader 在磁盘存储失败时的回退逻辑 - 添加缓存命中统计调用 (IncrementDiskCacheHits/IncrementMemoryCacheHits) - 修复 gin.go 中 Seek 错误被忽略的问题 - 在 api-router 添加 BodyStorageCleanup 中间件 - 修复前端 formatBytes 对异常值的处理 Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
365
common/body_storage.go
Normal file
365
common/body_storage.go
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BodyStorage 请求体存储接口
|
||||||
|
type BodyStorage interface {
|
||||||
|
io.ReadSeeker
|
||||||
|
io.Closer
|
||||||
|
// Bytes 获取全部内容
|
||||||
|
Bytes() ([]byte, error)
|
||||||
|
// Size 获取数据大小
|
||||||
|
Size() int64
|
||||||
|
// IsDisk 是否是磁盘存储
|
||||||
|
IsDisk() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrStorageClosed 存储已关闭错误
|
||||||
|
var ErrStorageClosed = fmt.Errorf("body storage is closed")
|
||||||
|
|
||||||
|
// memoryStorage 内存存储实现
|
||||||
|
type memoryStorage struct {
|
||||||
|
data []byte
|
||||||
|
reader *bytes.Reader
|
||||||
|
size int64
|
||||||
|
closed int32
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMemoryStorage(data []byte) *memoryStorage {
|
||||||
|
size := int64(len(data))
|
||||||
|
IncrementMemoryBuffers(size)
|
||||||
|
return &memoryStorage{
|
||||||
|
data: data,
|
||||||
|
reader: bytes.NewReader(data),
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryStorage) Read(p []byte) (n int, err error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if atomic.LoadInt32(&m.closed) == 1 {
|
||||||
|
return 0, ErrStorageClosed
|
||||||
|
}
|
||||||
|
return m.reader.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryStorage) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if atomic.LoadInt32(&m.closed) == 1 {
|
||||||
|
return 0, ErrStorageClosed
|
||||||
|
}
|
||||||
|
return m.reader.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryStorage) Close() error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if atomic.CompareAndSwapInt32(&m.closed, 0, 1) {
|
||||||
|
DecrementMemoryBuffers(m.size)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryStorage) Bytes() ([]byte, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if atomic.LoadInt32(&m.closed) == 1 {
|
||||||
|
return nil, ErrStorageClosed
|
||||||
|
}
|
||||||
|
return m.data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryStorage) Size() int64 {
|
||||||
|
return m.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryStorage) IsDisk() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// diskStorage 磁盘存储实现
|
||||||
|
type diskStorage struct {
|
||||||
|
file *os.File
|
||||||
|
filePath string
|
||||||
|
size int64
|
||||||
|
closed int32
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDiskStorage(data []byte, cachePath string) (*diskStorage, error) {
|
||||||
|
// 确定缓存目录
|
||||||
|
dir := cachePath
|
||||||
|
if dir == "" {
|
||||||
|
dir = os.TempDir()
|
||||||
|
}
|
||||||
|
dir = filepath.Join(dir, "new-api-body-cache")
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建临时文件
|
||||||
|
filename := fmt.Sprintf("body-%s-%d.tmp", uuid.New().String()[:8], time.Now().UnixNano())
|
||||||
|
filePath := filepath.Join(dir, filename)
|
||||||
|
|
||||||
|
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入数据
|
||||||
|
n, err := file.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
return nil, fmt.Errorf("failed to write to temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置文件指针
|
||||||
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
return nil, fmt.Errorf("failed to seek temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
size := int64(n)
|
||||||
|
IncrementDiskFiles(size)
|
||||||
|
|
||||||
|
return &diskStorage{
|
||||||
|
file: file,
|
||||||
|
filePath: filePath,
|
||||||
|
size: size,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDiskStorageFromReader(reader io.Reader, maxBytes int64, cachePath string) (*diskStorage, error) {
|
||||||
|
// 确定缓存目录
|
||||||
|
dir := cachePath
|
||||||
|
if dir == "" {
|
||||||
|
dir = os.TempDir()
|
||||||
|
}
|
||||||
|
dir = filepath.Join(dir, "new-api-body-cache")
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建临时文件
|
||||||
|
filename := fmt.Sprintf("body-%s-%d.tmp", uuid.New().String()[:8], time.Now().UnixNano())
|
||||||
|
filePath := filepath.Join(dir, filename)
|
||||||
|
|
||||||
|
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 reader 读取并写入文件
|
||||||
|
written, err := io.Copy(file, io.LimitReader(reader, maxBytes+1))
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
return nil, fmt.Errorf("failed to write to temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if written > maxBytes {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
return nil, ErrRequestBodyTooLarge
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置文件指针
|
||||||
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
return nil, fmt.Errorf("failed to seek temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
IncrementDiskFiles(written)
|
||||||
|
|
||||||
|
return &diskStorage{
|
||||||
|
file: file,
|
||||||
|
filePath: filePath,
|
||||||
|
size: written,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStorage) Read(p []byte) (n int, err error) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
if atomic.LoadInt32(&d.closed) == 1 {
|
||||||
|
return 0, ErrStorageClosed
|
||||||
|
}
|
||||||
|
return d.file.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStorage) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
if atomic.LoadInt32(&d.closed) == 1 {
|
||||||
|
return 0, ErrStorageClosed
|
||||||
|
}
|
||||||
|
return d.file.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStorage) Close() error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
if atomic.CompareAndSwapInt32(&d.closed, 0, 1) {
|
||||||
|
d.file.Close()
|
||||||
|
os.Remove(d.filePath)
|
||||||
|
DecrementDiskFiles(d.size)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStorage) Bytes() ([]byte, error) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
if atomic.LoadInt32(&d.closed) == 1 {
|
||||||
|
return nil, ErrStorageClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前位置
|
||||||
|
currentPos, err := d.file.Seek(0, io.SeekCurrent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动到开头
|
||||||
|
if _, err := d.file.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取全部内容
|
||||||
|
data := make([]byte, d.size)
|
||||||
|
_, err = io.ReadFull(d.file, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复位置
|
||||||
|
if _, err := d.file.Seek(currentPos, io.SeekStart); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStorage) Size() int64 {
|
||||||
|
return d.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStorage) IsDisk() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBodyStorage 根据数据大小创建合适的存储
|
||||||
|
func CreateBodyStorage(data []byte) (BodyStorage, error) {
|
||||||
|
size := int64(len(data))
|
||||||
|
threshold := GetDiskCacheThresholdBytes()
|
||||||
|
|
||||||
|
// 检查是否应该使用磁盘缓存
|
||||||
|
if IsDiskCacheEnabled() &&
|
||||||
|
size >= threshold &&
|
||||||
|
IsDiskCacheAvailable(size) {
|
||||||
|
storage, err := newDiskStorage(data, GetDiskCachePath())
|
||||||
|
if err != nil {
|
||||||
|
// 如果磁盘存储失败,回退到内存存储
|
||||||
|
SysError(fmt.Sprintf("failed to create disk storage, falling back to memory: %v", err))
|
||||||
|
return newMemoryStorage(data), nil
|
||||||
|
}
|
||||||
|
return storage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return newMemoryStorage(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBodyStorageFromReader 从 Reader 创建存储(用于大请求的流式处理)
|
||||||
|
func CreateBodyStorageFromReader(reader io.Reader, contentLength int64, maxBytes int64) (BodyStorage, error) {
|
||||||
|
threshold := GetDiskCacheThresholdBytes()
|
||||||
|
|
||||||
|
// 如果启用了磁盘缓存且内容长度超过阈值,直接使用磁盘存储
|
||||||
|
if IsDiskCacheEnabled() &&
|
||||||
|
contentLength > 0 &&
|
||||||
|
contentLength >= threshold &&
|
||||||
|
IsDiskCacheAvailable(contentLength) {
|
||||||
|
storage, err := newDiskStorageFromReader(reader, maxBytes, GetDiskCachePath())
|
||||||
|
if err != nil {
|
||||||
|
if IsRequestBodyTooLargeError(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 磁盘存储失败,reader 已被消费,无法安全回退
|
||||||
|
// 直接返回错误而非尝试回退(因为 reader 数据已丢失)
|
||||||
|
return nil, fmt.Errorf("disk storage creation failed: %w", err)
|
||||||
|
}
|
||||||
|
IncrementDiskCacheHits()
|
||||||
|
return storage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用内存读取
|
||||||
|
data, err := io.ReadAll(io.LimitReader(reader, maxBytes+1))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if int64(len(data)) > maxBytes {
|
||||||
|
return nil, ErrRequestBodyTooLarge
|
||||||
|
}
|
||||||
|
|
||||||
|
storage, err := CreateBodyStorage(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 如果最终使用内存存储,记录内存缓存命中
|
||||||
|
if !storage.IsDisk() {
|
||||||
|
IncrementMemoryCacheHits()
|
||||||
|
} else {
|
||||||
|
IncrementDiskCacheHits()
|
||||||
|
}
|
||||||
|
return storage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupOldCacheFiles 清理旧的缓存文件(用于启动时清理残留)
|
||||||
|
func CleanupOldCacheFiles() {
|
||||||
|
cachePath := GetDiskCachePath()
|
||||||
|
if cachePath == "" {
|
||||||
|
cachePath = os.TempDir()
|
||||||
|
}
|
||||||
|
dir := filepath.Join(cachePath, "new-api-body-cache")
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return // 目录不存在或无法读取
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 删除超过 5 分钟的旧文件
|
||||||
|
if now.Sub(info.ModTime()) > 5*time.Minute {
|
||||||
|
os.Remove(filepath.Join(dir, entry.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
common/disk_cache_config.go
Normal file
156
common/disk_cache_config.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DiskCacheConfig 磁盘缓存配置(由 performance_setting 包更新)
|
||||||
|
type DiskCacheConfig struct {
|
||||||
|
// Enabled 是否启用磁盘缓存
|
||||||
|
Enabled bool
|
||||||
|
// ThresholdMB 触发磁盘缓存的请求体大小阈值(MB)
|
||||||
|
ThresholdMB int
|
||||||
|
// MaxSizeMB 磁盘缓存最大总大小(MB)
|
||||||
|
MaxSizeMB int
|
||||||
|
// Path 磁盘缓存目录
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局磁盘缓存配置
|
||||||
|
var diskCacheConfig = DiskCacheConfig{
|
||||||
|
Enabled: false,
|
||||||
|
ThresholdMB: 10,
|
||||||
|
MaxSizeMB: 1024,
|
||||||
|
Path: "",
|
||||||
|
}
|
||||||
|
var diskCacheConfigMu sync.RWMutex
|
||||||
|
|
||||||
|
// GetDiskCacheConfig 获取磁盘缓存配置
|
||||||
|
func GetDiskCacheConfig() DiskCacheConfig {
|
||||||
|
diskCacheConfigMu.RLock()
|
||||||
|
defer diskCacheConfigMu.RUnlock()
|
||||||
|
return diskCacheConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDiskCacheConfig 设置磁盘缓存配置
|
||||||
|
func SetDiskCacheConfig(config DiskCacheConfig) {
|
||||||
|
diskCacheConfigMu.Lock()
|
||||||
|
defer diskCacheConfigMu.Unlock()
|
||||||
|
diskCacheConfig = config
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDiskCacheEnabled 是否启用磁盘缓存
|
||||||
|
func IsDiskCacheEnabled() bool {
|
||||||
|
diskCacheConfigMu.RLock()
|
||||||
|
defer diskCacheConfigMu.RUnlock()
|
||||||
|
return diskCacheConfig.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDiskCacheThresholdBytes 获取磁盘缓存阈值(字节)
|
||||||
|
func GetDiskCacheThresholdBytes() int64 {
|
||||||
|
diskCacheConfigMu.RLock()
|
||||||
|
defer diskCacheConfigMu.RUnlock()
|
||||||
|
return int64(diskCacheConfig.ThresholdMB) << 20
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDiskCacheMaxSizeBytes 获取磁盘缓存最大大小(字节)
|
||||||
|
func GetDiskCacheMaxSizeBytes() int64 {
|
||||||
|
diskCacheConfigMu.RLock()
|
||||||
|
defer diskCacheConfigMu.RUnlock()
|
||||||
|
return int64(diskCacheConfig.MaxSizeMB) << 20
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDiskCachePath 获取磁盘缓存目录
|
||||||
|
func GetDiskCachePath() string {
|
||||||
|
diskCacheConfigMu.RLock()
|
||||||
|
defer diskCacheConfigMu.RUnlock()
|
||||||
|
return diskCacheConfig.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiskCacheStats 磁盘缓存统计信息
|
||||||
|
type DiskCacheStats struct {
|
||||||
|
// 当前活跃的磁盘缓存文件数
|
||||||
|
ActiveDiskFiles int64 `json:"active_disk_files"`
|
||||||
|
// 当前磁盘缓存总大小(字节)
|
||||||
|
CurrentDiskUsageBytes int64 `json:"current_disk_usage_bytes"`
|
||||||
|
// 当前内存缓存数量
|
||||||
|
ActiveMemoryBuffers int64 `json:"active_memory_buffers"`
|
||||||
|
// 当前内存缓存总大小(字节)
|
||||||
|
CurrentMemoryUsageBytes int64 `json:"current_memory_usage_bytes"`
|
||||||
|
// 磁盘缓存命中次数
|
||||||
|
DiskCacheHits int64 `json:"disk_cache_hits"`
|
||||||
|
// 内存缓存命中次数
|
||||||
|
MemoryCacheHits int64 `json:"memory_cache_hits"`
|
||||||
|
// 磁盘缓存最大限制(字节)
|
||||||
|
DiskCacheMaxBytes int64 `json:"disk_cache_max_bytes"`
|
||||||
|
// 磁盘缓存阈值(字节)
|
||||||
|
DiskCacheThresholdBytes int64 `json:"disk_cache_threshold_bytes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var diskCacheStats DiskCacheStats
|
||||||
|
|
||||||
|
// GetDiskCacheStats 获取缓存统计信息
|
||||||
|
func GetDiskCacheStats() DiskCacheStats {
|
||||||
|
stats := DiskCacheStats{
|
||||||
|
ActiveDiskFiles: atomic.LoadInt64(&diskCacheStats.ActiveDiskFiles),
|
||||||
|
CurrentDiskUsageBytes: atomic.LoadInt64(&diskCacheStats.CurrentDiskUsageBytes),
|
||||||
|
ActiveMemoryBuffers: atomic.LoadInt64(&diskCacheStats.ActiveMemoryBuffers),
|
||||||
|
CurrentMemoryUsageBytes: atomic.LoadInt64(&diskCacheStats.CurrentMemoryUsageBytes),
|
||||||
|
DiskCacheHits: atomic.LoadInt64(&diskCacheStats.DiskCacheHits),
|
||||||
|
MemoryCacheHits: atomic.LoadInt64(&diskCacheStats.MemoryCacheHits),
|
||||||
|
DiskCacheMaxBytes: GetDiskCacheMaxSizeBytes(),
|
||||||
|
DiskCacheThresholdBytes: GetDiskCacheThresholdBytes(),
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementDiskFiles 增加磁盘文件计数
|
||||||
|
func IncrementDiskFiles(size int64) {
|
||||||
|
atomic.AddInt64(&diskCacheStats.ActiveDiskFiles, 1)
|
||||||
|
atomic.AddInt64(&diskCacheStats.CurrentDiskUsageBytes, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecrementDiskFiles 减少磁盘文件计数
|
||||||
|
func DecrementDiskFiles(size int64) {
|
||||||
|
atomic.AddInt64(&diskCacheStats.ActiveDiskFiles, -1)
|
||||||
|
atomic.AddInt64(&diskCacheStats.CurrentDiskUsageBytes, -size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementMemoryBuffers 增加内存缓存计数
|
||||||
|
func IncrementMemoryBuffers(size int64) {
|
||||||
|
atomic.AddInt64(&diskCacheStats.ActiveMemoryBuffers, 1)
|
||||||
|
atomic.AddInt64(&diskCacheStats.CurrentMemoryUsageBytes, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecrementMemoryBuffers 减少内存缓存计数
|
||||||
|
func DecrementMemoryBuffers(size int64) {
|
||||||
|
atomic.AddInt64(&diskCacheStats.ActiveMemoryBuffers, -1)
|
||||||
|
atomic.AddInt64(&diskCacheStats.CurrentMemoryUsageBytes, -size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementDiskCacheHits 增加磁盘缓存命中次数
|
||||||
|
func IncrementDiskCacheHits() {
|
||||||
|
atomic.AddInt64(&diskCacheStats.DiskCacheHits, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementMemoryCacheHits 增加内存缓存命中次数
|
||||||
|
func IncrementMemoryCacheHits() {
|
||||||
|
atomic.AddInt64(&diskCacheStats.MemoryCacheHits, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetDiskCacheStats 重置统计信息(不重置当前使用量)
|
||||||
|
func ResetDiskCacheStats() {
|
||||||
|
atomic.StoreInt64(&diskCacheStats.DiskCacheHits, 0)
|
||||||
|
atomic.StoreInt64(&diskCacheStats.MemoryCacheHits, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDiskCacheAvailable 检查是否可以创建新的磁盘缓存
|
||||||
|
func IsDiskCacheAvailable(requestSize int64) bool {
|
||||||
|
if !IsDiskCacheEnabled() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
maxBytes := GetDiskCacheMaxSizeBytes()
|
||||||
|
currentUsage := atomic.LoadInt64(&diskCacheStats.CurrentDiskUsageBytes)
|
||||||
|
return currentUsage+requestSize <= maxBytes
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const KeyRequestBody = "key_request_body"
|
const KeyRequestBody = "key_request_body"
|
||||||
|
const KeyBodyStorage = "key_body_storage"
|
||||||
|
|
||||||
var ErrRequestBodyTooLarge = errors.New("request body too large")
|
var ErrRequestBodyTooLarge = errors.New("request body too large")
|
||||||
|
|
||||||
@@ -33,42 +34,99 @@ func IsRequestBodyTooLargeError(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetRequestBody(c *gin.Context) ([]byte, error) {
|
func GetRequestBody(c *gin.Context) ([]byte, error) {
|
||||||
|
// 首先检查是否有 BodyStorage 缓存
|
||||||
|
if storage, exists := c.Get(KeyBodyStorage); exists && storage != nil {
|
||||||
|
if bs, ok := storage.(BodyStorage); ok {
|
||||||
|
if _, err := bs.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to seek body storage: %w", err)
|
||||||
|
}
|
||||||
|
return bs.Bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查旧的缓存方式
|
||||||
cached, exists := c.Get(KeyRequestBody)
|
cached, exists := c.Get(KeyRequestBody)
|
||||||
if exists && cached != nil {
|
if exists && cached != nil {
|
||||||
if b, ok := cached.([]byte); ok {
|
if b, ok := cached.([]byte); ok {
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maxMB := constant.MaxRequestBodyMB
|
maxMB := constant.MaxRequestBodyMB
|
||||||
if maxMB <= 0 {
|
if maxMB <= 0 {
|
||||||
// no limit
|
maxMB = 128 // 默认 128MB
|
||||||
body, err := io.ReadAll(c.Request.Body)
|
|
||||||
_ = c.Request.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.Set(KeyRequestBody, body)
|
|
||||||
return body, nil
|
|
||||||
}
|
}
|
||||||
maxBytes := int64(maxMB) << 20
|
maxBytes := int64(maxMB) << 20
|
||||||
|
|
||||||
limited := io.LimitReader(c.Request.Body, maxBytes+1)
|
contentLength := c.Request.ContentLength
|
||||||
body, err := io.ReadAll(limited)
|
|
||||||
|
// 使用新的存储系统
|
||||||
|
storage, err := CreateBodyStorageFromReader(c.Request.Body, contentLength, maxBytes)
|
||||||
|
_ = c.Request.Body.Close()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Request.Body.Close()
|
|
||||||
if IsRequestBodyTooLargeError(err) {
|
if IsRequestBodyTooLargeError(err) {
|
||||||
return nil, errors.Wrap(ErrRequestBodyTooLarge, fmt.Sprintf("request body exceeds %d MB", maxMB))
|
return nil, errors.Wrap(ErrRequestBodyTooLarge, fmt.Sprintf("request body exceeds %d MB", maxMB))
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
_ = c.Request.Body.Close()
|
|
||||||
if int64(len(body)) > maxBytes {
|
// 缓存存储对象
|
||||||
return nil, errors.Wrap(ErrRequestBodyTooLarge, fmt.Sprintf("request body exceeds %d MB", maxMB))
|
c.Set(KeyBodyStorage, storage)
|
||||||
|
|
||||||
|
// 获取字节数据
|
||||||
|
body, err := storage.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同时设置旧的缓存键以保持兼容性
|
||||||
c.Set(KeyRequestBody, body)
|
c.Set(KeyRequestBody, body)
|
||||||
|
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBodyStorage 获取请求体存储对象(用于需要多次读取的场景)
|
||||||
|
func GetBodyStorage(c *gin.Context) (BodyStorage, error) {
|
||||||
|
// 检查是否已有存储
|
||||||
|
if storage, exists := c.Get(KeyBodyStorage); exists && storage != nil {
|
||||||
|
if bs, ok := storage.(BodyStorage); ok {
|
||||||
|
if _, err := bs.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to seek body storage: %w", err)
|
||||||
|
}
|
||||||
|
return bs, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有,调用 GetRequestBody 创建存储
|
||||||
|
_, err := GetRequestBody(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再次获取存储
|
||||||
|
if storage, exists := c.Get(KeyBodyStorage); exists && storage != nil {
|
||||||
|
if bs, ok := storage.(BodyStorage); ok {
|
||||||
|
if _, err := bs.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to seek body storage: %w", err)
|
||||||
|
}
|
||||||
|
return bs, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("failed to get body storage")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupBodyStorage 清理请求体存储(应在请求结束时调用)
|
||||||
|
func CleanupBodyStorage(c *gin.Context) {
|
||||||
|
if storage, exists := c.Get(KeyBodyStorage); exists && storage != nil {
|
||||||
|
if bs, ok := storage.(BodyStorage); ok {
|
||||||
|
bs.Close()
|
||||||
|
}
|
||||||
|
c.Set(KeyBodyStorage, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func UnmarshalBodyReusable(c *gin.Context, v any) error {
|
func UnmarshalBodyReusable(c *gin.Context, v any) error {
|
||||||
requestBody, err := GetRequestBody(c)
|
requestBody, err := GetRequestBody(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ func GetStatus(c *gin.Context) {
|
|||||||
"user_agreement_enabled": legalSetting.UserAgreement != "",
|
"user_agreement_enabled": legalSetting.UserAgreement != "",
|
||||||
"privacy_policy_enabled": legalSetting.PrivacyPolicy != "",
|
"privacy_policy_enabled": legalSetting.PrivacyPolicy != "",
|
||||||
"checkin_enabled": operation_setting.GetCheckinSetting().Enabled,
|
"checkin_enabled": operation_setting.GetCheckinSetting().Enabled,
|
||||||
|
"_qn": "new-api",
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据启用状态注入可选内容
|
// 根据启用状态注入可选内容
|
||||||
|
|||||||
202
controller/performance.go
Normal file
202
controller/performance.go
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PerformanceStats 性能统计信息
|
||||||
|
type PerformanceStats struct {
|
||||||
|
// 缓存统计
|
||||||
|
CacheStats common.DiskCacheStats `json:"cache_stats"`
|
||||||
|
// 系统内存统计
|
||||||
|
MemoryStats MemoryStats `json:"memory_stats"`
|
||||||
|
// 磁盘缓存目录信息
|
||||||
|
DiskCacheInfo DiskCacheInfo `json:"disk_cache_info"`
|
||||||
|
// 磁盘空间信息
|
||||||
|
DiskSpaceInfo DiskSpaceInfo `json:"disk_space_info"`
|
||||||
|
// 配置信息
|
||||||
|
Config PerformanceConfig `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemoryStats 内存统计
|
||||||
|
type MemoryStats struct {
|
||||||
|
// 已分配内存(字节)
|
||||||
|
Alloc uint64 `json:"alloc"`
|
||||||
|
// 总分配内存(字节)
|
||||||
|
TotalAlloc uint64 `json:"total_alloc"`
|
||||||
|
// 系统内存(字节)
|
||||||
|
Sys uint64 `json:"sys"`
|
||||||
|
// GC 次数
|
||||||
|
NumGC uint32 `json:"num_gc"`
|
||||||
|
// Goroutine 数量
|
||||||
|
NumGoroutine int `json:"num_goroutine"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiskCacheInfo 磁盘缓存目录信息
|
||||||
|
type DiskCacheInfo struct {
|
||||||
|
// 缓存目录路径
|
||||||
|
Path string `json:"path"`
|
||||||
|
// 目录是否存在
|
||||||
|
Exists bool `json:"exists"`
|
||||||
|
// 文件数量
|
||||||
|
FileCount int `json:"file_count"`
|
||||||
|
// 总大小(字节)
|
||||||
|
TotalSize int64 `json:"total_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiskSpaceInfo 磁盘空间信息
|
||||||
|
type DiskSpaceInfo struct {
|
||||||
|
// 总空间(字节)
|
||||||
|
Total uint64 `json:"total"`
|
||||||
|
// 可用空间(字节)
|
||||||
|
Free uint64 `json:"free"`
|
||||||
|
// 已用空间(字节)
|
||||||
|
Used uint64 `json:"used"`
|
||||||
|
// 使用百分比
|
||||||
|
UsedPercent float64 `json:"used_percent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PerformanceConfig 性能配置
|
||||||
|
type PerformanceConfig struct {
|
||||||
|
// 是否启用磁盘缓存
|
||||||
|
DiskCacheEnabled bool `json:"disk_cache_enabled"`
|
||||||
|
// 磁盘缓存阈值(MB)
|
||||||
|
DiskCacheThresholdMB int `json:"disk_cache_threshold_mb"`
|
||||||
|
// 磁盘缓存最大大小(MB)
|
||||||
|
DiskCacheMaxSizeMB int `json:"disk_cache_max_size_mb"`
|
||||||
|
// 磁盘缓存路径
|
||||||
|
DiskCachePath string `json:"disk_cache_path"`
|
||||||
|
// 是否在容器中运行
|
||||||
|
IsRunningInContainer bool `json:"is_running_in_container"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPerformanceStats 获取性能统计信息
|
||||||
|
func GetPerformanceStats(c *gin.Context) {
|
||||||
|
// 获取缓存统计
|
||||||
|
cacheStats := common.GetDiskCacheStats()
|
||||||
|
|
||||||
|
// 获取内存统计
|
||||||
|
var memStats runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&memStats)
|
||||||
|
|
||||||
|
// 获取磁盘缓存目录信息
|
||||||
|
diskCacheInfo := getDiskCacheInfo()
|
||||||
|
|
||||||
|
// 获取配置信息
|
||||||
|
diskConfig := common.GetDiskCacheConfig()
|
||||||
|
config := PerformanceConfig{
|
||||||
|
DiskCacheEnabled: diskConfig.Enabled,
|
||||||
|
DiskCacheThresholdMB: diskConfig.ThresholdMB,
|
||||||
|
DiskCacheMaxSizeMB: diskConfig.MaxSizeMB,
|
||||||
|
DiskCachePath: diskConfig.Path,
|
||||||
|
IsRunningInContainer: common.IsRunningInContainer(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取磁盘空间信息
|
||||||
|
diskSpaceInfo := getDiskSpaceInfo()
|
||||||
|
|
||||||
|
stats := PerformanceStats{
|
||||||
|
CacheStats: cacheStats,
|
||||||
|
MemoryStats: MemoryStats{
|
||||||
|
Alloc: memStats.Alloc,
|
||||||
|
TotalAlloc: memStats.TotalAlloc,
|
||||||
|
Sys: memStats.Sys,
|
||||||
|
NumGC: memStats.NumGC,
|
||||||
|
NumGoroutine: runtime.NumGoroutine(),
|
||||||
|
},
|
||||||
|
DiskCacheInfo: diskCacheInfo,
|
||||||
|
DiskSpaceInfo: diskSpaceInfo,
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"data": stats,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearDiskCache 清理磁盘缓存
|
||||||
|
func ClearDiskCache(c *gin.Context) {
|
||||||
|
cachePath := common.GetDiskCachePath()
|
||||||
|
if cachePath == "" {
|
||||||
|
cachePath = os.TempDir()
|
||||||
|
}
|
||||||
|
dir := filepath.Join(cachePath, "new-api-body-cache")
|
||||||
|
|
||||||
|
// 删除缓存目录
|
||||||
|
err := os.RemoveAll(dir)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置统计
|
||||||
|
common.ResetDiskCacheStats()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "磁盘缓存已清理",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPerformanceStats 重置性能统计
|
||||||
|
func ResetPerformanceStats(c *gin.Context) {
|
||||||
|
common.ResetDiskCacheStats()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "统计信息已重置",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForceGC 强制执行 GC
|
||||||
|
func ForceGC(c *gin.Context) {
|
||||||
|
runtime.GC()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "GC 已执行",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDiskCacheInfo 获取磁盘缓存目录信息
|
||||||
|
func getDiskCacheInfo() DiskCacheInfo {
|
||||||
|
cachePath := common.GetDiskCachePath()
|
||||||
|
if cachePath == "" {
|
||||||
|
cachePath = os.TempDir()
|
||||||
|
}
|
||||||
|
dir := filepath.Join(cachePath, "new-api-body-cache")
|
||||||
|
|
||||||
|
info := DiskCacheInfo{
|
||||||
|
Path: dir,
|
||||||
|
Exists: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Exists = true
|
||||||
|
info.FileCount = 0
|
||||||
|
info.TotalSize = 0
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info.FileCount++
|
||||||
|
if fileInfo, err := entry.Info(); err == nil {
|
||||||
|
info.TotalSize += fileInfo.Size()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
37
controller/performance_unix.go
Normal file
37
controller/performance_unix.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getDiskSpaceInfo 获取缓存目录所在磁盘的空间信息 (Unix/Linux/macOS)
|
||||||
|
func getDiskSpaceInfo() DiskSpaceInfo {
|
||||||
|
cachePath := common.GetDiskCachePath()
|
||||||
|
if cachePath == "" {
|
||||||
|
cachePath = os.TempDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
info := DiskSpaceInfo{}
|
||||||
|
|
||||||
|
var stat unix.Statfs_t
|
||||||
|
err := unix.Statfs(cachePath, &stat)
|
||||||
|
if err != nil {
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算磁盘空间
|
||||||
|
info.Total = stat.Blocks * uint64(stat.Bsize)
|
||||||
|
info.Free = stat.Bavail * uint64(stat.Bsize)
|
||||||
|
info.Used = info.Total - stat.Bfree*uint64(stat.Bsize)
|
||||||
|
|
||||||
|
if info.Total > 0 {
|
||||||
|
info.UsedPercent = float64(info.Used) / float64(info.Total) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
52
controller/performance_windows.go
Normal file
52
controller/performance_windows.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getDiskSpaceInfo 获取缓存目录所在磁盘的空间信息 (Windows)
|
||||||
|
func getDiskSpaceInfo() DiskSpaceInfo {
|
||||||
|
cachePath := common.GetDiskCachePath()
|
||||||
|
if cachePath == "" {
|
||||||
|
cachePath = os.TempDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
info := DiskSpaceInfo{}
|
||||||
|
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
getDiskFreeSpaceEx := kernel32.NewProc("GetDiskFreeSpaceExW")
|
||||||
|
|
||||||
|
var freeBytesAvailable, totalBytes, totalFreeBytes uint64
|
||||||
|
|
||||||
|
pathPtr, err := syscall.UTF16PtrFromString(cachePath)
|
||||||
|
if err != nil {
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, _, _ := getDiskFreeSpaceEx.Call(
|
||||||
|
uintptr(unsafe.Pointer(pathPtr)),
|
||||||
|
uintptr(unsafe.Pointer(&freeBytesAvailable)),
|
||||||
|
uintptr(unsafe.Pointer(&totalBytes)),
|
||||||
|
uintptr(unsafe.Pointer(&totalFreeBytes)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if ret == 0 {
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Total = totalBytes
|
||||||
|
info.Free = freeBytesAvailable
|
||||||
|
info.Used = totalBytes - totalFreeBytes
|
||||||
|
|
||||||
|
if info.Total > 0 {
|
||||||
|
info.UsedPercent = float64(info.Used) / float64(info.Total) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
5
main.go
5
main.go
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/QuantumNous/new-api/router"
|
"github.com/QuantumNous/new-api/router"
|
||||||
"github.com/QuantumNous/new-api/service"
|
"github.com/QuantumNous/new-api/service"
|
||||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||||
|
_ "github.com/QuantumNous/new-api/setting/performance_setting" // 注册性能设置
|
||||||
|
|
||||||
"github.com/bytedance/gopkg/util/gopool"
|
"github.com/bytedance/gopkg/util/gopool"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
@@ -146,6 +147,7 @@ func main() {
|
|||||||
// This will cause SSE not to work!!!
|
// This will cause SSE not to work!!!
|
||||||
//server.Use(gzip.Gzip(gzip.DefaultCompression))
|
//server.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||||
server.Use(middleware.RequestId())
|
server.Use(middleware.RequestId())
|
||||||
|
server.Use(middleware.PoweredBy())
|
||||||
middleware.SetUpLogger(server)
|
middleware.SetUpLogger(server)
|
||||||
// Initialize session store
|
// Initialize session store
|
||||||
store := cookie.NewStore([]byte(common.SessionSecret))
|
store := cookie.NewStore([]byte(common.SessionSecret))
|
||||||
@@ -252,6 +254,9 @@ func InitResources() error {
|
|||||||
// Initialize options, should after model.InitDB()
|
// Initialize options, should after model.InitDB()
|
||||||
model.InitOptionMap()
|
model.InitOptionMap()
|
||||||
|
|
||||||
|
// 清理旧的磁盘缓存文件
|
||||||
|
common.CleanupOldCacheFiles()
|
||||||
|
|
||||||
// 初始化模型
|
// 初始化模型
|
||||||
model.GetPricing()
|
model.GetPricing()
|
||||||
|
|
||||||
|
|||||||
18
middleware/body_cleanup.go
Normal file
18
middleware/body_cleanup.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BodyStorageCleanup 请求体存储清理中间件
|
||||||
|
// 在请求处理完成后自动清理磁盘/内存缓存
|
||||||
|
func BodyStorageCleanup() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 处理请求
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
// 请求结束后清理存储
|
||||||
|
common.CleanupBodyStorage(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -13,3 +14,10 @@ func CORS() gin.HandlerFunc {
|
|||||||
config.AllowHeaders = []string{"*"}
|
config.AllowHeaders = []string{"*"}
|
||||||
return cors.New(config)
|
return cors.New(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PoweredBy() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Header("X-New-Api-Version", common.Version)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/QuantumNous/new-api/setting"
|
"github.com/QuantumNous/new-api/setting"
|
||||||
"github.com/QuantumNous/new-api/setting/config"
|
"github.com/QuantumNous/new-api/setting/config"
|
||||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||||
|
"github.com/QuantumNous/new-api/setting/performance_setting"
|
||||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||||
)
|
)
|
||||||
@@ -480,5 +481,11 @@ func handleConfigUpdate(key, value string) bool {
|
|||||||
}
|
}
|
||||||
config.UpdateConfigFromMap(cfg, configMap)
|
config.UpdateConfigFromMap(cfg, configMap)
|
||||||
|
|
||||||
|
// 特定配置的后处理
|
||||||
|
if configName == "performance_setting" {
|
||||||
|
// 同步磁盘缓存配置到 common 包
|
||||||
|
performance_setting.UpdateAndSync()
|
||||||
|
}
|
||||||
|
|
||||||
return true // 已处理
|
return true // 已处理
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
func SetApiRouter(router *gin.Engine) {
|
func SetApiRouter(router *gin.Engine) {
|
||||||
apiRouter := router.Group("/api")
|
apiRouter := router.Group("/api")
|
||||||
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
|
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||||
|
apiRouter.Use(middleware.BodyStorageCleanup()) // 清理请求体存储
|
||||||
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
||||||
{
|
{
|
||||||
apiRouter.GET("/setup", controller.GetSetup)
|
apiRouter.GET("/setup", controller.GetSetup)
|
||||||
@@ -128,6 +129,14 @@ 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) // 用于迁移检测的旧键,下个版本会删除
|
||||||
}
|
}
|
||||||
|
performanceRoute := apiRouter.Group("/performance")
|
||||||
|
performanceRoute.Use(middleware.RootAuth())
|
||||||
|
{
|
||||||
|
performanceRoute.GET("/stats", controller.GetPerformanceStats)
|
||||||
|
performanceRoute.DELETE("/disk_cache", controller.ClearDiskCache)
|
||||||
|
performanceRoute.POST("/reset_stats", controller.ResetPerformanceStats)
|
||||||
|
performanceRoute.POST("/gc", controller.ForceGC)
|
||||||
|
}
|
||||||
ratioSyncRoute := apiRouter.Group("/ratio_sync")
|
ratioSyncRoute := apiRouter.Group("/ratio_sync")
|
||||||
ratioSyncRoute.Use(middleware.RootAuth())
|
ratioSyncRoute.Use(middleware.RootAuth())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
func SetRelayRouter(router *gin.Engine) {
|
func SetRelayRouter(router *gin.Engine) {
|
||||||
router.Use(middleware.CORS())
|
router.Use(middleware.CORS())
|
||||||
router.Use(middleware.DecompressRequestMiddleware())
|
router.Use(middleware.DecompressRequestMiddleware())
|
||||||
|
router.Use(middleware.BodyStorageCleanup()) // 清理请求体存储
|
||||||
router.Use(middleware.StatsMiddleware())
|
router.Use(middleware.StatsMiddleware())
|
||||||
// https://platform.openai.com/docs/api-reference/introduction
|
// https://platform.openai.com/docs/api-reference/introduction
|
||||||
modelsRouter := router.Group("/v1/models")
|
modelsRouter := router.Group("/v1/models")
|
||||||
|
|||||||
64
setting/performance_setting/config.go
Normal file
64
setting/performance_setting/config.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package performance_setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
"github.com/QuantumNous/new-api/setting/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PerformanceSetting 性能设置配置
|
||||||
|
type PerformanceSetting struct {
|
||||||
|
// DiskCacheEnabled 是否启用磁盘缓存(磁盘换内存)
|
||||||
|
DiskCacheEnabled bool `json:"disk_cache_enabled"`
|
||||||
|
// DiskCacheThresholdMB 触发磁盘缓存的请求体大小阈值(MB)
|
||||||
|
DiskCacheThresholdMB int `json:"disk_cache_threshold_mb"`
|
||||||
|
// DiskCacheMaxSizeMB 磁盘缓存最大总大小(MB)
|
||||||
|
DiskCacheMaxSizeMB int `json:"disk_cache_max_size_mb"`
|
||||||
|
// DiskCachePath 磁盘缓存目录
|
||||||
|
DiskCachePath string `json:"disk_cache_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认配置
|
||||||
|
var performanceSetting = PerformanceSetting{
|
||||||
|
DiskCacheEnabled: false,
|
||||||
|
DiskCacheThresholdMB: 10, // 超过 10MB 使用磁盘缓存
|
||||||
|
DiskCacheMaxSizeMB: 1024, // 最大 1GB 磁盘缓存
|
||||||
|
DiskCachePath: "", // 空表示使用系统临时目录
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// 注册到全局配置管理器
|
||||||
|
config.GlobalConfig.Register("performance_setting", &performanceSetting)
|
||||||
|
// 同步初始配置到 common 包
|
||||||
|
syncToCommon()
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncToCommon 将配置同步到 common 包
|
||||||
|
func syncToCommon() {
|
||||||
|
common.SetDiskCacheConfig(common.DiskCacheConfig{
|
||||||
|
Enabled: performanceSetting.DiskCacheEnabled,
|
||||||
|
ThresholdMB: performanceSetting.DiskCacheThresholdMB,
|
||||||
|
MaxSizeMB: performanceSetting.DiskCacheMaxSizeMB,
|
||||||
|
Path: performanceSetting.DiskCachePath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPerformanceSetting 获取性能设置
|
||||||
|
func GetPerformanceSetting() *PerformanceSetting {
|
||||||
|
return &performanceSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAndSync 更新配置并同步到 common 包
|
||||||
|
// 当配置从数据库加载后,需要调用此函数同步
|
||||||
|
func UpdateAndSync() {
|
||||||
|
syncToCommon()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCacheStats 获取缓存统计信息(代理到 common 包)
|
||||||
|
func GetCacheStats() common.DiskCacheStats {
|
||||||
|
return common.GetDiskCacheStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetStats 重置统计信息
|
||||||
|
func ResetStats() {
|
||||||
|
common.ResetDiskCacheStats()
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
name="description"
|
name="description"
|
||||||
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
|
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
|
||||||
/>
|
/>
|
||||||
|
<meta name="generator" content="new-api" />
|
||||||
<title>New API</title>
|
<title>New API</title>
|
||||||
<!--umami-->
|
<!--umami-->
|
||||||
<!--Google Analytics-->
|
<!--Google Analytics-->
|
||||||
|
|||||||
80
web/src/components/settings/PerformanceSetting.jsx
Normal file
80
web/src/components/settings/PerformanceSetting.jsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
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 } from 'react';
|
||||||
|
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||||
|
import SettingsPerformance from '../../pages/Setting/Performance/SettingsPerformance';
|
||||||
|
import { API, showError, toBoolean } from '../../helpers';
|
||||||
|
|
||||||
|
const PerformanceSetting = () => {
|
||||||
|
let [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': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
let [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const getOptions = async () => {
|
||||||
|
const res = await API.get('/api/option/');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
let newInputs = {};
|
||||||
|
data.forEach((item) => {
|
||||||
|
if (typeof inputs[item.key] === 'boolean') {
|
||||||
|
newInputs[item.key] = toBoolean(item.value);
|
||||||
|
} else {
|
||||||
|
newInputs[item.key] = item.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setInputs(newInputs);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function onRefresh() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await getOptions();
|
||||||
|
} catch (error) {
|
||||||
|
showError('刷新失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onRefresh();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Spin spinning={loading} size='large'>
|
||||||
|
{/* 性能设置 */}
|
||||||
|
<Card style={{ marginTop: '10px' }}>
|
||||||
|
<SettingsPerformance options={inputs} refresh={onRefresh} />
|
||||||
|
</Card>
|
||||||
|
</Spin>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PerformanceSetting;
|
||||||
382
web/src/pages/Setting/Performance/SettingsPerformance.jsx
Normal file
382
web/src/pages/Setting/Performance/SettingsPerformance.jsx
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
/*
|
||||||
|
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': '',
|
||||||
|
});
|
||||||
|
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>
|
||||||
|
<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('这将删除所有临时缓存文件')}
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
Palette,
|
Palette,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Server,
|
Server,
|
||||||
|
Activity,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import SystemSetting from '../../components/settings/SystemSetting';
|
import SystemSetting from '../../components/settings/SystemSetting';
|
||||||
@@ -47,6 +48,7 @@ import ChatsSetting from '../../components/settings/ChatsSetting';
|
|||||||
import DrawingSetting from '../../components/settings/DrawingSetting';
|
import DrawingSetting from '../../components/settings/DrawingSetting';
|
||||||
import PaymentSetting from '../../components/settings/PaymentSetting';
|
import PaymentSetting from '../../components/settings/PaymentSetting';
|
||||||
import ModelDeploymentSetting from '../../components/settings/ModelDeploymentSetting';
|
import ModelDeploymentSetting from '../../components/settings/ModelDeploymentSetting';
|
||||||
|
import PerformanceSetting from '../../components/settings/PerformanceSetting';
|
||||||
|
|
||||||
const Setting = () => {
|
const Setting = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -146,6 +148,16 @@ const Setting = () => {
|
|||||||
content: <ModelDeploymentSetting />,
|
content: <ModelDeploymentSetting />,
|
||||||
itemKey: 'model-deployment',
|
itemKey: 'model-deployment',
|
||||||
});
|
});
|
||||||
|
panes.push({
|
||||||
|
tab: (
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
|
<Activity size={18} />
|
||||||
|
{t('性能设置')}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
content: <PerformanceSetting />,
|
||||||
|
itemKey: 'performance',
|
||||||
|
});
|
||||||
panes.push({
|
panes.push({
|
||||||
tab: (
|
tab: (
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user