Ver código fonte

feat(cache): enhance disk cache management with concurrency control and cleanup optimizations

CaIon 1 semana atrás
pai
commit
e2ebd42a8c

+ 5 - 1
common/disk_cache.go

@@ -127,7 +127,11 @@ func CleanupOldDiskCacheFiles(maxAge time.Duration) error {
 			continue
 			continue
 		}
 		}
 		if now.Sub(info.ModTime()) > maxAge {
 		if now.Sub(info.ModTime()) > maxAge {
-			os.Remove(filepath.Join(dir, entry.Name()))
+			// 注意:后台清理任务删除文件时,由于无法得知原始 base64Size,
+			// 只能按磁盘文件大小扣减。这在目前 base64 存储模式下是准确的。
+			if err := os.Remove(filepath.Join(dir, entry.Name())); err == nil {
+				DecrementDiskFiles(info.Size())
+			}
 		}
 		}
 	}
 	}
 	return nil
 	return nil

+ 6 - 2
common/disk_cache_config.go

@@ -113,8 +113,12 @@ func IncrementDiskFiles(size int64) {
 
 
 // DecrementDiskFiles 减少磁盘文件计数
 // DecrementDiskFiles 减少磁盘文件计数
 func DecrementDiskFiles(size int64) {
 func DecrementDiskFiles(size int64) {
-	atomic.AddInt64(&diskCacheStats.ActiveDiskFiles, -1)
-	atomic.AddInt64(&diskCacheStats.CurrentDiskUsageBytes, -size)
+	if atomic.AddInt64(&diskCacheStats.ActiveDiskFiles, -1) < 0 {
+		atomic.StoreInt64(&diskCacheStats.ActiveDiskFiles, 0)
+	}
+	if atomic.AddInt64(&diskCacheStats.CurrentDiskUsageBytes, -size) < 0 {
+		atomic.StoreInt64(&diskCacheStats.CurrentDiskUsageBytes, 0)
+	}
 }
 }
 
 
 // IncrementMemoryBuffers 增加内存缓存计数
 // IncrementMemoryBuffers 增加内存缓存计数

+ 9 - 16
controller/performance.go

@@ -4,6 +4,7 @@ import (
 	"net/http"
 	"net/http"
 	"os"
 	"os"
 	"runtime"
 	"runtime"
+	"time"
 
 
 	"github.com/QuantumNous/new-api/common"
 	"github.com/QuantumNous/new-api/common"
 	"github.com/gin-gonic/gin"
 	"github.com/gin-gonic/gin"
@@ -77,10 +78,8 @@ type PerformanceConfig struct {
 
 
 // GetPerformanceStats 获取性能统计信息
 // GetPerformanceStats 获取性能统计信息
 func GetPerformanceStats(c *gin.Context) {
 func GetPerformanceStats(c *gin.Context) {
-	// 先同步磁盘缓存统计,确保显示准确
-	common.SyncDiskCacheStats()
-
-	// 获取缓存统计
+	// 不再每次获取统计都全量扫描磁盘,依赖原子计数器保证性能
+	// 仅在系统启动或显式清理时同步
 	cacheStats := common.GetDiskCacheStats()
 	cacheStats := common.GetDiskCacheStats()
 
 
 	// 获取内存统计
 	// 获取内存统计
@@ -123,25 +122,19 @@ func GetPerformanceStats(c *gin.Context) {
 	})
 	})
 }
 }
 
 
-// ClearDiskCache 清理磁盘缓存
+// ClearDiskCache 清理不活跃的磁盘缓存
 func ClearDiskCache(c *gin.Context) {
 func ClearDiskCache(c *gin.Context) {
-	// 使用统一的缓存目录
-	dir := common.GetDiskCacheDir()
-
-	// 删除缓存目录
-	err := os.RemoveAll(dir)
-	if err != nil && !os.IsNotExist(err) {
+	// 清理超过 10 分钟未使用的缓存文件
+	// 10 分钟是一个安全的阈值,确保正在进行的请求不会被误删
+	err := common.CleanupOldDiskCacheFiles(10 * time.Minute)
+	if err != nil {
 		common.ApiError(c, err)
 		common.ApiError(c, err)
 		return
 		return
 	}
 	}
 
 
-	// 重置统计(包括命中次数和使用量)
-	common.ResetDiskCacheStats()
-	common.ResetDiskCacheUsage()
-
 	c.JSON(http.StatusOK, gin.H{
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"success": true,
-		"message": "磁盘缓存已清理",
+		"message": "不活跃的磁盘缓存已清理",
 	})
 	})
 }
 }
 
 

