Explorar o código

Merge pull request #3369 from RedwindA/feat/logsManagement

feat: add server log file management to performance settings
Seefs hai 1 mes
pai
achega
87b426f306

+ 15 - 8
common/sys_log.go

@@ -3,53 +3,60 @@ package common
 import (
 	"fmt"
 	"os"
+	"sync"
 	"time"
 
 	"github.com/gin-gonic/gin"
 )
 
+// LogWriterMu protects concurrent access to gin.DefaultWriter/gin.DefaultErrorWriter
+// during log file rotation. Acquire RLock when reading/writing through the writers,
+// acquire Lock when swapping writers and closing old files.
+var LogWriterMu sync.RWMutex
+
 func SysLog(s string) {
 	t := time.Now()
+	LogWriterMu.RLock()
 	_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
+	LogWriterMu.RUnlock()
 }
 
 func SysError(s string) {
 	t := time.Now()
+	LogWriterMu.RLock()
 	_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
+	LogWriterMu.RUnlock()
 }
 
 func FatalLog(v ...any) {
 	t := time.Now()
+	LogWriterMu.RLock()
 	_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
+	LogWriterMu.RUnlock()
 	os.Exit(1)
 }
 
 func LogStartupSuccess(startTime time.Time, port string) {
-
 	duration := time.Since(startTime)
 	durationMs := duration.Milliseconds()
 
 	// Get network IPs
 	networkIps := GetNetworkIps()
 
-	// Print blank line for spacing
-	fmt.Fprintf(gin.DefaultWriter, "\n")
+	LogWriterMu.RLock()
+	defer LogWriterMu.RUnlock()
 
-	// Print the main success message
+	fmt.Fprintf(gin.DefaultWriter, "\n")
 	fmt.Fprintf(gin.DefaultWriter, "  \033[32m%s %s\033[0m  ready in %d ms\n", SystemName, Version, durationMs)
 	fmt.Fprintf(gin.DefaultWriter, "\n")
 
-	// Skip fancy startup message in container environments
 	if !IsRunningInContainer() {
-		// Print local URL
 		fmt.Fprintf(gin.DefaultWriter, "  ➜  \033[1mLocal:\033[0m   http://localhost:%s/\n", port)
 	}
 
-	// Print network URLs
 	for _, ip := range networkIps {
 		fmt.Fprintf(gin.DefaultWriter, "  ➜  \033[1mNetwork:\033[0m http://%s:%s/\n", ip, port)
 	}
 
-	// Print blank line for spacing
 	fmt.Fprintf(gin.DefaultWriter, "\n")
 }

+ 183 - 0
controller/performance.go

@@ -1,12 +1,18 @@
 package controller
 
 import (
+	"fmt"
 	"net/http"
 	"os"
+	"path/filepath"
 	"runtime"
+	"sort"
+	"strconv"
+	"strings"
 	"time"
 
 	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/logger"
 	"github.com/gin-gonic/gin"
 )
 
@@ -169,6 +175,183 @@ func ForceGC(c *gin.Context) {
 	})
 }
 
