Update: Js 4 Log.html 80%

This commit is contained in:
XOF
2025-11-26 20:36:25 +08:00
parent 01c9b34600
commit c86e7a7ba4
17 changed files with 1120 additions and 473 deletions

View File

@@ -87,6 +87,9 @@ func (h *ProxyHandler) HandleProxy(c *gin.Context) {
return
}
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
c.Request.ContentLength = int64(len(requestBody))
modelName := h.channel.ExtractModel(c, requestBody)
groupName := c.Param("group_name")
isPreciseRouting := groupName != ""

View File

@@ -7,6 +7,7 @@ import (
"gemini-balancer/internal/settings"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
type SettingHandler struct {
@@ -23,16 +24,35 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
func (h *SettingHandler) UpdateSettings(c *gin.Context) {
var newSettingsMap map[string]interface{}
if err := c.ShouldBindJSON(&newSettingsMap); err != nil {
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
logrus.WithError(err).Error("Failed to bind JSON in UpdateSettings")
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, "Invalid JSON: "+err.Error()))
return
}
if err := h.settingsManager.UpdateSettings(newSettingsMap); err != nil {
// TODO 可以根据错误类型返回更具体的错误
logrus.Debugf("Received settings update: %+v", newSettingsMap)
validKeys := make(map[string]interface{})
for key, value := range newSettingsMap {
if _, exists := h.settingsManager.IsValidKey(key); exists {
validKeys[key] = value
} else {
logrus.Warnf("Invalid key received: %s", key)
}
}
if len(validKeys) == 0 {
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, "No valid settings keys provided"))
return
}
logrus.Debugf("Valid keys to update: %+v", validKeys)
if err := h.settingsManager.UpdateSettings(validKeys); err != nil {
logrus.WithError(err).Error("Failed to update settings")
response.Error(c, errors.NewAPIError(errors.ErrInternalServer, err.Error()))
return
}
response.Success(c, gin.H{"message": "Settings update request processed successfully."})
response.Success(c, gin.H{"message": "Settings updated successfully"})
}
// ResetSettingsToDefaults resets all settings to their default values

View File