+ 63 - 43
service/file_service.go

@@ -36,11 +36,44 @@ func LoadFileSource(c *gin.Context, source *types.FileSource, reason ...string)
 		return nil, fmt.Errorf("file source is nil")
 		return nil, fmt.Errorf("file source is nil")
 	}
 	}
 
 
-	// 如果已有缓存,直接返回
+	if common.DebugEnabled {
+		logger.LogDebug(c, fmt.Sprintf("LoadFileSource starting for: %s", source.GetIdentifier()))
+	}
+
+	// 1. 快速检查内部缓存
 	if source.HasCache() {
 	if source.HasCache() {
+		// 即使命中内部缓存,也要确保注册到清理列表(如果尚未注册)
+		if c != nil {
+			registerSourceForCleanup(c, source)
+		}
 		return source.GetCache(), nil
 		return source.GetCache(), nil
 	}
 	}
 
 
+	// 2. 加锁保护加载过程
+	source.Mu().Lock()
+	defer source.Mu().Unlock()
+
+	// 3. 双重检查
+	if source.HasCache() {
+		if c != nil {
+			registerSourceForCleanup(c, source)
+		}
+		return source.GetCache(), nil
+	}
+
+	// 4. 如果是 URL,检查 Context 缓存
+	var contextKey string
+	if source.IsURL() && c != nil {
+		contextKey = getContextCacheKey(source.URL)
+		if cachedData, exists := c.Get(contextKey); exists {
+			data := cachedData.(*types.CachedFileData)
+			source.SetCache(data)
+			registerSourceForCleanup(c, source)
+			return data, nil
+		}
+	}
+
+	// 5. 执行加载逻辑
 	var cachedData *types.CachedFileData
 	var cachedData *types.CachedFileData
 	var err error
 	var err error
 
 
@@ -54,10 +87,13 @@ func LoadFileSource(c *gin.Context, source *types.FileSource, reason ...string)
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	// 设置缓存
+	// 6. 设置缓存
 	source.SetCache(cachedData)
 	source.SetCache(cachedData)
+	if contextKey != "" && c != nil {
+		c.Set(contextKey, cachedData)
+	}
 
 
-	// 注册到 context 以便请求结束时自动清理
+	// 7. 注册到 context 以便请求结束时自动清理
 	if c != nil {
 	if c != nil {
 		registerSourceForCleanup(c, source)
 		registerSourceForCleanup(c, source)
 	}
 	}