+// LogFileInfo 日志文件信息
+type LogFileInfo struct {
+	Name    string    `json:"name"`
+	Size    int64     `json:"size"`
+	ModTime time.Time `json:"mod_time"`
+}
+
+// LogFilesResponse 日志文件列表响应
+type LogFilesResponse struct {
+	LogDir     string        `json:"log_dir"`
+	Enabled    bool          `json:"enabled"`
+	FileCount  int           `json:"file_count"`
+	TotalSize  int64         `json:"total_size"`
+	OldestTime *time.Time    `json:"oldest_time,omitempty"`
+	NewestTime *time.Time    `json:"newest_time,omitempty"`
+	Files      []LogFileInfo `json:"files"`
+}
+
+// getLogFiles 读取日志目录中的日志文件列表
+func getLogFiles() ([]LogFileInfo, error) {
+	if *common.LogDir == "" {
+		return nil, nil
+	}
+	entries, err := os.ReadDir(*common.LogDir)
+	if err != nil {
+		return nil, err
+	}
+	var files []LogFileInfo
+	for _, entry := range entries {
+		if entry.IsDir() {
+			continue
+		}
+		name := entry.Name()
+		if !strings.HasPrefix(name, "oneapi-") || !strings.HasSuffix(name, ".log") {
+			continue
+		}
+		info, err := entry.Info()
+		if err != nil {
+			continue
+		}
+		files = append(files, LogFileInfo{
+			Name:    name,
+			Size:    info.Size(),
+			ModTime: info.ModTime(),
+		})
+	}
+	// 按文件名降序排列(最新在前)
+	sort.Slice(files, func(i, j int) bool {
+		return files[i].Name > files[j].Name
+	})
+	return files, nil
+}
+
+// GetLogFiles 获取日志文件列表
+func GetLogFiles(c *gin.Context) {
+	if *common.LogDir == "" {
+		common.ApiSuccess(c, LogFilesResponse{Enabled: false})
+		return
+	}
+	files, err := getLogFiles()
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+	var totalSize int64
+	var oldest, newest time.Time
+	for i, f := range files {
+		totalSize += f.Size
+		if i == 0 || f.ModTime.Before(oldest) {
+			oldest = f.ModTime
+		}
+		if i == 0 || f.ModTime.After(newest) {
+			newest = f.ModTime
+		}
+	}
+	resp := LogFilesResponse{
+		LogDir:    *common.LogDir,
+		Enabled:   true,
+		FileCount: len(files),
+		TotalSize: totalSize,
+		Files:     files,
+	}
+	if len(files) > 0 {
+		resp.OldestTime = &oldest
+		resp.NewestTime = &newest
+	}
+	common.ApiSuccess(c, resp)
+}
+
+// CleanupLogFiles 清理过期日志文件
+func CleanupLogFiles(c *gin.Context) {
+	mode := c.Query("mode")
+	valueStr := c.Query("value")
+	if mode != "by_count" && mode != "by_days" {
+		common.ApiErrorMsg(c, "invalid mode, must be by_count or by_days")
+		return
+	}
+	value, err := strconv.Atoi(valueStr)
+	if err != nil || value < 1 {
+		common.ApiErrorMsg(c, "invalid value, must be a positive integer")
+		return
+	}
+	if *common.LogDir == "" {
+		common.ApiErrorMsg(c, "log directory not configured")
+		return
+	}
+
+	files, err := getLogFiles()
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	activeLogPath := logger.GetCurrentLogPath()
+	var toDelete []LogFileInfo
+
+	switch mode {
+	case "by_count":
+		// files 已按名称降序(最新在前),保留前 value 个
+		for i, f := range files {
+			if i < value {
+				continue
+			}
+			fullPath := filepath.Join(*common.LogDir, f.Name)
+			if fullPath == activeLogPath {
+				continue
+			}
+			toDelete = append(toDelete, f)
+		}
+	case "by_days":
+		cutoff := time.Now().AddDate(0, 0, -value)
+		for _, f := range files {
+			if f.ModTime.Before(cutoff) {
+				fullPath := filepath.Join(*common.LogDir, f.Name)
+				if fullPath == activeLogPath {
+					continue
+				}
+				toDelete = append(toDelete, f)
+			}
+		}
+	}
+
+	var deletedCount int
+	var freedBytes int64
+	var failedFiles []string
+	for _, f := range toDelete {
+		fullPath := filepath.Join(*common.LogDir, f.Name)
+		if err := os.Remove(fullPath); err != nil {
+			failedFiles = append(failedFiles, f.Name)
+			continue
+		}
+		deletedCount++
+		freedBytes += f.Size
+	}
+
+	result := gin.H{
+		"deleted_count": deletedCount,
+		"freed_bytes":   freedBytes,
+		"failed_files":  failedFiles,
+	}
+
+	if len(failedFiles) > 0 {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": fmt.Sprintf("部分文件删除失败(%d/%d)", len(failedFiles), len(toDelete)),
+			"data":    result,
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data":    result,
+	})
+}
+
 // getDiskCacheInfo 获取磁盘缓存目录信息
 func getDiskCacheInfo() DiskCacheInfo {
 	// 使用统一的缓存目录

+ 26 - 4
logger/logger.go

@@ -29,6 +29,15 @@ const maxLogCount = 1000000
 var logCount int
 var setupLogLock sync.Mutex
 var setupLogWorking bool
+var currentLogPath string
+var currentLogPathMu sync.RWMutex
+var currentLogFile *os.File
+
+func GetCurrentLogPath() string {
+	currentLogPathMu.RLock()
+	defer currentLogPathMu.RUnlock()
+	return currentLogPath
+}
 
 func SetupLogger() {
 	defer func() {
@@ -48,8 +57,19 @@ func SetupLogger() {
 		if err != nil {
 			log.Fatal("failed to open log file")
 		}
+		currentLogPathMu.Lock()
+		oldFile := currentLogFile
+		currentLogPath = logPath
+		currentLogFile = fd
+		currentLogPathMu.Unlock()
+
+		common.LogWriterMu.Lock()
 		gin.DefaultWriter = io.MultiWriter(os.Stdout, fd)
 		gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd)
+		if oldFile != nil {
+			_ = oldFile.Close()
+		}
+		common.LogWriterMu.Unlock()
 	}
 }
 
@@ -75,16 +95,18 @@ func LogDebug(ctx context.Context, msg string, args ...any) {
 }
 
 func logHelper(ctx context.Context, level string, msg string) {
-	writer := gin.DefaultErrorWriter
-	if level == loggerINFO {
-		writer = gin.DefaultWriter
-	}
 	id := ctx.Value(common.RequestIdKey)
 	if id == nil {
 		id = "SYSTEM"
 	}
 	now := time.Now()
+	common.LogWriterMu.RLock()
+	writer := gin.DefaultErrorWriter
+	if level == loggerINFO {
+		writer = gin.DefaultWriter
+	}
 	_, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg)
+	common.LogWriterMu.RUnlock()
 	logCount++ // we don't need accurate count, so no lock here
 	if logCount > maxLogCount && !setupLogWorking {
 		logCount = 0

+ 2 - 0
router/api-router.go

@@ -194,6 +194,8 @@ func SetApiRouter(router *gin.Engine) {
 			performanceRoute.DELETE("/disk_cache", controller.ClearDiskCache)
 			performanceRoute.POST("/reset_stats", controller.ResetPerformanceStats)
 			performanceRoute.POST("/gc", controller.ForceGC)
+			performanceRoute.GET("/logs", controller.GetLogFiles)
+			performanceRoute.DELETE("/logs", controller.CleanupLogFiles)
 		}
 		ratioSyncRoute := apiRouter.Group("/ratio_sync")
 		ratioSyncRoute.Use(middleware.RootAuth())

+ 18 - 0
web/src/i18n/locales/en.json

@@ -503,6 +503,10 @@
     "保存邮箱域名白名单设置": "Save Email Domain Whitelist Settings",
     "保存额度设置": "Save Quota Settings",
     "保留原值(目标已有值时不覆盖)": "Keep original value (do not overwrite if target already has a value)",
+    "保留天数": "Days to Retain",
+    "保留文件数": "Files to Retain",
+    "保留最近N个文件": "Retain last N files",
+    "保留最近N天": "Retain last N days",
     "修复数据库一致性": "Fix database consistency",
     "修改为": "Modify to",
     "修改子渠道优先级": "Modify sub-channel priority",
@@ -1116,8 +1120,10 @@
     "将为选中的 ": "Will set for selected ",
     "将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "Only the first key file will be retained, and the remaining files will be removed. Continue?",
     "将删除": "Deleting",
+    "将删除 {{value}} 天前的日志文件。": "Log files older than {{value}} days will be deleted.",
     "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "This will delete all used, disabled, and expired redemption codes, this operation cannot be undone.",
     "将删除所有仍在内存中的渠道亲和性缓存条目。": "This will delete all channel affinity cache entries still in memory.",
+    "将只保留最近 {{value}} 个日志文件,其余将被删除。": "Only the last {{value}} log files will be retained; the rest will be deleted.",
     "将大请求体临时存储到磁盘": "Store large request bodies temporarily on disk",
     "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "This will clear all saved configurations and restore default settings, this operation cannot be undone. Continue?",
     "将清除选定时间之前的所有日志": "This will clear all logs before the selected time",
@@ -1202,6 +1208,7 @@
     "已添加 {{count}} 个模板_one": "Added {{count}} template",
     "已添加 {{count}} 个模板_other": "Added {{count}} templates",
     "已添加到白名单": "Added to whitelist",
+    "已清理 {{count}} 个日志文件,释放 {{size}}": "Cleaned up {{count}} log files, freed {{size}}",
     "已清空": "Cleared",
     "已清空测试结果": "Cleared test results",
     "已生成授权凭据": "Authorization credentials generated",
@@ -1570,8 +1577,12 @@
     "日志已下载": "Logs downloaded",
     "日志已加载": "Logs loaded",
     "日志已复制到剪贴板": "Logs copied to clipboard",
+    "日志时间范围": "Log Date Range",
+    "日志总大小": "Total Log Size",
+    "日志文件数": "Log File Count",
     "日志流": "Log Stream",
     "日志清理失败:": "Log cleanup failed:",
+    "日志目录": "Log Directory",
     "日志类型": "Log type",
     "日志设置": "Log settings",
     "日志详情": "Log details",
@@ -1690,6 +1701,8 @@
     "服务可用性": "Service Status",
     "服务商": "Service Provider",
     "服务器地址": "Server Address",
+    "服务器日志功能未启用(未配置日志目录)": "Server logging is not enabled (log directory not configured)",
+    "服务器日志管理": "Server Log Management",
     "服务显示名称": "Service Display Name",
     "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "No matching models. Press Enter to add \"{{name}}\" as a custom model name.",
     "未发现新增模型": "No new models were added",
@@ -1986,6 +1999,8 @@
     "添加额度": "Add quota",
     "清理不活跃缓存": "Clean up inactive cache",
     "清理失败": "Cleanup failed",
+    "清理方式": "Cleanup Mode",
+    "清理日志文件": "Clean Up Log Files",
     "清空": "Clear",
     "清空全部缓存": "Clear All Cache",
     "清空该规则缓存": "Clear This Rule's Cache",
@@ -2186,6 +2201,7 @@
     "确认操作": "Confirm Operation",
     "确认新密码": "Confirm new password",
     "确认清理不活跃的磁盘缓存?": "Confirm cleanup of inactive disk cache?",
+    "确认清理日志文件?": "Confirm log file cleanup?",
     "确认清空全部渠道亲和性缓存": "Confirm clearing all channel affinity cache",
     "确认清空该规则缓存": "Confirm clearing this rule's cache",
     "确认清除历史日志": "Confirm clear historical logs",
@@ -2285,6 +2301,7 @@
     "管理员设置了外部链接,点击下方按钮访问": "Administrator has set up external links, click the button below to access",
     "管理员账号": "Admin account",
     "管理员账号已经初始化过,请继续设置其他参数": "The admin account has already been initialized, please continue to set other parameters",
+    "管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。": "Manage server log files. Log files accumulate over time; regular cleanup is recommended to free disk space.",
     "管理模型、标签、端点等预填组": "Manage model, tag, endpoint, etc. pre-filled groups",
     "管理用户已绑定的第三方账户,支持筛选与解绑": "Manage users' linked third-party accounts, with filtering and unbinding support",
     "管理绑定": "Manage Bindings",
@@ -2755,6 +2772,7 @@
     "请输入新的部署名称": "Please enter new deployment name",
     "请输入显示名称": "Please enter display name",
     "请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "Please enter a valid JSON format request body. You can refer to the default request body format in the preview panel.",
+    "请输入有效的数值": "Please enter a valid number",
     "请输入有效的数字": "Please enter a valid number",
     "请输入有效的镜像地址": "Please enter a valid image address",
     "请输入标签名称": "Please enter the tag name",

+ 18 - 0
web/src/i18n/locales/fr.json

@@ -498,6 +498,10 @@
     "保存邮箱域名白名单设置": "Enregistrer les paramètres de liste blanche des domaines de messagerie",
     "保存额度设置": "Enregistrer les paramètres de quota",
     "保留原值(目标已有值时不覆盖)": "Conserver la valeur originale (ne pas écraser si la cible a déjà une valeur)",
+    "保留天数": "Jours à conserver",
+    "保留文件数": "Fichiers à conserver",
+    "保留最近N个文件": "Conserver les N derniers fichiers",
+    "保留最近N天": "Conserver les N derniers jours",
     "修复数据库一致性": "Réparer la cohérence de la base de données",
     "修改为": "Modifier en",
     "修改子渠道优先级": "Modifier la priorité du sous-canal",
@@ -1108,8 +1112,10 @@
     "将为选中的 ": "Définira pour la sélection ",
     "将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "Seul le premier fichier de clé sera conservé, et les fichiers restants seront supprimés. Continuer ?",
     "将删除": "Supprimera",
+    "将删除 {{value}} 天前的日志文件。": "Les fichiers journaux de plus de {{value}} jours seront supprimés.",
     "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "Cela supprimera tous les codes d'échange utilisés, désactivés et expirés, cette opération ne peut pas être annulée.",
     "将删除所有仍在内存中的渠道亲和性缓存条目。": "Ceci supprimera toutes les entrées de cache d'affinité de canal encore en mémoire.",
+    "将只保留最近 {{value}} 个日志文件,其余将被删除。": "Seuls les {{value}} derniers fichiers journaux seront conservés ; le reste sera supprimé.",
     "将大请求体临时存储到磁盘": "Stocker temporairement les grands corps de requête sur le disque",
     "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "Effacera toutes les configurations enregistrées et rétablira les paramètres par défaut. Cette opération ne peut pas être annulée. Continuer ?",
     "将清除选定时间之前的所有日志": "Effacera tous les journaux avant l'heure sélectionnée",
@@ -1201,6 +1207,7 @@
     "已添加到白名单": "Ajouté à la liste blanche",
     "已清空": "Vidé",
     "已清空测试结果": "Résultats de test effacés",
+    "已清理 {{count}} 个日志文件,释放 {{size}}": "{{count}} fichiers journaux nettoyés, {{size}} libérés",
     "已生成授权凭据": "Identifiants d'autorisation générés",
     "已用": "Used",
     "已用/剩余": "Utilisé/Restant",
@@ -1563,6 +1570,10 @@
     "日志类型": "Type de journal",
     "日志设置": "Config. journaux",
     "日志详情": "Détails du journal",
+    "日志目录": "Répertoire des journaux",
+    "日志文件数": "Nombre de fichiers journaux",
+    "日志时间范围": "Plage de dates des journaux",
+    "日志总大小": "Taille totale des journaux",
     "旧格式(JSON 对象)": "Ancien format (Objet JSON)",
     "旧格式(直接覆盖):": "Ancien format (remplacement direct) :",
     "旧格式必须是 JSON 对象": "L'ancien format doit être un objet JSON",
@@ -1677,6 +1688,8 @@
     "服务可用性": "État du service",
     "服务商": "Service Provider",
     "服务器地址": "Adresse du serveur",
+    "服务器日志功能未启用(未配置日志目录)": "La journalisation du serveur n'est pas activée (répertoire de journaux non configuré)",
+    "服务器日志管理": "Gestion des journaux du serveur",
     "服务显示名称": "Nom d'affichage du service",
     "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "Aucun modèle correspondant. Appuyez sur Entrée pour ajouter «{{name}}» comme nom de modèle personnalisé.",
     "未发现新增模型": "Aucun nouveau modèle n'a été ajouté",
@@ -1966,6 +1979,8 @@
     "添加额度": "Ajouter un quota",
     "清理不活跃缓存": "Nettoyer le cache inactif",
     "清理失败": "Échec du nettoyage",
+    "清理方式": "Mode de nettoyage",
+    "清理日志文件": "Nettoyer les fichiers journaux",
     "清空": "Clear",
     "清空全部缓存": "Vider tout le cache",
     "清空该规则缓存": "Vider le cache de cette règle",
@@ -2164,6 +2179,7 @@
     "确认操作": "Confirm Operation",
     "确认新密码": "Confirmer le nouveau mot de passe",
     "确认清理不活跃的磁盘缓存?": "Confirmer le nettoyage du cache disque inactif ?",
+    "确认清理日志文件?": "Confirmer le nettoyage des fichiers journaux ?",
     "确认清空全部渠道亲和性缓存": "Confirmer la suppression de tout le cache d'affinité de canal",
     "确认清空该规则缓存": "Confirmer la suppression du cache de cette règle",
     "确认清除历史日志": "Confirmer l'effacement des journaux historiques",
@@ -2249,6 +2265,7 @@
     "简洁模式仅返回 message;状态码和错误类型将使用系统默认值。": "Le mode simple renvoie uniquement le message ; le code d'état et le type d'erreur utiliseront les valeurs par défaut du système.",
     "管理": "Gérer",
     "管理 Ollama 模型的拉取和删除": "Manage Ollama model pulling and deletion",
+    "管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。": "Gérer les fichiers journaux du serveur. Les fichiers journaux s'accumulent au fil du temps ; un nettoyage régulier est recommandé pour libérer de l'espace disque.",
     "管理你的 LinuxDO OAuth App": "Gérer votre application OAuth LinuxDO",
     "管理员": "Admin",
     "管理员区域": "Zone administrateur",
@@ -2714,6 +2731,7 @@
     "请输入新的部署名称": "Please enter new deployment name",
     "请输入显示名称": "Veuillez saisir un nom d'affichage",
     "请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "Veuillez entrer un corps de requête au format JSON valide. Vous pouvez vous référer au format de corps de requête par défaut dans le panneau d'aperçu.",
+    "请输入有效的数值": "Veuillez saisir une valeur valide",
     "请输入有效的数字": "Veuillez saisir un nombre valide",
     "请输入有效的镜像地址": "Please enter a valid image address",
     "请输入标签名称": "Veuillez saisir le nom de l'étiquette",

+ 18 - 0
web/src/i18n/locales/ja.json

@@ -494,6 +494,10 @@
     "保存邮箱域名白名单设置": "メールドメインのホワイトリスト設定を保存",
     "保存额度设置": "クォータ設定を保存",
     "保留原值(目标已有值时不覆盖)": "元の値を保持(ターゲットに既に値がある場合は上書きしない)",
+    "保留天数": "保持日数",
+    "保留文件数": "保持ファイル数",
+    "保留最近N个文件": "最新のN個のファイルを保持",
+    "保留最近N天": "最新のN日間を保持",
     "修复数据库一致性": "データベースの整合性を修復",
     "修改为": "変更先:",
     "修改子渠道优先级": "サブチャネルの優先度を変更",
@@ -1101,6 +1105,8 @@
     "将删除": "削除の確認",
     "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "使用済み、無効、および有効期限切れの引き換えコードを削除します。この操作は元に戻すことはできません。",
     "将删除所有仍在内存中的渠道亲和性缓存条目。": "メモリ内に残っているすべてのチャネルアフィニティキャッシュエントリが削除されます。",
+    "将删除 {{value}} 天前的日志文件。": "{{value}} 日より前のログファイルが削除されます。",
+    "将只保留最近 {{value}} 个日志文件,其余将被删除。": "最新の {{value}} 個のログファイルのみ保持し、残りは削除されます。",
     "将大请求体临时存储到磁盘": "大きなリクエストボディをディスクに一時保存",
     "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "保存されているすべての設定がクリアされ、デフォルト設定に復元されます。この操作は元に戻すことはできません。続行しますか?",
     "将清除选定时间之前的所有日志": "選択した日時以前のすべてのログをクリアします",
@@ -1185,6 +1191,7 @@
     "已添加到白名单": "ホワイトリストに追加されました",
     "已清空": "クリア済み",
     "已清空测试结果": "テスト結果がクリアされました",
+    "已清理 {{count}} 个日志文件,释放 {{size}}": "{{count}} 個のログファイルをクリーンアップし、{{size}} を解放しました",
     "已生成授权凭据": "認可資格情報が生成されました",
     "已用": "Used",
     "已用/剩余": "使用済み/残り",
@@ -1546,6 +1553,10 @@
     "日志类型": "ログタイプ",
     "日志设置": "ログ設定",
     "日志详情": "ログ詳細",
+    "日志目录": "ログディレクトリ",
+    "日志文件数": "ログファイル数",
+    "日志时间范围": "ログ期間",
+    "日志总大小": "ログ合計サイズ",
     "旧格式(JSON 对象)": "レガシー形式(JSONオブジェクト)",
     "旧格式(直接覆盖):": "旧形式(直接上書き):",
     "旧格式必须是 JSON 对象": "レガシー形式はJSONオブジェクトである必要があります",
@@ -1660,6 +1671,8 @@
     "服务可用性": "サービスの可用性",
     "服务商": "Service Provider",
     "服务器地址": "サーバーURL",
+    "服务器日志功能未启用(未配置日志目录)": "サーバーログ機能が有効になっていません(ログディレクトリが未設定)",
+    "服务器日志管理": "サーバーログ管理",
     "服务显示名称": "サービス表示名",
     "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "一致するモデルが見つかりません。Enterキーで「{{name}}」をカスタムモデル名として追加できます。",
     "未发现新增模型": "追加された新規モデルはありません",
@@ -1949,6 +1962,8 @@
     "添加额度": "残高追加",
     "清理不活跃缓存": "非アクティブなキャッシュをクリーンアップ",
     "清理失败": "クリーンアップに失敗しました",
+    "清理方式": "クリーンアップモード",
+    "清理日志文件": "ログファイルをクリーンアップ",
     "清空": "Clear",
     "清空全部缓存": "すべてのキャッシュをクリア",
     "清空该规则缓存": "このルールのキャッシュをクリア",
@@ -2148,6 +2163,7 @@
     "确认清空全部渠道亲和性缓存": "すべてのチャネルアフィニティキャッシュのクリアを確認",
     "确认清空该规则缓存": "このルールのキャッシュクリアを確認",
     "确认清除历史日志": "履歴のクリアの確認",
+    "确认清理日志文件?": "ログファイルのクリーンアップを確認しますか?",
     "确认禁用": "無効化の確認",
     "确认补单": "手動チャージの確認",
     "确认解绑": "連携解除の確認",
@@ -2246,6 +2262,7 @@
     "管理模型、标签、端点等预填组": "モデル、タグ、エンドポイントなどの事前入力グループ管理",
     "管理用户已绑定的第三方账户,支持筛选与解绑": "ユーザーにリンクされたサードパーティアカウントを管理、フィルタリングとバインド解除をサポート",
     "管理绑定": "バインド管理",
+    "管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。": "サーバーログファイルを管理します。ログファイルは時間とともに蓄積されるため、定期的なクリーンアップでディスク容量を解放することをお勧めします。",
     "类型": "タイプ",
     "类型(常用)": "タイプ(一般的)",
     "粘贴图片失败": "画像の貼り付けに失敗しました",
@@ -2695,6 +2712,7 @@
     "请输入新的部署名称": "Please enter new deployment name",
     "请输入显示名称": "表示名を入力してください",
     "请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "有効なJSON形式のリクエストボディを入力してください。プレビューパネルのデフォルトのリクエストボディ形式を参照できます。",
+    "请输入有效的数值": "有効な値を入力してください",
     "请输入有效的数字": "有効な数値を入力してください",
     "请输入有效的镜像地址": "Please enter a valid image address",
     "请输入标签名称": "タグ名を入力してください",

+ 18 - 0
web/src/i18n/locales/ru.json

@@ -501,6 +501,10 @@
     "保存邮箱域名白名单设置": "Сохранить настройки белого списка доменов email",
     "保存额度设置": "Сохранить настройки лимитов",
     "保留原值(目标已有值时不覆盖)": "Сохранить исходное значение (не перезаписывать, если цель уже имеет значение)",
+    "保留天数": "Дней для хранения",
+    "保留文件数": "Файлов для хранения",
+    "保留最近N个文件": "Хранить последние N файлов",
+    "保留最近N天": "Хранить за последние N дней",
     "修复数据库一致性": "Исправить согласованность базы данных",
     "修改为": "Изменить на",
     "修改子渠道优先级": "Изменить приоритет дочерних каналов",
@@ -1116,6 +1120,8 @@
     "将删除": "Будет удалено",
     "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "Будут удалены использованные, отключенные и просроченные коды обмена, эта операция необратима.",
     "将删除所有仍在内存中的渠道亲和性缓存条目。": "Будут удалены все записи кэша аффинити каналов, оставшиеся в памяти.",
+    "将删除 {{value}} 天前的日志文件。": "Файлы журналов старше {{value}} дней будут удалены.",
+    "将只保留最近 {{value}} 个日志文件,其余将被删除。": "Будут сохранены только последние {{value}} файлов журналов; остальные будут удалены.",
     "将大请求体临时存储到磁盘": "Временное сохранение больших тел запросов на диск",
     "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "Будут очищены все сохраненные конфигурации и восстановлены настройки по умолчанию, эта операция необратима. Продолжить?",
     "将清除选定时间之前的所有日志": "Будут очищены все логи до выбранного времени",
@@ -1212,6 +1218,7 @@
     "已添加到白名单": "Добавлено в белый список",
     "已清空": "Очищено",
     "已清空测试结果": "Результаты тестов очищены",
+    "已清理 {{count}} 个日志文件,释放 {{size}}": "Очищено {{count}} файлов журналов, освобождено {{size}}",
     "已生成授权凭据": "Учётные данные авторизации сгенерированы",
     "已用": "Used",
     "已用/剩余": "Использовано/Осталось",
@@ -1575,6 +1582,10 @@
     "日志类型": "Тип журнала",
     "日志设置": "Настройки журнала",
     "日志详情": "Детали журнала",
+    "日志目录": "Каталог журналов",
+    "日志文件数": "Количество файлов журналов",
+    "日志时间范围": "Диапазон дат журналов",
+    "日志总大小": "Общий размер журналов",
     "旧格式(JSON 对象)": "Устаревший формат (JSON-объект)",
     "旧格式(直接覆盖):": "Старый формат (прямая перезапись):",
     "旧格式必须是 JSON 对象": "Устаревший формат должен быть JSON-объектом",
@@ -1689,6 +1700,8 @@
     "服务可用性": "Доступность сервиса",
     "服务商": "Service Provider",
     "服务器地址": "Адрес сервера",
+    "服务器日志功能未启用(未配置日志目录)": "Ведение журнала сервера не включено (каталог журналов не настроен)",
+    "服务器日志管理": "Управление журналами сервера",
     "服务显示名称": "Отображаемое имя сервиса",
     "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "Совпадающих моделей не найдено. Нажмите Enter, чтобы добавить «{{name}}» как пользовательское имя модели.",
     "未发现新增模型": "Новые модели не обнаружены",
@@ -1978,6 +1991,8 @@
     "添加额度": "Добавить лимит",
     "清理不活跃缓存": "Очистить неактивный кэш",
     "清理失败": "Ошибка очистки",
+    "清理方式": "Режим очистки",
+    "清理日志文件": "Очистить файлы журналов",
     "清空": "Clear",
     "清空全部缓存": "Очистить весь кэш",
     "清空该规则缓存": "Очистить кэш этого правила",
@@ -2181,6 +2196,7 @@
     "确认清空全部渠道亲和性缓存": "Подтвердить очистку всего кэша аффинити каналов",
     "确认清空该规则缓存": "Подтвердить очистку кэша этого правила",
     "确认清除历史日志": "Подтвердить очистку истории логов",
+    "确认清理日志文件?": "Подтвердить очистку файлов журналов?",
     "确认禁用": "Подтвердить отключение",
     "确认补单": "Подтвердить дополнение заказа",
     "确认解绑": "Подтвердить отвязку",
@@ -2279,6 +2295,7 @@
     "管理模型、标签、端点等预填组": "Управление предзаполненными группами моделей, тегов, конечных точек и т.д.",
     "管理用户已绑定的第三方账户,支持筛选与解绑": "Управление привязанными сторонними аккаунтами пользователей с поддержкой фильтрации и отвязки",
     "管理绑定": "Управление привязками",
+    "管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。": "Управление файлами журналов сервера. Файлы журналов накапливаются со временем; рекомендуется регулярная очистка для освобождения дискового пространства.",
     "类型": "Тип",
     "类型(常用)": "Тип (часто используемые)",
     "粘贴图片失败": "Ошибка вставки изображения",
@@ -2728,6 +2745,7 @@
     "请输入新的部署名称": "Please enter new deployment name",
     "请输入显示名称": "Пожалуйста, введите отображаемое имя",
     "请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "Пожалуйста, введите тело запроса в действительном формате JSON. Вы можете обратиться к формату тела запроса по умолчанию на панели предварительного просмотра.",
+    "请输入有效的数值": "Пожалуйста, введите допустимое значение",
     "请输入有效的数字": "Пожалуйста, введите действительное число",
     "请输入有效的镜像地址": "Please enter a valid image address",
     "请输入标签名称": "Пожалуйста, введите имя тега",

+ 18 - 0
web/src/i18n/locales/vi.json

@@ -495,6 +495,10 @@
     "保存邮箱域名白名单设置": "Lưu cài đặt danh sách trắng tên miền email",
     "保存额度设置": "Lưu cài đặt hạn ngạch",
     "保留原值(目标已有值时不覆盖)": "Giữ giá trị gốc (không ghi đè nếu mục tiêu đã có giá trị)",
+    "保留天数": "Số ngày giữ lại",
+    "保留文件数": "Số tệp giữ lại",
+    "保留最近N个文件": "Giữ lại N tệp gần nhất",
+    "保留最近N天": "Giữ lại N ngày gần nhất",
     "修复数据库一致性": "Sửa chữa tính nhất quán của cơ sở dữ liệu",
     "修改为": "Sửa đổi thành",
     "修改子渠道优先级": "Sửa đổi ưu tiên kênh phụ",
@@ -1102,6 +1106,8 @@
     "将删除": "Đang xóa",
     "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "Thao tác này sẽ xóa tất cả các mã đổi thưởng đã sử dụng, bị vô hiệu hóa và hết hạn, thao tác này không thể hoàn tác.",
     "将删除所有仍在内存中的渠道亲和性缓存条目。": "Sẽ xóa tất cả mục bộ nhớ đệm ưu ái kênh còn trong bộ nhớ.",
+    "将删除 {{value}} 天前的日志文件。": "Tệp nhật ký cũ hơn {{value}} ngày sẽ bị xóa.",
+    "将只保留最近 {{value}} 个日志文件,其余将被删除。": "Chỉ giữ lại {{value}} tệp nhật ký gần nhất; phần còn lại sẽ bị xóa.",
     "将大请求体临时存储到磁盘": "Lưu tạm body yêu cầu lớn vào đĩa",
     "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "Thao tác này sẽ xóa tất cả các cấu hình đã lưu và khôi phục cài đặt mặc định, thao tác này không thể hoàn tác. Tiếp tục?",
     "将清除选定时间之前的所有日志": "Thao tác này sẽ xóa tất cả nhật ký trước thời gian đã chọn",
@@ -1186,6 +1192,7 @@
     "已添加到白名单": "Đã thêm vào danh sách trắng",
     "已清空": "Đã xóa sạch",
     "已清空测试结果": "Đã xóa kết quả kiểm tra",
+    "已清理 {{count}} 个日志文件,释放 {{size}}": "Đã dọn dẹp {{count}} tệp nhật ký, giải phóng {{size}}",
     "已生成授权凭据": "Đã tạo thông tin xác thực",
     "已用": "Used",
     "已用/剩余": "Đã dùng/Còn lại",
@@ -1547,6 +1554,10 @@
     "日志类型": "Loại nhật ký",
     "日志设置": "Cài đặt nhật ký",
     "日志详情": "Chi tiết nhật ký",
+    "日志目录": "Thư mục nhật ký",
+    "日志文件数": "Số lượng tệp nhật ký",
+    "日志时间范围": "Phạm vi thời gian nhật ký",
+    "日志总大小": "Tổng kích thước nhật ký",
     "旧格式(JSON 对象)": "Định dạng cũ (Đối tượng JSON)",
     "旧格式(直接覆盖):": "Định dạng cũ (ghi đè trực tiếp):",
     "旧格式必须是 JSON 对象": "Định dạng cũ phải là đối tượng JSON",
@@ -1661,6 +1672,8 @@
     "服务可用性": "Trạng thái dịch vụ",
     "服务商": "Service Provider",
     "服务器地址": "Địa chỉ máy chủ",
+    "服务器日志功能未启用(未配置日志目录)": "Ghi nhật ký máy chủ chưa được bật (chưa cấu hình thư mục nhật ký)",
+    "服务器日志管理": "Quản lý nhật ký máy chủ",
     "服务显示名称": "Tên hiển thị dịch vụ",
     "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "Không tìm thấy mô hình khớp. Nhấn Enter để thêm \"{{name}}\" làm tên mô hình tùy chỉnh.",
     "未发现新增模型": "Không có mô hình mới nào được thêm",
@@ -2047,6 +2060,8 @@
     "清理不活跃缓存": "Xóa cache không hoạt động",
     "清理历史日志": "Dọn dẹp nhật ký lịch sử",
     "清理失败": "Dọn dẹp thất bại",
+    "清理方式": "Chế độ dọn dẹp",
+    "清理日志文件": "Dọn dẹp tệp nhật ký",
     "清理成功": "Dọn dẹp thành công",
     "清理数据": "Dọn dẹp dữ liệu",
     "清理日志": "Dọn dẹp nhật ký",
@@ -2382,6 +2397,7 @@
     "确认清空该规则缓存": "Xác nhận xóa bộ nhớ đệm của quy tắc này",
     "确认清除": "Xác nhận xóa",
     "确认清除历史日志": "Xác nhận xóa nhật ký lịch sử",
+    "确认清理日志文件?": "Xác nhận dọn dẹp tệp nhật ký?",
     "确认禁用": "Xác nhận vô hiệu hóa",
     "确认补单": "Xác nhận hoàn thành đơn hàng",
     "确认解绑": "Xác nhận hủy liên kết",
@@ -2518,6 +2534,7 @@
     "管理用户": "Quản lý người dùng",
     "管理用户已绑定的第三方账户,支持筛选与解绑": "Quản lý tài khoản bên thứ ba đã liên kết của người dùng, hỗ trợ lọc và hủy liên kết",
     "管理绑定": "Quản lý liên kết",
+    "管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。": "Quản lý tệp nhật ký máy chủ. Tệp nhật ký tích lũy theo thời gian; nên dọn dẹp định kỳ để giải phóng dung lượng đĩa.",
     "管理面板": "Bảng quản lý",
     "类": "Lớp",
     "类型": "Loại",
@@ -3120,6 +3137,7 @@
     "请输入旧密码": "Vui lòng nhập mật khẩu cũ",
     "请输入显示名称": "Vui lòng nhập tên hiển thị",
     "请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "Vui lòng nhập nội dung yêu cầu có định dạng JSON hợp lệ. Bạn có thể tham khảo định dạng nội dung yêu cầu mặc định trong bảng xem trước.",
+    "请输入有效的数值": "Vui lòng nhập giá trị hợp lệ",
     "请输入有效的数字": "Vui lòng nhập số hợp lệ",
     "请输入有效的镜像地址": "Please enter a valid image address",
     "请输入标签名称": "Vui lòng nhập tên thẻ",

+ 18 - 0
web/src/i18n/locales/zh-CN.json

@@ -388,6 +388,10 @@
     "保存通用设置": "保存通用设置",
     "保存邮箱域名白名单设置": "保存邮箱域名白名单设置",
     "保存额度设置": "保存额度设置",
+    "保留天数": "保留天数",
+    "保留文件数": "保留文件数",
+    "保留最近N个文件": "保留最近N个文件",
+    "保留最近N天": "保留最近N天",
     "修复数据库一致性": "修复数据库一致性",
     "修改为": "修改为",
     "修改子渠道优先级": "修改子渠道优先级",
@@ -880,7 +884,9 @@
     "将为选中的 ": "将为选中的 ",
     "将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "将仅保留第一个密钥文件,其余文件将被移除,是否继续?",
     "将删除": "将删除",
+    "将删除 {{value}} 天前的日志文件。": "将删除 {{value}} 天前的日志文件。",
     "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。",
+    "将只保留最近 {{value}} 个日志文件,其余将被删除。": "将只保留最近 {{value}} 个日志文件,其余将被删除。",
     "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?",
     "将清除选定时间之前的所有日志": "将清除选定时间之前的所有日志",
     "小时": "小时",
@@ -944,6 +950,7 @@
     "已注销": "已注销",
     "已添加": "已添加",
     "已添加到白名单": "已添加到白名单",
+    "已清理 {{count}} 个日志文件,释放 {{size}}": "已清理 {{count}} 个日志文件,释放 {{size}}",
     "已清空测试结果": "已清空测试结果",
     "已用": "已用",
     "已用/剩余": "已用/剩余",
@@ -1240,8 +1247,12 @@
     "日志已下载": "日志已下载",
     "日志已加载": "日志已加载",
     "日志已复制到剪贴板": "日志已复制到剪贴板",
+    "日志时间范围": "日志时间范围",
+    "日志总大小": "日志总大小",
+    "日志文件数": "日志文件数",
     "日志流": "日志流",
     "日志清理失败:": "日志清理失败:",
+    "日志目录": "日志目录",
     "日志类型": "日志类型",
     "日志设置": "日志设置",
     "日志详情": "日志详情",
@@ -1340,6 +1351,8 @@
     "服务可用性": "服务可用性",
     "服务商": "服务商",
     "服务器地址": "服务器地址",
+    "服务器日志功能未启用(未配置日志目录)": "服务器日志功能未启用(未配置日志目录)",
+    "服务器日志管理": "服务器日志管理",
     "服务显示名称": "服务显示名称",
     "未发现新增模型": "未发现新增模型",
     "未发现重复密钥": "未发现重复密钥",
@@ -1591,6 +1604,8 @@
     "添加键值对": "添加键值对",
     "添加问答": "添加问答",
     "添加额度": "添加额度",
+    "清理方式": "清理方式",
+    "清理日志文件": "清理日志文件",
     "清空": "清空",
     "清空重定向": "清空重定向",
     "清除历史日志": "清除历史日志",
@@ -1756,6 +1771,7 @@
     "确认延长容器时长": "确认延长容器时长",
     "确认操作": "确认操作",
     "确认新密码": "确认新密码",
+    "确认清理日志文件?": "确认清理日志文件?",
     "确认清除历史日志": "确认清除历史日志",
     "确认禁用": "确认禁用",
     "确认补单": "确认补单",
@@ -1817,6 +1833,7 @@
     "管理员账号": "管理员账号",
     "管理员账号已经初始化过,请继续设置其他参数": "管理员账号已经初始化过,请继续设置其他参数",
     "管理模型、标签、端点等预填组": "管理模型、标签、端点等预填组",
+    "管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。": "管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。",
     "类型": "类型",
     "粘贴图片失败": "粘贴图片失败",
     "精确": "精确",
@@ -2211,6 +2228,7 @@
     "请输入新的部署名称": "请输入新的部署名称",
     "请输入显示名称": "请输入显示名称",
     "请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。",
+    "请输入有效的数值": "请输入有效的数值",
     "请输入有效的数字": "请输入有效的数字",
     "请输入有效的镜像地址": "请输入有效的镜像地址",
     "请输入标签名称": "请输入标签名称",

+ 18 - 0
web/src/i18n/locales/zh-TW.json

@@ -389,6 +389,10 @@
     "保存通用设置": "儲存通用設定",
     "保存邮箱域名白名单设置": "儲存信箱域名白名單設定",
     "保存额度设置": "儲存額度設定",
+    "保留天数": "保留天數",
+    "保留文件数": "保留檔案數",
+    "保留最近N个文件": "保留最近N個檔案",
+    "保留最近N天": "保留最近N天",
     "修复数据库一致性": "修復資料庫一致性",
     "修改为": "修改為",
     "修改子渠道优先级": "修改子管道優先級",
@@ -882,7 +886,9 @@
     "将 reasoning_content 转换为 <think> 标签拼接到内容中": "將 reasoning_content 轉換為 <think> 標籤拼接到內容中",
     "将为选中的 ": "將為選中的 ",
     "将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "將僅保留第一個密鑰檔案,其餘檔案將被移除,是否繼續?",
+    "将只保留最近 {{value}} 个日志文件,其余将被删除。": "將只保留最近 {{value}} 個日誌檔案,其餘將被刪除。",
     "将删除": "將刪除",
+    "将删除 {{value}} 天前的日志文件。": "將刪除 {{value}} 天前的日誌檔案。",
     "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "將刪除已使用、已禁用及過期的兌換碼,此操作不可撤銷。",
     "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "將清除所有儲存的設定並恢復預設設定,此操作不可撤銷。是否繼續?",
     "将清除选定时间之前的所有日志": "將清除選定時間之前的所有日誌",
@@ -947,6 +953,7 @@
     "已注销": "已註銷",
     "已添加": "已添加",
     "已添加到白名单": "已添加到白名單",
+    "已清理 {{count}} 个日志文件,释放 {{size}}": "已清理 {{count}} 個日誌檔案,釋放 {{size}}",
     "已清空测试结果": "已清空測試結果",
     "已用": "已用",
     "已用/剩余": "已用/剩餘",
@@ -1243,8 +1250,12 @@
     "日志已下载": "日誌已下載",
     "日志已加载": "日誌已載入",
     "日志已复制到剪贴板": "日誌已複製到剪貼板",
+    "日志时间范围": "日誌時間範圍",
+    "日志总大小": "日誌總大小",
+    "日志文件数": "日誌檔案數",
     "日志流": "日誌流",
     "日志清理失败:": "日誌清理失敗:",
+    "日志目录": "日誌目錄",
     "日志类型": "日誌類型",
     "日志设置": "日誌設定",
     "日志详情": "日誌詳情",
@@ -1344,6 +1355,8 @@
     "服务可用性": "服務可用性",
     "服务商": "服務商",
     "服务器地址": "伺服器位址",
+    "服务器日志功能未启用(未配置日志目录)": "伺服器日誌功能未啟用(未配置日誌目錄)",
+    "服务器日志管理": "伺服器日誌管理",
     "服务显示名称": "服務顯示名稱",
     "未发现新增模型": "未發現新增模型",
     "未发现重复密钥": "未發現重複密鑰",
@@ -1597,6 +1610,8 @@
     "添加键值对": "添加鍵值對",
     "添加问答": "添加問答",
     "添加额度": "添加額度",
+    "清理方式": "清理方式",
+    "清理日志文件": "清理日誌檔案",
     "清空": "清空",
     "清空重定向": "清空重定向",
     "清除历史日志": "清除歷史日誌",
@@ -1762,6 +1777,7 @@
     "确认延长容器时长": "確認延長容器時長",
     "确认操作": "確認操作",
     "确认新密码": "確認新密碼",
+    "确认清理日志文件?": "確認清理日誌檔案?",
     "确认清除历史日志": "確認清除歷史日誌",
     "确认禁用": "確認禁用",
     "确认补单": "確認補單",
@@ -1823,6 +1839,7 @@
     "管理员设置了外部链接,点击下方按钮访问": "管理員設定了外部連結,點擊下方按鈕訪問",
     "管理员账号": "管理員帳號",
     "管理员账号已经初始化过,请继续设置其他参数": "管理員帳號已經初始化過,請繼續設定其他參數",
+    "管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。": "管理伺服器運行日誌檔案。日誌檔案會隨運行時間不斷累積,建議定期清理以釋放磁碟空間。",
     "管理模型、标签、端点等预填组": "管理模型、標籤、端點等預填組",
     "类型": "類型",
     "粘贴图片失败": "貼上圖片失敗",
@@ -2219,6 +2236,7 @@
     "请输入新的部署名称": "請輸入新的部署名稱",
     "请输入显示名称": "請輸入顯示名稱",
     "请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "請輸入有效的JSON格式的請求體。您可以參考預覽面板中的預設請求體格式。",
+    "请输入有效的数值": "請輸入有效的數值",
     "请输入有效的数字": "請輸入有效的數位",
     "请输入有效的镜像地址": "請輸入有效的鏡像位址",
     "请输入标签名称": "請輸入標籤名稱",

+ 154 - 0
web/src/pages/Setting/Performance/SettingsPerformance.jsx

@@ -23,12 +23,15 @@ import {
   Button,
   Col,
   Form,
+  InputNumber,
   Row,
   Spin,
   Progress,
   Descriptions,
   Tag,
   Popconfirm,
+  RadioGroup,
+  Radio,
   Typography,
 } from '@douyinfe/semi-ui';
 import {
@@ -72,6 +75,10 @@ export default function SettingsPerformance(props) {
   });
   const refForm = useRef();
   const [inputsRow, setInputsRow] = useState(inputs);
+  const [logInfo, setLogInfo] = useState(null);
+  const [logCleanupMode, setLogCleanupMode] = useState('by_count');
+  const [logCleanupValue, setLogCleanupValue] = useState(10);
+  const [logCleanupLoading, setLogCleanupLoading] = useState(false);
 
   function handleFieldChange(fieldName) {
     return (value) => {
@@ -167,6 +174,46 @@ export default function SettingsPerformance(props) {
     }
   }
 
+  async function fetchLogInfo() {
+    try {
+      const res = await API.get('/api/performance/logs');
+      if (res.data.success) {
+        setLogInfo(res.data.data);
+      }
+    } catch (error) {
+      console.error('Failed to fetch log info:', error);
+    }
+  }
+
+  async function cleanupLogFiles() {
+    if (logCleanupValue == null || isNaN(logCleanupValue) || logCleanupValue < 1) {
+      showError(t('请输入有效的数值'));
+      return;
+    }
+    setLogCleanupLoading(true);
+    try {
+      const res = await API.delete(
+        `/api/performance/logs?mode=${logCleanupMode}&value=${logCleanupValue}`,
+      );
+      if (res.data.success) {
+        const { deleted_count, freed_bytes } = res.data.data;
+        showSuccess(
+          t('已清理 {{count}} 个日志文件,释放 {{size}}', {
+            count: deleted_count,
+            size: formatBytes(freed_bytes),
+          }),
+        );
+      } else {
+        showError(res.data.message || t('清理失败'));
+      }
+      fetchLogInfo();
+    } catch (error) {
+      showError(t('清理失败'));
+    } finally {
+      setLogCleanupLoading(false);
+    }
+  }
+
   useEffect(() => {
     const currentInputs = {};
     for (let key in props.options) {
@@ -187,6 +234,7 @@ export default function SettingsPerformance(props) {
       refForm.current.setValues({ ...inputs, ...currentInputs });
     }
     fetchStats();
+    fetchLogInfo();
   }, [props.options]);
 
   const diskCacheUsagePercent =
@@ -351,6 +399,112 @@ export default function SettingsPerformance(props) {
         </Form>
       </Spin>
 
+      {/* 服务器日志管理 */}
+      <Form.Section text={t('服务器日志管理')}>
+        <Banner
+          type='info'
+          description={t(
+            '管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。',
+          )}
+          style={{ marginBottom: 16 }}
+        />
+        {logInfo === null ? null : logInfo.enabled ? (
+          <>
+            <Descriptions
+              data={[
+                { key: t('日志目录'), value: logInfo.log_dir },
+                {
+                  key: t('日志文件数'),
+                  value: logInfo.file_count,
+                },
+                {
+                  key: t('日志总大小'),
+                  value: formatBytes(logInfo.total_size),
+                },
+                ...(logInfo.oldest_time && logInfo.newest_time
+                  ? [
+                      {
+                        key: t('日志时间范围'),
+                        value: `${new Date(logInfo.oldest_time).toLocaleDateString()} ~ ${new Date(logInfo.newest_time).toLocaleDateString()}`,
+                      },
+                    ]
+                  : []),
+              ]}
+              style={{ marginBottom: 16 }}
+            />
+            <Row gutter={16} style={{ marginBottom: 16 }}>
+              <Col xs={24} sm={12} md={8}>
+                <div style={{ marginBottom: 12 }}>
+                  <Text strong style={{ display: 'block', marginBottom: 8 }}>
+                    {t('清理方式')}
+                  </Text>
+                  <RadioGroup
+                    value={logCleanupMode}
+                    onChange={(e) => setLogCleanupMode(e.target.value)}
+                  >
+                    <Radio value='by_count'>{t('保留最近N个文件')}</Radio>
+                    <Radio value='by_days'>{t('保留最近N天')}</Radio>
+                  </RadioGroup>
+                </div>
+              </Col>
+              <Col xs={24} sm={12} md={8}>
+                <div style={{ marginBottom: 12 }}>
+                  <Text strong style={{ display: 'block', marginBottom: 8 }}>
+                    {logCleanupMode === 'by_count'
+                      ? t('保留文件数')
+                      : t('保留天数')}
+                  </Text>
+                  <InputNumber
+                    value={logCleanupValue}
+                    min={1}
+                    max={logCleanupMode === 'by_count' ? 1000 : 3650}
+                    onChange={(value) => setLogCleanupValue(value)}
+                    style={{ width: '100%' }}
+                  />
+                </div>
+              </Col>
+              <Col xs={24} sm={12} md={8}>
+                <div style={{ marginBottom: 12 }}>
+                  <Text
+                    strong
+                    style={{
+                      display: 'block',
+                      marginBottom: 8,
+                      visibility: 'hidden',
+                    }}
+                  >
+                    &nbsp;
+                  </Text>
+                <Popconfirm
+                  title={t('确认清理日志文件?')}
+                  content={
+                    logCleanupMode === 'by_count'
+                      ? t(
+                          '将只保留最近 {{value}} 个日志文件,其余将被删除。',
+                          { value: logCleanupValue },
+                        )
+                      : t('将删除 {{value}} 天前的日志文件。', {
+                          value: logCleanupValue,
+                        })
+                  }
+                  onConfirm={cleanupLogFiles}
+                >
+                  <Button type='danger' loading={logCleanupLoading}>
+                    {t('清理日志文件')}
+                  </Button>
+                </Popconfirm>
+                </div>
+              </Col>
+            </Row>
+          </>
+        ) : (
+          <Banner
+            type='warning'
+            description={t('服务器日志功能未启用(未配置日志目录)')}
+          />
+        )}
+      </Form.Section>
+
       {/* 性能统计 */}
       <Spin spinning={statsLoading}>
         <Form.Section text={t('性能监控')}>