@@ -71,6 +71,10 @@ type SystemSettings struct {
LogBufferCapacity int `json:"log_buffer_capacity" default:"1000" name:"日志缓冲区容量" category:"日志设置" desc:"内存中日志缓冲区的最大容量,超过则可能丢弃日志。"`
LogFlushBatchSize int `json:"log_flush_batch_size" default:"100" name:"日志刷新批次大小" category:"日志设置" desc:"每次向数据库批量写入日志的最大数量。"`
LogAutoCleanupEnabled bool `json:"log_auto_cleanup_enabled" default:"false" name:"开启请求日志自动清理" category:"日志配置" desc:"启用后,系统将每日定时删除旧的请求日志。"`
LogAutoCleanupRetentionDays int `json:"log_auto_cleanup_retention_days" default:"30" name:"日志保留天数" category:"日志配置" desc:"自动清理任务将保留最近 N 天的日志。"`
LogAutoCleanupTime string `json:"log_auto_cleanup_time" default:"04:05" name:"每日清理执行时间" category:"日志配置" desc:"自动清理任务执行的时间点24小时制例如 04:05。"`
// --- API配置 ---
CustomHeaders map[string]string `json:"custom_headers" name:"自定义Headers" category:"API配置" ` // 默认为nil

View File

@@ -1,42 +1,66 @@
// Filename: internal/scheduler/scheduler.go
// [REVISED] - 用这个更智能的版本完整替换
package scheduler
import (
"context"
"fmt" // [NEW] 导入 fmt
"gemini-balancer/internal/repository"
"gemini-balancer/internal/service"
"gemini-balancer/internal/settings"
"gemini-balancer/internal/store"
"strconv" // [NEW] 导入 strconv
"strings" // [NEW] 导入 strings
"sync"
"time"
"github.com/go-co-op/gocron"
"github.com/sirupsen/logrus"
)
// ... (Scheduler struct 和 NewScheduler 保持不变) ...
const LogCleanupTaskTag = "log-cleanup-task"
type Scheduler struct {
gocronScheduler *gocron.Scheduler
logger *logrus.Entry
statsService *service.StatsService
logService *service.LogService
settingsManager *settings.SettingsManager
keyRepo repository.KeyRepository
store store.Store
stopChan chan struct{}
wg sync.WaitGroup
}
func NewScheduler(statsSvc *service.StatsService, keyRepo repository.KeyRepository, logger *logrus.Logger) *Scheduler {
func NewScheduler(
statsSvc *service.StatsService,
logSvc *service.LogService,
keyRepo repository.KeyRepository,
settingsMgr *settings.SettingsManager,
store store.Store,
logger *logrus.Logger,
) *Scheduler {
s := gocron.NewScheduler(time.UTC)
s.TagsUnique()
return &Scheduler{
gocronScheduler: s,
logger: logger.WithField("component", "Scheduler📆"),
statsService: statsSvc,
logService: logSvc,
settingsManager: settingsMgr,
keyRepo: keyRepo,
store: store,
stopChan: make(chan struct{}),
}
}
// ... (Start 和 listenForSettingsUpdates 保持不变) ...
func (s *Scheduler) Start() {
s.logger.Info("Starting scheduler and registering jobs...")
// 任务一:每小时执行一次的统计聚合
// 使用CRON表达式精确定义“每小时的第5分钟”执行
// --- 注册静态定时任务 ---
_, err := s.gocronScheduler.Cron("5 * * * *").Tag("stats-aggregation").Do(func() {
s.logger.Info("Executing hourly request stats aggregation...")
// 为后台定时任务创建一个新的、空的 context
ctx := context.Background()
if err := s.statsService.AggregateHourlyStats(ctx); err != nil {
s.logger.WithError(err).Error("Hourly stats aggregation failed.")
@@ -47,17 +71,9 @@ func (s *Scheduler) Start() {
if err != nil {
s.logger.Errorf("Failed to schedule [stats-aggregation]: %v", err)
}
// 任务二:(预留) 自动健康检查
// 任务三每日执行一次的软删除Key清理
// Executes once daily at 3:15 AM UTC.
_, err = s.gocronScheduler.Cron("15 3 * * *").Tag("cleanup-soft-deleted-keys").Do(func() {
s.logger.Info("Executing daily cleanup of soft-deleted API keys...")
// [假设保留7天实际应来自配置
const retentionDays = 7
count, err := s.keyRepo.HardDeleteSoftDeletedBefore(time.Now().AddDate(0, 0, -retentionDays))
if err != nil {
s.logger.WithError(err).Error("Daily cleanup of soft-deleted keys failed.")
@@ -70,13 +86,125 @@ func (s *Scheduler) Start() {
if err != nil {
s.logger.Errorf("Failed to schedule [cleanup-soft-deleted-keys]: %v", err)
}
// --- 动态任务初始化 ---
if err := s.UpdateLogCleanupTask(); err != nil {
s.logger.WithError(err).Error("Failed to initialize log cleanup task on startup.")
}
// --- 启动后台监听器和调度器 ---
s.wg.Add(1)
go s.listenForSettingsUpdates()
s.gocronScheduler.StartAsync()
s.logger.Info("Scheduler started.")
}
func (s *Scheduler) listenForSettingsUpdates() {
defer s.wg.Done()
s.logger.Info("Starting listener for system settings updates...")
for {
select {
case <-s.stopChan:
s.logger.Info("Stopping settings update listener.")
return
default:
}
ctx, cancel := context.WithCancel(context.Background())
subscription, err := s.store.Subscribe(ctx, settings.SettingsUpdateChannel)
if err != nil {
s.logger.WithError(err).Warnf("Failed to subscribe to settings channel, retrying in 5s...")
cancel()
time.Sleep(5 * time.Second)
continue
}
s.logger.Infof("Successfully subscribed to channel '%s'.", settings.SettingsUpdateChannel)
listenLoop:
for {
select {
case msg, ok := <-subscription.Channel():
if !ok {
s.logger.Warn("Subscription channel closed by publisher. Re-subscribing...")
break listenLoop
}
s.logger.Infof("Received settings update notification: %s", string(msg.Payload))
if err := s.UpdateLogCleanupTask(); err != nil {
s.logger.WithError(err).Error("Failed to update log cleanup task after notification.")
}
case <-s.stopChan:
s.logger.Info("Stopping settings update listener.")
subscription.Close()
cancel()
return
}
}
subscription.Close()
cancel()
}
}
// [MODIFIED] - UpdateLogCleanupTask 现在会动态生成 cron 表达式
func (s *Scheduler) UpdateLogCleanupTask() error {
if err := s.gocronScheduler.RemoveByTag(LogCleanupTaskTag); err != nil {
// This is not an error, just means the job didn't exist
}
settings := s.settingsManager.GetSettings()
if !settings.LogAutoCleanupEnabled || settings.LogAutoCleanupRetentionDays <= 0 {
s.logger.Info("Log auto-cleanup is disabled. Task removed or not scheduled.")
return nil
}
days := settings.LogAutoCleanupRetentionDays
// [NEW] 解析时间并生成 cron 表达式
cronSpec, err := parseTimeToCron(settings.LogAutoCleanupTime)
if err != nil {
s.logger.WithError(err).Warnf("Invalid cleanup time format '%s'. Falling back to default '04:05'.", settings.LogAutoCleanupTime)
cronSpec = "5 4 * * *" // 安全回退
}
s.logger.Infof("Scheduling/updating daily log cleanup task to retain last %d days of logs, using cron spec: '%s'", days, cronSpec)
_, err = s.gocronScheduler.Cron(cronSpec).Tag(LogCleanupTaskTag).Do(func() {
s.logger.Infof("Executing daily log cleanup, deleting logs older than %d days...", days)
ctx := context.Background()
deletedCount, err := s.logService.DeleteOldLogs(ctx, days)
if err != nil {
s.logger.WithError(err).Error("Daily log cleanup task failed.")
} else {
s.logger.Infof("Daily log cleanup task completed. Deleted %d old logs.", deletedCount)
}
})
if err != nil {
s.logger.WithError(err).Error("Failed to schedule new log cleanup task.")
return err
}
s.logger.Info("Log cleanup task updated successfully.")
return nil
}
// [NEW] - 用于解析 "HH:mm" 格式时间为 cron 表达式的辅助函数
func parseTimeToCron(timeStr string) (string, error) {
parts := strings.Split(timeStr, ":")
if len(parts) != 2 {
return "", fmt.Errorf("invalid time format, expected HH:mm")
}
hour, err := strconv.Atoi(parts[0])
if err != nil || hour < 0 || hour > 23 {
return "", fmt.Errorf("invalid hour value: %s", parts[0])
}
minute, err := strconv.Atoi(parts[1])
if err != nil || minute < 0 || minute > 59 {
return "", fmt.Errorf("invalid minute value: %s", parts[1])
}
return fmt.Sprintf("%d %d * * *", minute, hour), nil
}
func (s *Scheduler) Stop() {
s.logger.Info("Stopping scheduler...")
close(s.stopChan)
s.gocronScheduler.Stop()
s.logger.Info("Scheduler stopped.")
s.wg.Wait()
s.logger.Info("Scheduler stopped gracefully.")
}

View File

@@ -87,7 +87,7 @@ func NewSettingsManager(db *gorm.DB, store store.Store, logger *logrus.Logger) (
return settings, nil
}
s, err := syncer.NewCacheSyncer(settingsLoader, store, SettingsUpdateChannel, logger,)
s, err := syncer.NewCacheSyncer(settingsLoader, store, SettingsUpdateChannel, logger)
if err != nil {
return nil, fmt.Errorf("failed to create system settings syncer: %w", err)
}
@@ -250,3 +250,9 @@ func (sm *SettingsManager) convertToDBValue(_ string, value interface{}, fieldTy
return fmt.Sprintf("%v", value), nil
}
}
// IsValidKey 检查给定的 JSON key 是否是有效的设置字段
func (sm *SettingsManager) IsValidKey(key string) (reflect.Type, bool) {
fieldType, ok := sm.jsonToFieldType[key]
return fieldType, ok
}