@@ -67,6 +103,10 @@ func LoadFileSource(c *gin.Context, source *types.FileSource, reason ...string)
 
 
 // registerSourceForCleanup 注册 FileSource 到 context 以便请求结束时清理
 // registerSourceForCleanup 注册 FileSource 到 context 以便请求结束时清理
 func registerSourceForCleanup(c *gin.Context, source *types.FileSource) {
 func registerSourceForCleanup(c *gin.Context, source *types.FileSource) {
+	if source.IsRegistered() {
+		return
+	}
+
 	key := string(constant.ContextKeyFileSourcesToCleanup)
 	key := string(constant.ContextKeyFileSourcesToCleanup)
 	var sources []*types.FileSource
 	var sources []*types.FileSource
 	if existing, exists := c.Get(key); exists {
 	if existing, exists := c.Get(key); exists {
@@ -74,6 +114,7 @@ func registerSourceForCleanup(c *gin.Context, source *types.FileSource) {
 	}
 	}
 	sources = append(sources, source)
 	sources = append(sources, source)
 	c.Set(key, sources)
 	c.Set(key, sources)
+	source.SetRegistered(true)
 }
 }
 
 
 // CleanupFileSources 清理请求中所有注册的 FileSource
 // CleanupFileSources 清理请求中所有注册的 FileSource
@@ -83,9 +124,6 @@ func CleanupFileSources(c *gin.Context) {
 	if sources, exists := c.Get(key); exists {
 	if sources, exists := c.Get(key); exists {
 		for _, source := range sources.([]*types.FileSource) {
 		for _, source := range sources.([]*types.FileSource) {
 			if cache := source.GetCache(); cache != nil {
 			if cache := source.GetCache(); cache != nil {
-				if cache.IsDisk() {
-					common.DecrementDiskFiles(cache.Size)
-				}
 				cache.Close()
 				cache.Close()
 			}
 			}
 		}
 		}
@@ -94,21 +132,13 @@ func CleanupFileSources(c *gin.Context) {
 }
 }
 
 
 // loadFromURL 从 URL 加载文件
 // loadFromURL 从 URL 加载文件
-// 支持磁盘缓存:当文件大小超过阈值且磁盘缓存可用时,将数据存储到磁盘
 func loadFromURL(c *gin.Context, url string, reason ...string) (*types.CachedFileData, error) {
 func loadFromURL(c *gin.Context, url string, reason ...string) (*types.CachedFileData, error) {
-	contextKey := getContextCacheKey(url)
-
-	// 检查 context 缓存
-	if cachedData, exists := c.Get(contextKey); exists {
-		if common.DebugEnabled {
-			logger.LogDebug(c, fmt.Sprintf("Using cached file data for URL: %s", url))
-		}
-		return cachedData.(*types.CachedFileData), nil
-	}
-
 	// 下载文件
 	// 下载文件
 	var maxFileSize = constant.MaxFileDownloadMB * 1024 * 1024
 	var maxFileSize = constant.MaxFileDownloadMB * 1024 * 1024
 
 
+	if common.DebugEnabled {
+		logger.LogDebug(c, "loadFromURL: initiating download")
+	}
 	resp, err := DoDownloadRequest(url, reason...)
 	resp, err := DoDownloadRequest(url, reason...)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("failed to download file from %s: %w", url, err)
 		return nil, fmt.Errorf("failed to download file from %s: %w", url, err)
@@ -120,6 +150,9 @@ func loadFromURL(c *gin.Context, url string, reason ...string) (*types.CachedFil
 	}
 	}
 
 
 	// 读取文件内容(限制大小)
 	// 读取文件内容(限制大小)
+	if common.DebugEnabled {
+		logger.LogDebug(c, "loadFromURL: reading response body")
+	}
 	fileBytes, err := io.ReadAll(io.LimitReader(resp.Body, int64(maxFileSize+1)))
 	fileBytes, err := io.ReadAll(io.LimitReader(resp.Body, int64(maxFileSize+1)))
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("failed to read file content: %w", err)
 		return nil, fmt.Errorf("failed to read file content: %w", err)
@@ -147,6 +180,10 @@ func loadFromURL(c *gin.Context, url string, reason ...string) (*types.CachedFil
 			cachedData = types.NewMemoryCachedData(base64Data, mimeType, int64(len(fileBytes)))
 			cachedData = types.NewMemoryCachedData(base64Data, mimeType, int64(len(fileBytes)))
 		} else {
 		} else {
 			cachedData = types.NewDiskCachedData(diskPath, mimeType, int64(len(fileBytes)))
 			cachedData = types.NewDiskCachedData(diskPath, mimeType, int64(len(fileBytes)))
+			cachedData.DiskSize = base64Size
+			cachedData.OnClose = func(size int64) {
+				common.DecrementDiskFiles(size)
+			}
 			common.IncrementDiskFiles(base64Size)
 			common.IncrementDiskFiles(base64Size)
 			if common.DebugEnabled {
 			if common.DebugEnabled {
 				logger.LogDebug(c, fmt.Sprintf("File cached to disk: %s, size: %d bytes", diskPath, base64Size))
 				logger.LogDebug(c, fmt.Sprintf("File cached to disk: %s, size: %d bytes", diskPath, base64Size))
@@ -159,6 +196,9 @@ func loadFromURL(c *gin.Context, url string, reason ...string) (*types.CachedFil
 
 
 	// 如果是图片,尝试获取图片配置
 	// 如果是图片,尝试获取图片配置
 	if strings.HasPrefix(mimeType, "image/") {
 	if strings.HasPrefix(mimeType, "image/") {
+		if common.DebugEnabled {
+			logger.LogDebug(c, "loadFromURL: decoding image config")
+		}
 		config, format, err := decodeImageConfig(fileBytes)
 		config, format, err := decodeImageConfig(fileBytes)
 		if err == nil {
 		if err == nil {
 			cachedData.ImageConfig = &config
 			cachedData.ImageConfig = &config
@@ -170,9 +210,6 @@ func loadFromURL(c *gin.Context, url string, reason ...string) (*types.CachedFil
 		}
 		}
 	}
 	}
 
 
-	// 存入 context 缓存
-	c.Set(contextKey, cachedData)
-
 	return cachedData, nil
 	return cachedData, nil
 }
 }
 
 
@@ -187,7 +224,6 @@ func writeToDiskCache(base64Data string) (string, error) {
 }
 }
 
 
 // smartDetectMimeType 智能检测 MIME 类型
 // smartDetectMimeType 智能检测 MIME 类型
-// 优先级:Content-Type header > Content-Disposition filename > URL 路径 > 内容嗅探 > 图片解码
 func smartDetectMimeType(resp *http.Response, url string, fileBytes []byte) string {
 func smartDetectMimeType(resp *http.Response, url string, fileBytes []byte) string {
 	// 1. 尝试从 Content-Type header 获取
 	// 1. 尝试从 Content-Type header 获取
 	mimeType := resp.Header.Get("Content-Type")
 	mimeType := resp.Header.Get("Content-Type")
@@ -259,13 +295,11 @@ func loadFromBase64(base64String string, providedMimeType string) (*types.Cached
 
 
 	// 处理 data: 前缀
 	// 处理 data: 前缀
 	if strings.HasPrefix(base64String, "data:") {
 	if strings.HasPrefix(base64String, "data:") {
-		// 格式: data:mime/type;base64,xxxxx
 		idx := strings.Index(base64String, ",")
 		idx := strings.Index(base64String, ",")
 		if idx != -1 {
 		if idx != -1 {
 			header := base64String[:idx]
 			header := base64String[:idx]
 			cleanBase64 = base64String[idx+1:]
 			cleanBase64 = base64String[idx+1:]
 
 
-			// 从 header 提取 MIME 类型
 			if strings.Contains(header, ":") && strings.Contains(header, ";") {
 			if strings.Contains(header, ":") && strings.Contains(header, ";") {
 				mimeStart := strings.Index(header, ":") + 1
 				mimeStart := strings.Index(header, ":") + 1
 				mimeEnd := strings.Index(header, ";")
 				mimeEnd := strings.Index(header, ";")
@@ -280,36 +314,34 @@ func loadFromBase64(base64String string, providedMimeType string) (*types.Cached
 		cleanBase64 = base64String
 		cleanBase64 = base64String
 	}
 	}
 
 
-	// 使用提供的 MIME 类型(如果有)
 	if providedMimeType != "" {
 	if providedMimeType != "" {
 		mimeType = providedMimeType
 		mimeType = providedMimeType
 	}
 	}
 
 
-	// 解码 base64
 	decodedData, err := base64.StdEncoding.DecodeString(cleanBase64)
 	decodedData, err := base64.StdEncoding.DecodeString(cleanBase64)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("failed to decode base64 data: %w", err)
 		return nil, fmt.Errorf("failed to decode base64 data: %w", err)
 	}
 	}
 
 
-	// 判断是否使用磁盘缓存(对于 base64 内联数据也支持磁盘缓存)
 	base64Size := int64(len(cleanBase64))
 	base64Size := int64(len(cleanBase64))
 	var cachedData *types.CachedFileData
 	var cachedData *types.CachedFileData
 
 
 	if shouldUseDiskCache(base64Size) {
 	if shouldUseDiskCache(base64Size) {
-		// 使用磁盘缓存
 		diskPath, err := writeToDiskCache(cleanBase64)
 		diskPath, err := writeToDiskCache(cleanBase64)
 		if err != nil {
 		if err != nil {
-			// 磁盘缓存失败,回退到内存
 			cachedData = types.NewMemoryCachedData(cleanBase64, mimeType, int64(len(decodedData)))
 			cachedData = types.NewMemoryCachedData(cleanBase64, mimeType, int64(len(decodedData)))
 		} else {
 		} else {
 			cachedData = types.NewDiskCachedData(diskPath, mimeType, int64(len(decodedData)))
 			cachedData = types.NewDiskCachedData(diskPath, mimeType, int64(len(decodedData)))
+			cachedData.DiskSize = base64Size
+			cachedData.OnClose = func(size int64) {
+				common.DecrementDiskFiles(size)
+			}
 			common.IncrementDiskFiles(base64Size)
 			common.IncrementDiskFiles(base64Size)
 		}
 		}
 	} else {
 	} else {
 		cachedData = types.NewMemoryCachedData(cleanBase64, mimeType, int64(len(decodedData)))
 		cachedData = types.NewMemoryCachedData(cleanBase64, mimeType, int64(len(decodedData)))
 	}
 	}
 
 
-	// 如果是图片或 MIME 类型未知,尝试解码图片获取更多信息
 	if mimeType == "" || strings.HasPrefix(mimeType, "image/") {
 	if mimeType == "" || strings.HasPrefix(mimeType, "image/") {
 		config, format, err := decodeImageConfig(decodedData)
 		config, format, err := decodeImageConfig(decodedData)
 		if err == nil {
 		if err == nil {
@@ -324,8 +356,7 @@ func loadFromBase64(base64String string, providedMimeType string) (*types.Cached
 	return cachedData, nil
 	return cachedData, nil
 }
 }
 
 
-// GetImageConfig 获取图片配置(宽高等信息)
-// 会自动处理缓存,避免重复下载/解码
+// GetImageConfig 获取图片配置
 func GetImageConfig(c *gin.Context, source *types.FileSource) (image.Config, string, error) {
 func GetImageConfig(c *gin.Context, source *types.FileSource) (image.Config, string, error) {
 	cachedData, err := LoadFileSource(c, source, "get_image_config")
 	cachedData, err := LoadFileSource(c, source, "get_image_config")
 	if err != nil {
 	if err != nil {
@@ -336,7 +367,6 @@ func GetImageConfig(c *gin.Context, source *types.FileSource) (image.Config, str
 		return *cachedData.ImageConfig, cachedData.ImageFormat, nil
 		return *cachedData.ImageConfig, cachedData.ImageFormat, nil
 	}
 	}
 
 
-	// 如果缓存中没有图片配置,尝试解码
 	base64Str, err := cachedData.GetBase64Data()
 	base64Str, err := cachedData.GetBase64Data()
 	if err != nil {
 	if err != nil {
 		return image.Config{}, "", fmt.Errorf("failed to get base64 data: %w", err)
 		return image.Config{}, "", fmt.Errorf("failed to get base64 data: %w", err)
@@ -351,7 +381,6 @@ func GetImageConfig(c *gin.Context, source *types.FileSource) (image.Config, str
 		return image.Config{}, "", err
 		return image.Config{}, "", err
 	}
 	}
 
 
-	// 更新缓存
 	cachedData.ImageConfig = &config
 	cachedData.ImageConfig = &config
 	cachedData.ImageFormat = format
 	cachedData.ImageFormat = format
 
 
@@ -359,8 +388,6 @@ func GetImageConfig(c *gin.Context, source *types.FileSource) (image.Config, str
 }
 }
 
 
 // GetBase64Data 获取 base64 编码的数据
 // GetBase64Data 获取 base64 编码的数据
-// 会自动处理缓存,避免重复下载
-// 支持内存缓存和磁盘缓存
 func GetBase64Data(c *gin.Context, source *types.FileSource, reason ...string) (string, string, error) {
 func GetBase64Data(c *gin.Context, source *types.FileSource, reason ...string) (string, string, error) {
 	cachedData, err := LoadFileSource(c, source, reason...)
 	cachedData, err := LoadFileSource(c, source, reason...)
 	if err != nil {
 	if err != nil {
@@ -375,12 +402,10 @@ func GetBase64Data(c *gin.Context, source *types.FileSource, reason ...string) (
 
 
 // GetMimeType 获取文件的 MIME 类型
 // GetMimeType 获取文件的 MIME 类型
 func GetMimeType(c *gin.Context, source *types.FileSource) (string, error) {
 func GetMimeType(c *gin.Context, source *types.FileSource) (string, error) {
-	// 如果已经有缓存,直接返回
 	if source.HasCache() {
 	if source.HasCache() {
 		return source.GetCache().MimeType, nil
 		return source.GetCache().MimeType, nil
 	}
 	}
 
 
-	// 如果是 URL,尝试只获取 header 而不下载完整文件
 	if source.IsURL() {
 	if source.IsURL() {
 		mimeType, err := GetFileTypeFromUrl(c, source.URL, "get_mime_type")
 		mimeType, err := GetFileTypeFromUrl(c, source.URL, "get_mime_type")
 		if err == nil && mimeType != "" && mimeType != "application/octet-stream" {
 		if err == nil && mimeType != "" && mimeType != "application/octet-stream" {
@@ -388,7 +413,6 @@ func GetMimeType(c *gin.Context, source *types.FileSource) (string, error) {
 		}
 		}
 	}
 	}
 
 
-	// 否则加载完整数据
 	cachedData, err := LoadFileSource(c, source, "get_mime_type")
 	cachedData, err := LoadFileSource(c, source, "get_mime_type")
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
@@ -396,7 +420,7 @@ func GetMimeType(c *gin.Context, source *types.FileSource) (string, error) {
 	return cachedData.MimeType, nil
 	return cachedData.MimeType, nil
 }
 }
 
 
-// DetectFileType 检测文件类型(image/audio/video/file)
+// DetectFileType 检测文件类型
 func DetectFileType(mimeType string) types.FileType {
 func DetectFileType(mimeType string) types.FileType {
 	if strings.HasPrefix(mimeType, "image/") {
 	if strings.HasPrefix(mimeType, "image/") {
 		return types.FileTypeImage
 		return types.FileTypeImage
@@ -414,13 +438,11 @@ func DetectFileType(mimeType string) types.FileType {
 func decodeImageConfig(data []byte) (image.Config, string, error) {
 func decodeImageConfig(data []byte) (image.Config, string, error) {
 	reader := bytes.NewReader(data)
 	reader := bytes.NewReader(data)
 
 
-	// 尝试标准格式
 	config, format, err := image.DecodeConfig(reader)
 	config, format, err := image.DecodeConfig(reader)
 	if err == nil {
 	if err == nil {
 		return config, format, nil
 		return config, format, nil
 	}
 	}
 
 
-	// 尝试 webp
 	reader.Seek(0, io.SeekStart)
 	reader.Seek(0, io.SeekStart)
 	config, err = webp.DecodeConfig(reader)
 	config, err = webp.DecodeConfig(reader)
 	if err == nil {
 	if err == nil {
@@ -432,13 +454,11 @@ func decodeImageConfig(data []byte) (image.Config, string, error) {
 
 
 // guessMimeTypeFromURL 从 URL 猜测 MIME 类型
 // guessMimeTypeFromURL 从 URL 猜测 MIME 类型
 func guessMimeTypeFromURL(url string) string {
 func guessMimeTypeFromURL(url string) string {
-	// 移除查询参数
 	cleanedURL := url
 	cleanedURL := url
 	if q := strings.Index(cleanedURL, "?"); q != -1 {
 	if q := strings.Index(cleanedURL, "?"); q != -1 {
 		cleanedURL = cleanedURL[:q]
 		cleanedURL = cleanedURL[:q]
 	}
 	}
 
 
-	// 获取最后一段
 	if slash := strings.LastIndex(cleanedURL, "/"); slash != -1 && slash+1 < len(cleanedURL) {
 	if slash := strings.LastIndex(cleanedURL, "/"); slash != -1 && slash+1 < len(cleanedURL) {
 		last := cleanedURL[slash+1:]
 		last := cleanedURL[slash+1:]
 		if dot := strings.LastIndex(last, "."); dot != -1 && dot+1 < len(last) {
 		if dot := strings.LastIndex(last, "."); dot != -1 && dot+1 < len(last) {

+ 6 - 4
service/token_counter.go

@@ -258,16 +258,18 @@ func EstimateRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *rela
 
 
 		// 如果文件类型未知且需要获取,通过 MIME 类型检测
 		// 如果文件类型未知且需要获取,通过 MIME 类型检测
 		if file.FileType == "" || (file.Source.IsURL() && shouldFetchFiles) {
 		if file.FileType == "" || (file.Source.IsURL() && shouldFetchFiles) {
-			mimeType, err := GetMimeType(c, file.Source)
+			// 注意:这里我们直接调用 LoadFileSource 而不是 GetMimeType
+			// 因为 GetMimeType 内部可能会调用 GetFileTypeFromUrl (HEAD 请求)
+			// 而我们这里既然要计算 token,通常需要完整数据
+			cachedData, err := LoadFileSource(c, file.Source, "token_counter")
 			if err != nil {
 			if err != nil {
 				if shouldFetchFiles {
 				if shouldFetchFiles {
 					return 0, fmt.Errorf("error getting file type: %v", err)
 					return 0, fmt.Errorf("error getting file type: %v", err)
 				}
 				}
-				// 如果不需要获取,使用默认类型
 				continue
 				continue
 			}
 			}
-			file.MimeType = mimeType
-			file.FileType = DetectFileType(mimeType)
+			file.MimeType = cachedData.MimeType
+			file.FileType = DetectFileType(cachedData.MimeType)
 		}
 		}
 	}
 	}
 
 

+ 33 - 15
types/file_source.go

@@ -25,8 +25,14 @@ type FileSource struct {
 
 
 	// 内部缓存(不导出,不序列化)
 	// 内部缓存(不导出,不序列化)
 	cachedData  *CachedFileData
 	cachedData  *CachedFileData
-	cacheMu     sync.RWMutex
 	cacheLoaded bool
 	cacheLoaded bool
+	registered  bool       // 是否已注册到清理列表
+	mu          sync.Mutex // 保护加载过程
+}
+
+// Mu 获取内部锁
+func (f *FileSource) Mu() *sync.Mutex {
+	return &f.mu
 }
 }
 
 
 // CachedFileData 缓存的文件数据
 // CachedFileData 缓存的文件数据
@@ -35,14 +41,19 @@ type CachedFileData struct {
 	base64Data  string        // 内存中的 base64 数据(小文件)
 	base64Data  string        // 内存中的 base64 数据(小文件)
 	MimeType    string        // MIME 类型
 	MimeType    string        // MIME 类型
 	Size        int64         // 文件大小(字节)
 	Size        int64         // 文件大小(字节)
+	DiskSize    int64         // 磁盘缓存实际占用大小(字节,通常是 base64 长度)
 	ImageConfig *image.Config // 图片配置(如果是图片)
 	ImageConfig *image.Config // 图片配置(如果是图片)
 	ImageFormat string        // 图片格式(如果是图片)
 	ImageFormat string        // 图片格式(如果是图片)
 
 
 	// 磁盘缓存相关
 	// 磁盘缓存相关
-	diskPath   string     // 磁盘缓存文件路径(大文件)
-	isDisk     bool       // 是否使用磁盘缓存
-	diskMu     sync.Mutex // 磁盘操作锁
-	diskClosed bool       // 是否已关闭/清理
+	diskPath        string     // 磁盘缓存文件路径(大文件)
+	isDisk          bool       // 是否使用磁盘缓存
+	diskMu          sync.Mutex // 磁盘操作锁(保护磁盘文件的读取和删除)
+	diskClosed      bool       // 是否已关闭/清理
+	statDecremented bool       // 是否已扣减统计
+
+	// 统计回调,避免循环依赖
+	OnClose func(size int64)
 }
 }
 
 
 // NewMemoryCachedData 创建内存缓存的数据
 // NewMemoryCachedData 创建内存缓存的数据
@@ -114,7 +125,13 @@ func (c *CachedFileData) Close() error {
 
 
 	c.diskClosed = true
 	c.diskClosed = true
 	if c.diskPath != "" {
 	if c.diskPath != "" {
-		return os.Remove(c.diskPath)
+		err := os.Remove(c.diskPath)
+		// 只有在删除成功且未扣减过统计时,才执行回调
+		if err == nil && !c.statDecremented && c.OnClose != nil {
+			c.OnClose(c.DiskSize)
+			c.statDecremented = true
+		}
+		return err
 	}
 	}
 	return nil
 	return nil
 }
 }
@@ -170,31 +187,32 @@ func (f *FileSource) GetRawData() string {
 
 
 // SetCache 设置缓存数据
 // SetCache 设置缓存数据
 func (f *FileSource) SetCache(data *CachedFileData) {
 func (f *FileSource) SetCache(data *CachedFileData) {
-	f.cacheMu.Lock()
-	defer f.cacheMu.Unlock()
 	f.cachedData = data
 	f.cachedData = data
 	f.cacheLoaded = true
 	f.cacheLoaded = true
 }
 }
 
 
+// IsRegistered 是否已注册到清理列表
+func (f *FileSource) IsRegistered() bool {
+	return f.registered
+}
+
+// SetRegistered 设置注册状态
+func (f *FileSource) SetRegistered(registered bool) {
+	f.registered = registered
+}
+
 // GetCache 获取缓存数据
 // GetCache 获取缓存数据
 func (f *FileSource) GetCache() *CachedFileData {
 func (f *FileSource) GetCache() *CachedFileData {
-	f.cacheMu.RLock()
-	defer f.cacheMu.RUnlock()
 	return f.cachedData
 	return f.cachedData
 }
 }
 
 
 // HasCache 是否有缓存
 // HasCache 是否有缓存
 func (f *FileSource) HasCache() bool {
 func (f *FileSource) HasCache() bool {
-	f.cacheMu.RLock()
-	defer f.cacheMu.RUnlock()
 	return f.cacheLoaded && f.cachedData != nil
 	return f.cacheLoaded && f.cachedData != nil
 }
 }
 
 
 // ClearCache 清除缓存,释放内存和磁盘文件
 // ClearCache 清除缓存,释放内存和磁盘文件
 func (f *FileSource) ClearCache() {
 func (f *FileSource) ClearCache() {
-	f.cacheMu.Lock()
-	defer f.cacheMu.Unlock()
-
 	// 如果有缓存数据,先关闭它(会清理磁盘文件)
 	// 如果有缓存数据,先关闭它(会清理磁盘文件)
 	if f.cachedData != nil {
 	if f.cachedData != nil {
 		f.cachedData.Close()
 		f.cachedData.Close()

+ 3 - 0
web/src/i18n/locales/zh.json

@@ -442,6 +442,9 @@
     "兑换人ID": "兑换人ID",
     "兑换人ID": "兑换人ID",
     "兑换成功!": "兑换成功!",
     "兑换成功!": "兑换成功!",
     "兑换码充值": "兑换码充值",
     "兑换码充值": "兑换码充值",
+    "确认清理不活跃的磁盘缓存?": "确认清理不活跃的磁盘缓存?",
+    "这将删除超过 10 分钟未使用的临时缓存文件": "这将删除超过 10 分钟未使用的临时缓存文件",
+    "清理不活跃缓存": "清理不活跃缓存",
     "兑换码创建成功": "兑换码创建成功",
     "兑换码创建成功": "兑换码创建成功",
     "兑换码创建成功,是否下载兑换码?": "兑换码创建成功,是否下载兑换码?",
     "兑换码创建成功,是否下载兑换码?": "兑换码创建成功,是否下载兑换码?",
     "兑换码创建成功!": "兑换码创建成功!",
     "兑换码创建成功!": "兑换码创建成功!",

+ 3 - 3
web/src/pages/Setting/Performance/SettingsPerformance.jsx

@@ -291,11 +291,11 @@ export default function SettingsPerformance(props) {
               <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
               <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                 <Button onClick={fetchStats}>{t('刷新统计')}</Button>
                 <Button onClick={fetchStats}>{t('刷新统计')}</Button>
                 <Popconfirm
                 <Popconfirm
-                  title={t('确认清理磁盘缓存?')}
-                  content={t('这将删除所有临时缓存文件')}
+                  title={t('确认清理不活跃的磁盘缓存?')}
+                  content={t('这将删除超过 10 分钟未使用的临时缓存文件')}
                   onConfirm={clearDiskCache}
                   onConfirm={clearDiskCache}
                 >
                 >
-                  <Button type='warning'>{t('清理磁盘缓存')}</Button>
+                  <Button type='warning'>{t('清理不活跃缓存')}</Button>
                 </Popconfirm>
                 </Popconfirm>
                 <Button onClick={resetStats}>{t('重置统计')}</Button>
                 <Button onClick={resetStats}>{t('重置统计')}</Button>
                 <Button onClick={forceGC}>{t('执行 GC')}</Button>
                 <Button onClick={forceGC}>{t('执行 GC')}</Button>