Преглед изворни кода

Merge branch 'main' into feat-04

neotf пре 8 месеци
родитељ
комит
81bc096872
100 измењених фајлова са 3641 додато и 7337 уклоњено
  1. 3 1
      README.md
  2. 7 0
      common/database.go
  3. 5 1
      common/redis.go
  4. 28 3
      common/utils.go
  5. 4 6
      constant/cache_key.go
  6. 1 0
      constant/user_setting.go
  7. 3 3
      controller/channel-test.go
  8. 21 14
      controller/channel.go
  9. 103 0
      controller/console_migrate.go
  10. 8 1
      controller/group.go
  11. 34 20
      controller/midjourney.go
  12. 64 43
      controller/misc.go
  13. 16 2
      controller/model.go
  14. 16 6
      controller/option.go
  15. 4 3
      controller/playground.go
  16. 8 1
      controller/pricing.go
  17. 39 3
      controller/redemption.go
  18. 2 2
      controller/relay.go
  19. 8 0
      controller/setup.go
  20. 30 14
      controller/task.go
  21. 12 4
      controller/token.go
  22. 96 111
      controller/uptime_kuma.go
  23. 8 0
      controller/user.go
  24. 8 1
      dto/claude.go
  25. 2 0
      dto/openai_request.go
  26. 3 1
      main.go
  27. 11 4
      middleware/distributor.go
  28. 21 16
      model/ability.go
  29. 40 1
      model/cache.go
  30. 24 10
      model/channel.go
  31. 46 9
      model/log.go
  32. 107 54
      model/main.go
  33. 37 0
      model/midjourney.go
  34. 10 4
      model/option.go
  35. 11 1
      model/redemption.go
  36. 61 0
      model/task.go
  37. 9 2
      model/token.go
  38. 1 1
      model/token_cache.go
  39. 5 3
      model/user.go
  40. 1 1
      model/user_cache.go
  41. 35 36
      relay/channel/claude/relay-claude.go
  42. 1 2
      relay/channel/cohere/relay-cohere.go
  43. 5 2
      relay/channel/gemini/adaptor.go
  44. 71 38
      relay/channel/gemini/relay-gemini.go
  45. 7 0
      relay/channel/openai/adaptor.go
  46. 5 3
      relay/channel/openai/relay-openai.go
  47. 1 2
      relay/channel/palm/relay-palm.go
  48. 1 1
      relay/claude_handler.go
  49. 2 0
      relay/common/relay_info.go
  50. 45 7
      relay/helper/price.go
  51. 14 0
      relay/relay-gemini.go
  52. 1 1
      relay/relay-image.go
  53. 6 5
      relay/relay-text.go
  54. 6 37
      relay/websocket.go
  55. 2 0
      router/api-router.go
  56. 2 0
      service/channel.go
  57. 2 2
      service/convert.go
  58. 10 6
      service/error.go
  59. 98 1
      service/file_decoder.go
  60. 8 7
      service/log_info_generate.go
  61. 26 9
      service/quota.go
  62. 31 0
      setting/auto_group.go
  63. 0 327
      setting/console.go
  64. 39 0
      setting/console_setting/config.go
  65. 304 0
      setting/console_setting/validation.go
  66. 48 5
      setting/group_ratio.go
  67. 17 2
      setting/operation_setting/model-ratio.go
  68. 7 0
      setting/user_usable_group.go
  69. 0 21
      web/README.md
  70. 0 2
      web/package.json
  71. 0 5584
      web/pnpm-lock.yaml
  72. 1 5
      web/src/components/layout/NoticeModal.js
  73. 57 3
      web/src/components/settings/DashboardSetting.js
  74. 7 1
      web/src/components/settings/OperationSetting.js
  75. 52 9
      web/src/components/settings/PersonalSetting.js
  76. 14 33
      web/src/components/table/ChannelsTable.js
  77. 127 58
      web/src/components/table/LogsTable.js
  78. 27 45
      web/src/components/table/MjLogsTable.js
  79. 79 36
      web/src/components/table/RedemptionsTable.js
  80. 35 51
      web/src/components/table/TaskLogsTable.js
  81. 42 40
      web/src/components/table/TokensTable.js
  82. 22 0
      web/src/components/table/UsersTable.js
  83. 1 0
      web/src/helpers/api.js
  84. 124 80
      web/src/helpers/render.js
  85. 6 7
      web/src/helpers/token.js
  86. 30 11
      web/src/i18n/locales/en.json
  87. 11 9
      web/src/index.css
  88. 13 6
      web/src/pages/Channel/EditChannel.js
  89. 13 6
      web/src/pages/Channel/EditTagModal.js
  90. 312 198
      web/src/pages/Detail/index.js
  91. 26 1
      web/src/pages/Redemption/EditRedemption.js
  92. 48 6
      web/src/pages/Setting/Dashboard/SettingsAPIInfo.js
  93. 45 6
      web/src/pages/Setting/Dashboard/SettingsAnnouncements.js
  94. 59 21
      web/src/pages/Setting/Dashboard/SettingsFAQ.js
  95. 422 126
      web/src/pages/Setting/Dashboard/SettingsUptimeKuma.js
  96. 4 3
      web/src/pages/Setting/Model/SettingGeminiModel.js
  97. 86 0
      web/src/pages/Setting/Operation/GroupRatioSettings.js
  98. 233 129
      web/src/pages/Token/EditToken.js
  99. 17 1
      web/src/pages/User/AddUser.js
  100. 17 0
      web/src/pages/User/EditUser.js

+ 3 - 1
README.md

@@ -27,6 +27,9 @@
   <a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
     <img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
   </a>
+  <a href="https://coderabbit.ai">
+    <img src="https://img.shields.io/coderabbit/prs/github/QuantumNous/new-api?utm_source=oss&utm_medium=github&utm_campaign=QuantumNous%2Fnew-api&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews" alt="CodeRabbit Pull Request Reviews">
+  </a>
 </p>
 </div>
 
@@ -180,7 +183,6 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
 
 其他基于New API的项目:
 - [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon):New API高性能优化版
-- [VoAPI](https://github.com/VoAPI/VoAPI):基于New API的前端美化版本
 
 ## 帮助支持
 

+ 7 - 0
common/database.go

@@ -1,7 +1,14 @@
 package common
 
+const (
+	DatabaseTypeMySQL      = "mysql"
+	DatabaseTypeSQLite     = "sqlite"
+	DatabaseTypePostgreSQL = "postgres"
+)
+
 var UsingSQLite = false
 var UsingPostgreSQL = false
+var LogSqlType = DatabaseTypeSQLite // Default to SQLite for logging SQL queries
 var UsingMySQL = false
 var UsingClickHouse = false
 

+ 5 - 1
common/redis.go

@@ -141,7 +141,11 @@ func RedisHSetObj(key string, obj interface{}, expiration time.Duration) error {
 
 	txn := RDB.TxPipeline()
 	txn.HSet(ctx, key, data)
-	txn.Expire(ctx, key, expiration)
+
+	// 只有在 expiration 大于 0 时才设置过期时间
+	if expiration > 0 {
+		txn.Expire(ctx, key, expiration)
+	}
 
 	_, err := txn.Exec(ctx)
 	if err != nil {

+ 28 - 3
common/utils.go

@@ -249,13 +249,38 @@ func SaveTmpFile(filename string, data io.Reader) (string, error) {
 }
 
 // GetAudioDuration returns the duration of an audio file in seconds.
-func GetAudioDuration(ctx context.Context, filename string) (float64, error) {
+func GetAudioDuration(ctx context.Context, filename string, ext string) (float64, error) {
 	// ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {{input}}
 	c := exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filename)
 	output, err := c.Output()
 	if err != nil {
 		return 0, errors.Wrap(err, "failed to get audio duration")
 	}
-
-	return strconv.ParseFloat(string(bytes.TrimSpace(output)), 64)
+  durationStr := string(bytes.TrimSpace(output))
+  if durationStr == "N/A" {
+    // Create a temporary output file name
+    tmpFp, err := os.CreateTemp("", "audio-*"+ext)
+    if err != nil {
+      return 0, errors.Wrap(err, "failed to create temporary file")
+    }
+    tmpName := tmpFp.Name()
+    // Close immediately so ffmpeg can open the file on Windows.
+    _ = tmpFp.Close()
+    defer os.Remove(tmpName)
+
+    // ffmpeg -y -i filename -vcodec copy -acodec copy <tmpName>
+    ffmpegCmd := exec.CommandContext(ctx, "ffmpeg", "-y", "-i", filename, "-vcodec", "copy", "-acodec", "copy", tmpName)
+    if err := ffmpegCmd.Run(); err != nil {
+      return 0, errors.Wrap(err, "failed to run ffmpeg")
+    }
+
+    // Recalculate the duration of the new file
+    c = exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", tmpName)
+    output, err := c.Output()
+    if err != nil {
+      return 0, errors.Wrap(err, "failed to get audio duration after ffmpeg")
+    }
+    durationStr = string(bytes.TrimSpace(output))
+  }
+	return strconv.ParseFloat(durationStr, 64)
 }

+ 4 - 6
constant/cache_key.go

@@ -2,12 +2,10 @@ package constant
 
 import "one-api/common"
 
-var (
-	TokenCacheSeconds         = common.SyncFrequency
-	UserId2GroupCacheSeconds  = common.SyncFrequency
-	UserId2QuotaCacheSeconds  = common.SyncFrequency
-	UserId2StatusCacheSeconds = common.SyncFrequency
-)
+// 使用函数来避免初始化顺序带来的赋值问题
+func RedisKeyCacheSeconds() int {
+	return common.SyncFrequency
+}
 
 // Cache keys
 const (

+ 1 - 0
constant/user_setting.go

@@ -7,6 +7,7 @@ var (
 	UserSettingWebhookSecret         = "webhook_secret"                 // WebhookSecret webhook密钥
 	UserSettingNotificationEmail     = "notification_email"             // NotificationEmail 通知邮箱地址
 	UserAcceptUnsetRatioModel        = "accept_unset_model_ratio_model" // AcceptUnsetRatioModel 是否接受未设置价格的模型
+	UserSettingRecordIpLog          = "record_ip_log"                   // 是否记录请求和错误日志IP
 )
 
 var (

+ 3 - 3
controller/channel-test.go

@@ -165,8 +165,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
 	tok := time.Now()
 	milliseconds := tok.Sub(tik).Milliseconds()
 	consumedTime := float64(milliseconds) / 1000.0
-	other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatio, priceData.CompletionRatio,
-		usage.PromptTokensDetails.CachedTokens, priceData.CacheRatio, priceData.ModelPrice)
+	other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatioInfo.GroupRatio, priceData.CompletionRatio,
+		usage.PromptTokensDetails.CachedTokens, priceData.CacheRatio, priceData.ModelPrice, priceData.GroupRatioInfo.GroupSpecialRatio)
 	model.RecordConsumeLog(c, 1, channel.Id, usage.PromptTokens, usage.CompletionTokens, info.OriginModelName, "模型测试",
 		quota, "模型测试", 0, quota, int(consumedTime), false, info.Group, other)
 	common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))
@@ -312,7 +312,7 @@ func testAllChannels(notify bool) error {
 			channel.UpdateResponseTime(milliseconds)
 			time.Sleep(common.RequestInterval)
 		}
-		
+
 		if notify {
 			service.NotifyRootUser(dto.NotifyTypeChannelTest, "通道测试完成", "所有通道测试已完成")
 		}

+ 21 - 14
controller/channel.go

@@ -43,22 +43,23 @@ type OpenAIModelsResponse struct {
 func GetAllChannels(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
 	pageSize, _ := strconv.Atoi(c.Query("page_size"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
 	}
-	if pageSize < 0 {
+	if pageSize < 1 {
 		pageSize = common.ItemsPerPage
 	}
 	channelData := make([]*model.Channel, 0)
 	idSort, _ := strconv.ParseBool(c.Query("id_sort"))
 	enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
+
+	var total int64
+
 	if enableTagMode {
-		tags, err := model.GetPaginatedTags(p*pageSize, pageSize)
+		// tag 分页:先分页 tag,再取各 tag 下 channels
+		tags, err := model.GetPaginatedTags((p-1)*pageSize, pageSize)
 		if err != nil {
-			c.JSON(http.StatusOK, gin.H{
-				"success": false,
-				"message": err.Error(),
-			})
+			c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
 			return
 		}
 		for _, tag := range tags {
@@ -69,21 +70,27 @@ func GetAllChannels(c *gin.Context) {
 				}
 			}
 		}
+		// 计算 tag 总数用于分页
+		total, _ = model.CountAllTags()
 	} else {
-		channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
+		channels, err := model.GetAllChannels((p-1)*pageSize, pageSize, false, idSort)
 		if err != nil {
-			c.JSON(http.StatusOK, gin.H{
-				"success": false,
-				"message": err.Error(),
-			})
+			c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
 			return
 		}
 		channelData = channels
+		total, _ = model.CountAllChannels()
 	}
+
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",
-		"data":    channelData,
+		"data": gin.H{
+			"items":     channelData,
+			"total":     total,
+			"page":      p,
+			"page_size": pageSize,
+		},
 	})
 	return
 }

+ 103 - 0
controller/console_migrate.go

@@ -0,0 +1,103 @@
+// 用于迁移检测的旧键,该文件下个版本会删除
+
+package controller
+
+import (
+    "encoding/json"
+    "net/http"
+    "one-api/common"
+    "one-api/model"
+    "github.com/gin-gonic/gin"
+)
+
+// MigrateConsoleSetting 迁移旧的控制台相关配置到 console_setting.*
+func MigrateConsoleSetting(c *gin.Context) {
+    // 读取全部 option
+    opts, err := model.AllOption()
+    if err != nil {
+        c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
+        return
+    }
+    // 建立 map
+    valMap := map[string]string{}
+    for _, o := range opts {
+        valMap[o.Key] = o.Value
+    }
+
+    // 处理 APIInfo
+    if v := valMap["ApiInfo"]; v != "" {
+        var arr []map[string]interface{}
+        if err := json.Unmarshal([]byte(v), &arr); err == nil {
+            if len(arr) > 50 {
+                arr = arr[:50]
+            }
+            bytes, _ := json.Marshal(arr)
+            model.UpdateOption("console_setting.api_info", string(bytes))
+        }
+        model.UpdateOption("ApiInfo", "")
+    }
+    // Announcements 直接搬
+    if v := valMap["Announcements"]; v != "" {
+        model.UpdateOption("console_setting.announcements", v)
+        model.UpdateOption("Announcements", "")
+    }
+    // FAQ 转换
+    if v := valMap["FAQ"]; v != "" {
+        var arr []map[string]interface{}
+        if err := json.Unmarshal([]byte(v), &arr); err == nil {
+            out := []map[string]interface{}{}
+            for _, item := range arr {
+                q, _ := item["question"].(string)
+                if q == "" {
+                    q, _ = item["title"].(string)
+                }
+                a, _ := item["answer"].(string)
+                if a == "" {
+                    a, _ = item["content"].(string)
+                }
+                if q != "" && a != "" {
+                    out = append(out, map[string]interface{}{"question": q, "answer": a})
+                }
+            }
+            if len(out) > 50 {
+                out = out[:50]
+            }
+            bytes, _ := json.Marshal(out)
+            model.UpdateOption("console_setting.faq", string(bytes))
+        }
+        model.UpdateOption("FAQ", "")
+    }
+    // Uptime Kuma 迁移到新的 groups 结构(console_setting.uptime_kuma_groups)
+    url := valMap["UptimeKumaUrl"]
+    slug := valMap["UptimeKumaSlug"]
+    if url != "" && slug != "" {
+        // 仅当同时存在 URL 与 Slug 时才进行迁移
+        groups := []map[string]interface{}{
+            {
+                "id":           1,
+                "categoryName": "old",
+                "url":          url,
+                "slug":         slug,
+                "description":  "",
+            },
+        }
+        bytes, _ := json.Marshal(groups)
+        model.UpdateOption("console_setting.uptime_kuma_groups", string(bytes))
+    }
+    // 清空旧键内容
+    if url != "" {
+        model.UpdateOption("UptimeKumaUrl", "")
+    }
+    if slug != "" {
+        model.UpdateOption("UptimeKumaSlug", "")
+    }
+
+    // 删除旧键记录
+    oldKeys := []string{"ApiInfo", "Announcements", "FAQ", "UptimeKumaUrl", "UptimeKumaSlug"}
+    model.DB.Where("key IN ?", oldKeys).Delete(&model.Option{})
+
+    // 重新加载 OptionMap
+    model.InitOptionMap()
+    common.SysLog("console setting migrated")
+    c.JSON(http.StatusOK, gin.H{"success": true, "message": "migrated"})
+} 

+ 8 - 1
controller/group.go

@@ -1,10 +1,11 @@
 package controller
 
 import (
-	"github.com/gin-gonic/gin"
 	"net/http"
 	"one-api/model"
 	"one-api/setting"
+
+	"github.com/gin-gonic/gin"
 )
 
 func GetGroups(c *gin.Context) {
@@ -34,6 +35,12 @@ func GetUserGroups(c *gin.Context) {
 			}
 		}
 	}
+	if setting.GroupInUserUsableGroups("auto") {
+		usableGroups["auto"] = map[string]interface{}{
+			"ratio": "自动",
+			"desc":  setting.GetUsableGroupDescription("auto"),
+		}
+	}
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",

+ 34 - 20
controller/midjourney.go

@@ -7,7 +7,6 @@ import (
 	"fmt"
 	"github.com/gin-gonic/gin"
 	"io"
-	"log"
 	"net/http"
 	"one-api/common"
 	"one-api/dto"
@@ -215,8 +214,12 @@ func checkMjTaskNeedUpdate(oldTask *model.Midjourney, newTask dto.MidjourneyDto)
 
 func GetAllMidjourney(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
+	}
+	pageSize, _ := strconv.Atoi(c.Query("page_size"))
+	if pageSize <= 0 {
+		pageSize = common.ItemsPerPage
 	}
 
 	// 解析其他查询参数
@@ -227,31 +230,38 @@ func GetAllMidjourney(c *gin.Context) {
 		EndTimestamp:   c.Query("end_timestamp"),
 	}
 
-	logs := model.GetAllTasks(p*common.ItemsPerPage, common.ItemsPerPage, queryParams)
-	if logs == nil {
-		logs = make([]*model.Midjourney, 0)
-	}
+	items := model.GetAllTasks((p-1)*pageSize, pageSize, queryParams)
+	total := model.CountAllTasks(queryParams)
+
 	if setting.MjForwardUrlEnabled {
-		for i, midjourney := range logs {
+		for i, midjourney := range items {
 			midjourney.ImageUrl = setting.ServerAddress + "/mj/image/" + midjourney.MjId
-			logs[i] = midjourney
+			items[i] = midjourney
 		}
 	}
 	c.JSON(200, gin.H{
 		"success": true,
 		"message": "",
-		"data":    logs,
+		"data": gin.H{
+			"items":     items,
+			"total":     total,
+			"page":      p,
+			"page_size": pageSize,
+		},
 	})
 }
 
 func GetUserMidjourney(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
+	}
+	pageSize, _ := strconv.Atoi(c.Query("page_size"))
+	if pageSize <= 0 {
+		pageSize = common.ItemsPerPage
 	}
 
 	userId := c.GetInt("id")
-	log.Printf("userId = %d \n", userId)
 
 	queryParams := model.TaskQueryParams{
 		MjID:           c.Query("mj_id"),
@@ -259,19 +269,23 @@ func GetUserMidjourney(c *gin.Context) {
 		EndTimestamp:   c.Query("end_timestamp"),
 	}
 
-	logs := model.GetAllUserTask(userId, p*common.ItemsPerPage, common.ItemsPerPage, queryParams)
-	if logs == nil {
-		logs = make([]*model.Midjourney, 0)
-	}
+	items := model.GetAllUserTask(userId, (p-1)*pageSize, pageSize, queryParams)
+	total := model.CountAllUserTask(userId, queryParams)
+
 	if setting.MjForwardUrlEnabled {
-		for i, midjourney := range logs {
+		for i, midjourney := range items {
 			midjourney.ImageUrl = setting.ServerAddress + "/mj/image/" + midjourney.MjId
-			logs[i] = midjourney
+			items[i] = midjourney
 		}
 	}
 	c.JSON(200, gin.H{
 		"success": true,
 		"message": "",
-		"data":    logs,
+		"data": gin.H{
+			"items":     items,
+			"total":     total,
+			"page":      p,
+			"page_size": pageSize,
+		},
 	})
 }

+ 64 - 43
controller/misc.go

@@ -9,6 +9,7 @@ import (
 	"one-api/middleware"
 	"one-api/model"
 	"one-api/setting"
+	"one-api/setting/console_setting"
 	"one-api/setting/operation_setting"
 	"one-api/setting/system_setting"
 	"strings"
@@ -37,52 +38,72 @@ func TestStatus(c *gin.Context) {
 
 func GetStatus(c *gin.Context) {
 
+	cs := console_setting.GetConsoleSetting()
+
+	data := gin.H{
+		"version":                  common.Version,
+		"start_time":               common.StartTime,
+		"email_verification":       common.EmailVerificationEnabled,
+		"github_oauth":             common.GitHubOAuthEnabled,
+		"github_client_id":         common.GitHubClientId,
+		"linuxdo_oauth":            common.LinuxDOOAuthEnabled,
+		"linuxdo_client_id":        common.LinuxDOClientId,
+		"telegram_oauth":           common.TelegramOAuthEnabled,
+		"telegram_bot_name":        common.TelegramBotName,
+		"system_name":              common.SystemName,
+		"logo":                     common.Logo,
+		"footer_html":              common.Footer,
+		"wechat_qrcode":            common.WeChatAccountQRCodeImageURL,
+		"wechat_login":             common.WeChatAuthEnabled,
+		"server_address":           setting.ServerAddress,
+		"price":                    setting.Price,
+		"min_topup":                setting.MinTopUp,
+		"turnstile_check":          common.TurnstileCheckEnabled,
+		"turnstile_site_key":       common.TurnstileSiteKey,
+		"top_up_link":              common.TopUpLink,
+		"docs_link":                operation_setting.GetGeneralSetting().DocsLink,
+		"quota_per_unit":           common.QuotaPerUnit,
+		"display_in_currency":      common.DisplayInCurrencyEnabled,
+		"enable_batch_update":      common.BatchUpdateEnabled,
+		"enable_drawing":           common.DrawingEnabled,
+		"enable_task":              common.TaskEnabled,
+		"enable_data_export":       common.DataExportEnabled,
+		"data_export_default_time": common.DataExportDefaultTime,
+		"default_collapse_sidebar": common.DefaultCollapseSidebar,
+		"enable_online_topup":      setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
+		"mj_notify_enabled":        setting.MjNotifyEnabled,
+		"chats":                    setting.Chats,
+		"demo_site_enabled":        operation_setting.DemoSiteEnabled,
+		"self_use_mode_enabled":    operation_setting.SelfUseModeEnabled,
+		"default_use_auto_group":   setting.DefaultUseAutoGroup,
+
+		// 面板启用开关
+		"api_info_enabled":      cs.ApiInfoEnabled,
+		"uptime_kuma_enabled":   cs.UptimeKumaEnabled,
+		"announcements_enabled": cs.AnnouncementsEnabled,
+		"faq_enabled":           cs.FAQEnabled,
+
+		"oidc_enabled":                system_setting.GetOIDCSettings().Enabled,
+		"oidc_client_id":              system_setting.GetOIDCSettings().ClientId,
+		"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
+		"setup":                       constant.Setup,
+	}
+
+	// 根据启用状态注入可选内容
+	if cs.ApiInfoEnabled {
+		data["api_info"] = console_setting.GetApiInfo()
+	}
+	if cs.AnnouncementsEnabled {
+		data["announcements"] = console_setting.GetAnnouncements()
+	}
+	if cs.FAQEnabled {
+		data["faq"] = console_setting.GetFAQ()
+	}
+
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",
-		"data": gin.H{
-			"version":                     common.Version,
-			"start_time":                  common.StartTime,
-			"email_verification":          common.EmailVerificationEnabled,
-			"github_oauth":                common.GitHubOAuthEnabled,
-			"github_client_id":            common.GitHubClientId,
-			"linuxdo_oauth":               common.LinuxDOOAuthEnabled,
-			"linuxdo_client_id":           common.LinuxDOClientId,
-			"telegram_oauth":              common.TelegramOAuthEnabled,
-			"telegram_bot_name":           common.TelegramBotName,
-			"system_name":                 common.SystemName,
-			"logo":                        common.Logo,
-			"footer_html":                 common.Footer,
-			"wechat_qrcode":               common.WeChatAccountQRCodeImageURL,
-			"wechat_login":                common.WeChatAuthEnabled,
-			"server_address":              setting.ServerAddress,
-			"price":                       setting.Price,
-			"min_topup":                   setting.MinTopUp,
-			"turnstile_check":             common.TurnstileCheckEnabled,
-			"turnstile_site_key":          common.TurnstileSiteKey,
-			"top_up_link":                 common.TopUpLink,
-			"docs_link":                   operation_setting.GetGeneralSetting().DocsLink,
-			"quota_per_unit":              common.QuotaPerUnit,
-			"display_in_currency":         common.DisplayInCurrencyEnabled,
-			"enable_batch_update":         common.BatchUpdateEnabled,
-			"enable_drawing":              common.DrawingEnabled,
-			"enable_task":                 common.TaskEnabled,
-			"enable_data_export":          common.DataExportEnabled,
-			"data_export_default_time":    common.DataExportDefaultTime,
-			"default_collapse_sidebar":    common.DefaultCollapseSidebar,
-			"enable_online_topup":         setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
-			"mj_notify_enabled":           setting.MjNotifyEnabled,
-			"chats":                       setting.Chats,
-			"demo_site_enabled":           operation_setting.DemoSiteEnabled,
-			"self_use_mode_enabled":       operation_setting.SelfUseModeEnabled,
-			"oidc_enabled":                system_setting.GetOIDCSettings().Enabled,
-			"oidc_client_id":              system_setting.GetOIDCSettings().ClientId,
-			"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
-			"setup":                       constant.Setup,
-			"api_info":                    setting.GetApiInfo(),
-			"announcements":               setting.GetAnnouncements(),
-			"faq":                         setting.GetFAQ(),
-		},
+		"data":    data,
 	})
 	return
 }

+ 16 - 2
controller/model.go

@@ -2,7 +2,6 @@ package controller
 
 import (
 	"fmt"
-	"github.com/gin-gonic/gin"
 	"net/http"
 	"one-api/common"
 	"one-api/constant"
@@ -15,6 +14,9 @@ import (
 	"one-api/relay/channel/moonshot"
 	relaycommon "one-api/relay/common"
 	relayconstant "one-api/relay/constant"
+	"one-api/setting"
+
+	"github.com/gin-gonic/gin"
 )
 
 // https://platform.openai.com/docs/api-reference/models/list
@@ -179,7 +181,19 @@ func ListModels(c *gin.Context) {
 		if tokenGroup != "" {
 			group = tokenGroup
 		}
-		models := model.GetGroupModels(group)
+		var models []string
+		if tokenGroup == "auto" {
+			for _, autoGroup := range setting.AutoGroups {
+				groupModels := model.GetGroupModels(autoGroup)
+				for _, g := range groupModels {
+					if !common.StringsContains(models, g) {
+						models = append(models, g)
+					}
+				}
+			}
+		} else {
+			models = model.GetGroupModels(group)
+		}
 		for _, s := range models {
 			if _, ok := openAIModelsMap[s]; ok {
 				userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])

+ 16 - 6
controller/option.go

@@ -6,6 +6,7 @@ import (
 	"one-api/common"
 	"one-api/model"
 	"one-api/setting"
+	"one-api/setting/console_setting"
 	"one-api/setting/system_setting"
 	"strings"
 
@@ -119,8 +120,8 @@ func UpdateOption(c *gin.Context) {
 			})
 			return
 		}
-	case "ApiInfo":
-		err = setting.ValidateApiInfo(option.Value)
+	case "console_setting.api_info":
+		err = console_setting.ValidateConsoleSettings(option.Value, "ApiInfo")
 		if err != nil {
 			c.JSON(http.StatusOK, gin.H{
 				"success": false,
@@ -128,8 +129,8 @@ func UpdateOption(c *gin.Context) {
 			})
 			return
 		}
-	case "Announcements":
-		err = setting.ValidateConsoleSettings(option.Value, "Announcements")
+	case "console_setting.announcements":
+		err = console_setting.ValidateConsoleSettings(option.Value, "Announcements")
 		if err != nil {
 			c.JSON(http.StatusOK, gin.H{
 				"success": false,
@@ -137,8 +138,17 @@ func UpdateOption(c *gin.Context) {
 			})
 			return
 		}
-	case "FAQ":
-		err = setting.ValidateConsoleSettings(option.Value, "FAQ")
+	case "console_setting.faq":
+		err = console_setting.ValidateConsoleSettings(option.Value, "FAQ")
+		if err != nil {
+			c.JSON(http.StatusOK, gin.H{
+				"success": false,
+				"message": err.Error(),
+			})
+			return
+		}
+	case "console_setting.uptime_kuma_groups":
+		err = console_setting.ValidateConsoleSettings(option.Value, "UptimeKumaGroups")
 		if err != nil {
 			c.JSON(http.StatusOK, gin.H{
 				"success": false,

+ 4 - 3
controller/playground.go

@@ -3,7 +3,6 @@ package controller
 import (
 	"errors"
 	"fmt"
-	"github.com/gin-gonic/gin"
 	"net/http"
 	"one-api/common"
 	"one-api/constant"
@@ -13,6 +12,8 @@ import (
 	"one-api/service"
 	"one-api/setting"
 	"time"
+
+	"github.com/gin-gonic/gin"
 )
 
 func Playground(c *gin.Context) {
@@ -57,9 +58,9 @@ func Playground(c *gin.Context) {
 		c.Set("group", group)
 	}
 	c.Set("token_name", "playground-"+group)
-	channel, err := model.CacheGetRandomSatisfiedChannel(group, playgroundRequest.Model, 0)
+	channel, finalGroup, err := model.CacheGetRandomSatisfiedChannel(c, group, playgroundRequest.Model, 0)
 	if err != nil {
-		message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", group, playgroundRequest.Model)
+		message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", finalGroup, playgroundRequest.Model)
 		openaiErr = service.OpenAIErrorWrapperLocal(errors.New(message), "get_playground_channel_failed", http.StatusInternalServerError)
 		return
 	}

+ 8 - 1
controller/pricing.go

@@ -1,10 +1,11 @@
 package controller
 
 import (
-	"github.com/gin-gonic/gin"
 	"one-api/model"
 	"one-api/setting"
 	"one-api/setting/operation_setting"
+
+	"github.com/gin-gonic/gin"
 )
 
 func GetPricing(c *gin.Context) {
@@ -20,6 +21,12 @@ func GetPricing(c *gin.Context) {
 		user, err := model.GetUserCache(userId.(int))
 		if err == nil {
 			group = user.Group
+			for g := range groupRatio {
+				ratio, ok := setting.GetGroupGroupRatio(group, g)
+				if ok {
+					groupRatio[g] = ratio
+				}
+			}
 		}
 	}
 

+ 39 - 3
controller/redemption.go

@@ -5,6 +5,7 @@ import (
 	"one-api/common"
 	"one-api/model"
 	"strconv"
+	"errors"
 
 	"github.com/gin-gonic/gin"
 )
@@ -126,6 +127,10 @@ func AddRedemption(c *gin.Context) {
 		})
 		return
 	}
+	if err := validateExpiredTime(redemption.ExpiredTime); err != nil {
+		c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
+		return
+	}
 	var keys []string
 	for i := 0; i < redemption.Count; i++ {
 		key := common.GetUUID()
@@ -135,6 +140,7 @@ func AddRedemption(c *gin.Context) {
 			Key:         key,
 			CreatedTime: common.GetTimestamp(),
 			Quota:       redemption.Quota,
+			ExpiredTime: redemption.ExpiredTime,
 		}
 		err = cleanRedemption.Insert()
 		if err != nil {
@@ -191,12 +197,18 @@ func UpdateRedemption(c *gin.Context) {
 		})
 		return
 	}
-	if statusOnly != "" {
-		cleanRedemption.Status = redemption.Status
-	} else {
+	if statusOnly == "" {
+		if err := validateExpiredTime(redemption.ExpiredTime); err != nil {
+			c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
+			return
+		}
 		// If you add more fields, please also update redemption.Update()
 		cleanRedemption.Name = redemption.Name
 		cleanRedemption.Quota = redemption.Quota
+		cleanRedemption.ExpiredTime = redemption.ExpiredTime
+	}
+	if statusOnly != "" {
+		cleanRedemption.Status = redemption.Status
 	}
 	err = cleanRedemption.Update()
 	if err != nil {
@@ -213,3 +225,27 @@ func UpdateRedemption(c *gin.Context) {
 	})
 	return
 }
+
+func DeleteInvalidRedemption(c *gin.Context) {
+	rows, err := model.DeleteInvalidRedemptions()
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data": rows,
+	})
+	return
+}
+
+func validateExpiredTime(expired int64) error {
+	if expired != 0 && expired < common.GetTimestamp() {
+		return errors.New("过期时间不能早于当前时间")
+	}
+	return nil
+}

+ 2 - 2
controller/relay.go

@@ -259,7 +259,7 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m
 			AutoBan: &autoBanInt,
 		}, nil
 	}
-	channel, err := model.CacheGetRandomSatisfiedChannel(group, originalModel, retryCount)
+	channel, _, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, retryCount)
 	if err != nil {
 		return nil, errors.New(fmt.Sprintf("获取重试渠道失败: %s", err.Error()))
 	}
@@ -388,7 +388,7 @@ func RelayTask(c *gin.Context) {
 		retryTimes = 0
 	}
 	for i := 0; shouldRetryTaskRelay(c, channelId, taskErr, retryTimes) && i < retryTimes; i++ {
-		channel, err := model.CacheGetRandomSatisfiedChannel(group, originalModel, i)
+		channel, _, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, i)
 		if err != nil {
 			common.LogError(c, fmt.Sprintf("CacheGetRandomSatisfiedChannel failed: %s", err.Error()))
 			break

+ 8 - 0
controller/setup.go

@@ -75,6 +75,14 @@ func PostSetup(c *gin.Context) {
 
 	// If root doesn't exist, validate and create admin account
 	if !rootExists {
+		// Validate username length: max 12 characters to align with model.User validation
+		if len(req.Username) > 12 {
+			c.JSON(400, gin.H{
+				"success": false,
+				"message": "用户名长度不能超过12个字符",
+			})
+			return
+		}
 		// Validate password
 		if req.Password != req.ConfirmPassword {
 			c.JSON(400, gin.H{

+ 30 - 14
controller/task.go

@@ -224,9 +224,14 @@ func checkTaskNeedUpdate(oldTask *model.Task, newTask dto.SunoDataResponse) bool
 
 func GetAllTask(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
 	}
+	pageSize, _ := strconv.Atoi(c.Query("page_size"))
+	if pageSize <= 0 {
+		pageSize = common.ItemsPerPage
+	}
+
 	startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
 	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
 	// 解析其他查询参数
@@ -237,24 +242,32 @@ func GetAllTask(c *gin.Context) {
 		Action:         c.Query("action"),
 		StartTimestamp: startTimestamp,
 		EndTimestamp:   endTimestamp,
+		ChannelID:      c.Query("channel_id"),
 	}
 
-	logs := model.TaskGetAllTasks(p*common.ItemsPerPage, common.ItemsPerPage, queryParams)
-	if logs == nil {
-		logs = make([]*model.Task, 0)
-	}
+	items := model.TaskGetAllTasks((p-1)*pageSize, pageSize, queryParams)
+	total := model.TaskCountAllTasks(queryParams)
 
 	c.JSON(200, gin.H{
 		"success": true,
 		"message": "",
-		"data":    logs,
+		"data": gin.H{
+			"items":     items,
+			"total":     total,
+			"page":      p,
+			"page_size": pageSize,
+		},
 	})
 }
 
 func GetUserTask(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
+	}
+	pageSize, _ := strconv.Atoi(c.Query("page_size"))
+	if pageSize <= 0 {
+		pageSize = common.ItemsPerPage
 	}
 
 	userId := c.GetInt("id")
@@ -271,14 +284,17 @@ func GetUserTask(c *gin.Context) {
 		EndTimestamp:   endTimestamp,
 	}
 
-	logs := model.TaskGetAllUserTask(userId, p*common.ItemsPerPage, common.ItemsPerPage, queryParams)
-	if logs == nil {
-		logs = make([]*model.Task, 0)
-	}
+	items := model.TaskGetAllUserTask(userId, (p-1)*pageSize, pageSize, queryParams)
+	total := model.TaskCountAllUserTask(userId, queryParams)
 
 	c.JSON(200, gin.H{
 		"success": true,
 		"message": "",
-		"data":    logs,
+		"data": gin.H{
+			"items":     items,
+			"total":     total,
+			"page":      p,
+			"page_size": pageSize,
+		},
 	})
 }

+ 12 - 4
controller/token.go

@@ -12,15 +12,15 @@ func GetAllTokens(c *gin.Context) {
 	userId := c.GetInt("id")
 	p, _ := strconv.Atoi(c.Query("p"))
 	size, _ := strconv.Atoi(c.Query("size"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
 	}
 	if size <= 0 {
 		size = common.ItemsPerPage
 	} else if size > 100 {
 		size = 100
 	}
-	tokens, err := model.GetAllUserTokens(userId, p*size, size)
+	tokens, err := model.GetAllUserTokens(userId, (p-1)*size, size)
 	if err != nil {
 		c.JSON(http.StatusOK, gin.H{
 			"success": false,
@@ -28,10 +28,18 @@ func GetAllTokens(c *gin.Context) {
 		})
 		return
 	}
+	// Get total count for pagination
+	total, _ := model.CountUserTokens(userId)
+
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",
-		"data":    tokens,
+		"data": gin.H{
+			"items":     tokens,
+			"total":     total,
+			"page":      p,
+			"page_size": size,
+		},
 	})
 	return
 }

+ 96 - 111
controller/uptime_kuma.go

@@ -4,9 +4,9 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
-	"fmt"
 	"net/http"
-	"one-api/common"
+	"one-api/setting/console_setting"
+	"strconv"
 	"strings"
 	"time"
 
@@ -14,45 +14,25 @@ import (
 	"golang.org/x/sync/errgroup"
 )
 
-type UptimeKumaMonitor struct {
-	ID   int    `json:"id"`
-	Name string `json:"name"`
-	Type string `json:"type"`
-}
-
-type UptimeKumaGroup struct {
-	ID          int                  `json:"id"`
-	Name        string               `json:"name"`
-	Weight      int                  `json:"weight"`
-	MonitorList []UptimeKumaMonitor  `json:"monitorList"`
-}
-
-type UptimeKumaHeartbeat struct {
-	Status int      `json:"status"`
-	Time   string   `json:"time"`
-	Msg    string   `json:"msg"`
-	Ping   *float64 `json:"ping"`
-}
-
-type UptimeKumaStatusResponse struct {
-	PublicGroupList []UptimeKumaGroup `json:"publicGroupList"`
-}
-
-type UptimeKumaHeartbeatResponse struct {
-	HeartbeatList map[string][]UptimeKumaHeartbeat `json:"heartbeatList"`
-	UptimeList    map[string]float64               `json:"uptimeList"`
-}
+const (
+	requestTimeout   = 30 * time.Second
+	httpTimeout      = 10 * time.Second
+	uptimeKeySuffix  = "_24"
+	apiStatusPath    = "/api/status-page/"
+	apiHeartbeatPath = "/api/status-page/heartbeat/"
+)
 
-type MonitorStatus struct {
+type Monitor struct {
 	Name   string  `json:"name"`
 	Uptime float64 `json:"uptime"`
 	Status int     `json:"status"`
+	Group  string  `json:"group,omitempty"`
 }
 
-var (
-	ErrUpstreamNon200 = errors.New("upstream non-200")
-	ErrTimeout        = errors.New("context deadline exceeded")
-)
+type UptimeGroupResult struct {
+	CategoryName string    `json:"categoryName"`
+	Monitors  []Monitor `json:"monitors"`
+}
 
 func getAndDecode(ctx context.Context, client *http.Client, url string, dest interface{}) error {
 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
@@ -62,108 +42,113 @@ func getAndDecode(ctx context.Context, client *http.Client, url string, dest int
 
 	resp, err := client.Do(req)
 	if err != nil {
-		if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
-			return ErrTimeout
-		}
 		return err
 	}
 	defer resp.Body.Close()
 
 	if resp.StatusCode != http.StatusOK {
-		return ErrUpstreamNon200
+		return errors.New("non-200 status")
 	}
 
 	return json.NewDecoder(resp.Body).Decode(dest)
 }
 
-func GetUptimeKumaStatus(c *gin.Context) {
-	common.OptionMapRWMutex.RLock()
-	uptimeKumaUrl := common.OptionMap["UptimeKumaUrl"]
-	slug := common.OptionMap["UptimeKumaSlug"]
-	common.OptionMapRWMutex.RUnlock()
-
-	if uptimeKumaUrl == "" || slug == "" {
-		c.JSON(http.StatusOK, gin.H{
-			"success": true,
-			"message": "",
-			"data":    []MonitorStatus{},
-		})
-		return
+func fetchGroupData(ctx context.Context, client *http.Client, groupConfig map[string]interface{}) UptimeGroupResult {
+	url, _ := groupConfig["url"].(string)
+	slug, _ := groupConfig["slug"].(string)
+	categoryName, _ := groupConfig["categoryName"].(string)
+	
+	result := UptimeGroupResult{
+		CategoryName: categoryName,
+		Monitors:  []Monitor{},
+	}
+	
+	if url == "" || slug == "" {
+		return result
 	}
 
-	uptimeKumaUrl = strings.TrimSuffix(uptimeKumaUrl, "/")
-
-	ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
-	defer cancel()
-
-	client := &http.Client{}
-
-	statusPageUrl := fmt.Sprintf("%s/api/status-page/%s", uptimeKumaUrl, slug)
-	heartbeatUrl := fmt.Sprintf("%s/api/status-page/heartbeat/%s", uptimeKumaUrl, slug)
-
-	var (
-		statusData    UptimeKumaStatusResponse
-		heartbeatData UptimeKumaHeartbeatResponse
-	)
+	baseURL := strings.TrimSuffix(url, "/")
+	
+	var statusData struct {
+		PublicGroupList []struct {
+			ID   int    `json:"id"`
+			Name string `json:"name"`
+			MonitorList []struct {
+				ID   int    `json:"id"`
+				Name string `json:"name"`
+			} `json:"monitorList"`
+		} `json:"publicGroupList"`
+	}
+	
+	var heartbeatData struct {
+		HeartbeatList map[string][]struct {
+			Status int `json:"status"`
+		} `json:"heartbeatList"`
+		UptimeList map[string]float64 `json:"uptimeList"`
+	}
 
 	g, gCtx := errgroup.WithContext(ctx)
-
-	g.Go(func() error {
-		return getAndDecode(gCtx, client, statusPageUrl, &statusData)
+	g.Go(func() error { 
+		return getAndDecode(gCtx, client, baseURL+apiStatusPath+slug, &statusData) 
 	})
-
-	g.Go(func() error {
-		return getAndDecode(gCtx, client, heartbeatUrl, &heartbeatData)
+	g.Go(func() error { 
+		return getAndDecode(gCtx, client, baseURL+apiHeartbeatPath+slug, &heartbeatData) 
 	})
 
-	if err := g.Wait(); err != nil {
-		switch err {
-		case ErrUpstreamNon200:
-			c.JSON(http.StatusBadRequest, gin.H{
-				"success": false,
-				"message": "上游接口出现问题",
-			})
-		case ErrTimeout:
-			c.JSON(http.StatusRequestTimeout, gin.H{
-				"success": false,
-				"message": "请求上游接口超时",
-			})
-		default:
-			c.JSON(http.StatusBadRequest, gin.H{
-				"success": false,
-				"message": err.Error(),
-			})
-		}
-		return
+	if g.Wait() != nil {
+		return result
 	}
 
-	var monitors []MonitorStatus
-	for _, group := range statusData.PublicGroupList {
-		for _, monitor := range group.MonitorList {
-			monitorStatus := MonitorStatus{
-				Name:   monitor.Name,
-				Uptime: 0.0,
-				Status: 0,
+	for _, pg := range statusData.PublicGroupList {
+		if len(pg.MonitorList) == 0 {
+			continue
+		}
+
+		for _, m := range pg.MonitorList {
+			monitor := Monitor{
+				Name:  m.Name,
+				Group: pg.Name,
 			}
 
-			uptimeKey := fmt.Sprintf("%d_24", monitor.ID)
-			if uptime, exists := heartbeatData.UptimeList[uptimeKey]; exists {
-				monitorStatus.Uptime = uptime
+			monitorID := strconv.Itoa(m.ID)
+
+			if uptime, exists := heartbeatData.UptimeList[monitorID+uptimeKeySuffix]; exists {
+				monitor.Uptime = uptime
 			}
 
-			heartbeatKey := fmt.Sprintf("%d", monitor.ID)
-			if heartbeats, exists := heartbeatData.HeartbeatList[heartbeatKey]; exists && len(heartbeats) > 0 {
-				latestHeartbeat := heartbeats[0]
-				monitorStatus.Status = latestHeartbeat.Status
+			if heartbeats, exists := heartbeatData.HeartbeatList[monitorID]; exists && len(heartbeats) > 0 {
+				monitor.Status = heartbeats[0].Status
 			}
 
-			monitors = append(monitors, monitorStatus)
+			result.Monitors = append(result.Monitors, monitor)
 		}
 	}
 
-	c.JSON(http.StatusOK, gin.H{
-		"success": true,
-		"message": "",
-		"data":    monitors,
-	})
+	return result
+}
+
+func GetUptimeKumaStatus(c *gin.Context) {
+	groups := console_setting.GetUptimeKumaGroups()
+	if len(groups) == 0 {
+		c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": []UptimeGroupResult{}})
+		return
+	}
+
+	ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
+	defer cancel()
+
+	client := &http.Client{Timeout: httpTimeout}
+	results := make([]UptimeGroupResult, len(groups))
+	
+	g, gCtx := errgroup.WithContext(ctx)
+	for i, group := range groups {
+		i, group := i, group
+		g.Go(func() error {
+			results[i] = fetchGroupData(gCtx, client, group)
+			return nil
+		})
+	}
+	
+	g.Wait()
+	c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": results})
 } 

+ 8 - 0
controller/user.go

@@ -226,6 +226,9 @@ func Register(c *gin.Context) {
 			UnlimitedQuota:     true,
 			ModelLimitsEnabled: false,
 		}
+		if setting.DefaultUseAutoGroup {
+			token.Group = "auto"
+		}
 		if err := token.Insert(); err != nil {
 			c.JSON(http.StatusOK, gin.H{
 				"success": false,
@@ -459,6 +462,9 @@ func GetSelf(c *gin.Context) {
 		})
 		return
 	}
+	// Hide admin remarks: set to empty to trigger omitempty tag, ensuring the remark field is not included in JSON returned to regular users
+	user.Remark = ""
+
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",
@@ -943,6 +949,7 @@ type UpdateUserSettingRequest struct {
 	WebhookSecret              string  `json:"webhook_secret,omitempty"`
 	NotificationEmail          string  `json:"notification_email,omitempty"`
 	AcceptUnsetModelRatioModel bool    `json:"accept_unset_model_ratio_model"`
+	RecordIpLog                bool    `json:"record_ip_log"`
 }
 
 func UpdateUserSetting(c *gin.Context) {
@@ -1019,6 +1026,7 @@ func UpdateUserSetting(c *gin.Context) {
 		constant.UserSettingNotifyType:            req.QuotaWarningType,
 		constant.UserSettingQuotaWarningThreshold: req.QuotaWarningThreshold,
 		"accept_unset_model_ratio_model":          req.AcceptUnsetModelRatioModel,
+		constant.UserSettingRecordIpLog:           req.RecordIpLog,
 	}
 
 	// 如果是webhook类型,添加webhook相关设置

+ 8 - 1
dto/claude.go

@@ -178,7 +178,14 @@ type ClaudeRequest struct {
 
 type Thinking struct {
 	Type         string `json:"type"`
-	BudgetTokens int    `json:"budget_tokens"`
+	BudgetTokens *int   `json:"budget_tokens,omitempty"`
+}
+
+func (c *Thinking) GetBudgetTokens() int {
+	if c.BudgetTokens == nil {
+		return 0
+	}
+	return *c.BudgetTokens
 }
 
 func (c *ClaudeRequest) IsStringSystem() bool {

+ 2 - 0
dto/openai_request.go

@@ -58,6 +58,8 @@ type GeneralOpenAIRequest struct {
 	// OpenRouter Params
 	Usage     json.RawMessage `json:"usage,omitempty"`
 	Reasoning json.RawMessage `json:"reasoning,omitempty"`
+	// Ali Qwen Params
+	VlHighResolutionImages json.RawMessage `json:"vl_high_resolution_images,omitempty"`
 }
 
 func (r *GeneralOpenAIRequest) ToMap() map[string]any {

+ 3 - 1
main.go

@@ -105,10 +105,12 @@ func main() {
 			model.InitChannelCache()
 		}()
 
-		go model.SyncOptions(common.SyncFrequency)
 		go model.SyncChannelCache(common.SyncFrequency)
 	}
 
+	// 热更新配置
+	go model.SyncOptions(common.SyncFrequency)
+
 	// 数据看板
 	go model.UpdateQuotaData()
 

+ 11 - 4
middleware/distributor.go

@@ -49,8 +49,10 @@ func Distribute() func(c *gin.Context) {
 			}
 			// check group in common.GroupRatio
 			if !setting.ContainsGroupRatio(tokenGroup) {
-				abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup))
-				return
+				if tokenGroup != "auto" {
+					abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup))
+					return
+				}
 			}
 			userGroup = tokenGroup
 		}
@@ -95,9 +97,14 @@ func Distribute() func(c *gin.Context) {
 			}
 
 			if shouldSelectChannel {
-				channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model, 0)
+				var selectGroup string
+				channel, selectGroup, err = model.CacheGetRandomSatisfiedChannel(c, userGroup, modelRequest.Model, 0)
 				if err != nil {
-					message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model)
+					showGroup := userGroup
+					if userGroup == "auto" {
+						showGroup = fmt.Sprintf("auto(%s)", selectGroup)
+					}
+					message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", showGroup, modelRequest.Model)
 					// 如果错误,但是渠道不为空,说明是数据库一致性问题
 					if channel != nil {
 						common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id))

+ 21 - 16
model/ability.go

@@ -8,6 +8,7 @@ import (
 
 	"github.com/samber/lo"
 	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
 )
 
 type Ability struct {
@@ -23,7 +24,7 @@ type Ability struct {
 func GetGroupModels(group string) []string {
 	var models []string
 	// Find distinct models
-	DB.Table("abilities").Where(groupCol+" = ? and enabled = ?", group, true).Distinct("model").Pluck("model", &models)
+	DB.Table("abilities").Where(commonGroupCol+" = ? and enabled = ?", group, true).Distinct("model").Pluck("model", &models)
 	return models
 }
 
@@ -41,16 +42,12 @@ func GetAllEnableAbilities() []Ability {
 }
 
 func getPriority(group string, model string, retry int) (int, error) {
-	trueVal := "1"
-	if common.UsingPostgreSQL {
-		trueVal = "true"
-	}
 
 	var priorities []int
 	err := DB.Model(&Ability{}).
 		Select("DISTINCT(priority)").
-		Where(groupCol+" = ? and model = ? and enabled = "+trueVal, group, model).
-		Order("priority DESC"). // 按优先级降序排序
+		Where(commonGroupCol+" = ? and model = ? and enabled = ?", group, model, commonTrueVal).
+		Order("priority DESC").              // 按优先级降序排序
 		Pluck("priority", &priorities).Error // Pluck用于将查询的结果直接扫描到一个切片中
 
 	if err != nil {
@@ -75,18 +72,14 @@ func getPriority(group string, model string, retry int) (int, error) {
 }
 
 func getChannelQuery(group string, model string, retry int) *gorm.DB {
-	trueVal := "1"
-	if common.UsingPostgreSQL {
-		trueVal = "true"
-	}
-	maxPrioritySubQuery := DB.Model(&Ability{}).Select("MAX(priority)").Where(groupCol+" = ? and model = ? and enabled = "+trueVal, group, model)
-	channelQuery := DB.Where(groupCol+" = ? and model = ? and enabled = "+trueVal+" and priority = (?)", group, model, maxPrioritySubQuery)
+	maxPrioritySubQuery := DB.Model(&Ability{}).Select("MAX(priority)").Where(commonGroupCol+" = ? and model = ? and enabled = ?", group, model, commonTrueVal)
+	channelQuery := DB.Where(commonGroupCol+" = ? and model = ? and enabled = ? and priority = (?)", group, model, commonTrueVal, maxPrioritySubQuery)
 	if retry != 0 {
 		priority, err := getPriority(group, model, retry)
 		if err != nil {
 			common.SysError(fmt.Sprintf("Get priority failed: %s", err.Error()))
 		} else {
-			channelQuery = DB.Where(groupCol+" = ? and model = ? and enabled = "+trueVal+" and priority = ?", group, model, priority)
+			channelQuery = DB.Where(commonGroupCol+" = ? and model = ? and enabled = ? and priority = ?", group, model, commonTrueVal, priority)
 		}
 	}
 
@@ -133,9 +126,15 @@ func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
 func (channel *Channel) AddAbilities() error {
 	models_ := strings.Split(channel.Models, ",")
 	groups_ := strings.Split(channel.Group, ",")
+	abilitySet := make(map[string]struct{})
 	abilities := make([]Ability, 0, len(models_))
 	for _, model := range models_ {
 		for _, group := range groups_ {
+			key := group + "|" + model
+			if _, exists := abilitySet[key]; exists {
+				continue
+			}
+			abilitySet[key] = struct{}{}
 			ability := Ability{
 				Group:     group,
 				Model:     model,
@@ -152,7 +151,7 @@ func (channel *Channel) AddAbilities() error {
 		return nil
 	}
 	for _, chunk := range lo.Chunk(abilities, 50) {
-		err := DB.Create(&chunk).Error
+		err := DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&chunk).Error
 		if err != nil {
 			return err
 		}
@@ -194,9 +193,15 @@ func (channel *Channel) UpdateAbilities(tx *gorm.DB) error {
 	// Then add new abilities
 	models_ := strings.Split(channel.Models, ",")
 	groups_ := strings.Split(channel.Group, ",")
+	abilitySet := make(map[string]struct{})
 	abilities := make([]Ability, 0, len(models_))
 	for _, model := range models_ {
 		for _, group := range groups_ {
+			key := group + "|" + model
+			if _, exists := abilitySet[key]; exists {
+				continue
+			}
+			abilitySet[key] = struct{}{}
 			ability := Ability{
 				Group:     group,
 				Model:     model,
@@ -212,7 +217,7 @@ func (channel *Channel) UpdateAbilities(tx *gorm.DB) error {
 
 	if len(abilities) > 0 {
 		for _, chunk := range lo.Chunk(abilities, 50) {
-			err = tx.Create(&chunk).Error
+			err = tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&chunk).Error
 			if err != nil {
 				if isNewTx {
 					tx.Rollback()

+ 40 - 1
model/cache.go

@@ -5,10 +5,13 @@ import (
 	"fmt"
 	"math/rand"
 	"one-api/common"
+	"one-api/setting"
 	"sort"
 	"strings"
 	"sync"
 	"time"
+
+	"github.com/gin-gonic/gin"
 )
 
 var group2model2channels map[string]map[string][]*Channel
@@ -75,7 +78,43 @@ func SyncChannelCache(frequency int) {
 	}
 }
 
-func CacheGetRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) {
+func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, model string, retry int) (*Channel, string, error) {
+	var channel *Channel
+	var err error
+	selectGroup := group
+	if group == "auto" {
+		if len(setting.AutoGroups) == 0 {
+			return nil, selectGroup, errors.New("auto groups is not enabled")
+		}
+		for _, autoGroup := range setting.AutoGroups {
+			if common.DebugEnabled {
+				println("autoGroup:", autoGroup)
+			}
+			channel, _ = getRandomSatisfiedChannel(autoGroup, model, retry)
+			if channel == nil {
+				continue
+			} else {
+				c.Set("auto_group", autoGroup)
+				selectGroup = autoGroup
+				if common.DebugEnabled {
+					println("selectGroup:", selectGroup)
+				}
+				break
+			}
+		}
+	} else {
+		channel, err = getRandomSatisfiedChannel(group, model, retry)
+		if err != nil {
+			return nil, group, err
+		}
+	}
+	if channel == nil {
+		return nil, group, errors.New("channel not found")
+	}
+	return channel, selectGroup, nil
+}
+
+func getRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) {
 	if strings.HasPrefix(model, "gpt-4-gizmo") {
 		model = "gpt-4-gizmo-*"
 	}

+ 24 - 10
model/channel.go

@@ -145,7 +145,7 @@ func SearchChannels(keyword string, group string, model string, idSort bool) ([]
 	}
 
 	// 构造基础查询
-	baseQuery := DB.Model(&Channel{}).Omit(keyCol)
+	baseQuery := DB.Model(&Channel{}).Omit("key")
 
 	// 构造WHERE子句
 	var whereClause string
@@ -153,15 +153,15 @@ func SearchChannels(keyword string, group string, model string, idSort bool) ([]
 	if group != "" && group != "null" {
 		var groupCondition string
 		if common.UsingMySQL {
-			groupCondition = `CONCAT(',', ` + groupCol + `, ',') LIKE ?`
+			groupCondition = `CONCAT(',', ` + commonGroupCol + `, ',') LIKE ?`
 		} else {
 			// sqlite, PostgreSQL
-			groupCondition = `(',' || ` + groupCol + ` || ',') LIKE ?`
+			groupCondition = `(',' || ` + commonGroupCol + ` || ',') LIKE ?`
 		}
-		whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ? OR " + baseURLCol + " LIKE ?) AND " + modelsCol + ` LIKE ? AND ` + groupCondition
+		whereClause = "(id = ? OR name LIKE ? OR " + commonKeyCol + " = ? OR " + baseURLCol + " LIKE ?) AND " + modelsCol + ` LIKE ? AND ` + groupCondition
 		args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+keyword+"%", "%"+model+"%", "%,"+group+",%")
 	} else {
-		whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ? OR " + baseURLCol + " LIKE ?) AND " + modelsCol + " LIKE ?"
+		whereClause = "(id = ? OR name LIKE ? OR " + commonKeyCol + " = ? OR " + baseURLCol + " LIKE ?) AND " + modelsCol + " LIKE ?"
 		args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+keyword+"%", "%"+model+"%")
 	}
 
@@ -478,7 +478,7 @@ func SearchTags(keyword string, group string, model string, idSort bool) ([]*str
 	}
 
 	// 构造基础查询
-	baseQuery := DB.Model(&Channel{}).Omit(keyCol)
+	baseQuery := DB.Model(&Channel{}).Omit("key")
 
 	// 构造WHERE子句
 	var whereClause string
@@ -486,15 +486,15 @@ func SearchTags(keyword string, group string, model string, idSort bool) ([]*str
 	if group != "" && group != "null" {
 		var groupCondition string
 		if common.UsingMySQL {
-			groupCondition = `CONCAT(',', ` + groupCol + `, ',') LIKE ?`
+			groupCondition = `CONCAT(',', ` + commonGroupCol + `, ',') LIKE ?`
 		} else {
 			// sqlite, PostgreSQL
-			groupCondition = `(',' || ` + groupCol + ` || ',') LIKE ?`
+			groupCondition = `(',' || ` + commonGroupCol + ` || ',') LIKE ?`
 		}
-		whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ? OR " + baseURLCol + " LIKE ?) AND " + modelsCol + ` LIKE ? AND ` + groupCondition
+		whereClause = "(id = ? OR name LIKE ? OR " + commonKeyCol + " = ? OR " + baseURLCol + " LIKE ?) AND " + modelsCol + ` LIKE ? AND ` + groupCondition
 		args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+keyword+"%", "%"+model+"%", "%,"+group+",%")
 	} else {
-		whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ? OR " + baseURLCol + " LIKE ?) AND " + modelsCol + " LIKE ?"
+		whereClause = "(id = ? OR name LIKE ? OR " + commonKeyCol + " = ? OR " + baseURLCol + " LIKE ?) AND " + modelsCol + " LIKE ?"
 		args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+keyword+"%", "%"+model+"%")
 	}
 
@@ -583,3 +583,17 @@ func BatchSetChannelTag(ids []int, tag *string) error {
 	// 提交事务
 	return tx.Commit().Error
 }
+
+// CountAllChannels returns total channels in DB
+func CountAllChannels() (int64, error) {
+	var total int64
+	err := DB.Model(&Channel{}).Count(&total).Error
+	return total, err
+}
+
+// CountAllTags returns number of non-empty distinct tags
+func CountAllTags() (int64, error) {
+	var total int64
+	err := DB.Model(&Channel{}).Where("tag is not null AND tag != ''").Distinct("tag").Count(&total).Error
+	return total, err
+}

+ 46 - 9
model/log.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"one-api/common"
+	"one-api/constant"
 	"os"
 	"strings"
 	"time"
@@ -32,6 +33,7 @@ type Log struct {
 	ChannelName      string `json:"channel_name" gorm:"->"`
 	TokenId          int    `json:"token_id" gorm:"default:0;index"`
 	Group            string `json:"group" gorm:"index"`
+	Ip               string `json:"ip" gorm:"index;default:''"`
 	Other            string `json:"other"`
 }
 
@@ -61,7 +63,7 @@ func formatUserLogs(logs []*Log) {
 func GetLogByKey(key string) (logs []*Log, err error) {
 	if os.Getenv("LOG_SQL_DSN") != "" {
 		var tk Token
-		if err = DB.Model(&Token{}).Where(keyCol+"=?", strings.TrimPrefix(key, "sk-")).First(&tk).Error; err != nil {
+		if err = DB.Model(&Token{}).Where(logKeyCol+"=?", strings.TrimPrefix(key, "sk-")).First(&tk).Error; err != nil {
 			return nil, err
 		}
 		err = LOG_DB.Model(&Log{}).Where("token_id=?", tk.Id).Find(&logs).Error
@@ -95,6 +97,15 @@ func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string,
 	common.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, content))
 	username := c.GetString("username")
 	otherStr := common.MapToJsonStr(other)
+	// 判断是否需要记录 IP
+	needRecordIp := false
+	if settingMap, err := GetUserSetting(userId, false); err == nil {
+		if v, ok := settingMap[constant.UserSettingRecordIpLog]; ok {
+			if vb, ok := v.(bool); ok && vb {
+				needRecordIp = true
+			}
+		}
+	}
 	log := &Log{
 		UserId:           userId,
 		Username:         username,
@@ -111,7 +122,13 @@ func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string,
 		UseTime:          useTimeSeconds,
 		IsStream:         isStream,
 		Group:            group,
-		Other:            otherStr,
+		Ip: func() string {
+			if needRecordIp {
+				return c.ClientIP()
+			}
+			return ""
+		}(),
+		Other: otherStr,
 	}
 	err := LOG_DB.Create(log).Error
 	if err != nil {
@@ -128,6 +145,15 @@ func RecordConsumeLog(c *gin.Context, userId int, channelId int, promptTokens in
 	}
 	username := c.GetString("username")
 	otherStr := common.MapToJsonStr(other)
+	// 判断是否需要记录 IP
+	needRecordIp := false
+	if settingMap, err := GetUserSetting(userId, false); err == nil {
+		if v, ok := settingMap[constant.UserSettingRecordIpLog]; ok {
+			if vb, ok := v.(bool); ok && vb {
+				needRecordIp = true
+			}
+		}
+	}
 	log := &Log{
 		UserId:           userId,
 		Username:         username,
@@ -144,7 +170,13 @@ func RecordConsumeLog(c *gin.Context, userId int, channelId int, promptTokens in
 		UseTime:          useTimeSeconds,
 		IsStream:         isStream,
 		Group:            group,
-		Other:            otherStr,
+		Ip: func() string {
+			if needRecordIp {
+				return c.ClientIP()
+			}
+			return ""
+		}(),
+		Other: otherStr,
 	}
 	err := LOG_DB.Create(log).Error
 	if err != nil {
@@ -184,7 +216,7 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
 		tx = tx.Where("logs.channel_id = ?", channel)
 	}
 	if group != "" {
-		tx = tx.Where("logs."+groupCol+" = ?", group)
+		tx = tx.Where("logs."+logGroupCol+" = ?", group)
 	}
 	err = tx.Model(&Log{}).Count(&total).Error
 	if err != nil {
@@ -195,13 +227,18 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
 		return nil, 0, err
 	}
 
-	channelIds := make([]int, 0)
+	channelIdsMap := make(map[int]struct{})
 	channelMap := make(map[int]string)
 	for _, log := range logs {
 		if log.ChannelId != 0 {
-			channelIds = append(channelIds, log.ChannelId)
+			channelIdsMap[log.ChannelId] = struct{}{}
 		}
 	}
+
+	channelIds := make([]int, 0, len(channelIdsMap))
+	for channelId := range channelIdsMap {
+		channelIds = append(channelIds, channelId)
+	}
 	if len(channelIds) > 0 {
 		var channels []struct {
 			Id   int    `gorm:"column:id"`
@@ -242,7 +279,7 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
 		tx = tx.Where("logs.created_at <= ?", endTimestamp)
 	}
 	if group != "" {
-		tx = tx.Where("logs."+groupCol+" = ?", group)
+		tx = tx.Where("logs."+logGroupCol+" = ?", group)
 	}
 	err = tx.Model(&Log{}).Count(&total).Error
 	if err != nil {
@@ -303,8 +340,8 @@ func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelNa
 		rpmTpmQuery = rpmTpmQuery.Where("channel_id = ?", channel)
 	}
 	if group != "" {
-		tx = tx.Where(groupCol+" = ?", group)
-		rpmTpmQuery = rpmTpmQuery.Where(groupCol+" = ?", group)
+		tx = tx.Where(logGroupCol+" = ?", group)
+		rpmTpmQuery = rpmTpmQuery.Where(logGroupCol+" = ?", group)
 	}
 
 	tx = tx.Where("type = ?", LogTypeConsume)

+ 107 - 54
model/main.go

@@ -1,6 +1,7 @@
 package model
 
 import (
+	"fmt"
 	"log"
 	"one-api/common"
 	"one-api/constant"
@@ -15,18 +16,39 @@ import (
 	"gorm.io/gorm"
 )
 
-var groupCol string
-var keyCol string
+var commonGroupCol string
+var commonKeyCol string
+var commonTrueVal string
+var commonFalseVal string
+
+var logKeyCol string
+var logGroupCol string
 
 func initCol() {
+	// init common column names
 	if common.UsingPostgreSQL {
-		groupCol = `"group"`
-		keyCol = `"key"`
-
+		commonGroupCol = `"group"`
+		commonKeyCol = `"key"`
+		commonTrueVal = "true"
+		commonFalseVal = "false"
 	} else {
-		groupCol = "`group`"
-		keyCol = "`key`"
+		commonGroupCol = "`group`"
+		commonKeyCol = "`key`"
+		commonTrueVal = "1"
+		commonFalseVal = "0"
+	}
+	if os.Getenv("LOG_SQL_DSN") != "" {
+		switch common.LogSqlType {
+		case common.DatabaseTypePostgreSQL:
+			logGroupCol = `"group"`
+			logKeyCol = `"key"`
+		default:
+			logGroupCol = commonGroupCol
+			logKeyCol = commonKeyCol
+		}
 	}
+	// log sql type and database type
+	common.SysLog("Using Log SQL Type: " + common.LogSqlType)
 }
 
 var DB *gorm.DB
@@ -83,7 +105,7 @@ func CheckSetup() {
 	}
 }
 
-func chooseDB(envName string) (*gorm.DB, error) {
+func chooseDB(envName string, isLog bool) (*gorm.DB, error) {
 	defer func() {
 		initCol()
 	}()
@@ -92,7 +114,11 @@ func chooseDB(envName string) (*gorm.DB, error) {
 		if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") {
 			// Use PostgreSQL
 			common.SysLog("using PostgreSQL as database")
-			common.UsingPostgreSQL = true
+			if !isLog {
+				common.UsingPostgreSQL = true
+			} else {
+				common.LogSqlType = common.DatabaseTypePostgreSQL
+			}
 			return gorm.Open(postgres.New(postgres.Config{
 				DSN:                  dsn,
 				PreferSimpleProtocol: true, // disables implicit prepared statement usage
@@ -102,7 +128,11 @@ func chooseDB(envName string) (*gorm.DB, error) {
 		}
 		if strings.HasPrefix(dsn, "local") {
 			common.SysLog("SQL_DSN not set, using SQLite as database")
-			common.UsingSQLite = true
+			if !isLog {
+				common.UsingSQLite = true
+			} else {
+				common.LogSqlType = common.DatabaseTypeSQLite
+			}
 			return gorm.Open(sqlite.Open(common.SQLitePath), &gorm.Config{
 				PrepareStmt: true, // precompile SQL
 			})
@@ -117,7 +147,11 @@ func chooseDB(envName string) (*gorm.DB, error) {
 				dsn += "?parseTime=true"
 			}
 		}
-		common.UsingMySQL = true
+		if !isLog {
+			common.UsingMySQL = true
+		} else {
+			common.LogSqlType = common.DatabaseTypeMySQL
+		}
 		return gorm.Open(mysql.Open(dsn), &gorm.Config{
 			PrepareStmt: true, // precompile SQL
 		})
@@ -131,7 +165,7 @@ func chooseDB(envName string) (*gorm.DB, error) {
 }
 
 func InitDB() (err error) {
-	db, err := chooseDB("SQL_DSN")
+	db, err := chooseDB("SQL_DSN", false)
 	if err == nil {
 		if common.DebugEnabled {
 			db = db.Debug()
@@ -149,7 +183,7 @@ func InitDB() (err error) {
 			return nil
 		}
 		if common.UsingMySQL {
-			_, _ = sqlDB.Exec("ALTER TABLE channels MODIFY model_mapping TEXT;") // TODO: delete this line when most users have upgraded
+			//_, _ = sqlDB.Exec("ALTER TABLE channels MODIFY model_mapping TEXT;") // TODO: delete this line when most users have upgraded
 		}
 		common.SysLog("database migration started")
 		err = migrateDB()
@@ -165,7 +199,7 @@ func InitLogDB() (err error) {
 		LOG_DB = DB
 		return
 	}
-	db, err := chooseDB("LOG_SQL_DSN")
+	db, err := chooseDB("LOG_SQL_DSN", true)
 	if err == nil {
 		if common.DebugEnabled {
 			db = db.Debug()
@@ -198,54 +232,73 @@ func InitLogDB() (err error) {
 }
 
 func migrateDB() error {
-	err := DB.AutoMigrate(&Channel{})
-	if err != nil {
-		return err
+	if !common.UsingPostgreSQL {
+		return migrateDBFast()
 	}
-	err = DB.AutoMigrate(&Token{})
+	err := DB.AutoMigrate(
+		&Channel{},
+		&Token{},
+		&User{},
+		&Option{},
+		&Redemption{},
+		&Ability{},
+		&Log{},
+		&Midjourney{},
+		&TopUp{},
+		&QuotaData{},
+		&Task{},
+		&Setup{},
+	)
 	if err != nil {
 		return err
 	}
-	err = DB.AutoMigrate(&User{})
-	if err != nil {
-		return err
-	}
-	err = DB.AutoMigrate(&Option{})
-	if err != nil {
-		return err
-	}
-	err = DB.AutoMigrate(&Redemption{})
-	if err != nil {
-		return err
-	}
-	err = DB.AutoMigrate(&Ability{})
-	if err != nil {
-		return err
-	}
-	err = DB.AutoMigrate(&Log{})
-	if err != nil {
-		return err
-	}
-	err = DB.AutoMigrate(&Midjourney{})
-	if err != nil {
-		return err
-	}
-	err = DB.AutoMigrate(&TopUp{})
-	if err != nil {
-		return err
+	return nil
+}
+
+func migrateDBFast() error {
+	var wg sync.WaitGroup
+	errChan := make(chan error, 12) // Buffer size matches number of migrations
+
+	migrations := []struct {
+		model interface{}
+		name  string
+	}{
+		{&Channel{}, "Channel"},
+		{&Token{}, "Token"},
+		{&User{}, "User"},
+		{&Option{}, "Option"},
+		{&Redemption{}, "Redemption"},
+		{&Ability{}, "Ability"},
+		{&Log{}, "Log"},
+		{&Midjourney{}, "Midjourney"},
+		{&TopUp{}, "TopUp"},
+		{&QuotaData{}, "QuotaData"},
+		{&Task{}, "Task"},
+		{&Setup{}, "Setup"},
 	}
-	err = DB.AutoMigrate(&QuotaData{})
-	if err != nil {
-		return err
+
+	for _, m := range migrations {
+		wg.Add(1)
+		go func(model interface{}, name string) {
+			defer wg.Done()
+			if err := DB.AutoMigrate(model); err != nil {
+				errChan <- fmt.Errorf("failed to migrate %s: %v", name, err)
+			}
+		}(m.model, m.name)
 	}
-	err = DB.AutoMigrate(&Task{})
-	if err != nil {
-		return err
+
+	// Wait for all migrations to complete
+	wg.Wait()
+	close(errChan)
+
+	// Check for any errors
+	for err := range errChan {
+		if err != nil {
+			return err
+		}
 	}
-	err = DB.AutoMigrate(&Setup{})
 	common.SysLog("database migrated")
-	//err = createRootAccountIfNeed()
-	return err
+	return nil
 }
 
 func migrateLOGDB() error {

+ 37 - 0
model/midjourney.go

@@ -166,3 +166,40 @@ func MjBulkUpdateByTaskIds(taskIDs []int, params map[string]any) error {
 		Where("id in (?)", taskIDs).
 		Updates(params).Error
 }
+
+// CountAllTasks returns total midjourney tasks for admin query
+func CountAllTasks(queryParams TaskQueryParams) int64 {
+	var total int64
+	query := DB.Model(&Midjourney{})
+	if queryParams.ChannelID != "" {
+		query = query.Where("channel_id = ?", queryParams.ChannelID)
+	}
+	if queryParams.MjID != "" {
+		query = query.Where("mj_id = ?", queryParams.MjID)
+	}
+	if queryParams.StartTimestamp != "" {
+		query = query.Where("submit_time >= ?", queryParams.StartTimestamp)
+	}
+	if queryParams.EndTimestamp != "" {
+		query = query.Where("submit_time <= ?", queryParams.EndTimestamp)
+	}
+	_ = query.Count(&total).Error
+	return total
+}
+
+// CountAllUserTask returns total midjourney tasks for user
+func CountAllUserTask(userId int, queryParams TaskQueryParams) int64 {
+	var total int64
+	query := DB.Model(&Midjourney{}).Where("user_id = ?", userId)
+	if queryParams.MjID != "" {
+		query = query.Where("mj_id = ?", queryParams.MjID)
+	}
+	if queryParams.StartTimestamp != "" {
+		query = query.Where("submit_time >= ?", queryParams.StartTimestamp)
+	}
+	if queryParams.EndTimestamp != "" {
+		query = query.Where("submit_time <= ?", queryParams.EndTimestamp)
+	}
+	_ = query.Count(&total).Error
+	return total
+}

+ 10 - 4
model/option.go

@@ -76,6 +76,8 @@ func InitOptionMap() {
 	common.OptionMap["MinTopUp"] = strconv.Itoa(setting.MinTopUp)
 	common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
 	common.OptionMap["Chats"] = setting.Chats2JsonString()
+	common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
+	common.OptionMap["DefaultUseAutoGroup"] = strconv.FormatBool(setting.DefaultUseAutoGroup)
 	common.OptionMap["GitHubClientId"] = ""
 	common.OptionMap["GitHubClientSecret"] = ""
 	common.OptionMap["TelegramBotToken"] = ""
@@ -98,6 +100,7 @@ func InitOptionMap() {
 	common.OptionMap["ModelPrice"] = operation_setting.ModelPrice2JSONString()
 	common.OptionMap["CacheRatio"] = operation_setting.CacheRatio2JSONString()
 	common.OptionMap["GroupRatio"] = setting.GroupRatio2JSONString()
+	common.OptionMap["GroupGroupRatio"] = setting.GroupGroupRatio2JSONString()
 	common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString()
 	common.OptionMap["CompletionRatio"] = operation_setting.CompletionRatio2JSONString()
 	common.OptionMap["TopUpLink"] = common.TopUpLink
@@ -122,9 +125,6 @@ func InitOptionMap() {
 	common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
 	common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
 	common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
-	common.OptionMap["ApiInfo"] = ""
-	common.OptionMap["UptimeKumaUrl"] = ""
-	common.OptionMap["UptimeKumaSlug"] = ""
 
 	// 自动添加所有注册的模型配置
 	modelConfigs := config.GlobalConfig.ExportAllConfigs()
@@ -194,7 +194,7 @@ func updateOptionMap(key string, value string) (err error) {
 			common.ImageDownloadPermission = intValue
 		}
 	}
-	if strings.HasSuffix(key, "Enabled") || key == "DefaultCollapseSidebar" {
+	if strings.HasSuffix(key, "Enabled") || key == "DefaultCollapseSidebar" || key == "DefaultUseAutoGroup" {
 		boolValue := value == "true"
 		switch key {
 		case "PasswordRegisterEnabled":
@@ -263,6 +263,8 @@ func updateOptionMap(key string, value string) (err error) {
 			common.SMTPSSLEnabled = boolValue
 		case "WorkerAllowHttpImageRequestEnabled":
 			setting.WorkerAllowHttpImageRequestEnabled = boolValue
+		case "DefaultUseAutoGroup":
+			setting.DefaultUseAutoGroup = boolValue
 		}
 	}
 	switch key {
@@ -289,6 +291,8 @@ func updateOptionMap(key string, value string) (err error) {
 		setting.PayAddress = value
 	case "Chats":
 		err = setting.UpdateChatsByJsonString(value)
+	case "AutoGroups":
+		err = setting.UpdateAutoGroupsByJsonString(value)
 	case "CustomCallbackAddress":
 		setting.CustomCallbackAddress = value
 	case "EpayId":
@@ -357,6 +361,8 @@ func updateOptionMap(key string, value string) (err error) {
 		err = operation_setting.UpdateModelRatioByJSONString(value)
 	case "GroupRatio":
 		err = setting.UpdateGroupRatioByJSONString(value)
+	case "GroupGroupRatio":
+		err = setting.UpdateGroupGroupRatioByJSONString(value)
 	case "UserUsableGroups":
 		err = setting.UpdateUserUsableGroupsByJSONString(value)
 	case "CompletionRatio":

+ 11 - 1
model/redemption.go

@@ -21,6 +21,7 @@ type Redemption struct {
 	Count        int            `json:"count" gorm:"-:all"` // only for api request
 	UsedUserId   int            `json:"used_user_id"`
 	DeletedAt    gorm.DeletedAt `gorm:"index"`
+	ExpiredTime  int64          `json:"expired_time" gorm:"bigint"` // 过期时间,0 表示不过期
 }
 
 func GetAllRedemptions(startIdx int, num int) (redemptions []*Redemption, total int64, err error) {
@@ -131,6 +132,9 @@ func Redeem(key string, userId int) (quota int, err error) {
 		if redemption.Status != common.RedemptionCodeStatusEnabled {
 			return errors.New("该兑换码已被使用")
 		}
+		if redemption.ExpiredTime != 0 && redemption.ExpiredTime < common.GetTimestamp() {
+			return errors.New("该兑换码已过期")
+		}
 		err = tx.Model(&User{}).Where("id = ?", userId).Update("quota", gorm.Expr("quota + ?", redemption.Quota)).Error
 		if err != nil {
 			return err
@@ -162,7 +166,7 @@ func (redemption *Redemption) SelectUpdate() error {
 // Update Make sure your token's fields is completed, because this will update non-zero values
 func (redemption *Redemption) Update() error {
 	var err error
-	err = DB.Model(redemption).Select("name", "status", "quota", "redeemed_time").Updates(redemption).Error
+	err = DB.Model(redemption).Select("name", "status", "quota", "redeemed_time", "expired_time").Updates(redemption).Error
 	return err
 }
 
@@ -183,3 +187,9 @@ func DeleteRedemptionById(id int) (err error) {
 	}
 	return redemption.Delete()
 }
+
+func DeleteInvalidRedemptions() (int64, error) {
+	now := common.GetTimestamp()
+	result := DB.Where("status IN ? OR (status = ? AND expired_time != 0 AND expired_time < ?)", []int{common.RedemptionCodeStatusUsed, common.RedemptionCodeStatusDisabled}, common.RedemptionCodeStatusEnabled, now).Delete(&Redemption{})
+	return result.RowsAffected, result.Error
+}

+ 61 - 0
model/task.go

@@ -302,3 +302,64 @@ func SumUsedTaskQuota(queryParams SyncTaskQueryParams) (stat []TaskQuotaUsage, e
 	err = query.Select("mode, sum(quota) as count").Group("mode").Find(&stat).Error
 	return stat, err
 }
+
+// TaskCountAllTasks returns total tasks that match the given query params (admin usage)
+func TaskCountAllTasks(queryParams SyncTaskQueryParams) int64 {
+	var total int64
+	query := DB.Model(&Task{})
+	if queryParams.ChannelID != "" {
+		query = query.Where("channel_id = ?", queryParams.ChannelID)
+	}
+	if queryParams.Platform != "" {
+		query = query.Where("platform = ?", queryParams.Platform)
+	}
+	if queryParams.UserID != "" {
+		query = query.Where("user_id = ?", queryParams.UserID)
+	}
+	if len(queryParams.UserIDs) != 0 {
+		query = query.Where("user_id in (?)", queryParams.UserIDs)
+	}
+	if queryParams.TaskID != "" {
+		query = query.Where("task_id = ?", queryParams.TaskID)
+	}
+	if queryParams.Action != "" {
+		query = query.Where("action = ?", queryParams.Action)
+	}
+	if queryParams.Status != "" {
+		query = query.Where("status = ?", queryParams.Status)
+	}
+	if queryParams.StartTimestamp != 0 {
+		query = query.Where("submit_time >= ?", queryParams.StartTimestamp)
+	}
+	if queryParams.EndTimestamp != 0 {
+		query = query.Where("submit_time <= ?", queryParams.EndTimestamp)
+	}
+	_ = query.Count(&total).Error
+	return total
+}
+
+// TaskCountAllUserTask returns total tasks for given user
+func TaskCountAllUserTask(userId int, queryParams SyncTaskQueryParams) int64 {
+	var total int64
+	query := DB.Model(&Task{}).Where("user_id = ?", userId)
+	if queryParams.TaskID != "" {
+		query = query.Where("task_id = ?", queryParams.TaskID)
+	}
+	if queryParams.Action != "" {
+		query = query.Where("action = ?", queryParams.Action)
+	}
+	if queryParams.Status != "" {
+		query = query.Where("status = ?", queryParams.Status)
+	}
+	if queryParams.Platform != "" {
+		query = query.Where("platform = ?", queryParams.Platform)
+	}
+	if queryParams.StartTimestamp != 0 {
+		query = query.Where("submit_time >= ?", queryParams.StartTimestamp)
+	}
+	if queryParams.EndTimestamp != 0 {
+		query = query.Where("submit_time <= ?", queryParams.EndTimestamp)
+	}
+	_ = query.Count(&total).Error
+	return total
+}

+ 9 - 2
model/token.go

@@ -66,7 +66,7 @@ func SearchUserTokens(userId int, keyword string, token string) (tokens []*Token
 	if token != "" {
 		token = strings.Trim(token, "sk-")
 	}
-	err = DB.Where("user_id = ?", userId).Where("name LIKE ?", "%"+keyword+"%").Where(keyCol+" LIKE ?", "%"+token+"%").Find(&tokens).Error
+	err = DB.Where("user_id = ?", userId).Where("name LIKE ?", "%"+keyword+"%").Where(commonKeyCol+" LIKE ?", "%"+token+"%").Find(&tokens).Error
 	return tokens, err
 }
 
@@ -161,7 +161,7 @@ func GetTokenByKey(key string, fromDB bool) (token *Token, err error) {
 		// Don't return error - fall through to DB
 	}
 	fromDB = true
-	err = DB.Where(keyCol+" = ?", key).First(&token).Error
+	err = DB.Where(commonKeyCol+" = ?", key).First(&token).Error
 	return token, err
 }
 
@@ -320,3 +320,10 @@ func decreaseTokenQuota(id int, quota int) (err error) {
 	).Error
 	return err
 }
+
+// CountUserTokens returns total number of tokens for the given user, used for pagination
+func CountUserTokens(userId int) (int64, error) {
+	var total int64
+	err := DB.Model(&Token{}).Where("user_id = ?", userId).Count(&total).Error
+	return total, err
+}

+ 1 - 1
model/token_cache.go

@@ -10,7 +10,7 @@ import (
 func cacheSetToken(token Token) error {
 	key := common.GenerateHMAC(token.Key)
 	token.Clean()
-	err := common.RedisHSetObj(fmt.Sprintf("token:%s", key), &token, time.Duration(constant.TokenCacheSeconds)*time.Second)
+	err := common.RedisHSetObj(fmt.Sprintf("token:%s", key), &token, time.Duration(constant.RedisKeyCacheSeconds())*time.Second)
 	if err != nil {
 		return err
 	}

+ 5 - 3
model/user.go

@@ -41,6 +41,7 @@ type User struct {
 	DeletedAt        gorm.DeletedAt `gorm:"index"`
 	LinuxDOId        string         `json:"linux_do_id" gorm:"column:linux_do_id;index"`
 	Setting          string         `json:"setting" gorm:"type:text;column:setting"`
+	Remark           string         `json:"remark,omitempty" gorm:"type:varchar(255)" validate:"max=255"`
 }
 
 func (user *User) ToBaseUser() *UserBase {
@@ -175,7 +176,7 @@ func SearchUsers(keyword string, group string, startIdx int, num int) ([]*User,
 		// 如果是数字,同时搜索ID和其他字段
 		likeCondition = "id = ? OR " + likeCondition
 		if group != "" {
-			query = query.Where("("+likeCondition+") AND "+groupCol+" = ?",
+			query = query.Where("("+likeCondition+") AND "+commonGroupCol+" = ?",
 				keywordInt, "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
 		} else {
 			query = query.Where(likeCondition,
@@ -184,7 +185,7 @@ func SearchUsers(keyword string, group string, startIdx int, num int) ([]*User,
 	} else {
 		// 非数字关键字,只搜索字符串字段
 		if group != "" {
-			query = query.Where("("+likeCondition+") AND "+groupCol+" = ?",
+			query = query.Where("("+likeCondition+") AND "+commonGroupCol+" = ?",
 				"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
 		} else {
 			query = query.Where(likeCondition,
@@ -366,6 +367,7 @@ func (user *User) Edit(updatePassword bool) error {
 		"display_name": newUser.DisplayName,
 		"group":        newUser.Group,
 		"quota":        newUser.Quota,
+		"remark":       newUser.Remark,
 	}
 	if updatePassword {
 		updates["password"] = newUser.Password
@@ -615,7 +617,7 @@ func GetUserGroup(id int, fromDB bool) (group string, err error) {
 		// Don't return error - fall through to DB
 	}
 	fromDB = true
-	err = DB.Model(&User{}).Where("id = ?", id).Select(groupCol).Find(&group).Error
+	err = DB.Model(&User{}).Where("id = ?", id).Select(commonGroupCol).Find(&group).Error
 	if err != nil {
 		return "", err
 	}

+ 1 - 1
model/user_cache.go

@@ -70,7 +70,7 @@ func updateUserCache(user User) error {
 	return common.RedisHSetObj(
 		getUserCacheKey(user.Id),
 		user.ToBaseUser(),
-		time.Duration(constant.UserId2QuotaCacheSeconds)*time.Second,
+		time.Duration(constant.RedisKeyCacheSeconds())*time.Second,
 	)
 }
 

+ 35 - 36
relay/channel/claude/relay-claude.go

@@ -113,7 +113,7 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla
 		// BudgetTokens 为 max_tokens 的 80%
 		claudeRequest.Thinking = &dto.Thinking{
 			Type:         "enabled",
-			BudgetTokens: int(float64(claudeRequest.MaxTokens) * model_setting.GetClaudeSettings().ThinkingAdapterBudgetTokensPercentage),
+			BudgetTokens: common.GetPointer[int](int(float64(claudeRequest.MaxTokens) * model_setting.GetClaudeSettings().ThinkingAdapterBudgetTokensPercentage)),
 		}
 		// TODO: 临时处理
 		// https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking
@@ -454,6 +454,7 @@ type ClaudeResponseInfo struct {
 	Model        string
 	ResponseText strings.Builder
 	Usage        *dto.Usage
+	Done         bool
 }
 
 func FormatClaudeResponseInfo(requestMode int, claudeResponse *dto.ClaudeResponse, oaiResponse *dto.ChatCompletionsStreamResponse, claudeInfo *ClaudeResponseInfo) bool {
@@ -461,20 +462,32 @@ func FormatClaudeResponseInfo(requestMode int, claudeResponse *dto.ClaudeRespons
 		claudeInfo.ResponseText.WriteString(claudeResponse.Completion)
 	} else {
 		if claudeResponse.Type == "message_start" {
-			// message_start, 获取usage
 			claudeInfo.ResponseId = claudeResponse.Message.Id
 			claudeInfo.Model = claudeResponse.Message.Model
+
+			// message_start, 获取usage
 			claudeInfo.Usage.PromptTokens = claudeResponse.Message.Usage.InputTokens
+			claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Message.Usage.CacheReadInputTokens
+			claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Message.Usage.CacheCreationInputTokens
+			claudeInfo.Usage.CompletionTokens = claudeResponse.Message.Usage.OutputTokens
 		} else if claudeResponse.Type == "content_block_delta" {
 			if claudeResponse.Delta.Text != nil {
 				claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Text)
 			}
+			if claudeResponse.Delta.Thinking != "" {
+				claudeInfo.ResponseText.WriteString(claudeResponse.Delta.Thinking)
+			}
 		} else if claudeResponse.Type == "message_delta" {
-			claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
+			// 最终的usage获取
 			if claudeResponse.Usage.InputTokens > 0 {
+				// 不叠加,只取最新的
 				claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
 			}
-			claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeResponse.Usage.OutputTokens
+			claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
+			claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeInfo.Usage.CompletionTokens
+
+			// 判断是否完整
+			claudeInfo.Done = true
 		} else if claudeResponse.Type == "content_block_start" {
 		} else {
 			return false
@@ -506,25 +519,15 @@ func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
 		}
 	}
 	if info.RelayFormat == relaycommon.RelayFormatClaude {
+		FormatClaudeResponseInfo(requestMode, &claudeResponse, nil, claudeInfo)
+
 		if requestMode == RequestModeCompletion {
-			claudeInfo.ResponseText.WriteString(claudeResponse.Completion)
 		} else {
 			if claudeResponse.Type == "message_start" {
 				// message_start, 获取usage
 				info.UpstreamModelName = claudeResponse.Message.Model
-				claudeInfo.Usage.PromptTokens = claudeResponse.Message.Usage.InputTokens
-				claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Message.Usage.CacheReadInputTokens
-				claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Message.Usage.CacheCreationInputTokens
-				claudeInfo.Usage.CompletionTokens = claudeResponse.Message.Usage.OutputTokens
 			} else if claudeResponse.Type == "content_block_delta" {
-				claudeInfo.ResponseText.WriteString(claudeResponse.Delta.GetText())
 			} else if claudeResponse.Type == "message_delta" {
-				if claudeResponse.Usage.InputTokens > 0 {
-					// 不叠加,只取最新的
-					claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
-				}
-				claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
-				claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeInfo.Usage.CompletionTokens
 			}
 		}
 		helper.ClaudeChunkData(c, claudeResponse, data)
@@ -544,29 +547,25 @@ func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
 }
 
 func HandleStreamFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, requestMode int) {
-	if info.RelayFormat == relaycommon.RelayFormatClaude {
-		if requestMode == RequestModeCompletion {
-			claudeInfo.Usage, _ = service.ResponseText2Usage(claudeInfo.ResponseText.String(), info.UpstreamModelName, info.PromptTokens)
-		} else {
-			// 说明流模式建立失败,可能为官方出错
-			if claudeInfo.Usage.PromptTokens == 0 {
-				//usage.PromptTokens = info.PromptTokens
-			}
-			if claudeInfo.Usage.CompletionTokens == 0 {
-				claudeInfo.Usage, _ = service.ResponseText2Usage(claudeInfo.ResponseText.String(), info.UpstreamModelName, claudeInfo.Usage.PromptTokens)
-			}
+
+	if requestMode == RequestModeCompletion {
+		claudeInfo.Usage, _ = service.ResponseText2Usage(claudeInfo.ResponseText.String(), info.UpstreamModelName, info.PromptTokens)
+	} else {
+		if claudeInfo.Usage.PromptTokens == 0 {
+			//上游出错
 		}
-	} else if info.RelayFormat == relaycommon.RelayFormatOpenAI {
-		if requestMode == RequestModeCompletion {
-			claudeInfo.Usage, _ = service.ResponseText2Usage(claudeInfo.ResponseText.String(), info.UpstreamModelName, info.PromptTokens)
-		} else {
-			if claudeInfo.Usage.PromptTokens == 0 {
-				//上游出错
-			}
-			if claudeInfo.Usage.CompletionTokens == 0 {
-				claudeInfo.Usage, _ = service.ResponseText2Usage(claudeInfo.ResponseText.String(), info.UpstreamModelName, claudeInfo.Usage.PromptTokens)
+		if claudeInfo.Usage.CompletionTokens == 0 || !claudeInfo.Done {
+			if common.DebugEnabled {
+				common.SysError("claude response usage is not complete, maybe upstream error")
 			}
+			claudeInfo.Usage, _ = service.ResponseText2Usage(claudeInfo.ResponseText.String(), info.UpstreamModelName, claudeInfo.Usage.PromptTokens)
 		}
+	}
+
+	if info.RelayFormat == relaycommon.RelayFormatClaude {
+		//
+	} else if info.RelayFormat == relaycommon.RelayFormatOpenAI {
+
 		if info.ShouldIncludeUsage {
 			response := helper.GenerateFinalUsageResponse(claudeInfo.ResponseId, claudeInfo.Created, info.UpstreamModelName, *claudeInfo.Usage)
 			err := helper.ObjectData(c, response)

+ 1 - 2
relay/channel/cohere/relay-cohere.go

@@ -3,7 +3,6 @@ package cohere
 import (
 	"bufio"
 	"encoding/json"
-	"fmt"
 	"github.com/gin-gonic/gin"
 	"io"
 	"net/http"
@@ -78,7 +77,7 @@ func stopReasonCohere2OpenAI(reason string) string {
 }
 
 func cohereStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
-	responseId := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
+	responseId := helper.GetResponseID(c)
 	createdTime := common.GetTimestamp()
 	usage := &dto.Usage{}
 	responseText := ""

+ 5 - 2
relay/channel/gemini/adaptor.go

@@ -72,8 +72,11 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
 func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
 
 	if model_setting.GetGeminiSettings().ThinkingAdapterEnabled {
-		// suffix -thinking and -nothinking
-		if strings.HasSuffix(info.OriginModelName, "-thinking") {
+		// 新增逻辑:处理 -thinking-<budget> 格式
+		if strings.Contains(info.OriginModelName, "-thinking-") {
+			parts := strings.Split(info.UpstreamModelName, "-thinking-")
+			info.UpstreamModelName = parts[0]
+		} else if strings.HasSuffix(info.OriginModelName, "-thinking") { // 旧的适配
 			info.UpstreamModelName = strings.TrimSuffix(info.UpstreamModelName, "-thinking")
 		} else if strings.HasSuffix(info.OriginModelName, "-nothinking") {
 			info.UpstreamModelName = strings.TrimSuffix(info.UpstreamModelName, "-nothinking")

+ 71 - 38
relay/channel/gemini/relay-gemini.go

@@ -12,6 +12,7 @@ import (
 	"one-api/relay/helper"
 	"one-api/service"
 	"one-api/setting/model_setting"
+	"strconv"
 	"strings"
 	"unicode/utf8"
 
@@ -36,6 +37,47 @@ var geminiSupportedMimeTypes = map[string]bool{
 	"video/flv":       true,
 }
 
+// Gemini 允许的思考预算范围
+const (
+	pro25MinBudget       = 128
+	pro25MaxBudget       = 32768
+	flash25MaxBudget     = 24576
+	flash25LiteMinBudget = 512
+	flash25LiteMaxBudget = 24576
+)
+
+// clampThinkingBudget 根据模型名称将预算限制在允许的范围内
+func clampThinkingBudget(modelName string, budget int) int {
+	isNew25Pro := strings.HasPrefix(modelName, "gemini-2.5-pro") &&
+		!strings.HasPrefix(modelName, "gemini-2.5-pro-preview-05-06") &&
+		!strings.HasPrefix(modelName, "gemini-2.5-pro-preview-03-25")
+	is25FlashLite := strings.HasPrefix(modelName, "gemini-2.5-flash-lite")
+
+	if is25FlashLite {
+		if budget < flash25LiteMinBudget {
+			return flash25LiteMinBudget
+		}
+		if budget > flash25LiteMaxBudget {
+			return flash25LiteMaxBudget
+		}
+	} else if isNew25Pro {
+		if budget < pro25MinBudget {
+			return pro25MinBudget
+		}
+		if budget > pro25MaxBudget {
+			return pro25MaxBudget
+		}
+	} else { // 其他模型
+		if budget < 0 {
+			return 0
+		}
+		if budget > flash25MaxBudget {
+			return flash25MaxBudget
+		}
+	}
+	return budget
+}
+
 // Setting safety to the lowest possible values since Gemini is already powerless enough
 func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon.RelayInfo) (*GeminiChatRequest, error) {
 
@@ -57,16 +99,31 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
 	}
 
 	if model_setting.GetGeminiSettings().ThinkingAdapterEnabled {
-		if strings.HasSuffix(info.OriginModelName, "-thinking") {
-			// 硬编码不支持 ThinkingBudget 的旧模型
+		modelName := info.OriginModelName
+		isNew25Pro := strings.HasPrefix(modelName, "gemini-2.5-pro") &&
+			!strings.HasPrefix(modelName, "gemini-2.5-pro-preview-05-06") &&
+			!strings.HasPrefix(modelName, "gemini-2.5-pro-preview-03-25")
+		is25FlashLite := strings.HasPrefix(modelName, "gemini-2.5-flash-lite")
+
+		if strings.Contains(modelName, "-thinking-") {
+			parts := strings.SplitN(modelName, "-thinking-", 2)
+			if len(parts) == 2 && parts[1] != "" {
+				if budgetTokens, err := strconv.Atoi(parts[1]); err == nil {
+					clampedBudget := clampThinkingBudget(modelName, budgetTokens)
+					geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
+						ThinkingBudget:  common.GetPointer(clampedBudget),
+						IncludeThoughts: true,
+					}
+				}
+			}
+		} else if strings.HasSuffix(modelName, "-thinking") {
 			unsupportedModels := []string{
 				"gemini-2.5-pro-preview-05-06",
 				"gemini-2.5-pro-preview-03-25",
 			}
-
 			isUnsupported := false
 			for _, unsupportedModel := range unsupportedModels {
-				if strings.HasPrefix(info.OriginModelName, unsupportedModel) {
+				if strings.HasPrefix(modelName, unsupportedModel) {
 					isUnsupported = true
 					break
 				}
@@ -78,39 +135,14 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
 				}
 			} else {
 				budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(geminiRequest.GenerationConfig.MaxOutputTokens)
-
-				// 检查是否为新的2.5pro模型(支持ThinkingBudget但有特殊范围)
-				isNew25Pro := strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro") &&
-					!strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro-preview-05-06") &&
-					!strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro-preview-03-25")
-
-				if isNew25Pro {
-					// 新的2.5pro模型:ThinkingBudget范围为128-32768
-					if budgetTokens == 0 || budgetTokens < 128 {
-						budgetTokens = 128
-					} else if budgetTokens > 32768 {
-						budgetTokens = 32768
-					}
-				} else {
-					// 其他模型:ThinkingBudget范围为0-24576
-					if budgetTokens == 0 || budgetTokens > 24576 {
-						budgetTokens = 24576
-					}
-				}
-
+				clampedBudget := clampThinkingBudget(modelName, int(budgetTokens))
 				geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
-					ThinkingBudget:  common.GetPointer(int(budgetTokens)),
+					ThinkingBudget:  common.GetPointer(clampedBudget),
 					IncludeThoughts: true,
 				}
 			}
-		} else if strings.HasSuffix(info.OriginModelName, "-nothinking") {
-			// 检查是否为新的2.5pro模型(不支持-nothinking,因为最低值只能为128)
-			isNew25Pro := strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro") &&
-				!strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro-preview-05-06") &&
-				!strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro-preview-03-25")
-
-			if !isNew25Pro {
-				// 只有非新2.5pro模型才支持-nothinking
+		} else if strings.HasSuffix(modelName, "-nothinking") {
+			if !isNew25Pro && !is25FlashLite {
 				geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
 					ThinkingBudget: common.GetPointer(0),
 				}
@@ -283,7 +315,8 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
 
 					// 校验 MimeType 是否在 Gemini 支持的白名单中
 					if _, ok := geminiSupportedMimeTypes[strings.ToLower(fileData.MimeType)]; !ok {
-						return nil, fmt.Errorf("MIME type '%s' from URL '%s' is not supported by Gemini. Supported types are: %v", fileData.MimeType, part.GetImageMedia().Url, getSupportedMimeTypesList())
+						url := part.GetImageMedia().Url
+						return nil, fmt.Errorf("mime type is not supported by Gemini: '%s', url: '%s', supported types are: %v", fileData.MimeType, url, getSupportedMimeTypesList())
 					}
 
 					parts = append(parts, GeminiPart{
@@ -611,9 +644,9 @@ func getResponseToolCall(item *GeminiPart) *dto.ToolCallResponse {
 	}
 }
 
-func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResponse {
+func responseGeminiChat2OpenAI(c *gin.Context, response *GeminiChatResponse) *dto.OpenAITextResponse {
 	fullTextResponse := dto.OpenAITextResponse{
-		Id:      fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
+		Id:      helper.GetResponseID(c),
 		Object:  "chat.completion",
 		Created: common.GetTimestamp(),
 		Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)),
@@ -754,7 +787,7 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C
 
 func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
 	// responseText := ""
-	id := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
+	id := helper.GetResponseID(c)
 	createAt := common.GetTimestamp()
 	var usage = &dto.Usage{}
 	var imageCount int
@@ -849,7 +882,7 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re
 			StatusCode: resp.StatusCode,
 		}, nil
 	}
-	fullTextResponse := responseGeminiChat2OpenAI(&geminiResponse)
+	fullTextResponse := responseGeminiChat2OpenAI(c, &geminiResponse)
 	fullTextResponse.Model = info.UpstreamModelName
 	usage := dto.Usage{
 		PromptTokens:     geminiResponse.UsageMetadata.PromptTokenCount,

+ 7 - 0
relay/channel/openai/adaptor.go

@@ -88,6 +88,13 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
 		requestURL := strings.Split(info.RequestURLPath, "?")[0]
 		requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
 		task := strings.TrimPrefix(requestURL, "/v1/")
+
+		// 特殊处理 responses API
+		if info.RelayMode == constant.RelayModeResponses {
+			requestURL = fmt.Sprintf("/openai/v1/responses?api-version=preview")
+			return relaycommon.GetFullRequestURL(info.BaseUrl, requestURL, info.ChannelType), nil
+		}
+
 		model_ := info.UpstreamModelName
 		// 2025年5月10日后创建的渠道不移除.
 		if info.ChannelCreateTime < constant2.AzureNoRemoveDotTime {

+ 5 - 3
relay/channel/openai/relay-openai.go

@@ -8,6 +8,7 @@ import (
 	"math"
 	"mime/multipart"
 	"net/http"
+	"path/filepath"
 	"one-api/common"
 	"one-api/constant"
 	"one-api/dto"
@@ -345,13 +346,14 @@ func countAudioTokens(c *gin.Context) (int, error) {
 	if err = c.ShouldBind(&reqBody); err != nil {
 		return 0, errors.WithStack(err)
 	}
-
+  ext := filepath.Ext(reqBody.File.Filename) // 获取文件扩展名
 	reqFp, err := reqBody.File.Open()
 	if err != nil {
 		return 0, errors.WithStack(err)
 	}
+  defer reqFp.Close()
 
-	tmpFp, err := os.CreateTemp("", "audio-*")
+	tmpFp, err := os.CreateTemp("", "audio-*"+ext)
 	if err != nil {
 		return 0, errors.WithStack(err)
 	}
@@ -365,7 +367,7 @@ func countAudioTokens(c *gin.Context) (int, error) {
 		return 0, errors.WithStack(err)
 	}
 
-	duration, err := common.GetAudioDuration(c.Request.Context(), tmpFp.Name())
+	duration, err := common.GetAudioDuration(c.Request.Context(), tmpFp.Name(), ext)
 	if err != nil {
 		return 0, errors.WithStack(err)
 	}

+ 1 - 2
relay/channel/palm/relay-palm.go

@@ -2,7 +2,6 @@ package palm
 
 import (
 	"encoding/json"
-	"fmt"
 	"github.com/gin-gonic/gin"
 	"io"
 	"net/http"
@@ -73,7 +72,7 @@ func streamResponsePaLM2OpenAI(palmResponse *PaLMChatResponse) *dto.ChatCompleti
 
 func palmStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, string) {
 	responseText := ""
-	responseId := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
+	responseId := helper.GetResponseID(c)
 	createdTime := common.GetTimestamp()
 	dataChan := make(chan string)
 	stopChan := make(chan bool)

+ 1 - 1
relay/claude_handler.go

@@ -98,7 +98,7 @@ func ClaudeHelper(c *gin.Context) (claudeError *dto.ClaudeErrorWithStatusCode) {
 			// BudgetTokens 为 max_tokens 的 80%
 			textRequest.Thinking = &dto.Thinking{
 				Type:         "enabled",
-				BudgetTokens: int(float64(textRequest.MaxTokens) * model_setting.GetClaudeSettings().ThinkingAdapterBudgetTokensPercentage),
+				BudgetTokens: common.GetPointer[int](int(float64(textRequest.MaxTokens) * model_setting.GetClaudeSettings().ThinkingAdapterBudgetTokensPercentage)),
 			}
 			// TODO: 临时处理
 			// https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking

+ 2 - 0
relay/common/relay_info.go

@@ -61,6 +61,7 @@ type RelayInfo struct {
 	TokenKey          string
 	UserId            int
 	Group             string
+	UserGroup         string
 	TokenUnlimited    bool
 	StartTime         time.Time
 	FirstResponseTime time.Time
@@ -204,6 +205,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
 		TokenKey:          tokenKey,
 		UserId:            userId,
 		Group:             group,
+		UserGroup:         c.GetString(constant.ContextKeyUserGroup),
 		TokenUnlimited:    tokenUnlimited,
 		StartTime:         startTime,
 		FirstResponseTime: startTime.Add(-time.Second),

+ 45 - 7
relay/helper/price.go

@@ -2,14 +2,20 @@ package helper
 
 import (
 	"fmt"
-	"github.com/gin-gonic/gin"
 	"one-api/common"
 	constant2 "one-api/constant"
 	relaycommon "one-api/relay/common"
 	"one-api/setting"
 	"one-api/setting/operation_setting"
+
+	"github.com/gin-gonic/gin"
 )
 
+type GroupRatioInfo struct {
+	GroupRatio        float64
+	GroupSpecialRatio float64
+}
+
 type PriceData struct {
 	ModelPrice             float64
 	ModelRatio             float64
@@ -17,18 +23,50 @@ type PriceData struct {
 	CacheRatio             float64
 	CacheCreationRatio     float64
 	ImageRatio             float64
-	GroupRatio             float64
 	UsePrice               bool
 	ShouldPreConsumedQuota int
+	GroupRatioInfo         GroupRatioInfo
 }
 
 func (p PriceData) ToSetting() string {
-	return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d, ImageRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.ShouldPreConsumedQuota, p.ImageRatio)
+	return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d, ImageRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.ShouldPreConsumedQuota, p.ImageRatio)
+}
+
+// HandleGroupRatio checks for "auto_group" in the context and updates the group ratio and relayInfo.Group if present
+func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) GroupRatioInfo {
+	groupRatioInfo := GroupRatioInfo{
+		GroupRatio:        1.0, // default ratio
+		GroupSpecialRatio: 1.0, // default user group ratio
+	}
+
+	// check auto group
+	autoGroup, exists := ctx.Get("auto_group")
+	if exists {
+		if common.DebugEnabled {
+			println(fmt.Sprintf("final group: %s", autoGroup))
+		}
+		relayInfo.Group = autoGroup.(string)
+	}
+
+	// check user group special ratio
+	userGroupRatio, ok := setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group)
+	if ok {
+		// user group special ratio
+		groupRatioInfo.GroupSpecialRatio = userGroupRatio
+		groupRatioInfo.GroupRatio = userGroupRatio
+	} else {
+		// normal group ratio
+		groupRatioInfo.GroupRatio = setting.GetGroupRatio(relayInfo.Group)
+	}
+
+	return groupRatioInfo
 }
 
 func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, maxTokens int) (PriceData, error) {
 	modelPrice, usePrice := operation_setting.GetModelPrice(info.OriginModelName, false)
-	groupRatio := setting.GetGroupRatio(info.Group)
+
+	groupRatioInfo := HandleGroupRatio(c, info)
+
 	var preConsumedQuota int
 	var modelRatio float64
 	var completionRatio float64
@@ -58,17 +96,17 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
 		cacheRatio, _ = operation_setting.GetCacheRatio(info.OriginModelName)
 		cacheCreationRatio, _ = operation_setting.GetCreateCacheRatio(info.OriginModelName)
 		imageRatio, _ = operation_setting.GetImageRatio(info.OriginModelName)
-		ratio := modelRatio * groupRatio
+		ratio := modelRatio * groupRatioInfo.GroupRatio
 		preConsumedQuota = int(float64(preConsumedTokens) * ratio)
 	} else {
-		preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatio)
+		preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
 	}
 
 	priceData := PriceData{
 		ModelPrice:             modelPrice,
 		ModelRatio:             modelRatio,
 		CompletionRatio:        completionRatio,
-		GroupRatio:             groupRatio,
+		GroupRatioInfo:         groupRatioInfo,
 		UsePrice:               usePrice,
 		CacheRatio:             cacheRatio,
 		ImageRatio:             imageRatio,

+ 14 - 0
relay/relay-gemini.go

@@ -136,6 +136,20 @@ func GeminiHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
 
 	adaptor.Init(relayInfo)
 
+	// Clean up empty system instruction
+	if req.SystemInstructions != nil {
+		hasContent := false
+		for _, part := range req.SystemInstructions.Parts {
+			if part.Text != "" {
+				hasContent = true
+				break
+			}
+		}
+		if !hasContent {
+			req.SystemInstructions = nil
+		}
+	}
+
 	requestBody, err := json.Marshal(req)
 	if err != nil {
 		return service.OpenAIErrorWrapperLocal(err, "marshal_text_request_failed", http.StatusInternalServerError)

+ 1 - 1
relay/relay-image.go

@@ -162,7 +162,7 @@ func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
 
 		// reset model price
 		priceData.ModelPrice *= sizeRatio * qualityRatio * float64(imageRequest.N)
-		quota = int(priceData.ModelPrice * priceData.GroupRatio * common.QuotaPerUnit)
+		quota = int(priceData.ModelPrice * priceData.GroupRatioInfo.GroupRatio * common.QuotaPerUnit)
 		userQuota, err = model.GetUserQuota(relayInfo.UserId, false)
 		if err != nil {
 			return service.OpenAIErrorWrapperLocal(err, "get_user_quota_failed", http.StatusInternalServerError)

+ 6 - 5
relay/relay-text.go

@@ -90,15 +90,16 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
 
 	// get & validate textRequest 获取并验证文本请求
 	textRequest, err := getAndValidateTextRequest(c, relayInfo)
-	if textRequest.WebSearchOptions != nil {
-		c.Set("chat_completion_web_search_context_size", textRequest.WebSearchOptions.SearchContextSize)
-	}
 
 	if err != nil {
 		common.LogError(c, fmt.Sprintf("getAndValidateTextRequest failed: %s", err.Error()))
 		return service.OpenAIErrorWrapperLocal(err, "invalid_text_request", http.StatusBadRequest)
 	}
 
+	if textRequest.WebSearchOptions != nil {
+		c.Set("chat_completion_web_search_context_size", textRequest.WebSearchOptions.SearchContextSize)
+	}
+
 	if setting.ShouldCheckPromptSensitive() {
 		words, err := checkRequestSensitive(textRequest, relayInfo)
 		if err != nil {
@@ -361,7 +362,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
 	cacheRatio := priceData.CacheRatio
 	imageRatio := priceData.ImageRatio
 	modelRatio := priceData.ModelRatio
-	groupRatio := priceData.GroupRatio
+	groupRatio := priceData.GroupRatioInfo.GroupRatio
 	modelPrice := priceData.ModelPrice
 
 	// Convert values to decimal for precise calculation
@@ -510,7 +511,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
 	if extraContent != "" {
 		logContent += ", " + extraContent
 	}
-	other := service.GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice)
+	other := service.GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice, priceData.GroupRatioInfo.GroupSpecialRatio)
 	if imageTokens != 0 {
 		other["image"] = true
 		other["image_ratio"] = imageRatio

+ 6 - 37
relay/websocket.go

@@ -6,12 +6,10 @@ import (
 	"github.com/gin-gonic/gin"
 	"github.com/gorilla/websocket"
 	"net/http"
-	"one-api/common"
 	"one-api/dto"
 	relaycommon "one-api/relay/common"
+	"one-api/relay/helper"
 	"one-api/service"
-	"one-api/setting"
-	"one-api/setting/operation_setting"
 )
 
 func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWithStatusCode) {
@@ -39,43 +37,14 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
 			//isModelMapped = true
 		}
 	}
-	//relayInfo.UpstreamModelName = textRequest.Model
-	modelPrice, getModelPriceSuccess := operation_setting.GetModelPrice(relayInfo.UpstreamModelName, false)
-	groupRatio := setting.GetGroupRatio(relayInfo.Group)
 
-	var preConsumedQuota int
-	var ratio float64
-	var modelRatio float64
-	//err := service.SensitiveWordsCheck(textRequest)
-
-	//if constant.ShouldCheckPromptSensitive() {
-	//	err = checkRequestSensitive(textRequest, relayInfo)
-	//	if err != nil {
-	//		return service.OpenAIErrorWrapperLocal(err, "sensitive_words_detected", http.StatusBadRequest)
-	//	}
-	//}
-
-	//promptTokens, err := getWssPromptTokens(realtimeEvent, relayInfo)
-	//// count messages token error 计算promptTokens错误
-	//if err != nil {
-	//	return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
-	//}
-	//
-	if !getModelPriceSuccess {
-		preConsumedTokens := common.PreConsumedQuota
-		//if realtimeEvent.Session.MaxResponseOutputTokens != 0 {
-		//	preConsumedTokens = promptTokens + int(realtimeEvent.Session.MaxResponseOutputTokens)
-		//}
-		modelRatio, _ = operation_setting.GetModelRatio(relayInfo.UpstreamModelName)
-		ratio = modelRatio * groupRatio
-		preConsumedQuota = int(float64(preConsumedTokens) * ratio)
-	} else {
-		preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatio)
-		relayInfo.UsePrice = true
+	priceData, err := helper.ModelPriceHelper(c, relayInfo, 0, 0)
+	if err != nil {
+		return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
 	}
 
 	// pre-consume quota 预消耗配额
-	preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, preConsumedQuota, relayInfo)
+	preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
 	if openaiErr != nil {
 		return openaiErr
 	}
@@ -113,6 +82,6 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
 		return openaiErr
 	}
 	service.PostWssConsumeQuota(c, relayInfo, relayInfo.UpstreamModelName, usage.(*dto.RealtimeUsage), preConsumedQuota,
-		userQuota, modelRatio, groupRatio, modelPrice, getModelPriceSuccess, "")
+		userQuota, priceData, "")
 	return nil
 }

+ 2 - 0
router/api-router.go

@@ -81,6 +81,7 @@ func SetApiRouter(router *gin.Engine) {
 			optionRoute.GET("/", controller.GetOptions)
 			optionRoute.PUT("/", controller.UpdateOption)
 			optionRoute.POST("/rest_model_ratio", controller.ResetModelRatio)
+			optionRoute.POST("/migrate_console_setting", controller.MigrateConsoleSetting) // 用于迁移检测的旧键,下个版本会删除
 		}
 		channelRoute := apiRouter.Group("/channel")
 		channelRoute.Use(middleware.AdminAuth())
@@ -126,6 +127,7 @@ func SetApiRouter(router *gin.Engine) {
 			redemptionRoute.GET("/:id", controller.GetRedemption)
 			redemptionRoute.POST("/", controller.AddRedemption)
 			redemptionRoute.PUT("/", controller.UpdateRedemption)
+			redemptionRoute.DELETE("/invalid", controller.DeleteInvalidRedemption)
 			redemptionRoute.DELETE("/:id", controller.DeleteRedemption)
 		}
 		logRoute := apiRouter.Group("/log")

+ 2 - 0
service/channel.go

@@ -59,6 +59,8 @@ func ShouldDisableChannel(channelType int, err *dto.OpenAIErrorWithStatusCode) b
 		return true
 	case "billing_not_active":
 		return true
+	case "pre_consume_token_quota_failed":
+		return true
 	}
 	switch err.Error.Type {
 	case "insufficient_quota":

+ 2 - 2
service/convert.go

@@ -21,10 +21,10 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re
 
 	isOpenRouter := info.ChannelType == common.ChannelTypeOpenRouter
 
-	if claudeRequest.Thinking != nil {
+	if claudeRequest.Thinking != nil && claudeRequest.Thinking.Type == "enabled" {
 		if isOpenRouter {
 			reasoning := openrouter.RequestReasoning{
-				MaxTokens: claudeRequest.Thinking.BudgetTokens,
+				MaxTokens: claudeRequest.Thinking.GetBudgetTokens(),
 			}
 			reasoningJSON, err := json.Marshal(reasoning)
 			if err != nil {

+ 10 - 6
service/error.go

@@ -29,9 +29,11 @@ func MidjourneyErrorWithStatusCodeWrapper(code int, desc string, statusCode int)
 func OpenAIErrorWrapper(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
 	text := err.Error()
 	lowerText := strings.ToLower(text)
-	if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
-		common.SysLog(fmt.Sprintf("error: %s", text))
-		text = "请求上游地址失败"
+	if !strings.HasPrefix(lowerText, "get file base64 from url") && !strings.HasPrefix(lowerText, "mime type is not supported") {
+		if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
+			common.SysLog(fmt.Sprintf("error: %s", text))
+			text = "请求上游地址失败"
+		}
 	}
 	openAIError := dto.OpenAIError{
 		Message: text,
@@ -53,9 +55,11 @@ func OpenAIErrorWrapperLocal(err error, code string, statusCode int) *dto.OpenAI
 func ClaudeErrorWrapper(err error, code string, statusCode int) *dto.ClaudeErrorWithStatusCode {
 	text := err.Error()
 	lowerText := strings.ToLower(text)
-	if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
-		common.SysLog(fmt.Sprintf("error: %s", text))
-		text = "请求上游地址失败"
+	if !strings.HasPrefix(lowerText, "get file base64 from url") {
+		if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
+			common.SysLog(fmt.Sprintf("error: %s", text))
+			text = "请求上游地址失败"
+		}
 	}
 	claudeError := dto.ClaudeError{
 		Message: text,

+ 98 - 1
service/file_decoder.go

@@ -4,8 +4,10 @@ import (
 	"encoding/base64"
 	"fmt"
 	"io"
+	"one-api/common"
 	"one-api/constant"
 	"one-api/dto"
+	"strings"
 )
 
 func GetFileBase64FromUrl(url string) (*dto.LocalFileData, error) {
@@ -30,9 +32,104 @@ func GetFileBase64FromUrl(url string) (*dto.LocalFileData, error) {
 	// Convert to base64
 	base64Data := base64.StdEncoding.EncodeToString(fileBytes)
 
+	mimeType := resp.Header.Get("Content-Type")
+	if len(strings.Split(mimeType, ";")) > 1 {
+		// If Content-Type has parameters, take the first part
+		mimeType = strings.Split(mimeType, ";")[0]
+	}
+	if mimeType == "application/octet-stream" {
+		if common.DebugEnabled {
+			println("MIME type is application/octet-stream, trying to guess from URL or filename")
+		}
+		// try to guess the MIME type from the url last segment
+		urlParts := strings.Split(url, "/")
+		if len(urlParts) > 0 {
+			lastSegment := urlParts[len(urlParts)-1]
+			if strings.Contains(lastSegment, ".") {
+				// Extract the file extension
+				filename := strings.Split(lastSegment, ".")
+				if len(filename) > 1 {
+					ext := strings.ToLower(filename[len(filename)-1])
+					// Guess MIME type based on file extension
+					mimeType = GetMimeTypeByExtension(ext)
+				}
+			}
+		} else {
+			// try to guess the MIME type from the file extension
+			fileName := resp.Header.Get("Content-Disposition")
+			if fileName != "" {
+				// Extract the filename from the Content-Disposition header
+				parts := strings.Split(fileName, ";")
+				for _, part := range parts {
+					if strings.HasPrefix(strings.TrimSpace(part), "filename=") {
+						fileName = strings.TrimSpace(strings.TrimPrefix(part, "filename="))
+						// Remove quotes if present
+						if len(fileName) > 2 && fileName[0] == '"' && fileName[len(fileName)-1] == '"' {
+							fileName = fileName[1 : len(fileName)-1]
+						}
+						// Guess MIME type based on file extension
+						if ext := strings.ToLower(strings.TrimPrefix(fileName, ".")); ext != "" {
+							mimeType = GetMimeTypeByExtension(ext)
+						}
+						break
+					}
+				}
+			}
+		}
+	}
+
 	return &dto.LocalFileData{
 		Base64Data: base64Data,
-		MimeType:   resp.Header.Get("Content-Type"),
+		MimeType:   mimeType,
 		Size:       int64(len(fileBytes)),
 	}, nil
 }
+
+func GetMimeTypeByExtension(ext string) string {
+	// Convert to lowercase for case-insensitive comparison
+	ext = strings.ToLower(ext)
+	switch ext {
+	// Text files
+	case "txt", "md", "markdown", "csv", "json", "xml", "html", "htm":
+		return "text/plain"
+
+	// Image files
+	case "jpg", "jpeg":
+		return "image/jpeg"
+	case "png":
+		return "image/png"
+	case "gif":
+		return "image/gif"
+
+	// Audio files
+	case "mp3":
+		return "audio/mp3"
+	case "wav":
+		return "audio/wav"
+	case "mpeg":
+		return "audio/mpeg"
+
+	// Video files
+	case "mp4":
+		return "video/mp4"
+	case "wmv":
+		return "video/wmv"
+	case "flv":
+		return "video/flv"
+	case "mov":
+		return "video/mov"
+	case "mpg":
+		return "video/mpg"
+	case "avi":
+		return "video/avi"
+	case "mpegps":
+		return "video/mpegps"
+
+	// Document files
+	case "pdf":
+		return "application/pdf"
+
+	default:
+		return "application/octet-stream" // Default for unknown types
+	}
+}

+ 8 - 7
service/log_info_generate.go

@@ -8,7 +8,7 @@ import (
 )
 
 func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64,
-	cacheTokens int, cacheRatio float64, modelPrice float64) map[string]interface{} {
+	cacheTokens int, cacheRatio float64, modelPrice float64, userGroupRatio float64) map[string]interface{} {
 	other := make(map[string]interface{})
 	other["model_ratio"] = modelRatio
 	other["group_ratio"] = groupRatio
@@ -16,6 +16,7 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m
 	other["cache_tokens"] = cacheTokens
 	other["cache_ratio"] = cacheRatio
 	other["model_price"] = modelPrice
+	other["user_group_ratio"] = userGroupRatio
 	other["frt"] = float64(relayInfo.FirstResponseTime.UnixMilli() - relayInfo.StartTime.UnixMilli())
 	if relayInfo.ReasoningEffort != "" {
 		other["reasoning_effort"] = relayInfo.ReasoningEffort
@@ -30,8 +31,8 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m
 	return other
 }
 
-func GenerateWssOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice float64) map[string]interface{} {
-	info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice)
+func GenerateWssOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice, userGroupRatio float64) map[string]interface{} {
+	info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice, userGroupRatio)
 	info["ws"] = true
 	info["audio_input"] = usage.InputTokenDetails.AudioTokens
 	info["audio_output"] = usage.OutputTokenDetails.AudioTokens
@@ -42,8 +43,8 @@ func GenerateWssOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, us
 	return info
 }
 
-func GenerateAudioOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice float64) map[string]interface{} {
-	info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice)
+func GenerateAudioOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice, userGroupRatio float64) map[string]interface{} {
+	info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice, userGroupRatio)
 	info["audio"] = true
 	info["audio_input"] = usage.PromptTokensDetails.AudioTokens
 	info["audio_output"] = usage.CompletionTokenDetails.AudioTokens
@@ -55,8 +56,8 @@ func GenerateAudioOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
 }
 
 func GenerateClaudeOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64,
-	cacheTokens int, cacheRatio float64, cacheCreationTokens int, cacheCreationRatio float64, modelPrice float64) map[string]interface{} {
-	info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice)
+	cacheTokens int, cacheRatio float64, cacheCreationTokens int, cacheCreationRatio float64, modelPrice float64, userGroupRatio float64) map[string]interface{} {
+	info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice, userGroupRatio)
 	info["claude"] = true
 	info["cache_creation_tokens"] = cacheCreationTokens
 	info["cache_creation_ratio"] = cacheCreationRatio

+ 26 - 9
service/quota.go

@@ -3,6 +3,7 @@ package service
 import (
 	"errors"
 	"fmt"
+	"log"
 	"math"
 	"one-api/common"
 	constant2 "one-api/constant"
@@ -97,6 +98,19 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
 	groupRatio := setting.GetGroupRatio(relayInfo.Group)
 	modelRatio, _ := operation_setting.GetModelRatio(modelName)
 
+	autoGroup, exists := ctx.Get("auto_group")
+	if exists {
+		groupRatio = setting.GetGroupRatio(autoGroup.(string))
+		log.Printf("final group ratio: %f", groupRatio)
+		relayInfo.Group = autoGroup.(string)
+	}
+
+	actualGroupRatio := groupRatio
+	userGroupRatio, ok := setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group)
+	if ok {
+		actualGroupRatio = userGroupRatio
+	}
+
 	quotaInfo := QuotaInfo{
 		InputDetails: TokenDetails{
 			TextTokens:  textInputTokens,
@@ -109,7 +123,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
 		ModelName:  modelName,
 		UsePrice:   relayInfo.UsePrice,
 		ModelRatio: modelRatio,
-		GroupRatio: groupRatio,
+		GroupRatio: actualGroupRatio,
 	}
 
 	quota := calculateAudioQuota(quotaInfo)
@@ -131,8 +145,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
 }
 
 func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelName string,
-	usage *dto.RealtimeUsage, preConsumedQuota int, userQuota int, modelRatio float64, groupRatio float64,
-	modelPrice float64, usePrice bool, extraContent string) {
+	usage *dto.RealtimeUsage, preConsumedQuota int, userQuota int, priceData helper.PriceData, extraContent string) {
 
 	useTimeSeconds := time.Now().Unix() - relayInfo.StartTime.Unix()
 	textInputTokens := usage.InputTokenDetails.TextTokens
@@ -146,6 +159,11 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
 	audioRatio := decimal.NewFromFloat(operation_setting.GetAudioRatio(relayInfo.OriginModelName))
 	audioCompletionRatio := decimal.NewFromFloat(operation_setting.GetAudioCompletionRatio(modelName))
 
+	modelRatio := priceData.ModelRatio
+	groupRatio := priceData.GroupRatioInfo.GroupRatio
+	modelPrice := priceData.ModelPrice
+	usePrice := priceData.UsePrice
+
 	quotaInfo := QuotaInfo{
 		InputDetails: TokenDetails{
 			TextTokens:  textInputTokens,
@@ -190,7 +208,7 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
 		logContent += ", " + extraContent
 	}
 	other := GenerateWssOtherInfo(ctx, relayInfo, usage, modelRatio, groupRatio,
-		completionRatio.InexactFloat64(), audioRatio.InexactFloat64(), audioCompletionRatio.InexactFloat64(), modelPrice)
+		completionRatio.InexactFloat64(), audioRatio.InexactFloat64(), audioCompletionRatio.InexactFloat64(), modelPrice, priceData.GroupRatioInfo.GroupSpecialRatio)
 	model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, usage.InputTokens, usage.OutputTokens, logModel,
 		tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
 }
@@ -206,9 +224,8 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
 	tokenName := ctx.GetString("token_name")
 	completionRatio := priceData.CompletionRatio
 	modelRatio := priceData.ModelRatio
-	groupRatio := priceData.GroupRatio
+	groupRatio := priceData.GroupRatioInfo.GroupRatio
 	modelPrice := priceData.ModelPrice
-
 	cacheRatio := priceData.CacheRatio
 	cacheTokens := usage.PromptTokensDetails.CachedTokens
 
@@ -262,7 +279,7 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
 	}
 
 	other := GenerateClaudeOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio,
-		cacheTokens, cacheRatio, cacheCreationTokens, cacheCreationRatio, modelPrice)
+		cacheTokens, cacheRatio, cacheCreationTokens, cacheCreationRatio, modelPrice, priceData.GroupRatioInfo.GroupSpecialRatio)
 	model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, modelName,
 		tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
 }
@@ -304,7 +321,7 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
 	audioCompletionRatio := decimal.NewFromFloat(operation_setting.GetAudioCompletionRatio(relayInfo.OriginModelName))
 
 	modelRatio := priceData.ModelRatio
-	groupRatio := priceData.GroupRatio
+	groupRatio := priceData.GroupRatioInfo.GroupRatio
 	modelPrice := priceData.ModelPrice
 	usePrice := priceData.UsePrice
 
@@ -360,7 +377,7 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
 		logContent += ", " + extraContent
 	}
 	other := GenerateAudioOtherInfo(ctx, relayInfo, usage, modelRatio, groupRatio,
-		completionRatio.InexactFloat64(), audioRatio.InexactFloat64(), audioCompletionRatio.InexactFloat64(), modelPrice)
+		completionRatio.InexactFloat64(), audioRatio.InexactFloat64(), audioCompletionRatio.InexactFloat64(), modelPrice, priceData.GroupRatioInfo.GroupSpecialRatio)
 	model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, usage.PromptTokens, usage.CompletionTokens, logModel,
 		tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
 }

+ 31 - 0
setting/auto_group.go

@@ -0,0 +1,31 @@
+package setting
+
+import "encoding/json"
+
+var AutoGroups = []string{
+	"default",
+}
+
+var DefaultUseAutoGroup = false
+
+func ContainsAutoGroup(group string) bool {
+	for _, autoGroup := range AutoGroups {
+		if autoGroup == group {
+			return true
+		}
+	}
+	return false
+}
+
+func UpdateAutoGroupsByJsonString(jsonString string) error {
+	AutoGroups = make([]string, 0)
+	return json.Unmarshal([]byte(jsonString), &AutoGroups)
+}
+
+func AutoGroups2JsonString() string {
+	jsonBytes, err := json.Marshal(AutoGroups)
+	if err != nil {
+		return "[]"
+	}
+	return string(jsonBytes)
+}

+ 0 - 327
setting/console.go

@@ -1,327 +0,0 @@
-package setting
-
-import (
-	"encoding/json"
-	"fmt"
-	"net/url"
-	"one-api/common"
-	"regexp"
-	"sort"
-	"strings"
-	"time"
-)
-
-// ValidateConsoleSettings 验证控制台设置信息格式
-func ValidateConsoleSettings(settingsStr string, settingType string) error {
-	if settingsStr == "" {
-		return nil // 空字符串是合法的
-	}
-	
-	switch settingType {
-	case "ApiInfo":
-		return validateApiInfo(settingsStr)
-	case "Announcements":
-		return validateAnnouncements(settingsStr)
-	case "FAQ":
-		return validateFAQ(settingsStr)
-	default:
-		return fmt.Errorf("未知的设置类型:%s", settingType)
-	}
-}
-
-// validateApiInfo 验证API信息格式
-func validateApiInfo(apiInfoStr string) error {
-	var apiInfoList []map[string]interface{}
-	if err := json.Unmarshal([]byte(apiInfoStr), &apiInfoList); err != nil {
-		return fmt.Errorf("API信息格式错误:%s", err.Error())
-	}
-	
-	// 验证数组长度
-	if len(apiInfoList) > 50 {
-		return fmt.Errorf("API信息数量不能超过50个")
-	}
-	
-	// 允许的颜色值
-	validColors := map[string]bool{
-		"blue": true, "green": true, "cyan": true, "purple": true, "pink": true,
-		"red": true, "orange": true, "amber": true, "yellow": true, "lime": true,
-		"light-green": true, "teal": true, "light-blue": true, "indigo": true,
-		"violet": true, "grey": true,
-	}
-	
-	// URL正则表达式,支持域名和IP地址格式
-	// 域名格式:https://example.com 或 https://sub.example.com:8080
-	// IP地址格式:https://192.168.1.1 或 https://192.168.1.1:8080
-	urlRegex := regexp.MustCompile(`^https?://(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(?::[0-9]{1,5})?(?:/.*)?$`)
-	
-	for i, apiInfo := range apiInfoList {
-		// 检查必填字段
-		urlStr, ok := apiInfo["url"].(string)
-		if !ok || urlStr == "" {
-			return fmt.Errorf("第%d个API信息缺少URL字段", i+1)
-		}
-		
-		route, ok := apiInfo["route"].(string)
-		if !ok || route == "" {
-			return fmt.Errorf("第%d个API信息缺少线路描述字段", i+1)
-		}
-		
-		description, ok := apiInfo["description"].(string)
-		if !ok || description == "" {
-			return fmt.Errorf("第%d个API信息缺少说明字段", i+1)
-		}
-		
-		color, ok := apiInfo["color"].(string)
-		if !ok || color == "" {
-			return fmt.Errorf("第%d个API信息缺少颜色字段", i+1)
-		}
-		
-		// 验证URL格式
-		if !urlRegex.MatchString(urlStr) {
-			return fmt.Errorf("第%d个API信息的URL格式不正确", i+1)
-		}
-		
-		// 验证URL可解析性
-		if _, err := url.Parse(urlStr); err != nil {
-			return fmt.Errorf("第%d个API信息的URL无法解析:%s", i+1, err.Error())
-		}
-		
-		// 验证字段长度
-		if len(urlStr) > 500 {
-			return fmt.Errorf("第%d个API信息的URL长度不能超过500字符", i+1)
-		}
-		
-		if len(route) > 100 {
-			return fmt.Errorf("第%d个API信息的线路描述长度不能超过100字符", i+1)
-		}
-		
-		if len(description) > 200 {
-			return fmt.Errorf("第%d个API信息的说明长度不能超过200字符", i+1)
-		}
-		
-		// 验证颜色值
-		if !validColors[color] {
-			return fmt.Errorf("第%d个API信息的颜色值不合法", i+1)
-		}
-		
-		// 检查并过滤危险字符(防止XSS)
-		dangerousChars := []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
-		for _, dangerous := range dangerousChars {
-			if strings.Contains(strings.ToLower(description), dangerous) {
-				return fmt.Errorf("第%d个API信息的说明包含不允许的内容", i+1)
-			}
-			if strings.Contains(strings.ToLower(route), dangerous) {
-				return fmt.Errorf("第%d个API信息的线路描述包含不允许的内容", i+1)
-			}
-		}
-	}
-	
-	return nil
-}
-
-// ValidateApiInfo 保持向后兼容的函数
-func ValidateApiInfo(apiInfoStr string) error {
-	return validateApiInfo(apiInfoStr)
-}
-
-// GetApiInfo 获取API信息列表
-func GetApiInfo() []map[string]interface{} {
-	// 从OptionMap中获取API信息,如果不存在则返回空数组
-	common.OptionMapRWMutex.RLock()
-	apiInfoStr, exists := common.OptionMap["ApiInfo"]
-	common.OptionMapRWMutex.RUnlock()
-	
-	if !exists || apiInfoStr == "" {
-		// 如果没有配置,返回空数组
-		return []map[string]interface{}{}
-	}
-	
-	// 解析存储的API信息
-	var apiInfo []map[string]interface{}
-	if err := json.Unmarshal([]byte(apiInfoStr), &apiInfo); err != nil {
-		// 如果解析失败,返回空数组
-		return []map[string]interface{}{}
-	}
-	
-	return apiInfo
-}
-
-// validateAnnouncements 验证系统公告格式
-func validateAnnouncements(announcementsStr string) error {
-	var announcementsList []map[string]interface{}
-	if err := json.Unmarshal([]byte(announcementsStr), &announcementsList); err != nil {
-		return fmt.Errorf("系统公告格式错误:%s", err.Error())
-	}
-	
-	// 验证数组长度
-	if len(announcementsList) > 100 {
-		return fmt.Errorf("系统公告数量不能超过100个")
-	}
-	
-	// 允许的类型值
-	validTypes := map[string]bool{
-		"default": true, "ongoing": true, "success": true, "warning": true, "error": true,
-	}
-	
-	for i, announcement := range announcementsList {
-		// 检查必填字段
-		content, ok := announcement["content"].(string)
-		if !ok || content == "" {
-			return fmt.Errorf("第%d个公告缺少内容字段", i+1)
-		}
-		
-		// 检查发布日期字段
-		publishDate, exists := announcement["publishDate"]
-		if !exists {
-			return fmt.Errorf("第%d个公告缺少发布日期字段", i+1)
-		}
-		
-		publishDateStr, ok := publishDate.(string)
-		if !ok || publishDateStr == "" {
-			return fmt.Errorf("第%d个公告的发布日期不能为空", i+1)
-		}
-		
-		// 验证ISO日期格式
-		if _, err := time.Parse(time.RFC3339, publishDateStr); err != nil {
-			return fmt.Errorf("第%d个公告的发布日期格式错误", i+1)
-		}
-		
-		// 验证可选字段
-		if announcementType, exists := announcement["type"]; exists {
-			if typeStr, ok := announcementType.(string); ok {
-				if !validTypes[typeStr] {
-					return fmt.Errorf("第%d个公告的类型值不合法", i+1)
-				}
-			}
-		}
-		
-		// 验证字段长度
-		if len(content) > 500 {
-			return fmt.Errorf("第%d个公告的内容长度不能超过500字符", i+1)
-		}
-		
-		if extra, exists := announcement["extra"]; exists {
-			if extraStr, ok := extra.(string); ok && len(extraStr) > 200 {
-				return fmt.Errorf("第%d个公告的说明长度不能超过200字符", i+1)
-			}
-		}
-		
-		// 检查并过滤危险字符(防止XSS)
-		dangerousChars := []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
-		for _, dangerous := range dangerousChars {
-			if strings.Contains(strings.ToLower(content), dangerous) {
-				return fmt.Errorf("第%d个公告的内容包含不允许的内容", i+1)
-			}
-		}
-	}
-	
-	return nil
-}
-
-// validateFAQ 验证常见问答格式
-func validateFAQ(faqStr string) error {
-	var faqList []map[string]interface{}
-	if err := json.Unmarshal([]byte(faqStr), &faqList); err != nil {
-		return fmt.Errorf("常见问答格式错误:%s", err.Error())
-	}
-	
-	// 验证数组长度
-	if len(faqList) > 100 {
-		return fmt.Errorf("常见问答数量不能超过100个")
-	}
-	
-	for i, faq := range faqList {
-		// 检查必填字段
-		title, ok := faq["title"].(string)
-		if !ok || title == "" {
-			return fmt.Errorf("第%d个问答缺少标题字段", i+1)
-		}
-		
-		content, ok := faq["content"].(string)
-		if !ok || content == "" {
-			return fmt.Errorf("第%d个问答缺少内容字段", i+1)
-		}
-		
-		// 验证字段长度
-		if len(title) > 200 {
-			return fmt.Errorf("第%d个问答的标题长度不能超过200字符", i+1)
-		}
-		
-		if len(content) > 1000 {
-			return fmt.Errorf("第%d个问答的内容长度不能超过1000字符", i+1)
-		}
-		
-		// 检查并过滤危险字符(防止XSS)
-		dangerousChars := []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
-		for _, dangerous := range dangerousChars {
-			if strings.Contains(strings.ToLower(title), dangerous) {
-				return fmt.Errorf("第%d个问答的标题包含不允许的内容", i+1)
-			}
-			if strings.Contains(strings.ToLower(content), dangerous) {
-				return fmt.Errorf("第%d个问答的内容包含不允许的内容", i+1)
-			}
-		}
-	}
-	
-	return nil
-}
-
-// GetAnnouncements 获取系统公告列表(返回最新的前20条)
-func GetAnnouncements() []map[string]interface{} {
-	common.OptionMapRWMutex.RLock()
-	announcementsStr, exists := common.OptionMap["Announcements"]
-	common.OptionMapRWMutex.RUnlock()
-	
-	if !exists || announcementsStr == "" {
-		return []map[string]interface{}{}
-	}
-	
-	var announcements []map[string]interface{}
-	if err := json.Unmarshal([]byte(announcementsStr), &announcements); err != nil {
-		return []map[string]interface{}{}
-	}
-	
-	// 按发布日期降序排序(最新的在前)
-	sort.Slice(announcements, func(i, j int) bool {
-		dateI, okI := announcements[i]["publishDate"].(string)
-		dateJ, okJ := announcements[j]["publishDate"].(string)
-		
-		if !okI || !okJ {
-			return false
-		}
-		
-		timeI, errI := time.Parse(time.RFC3339, dateI)
-		timeJ, errJ := time.Parse(time.RFC3339, dateJ)
-		
-		if errI != nil || errJ != nil {
-			return false
-		}
-		
-		return timeI.After(timeJ)
-	})
-	
-	// 限制返回前20条
-	if len(announcements) > 20 {
-		announcements = announcements[:20]
-	}
-	
-	return announcements
-}
-
-// GetFAQ 获取常见问答列表
-func GetFAQ() []map[string]interface{} {
-	common.OptionMapRWMutex.RLock()
-	faqStr, exists := common.OptionMap["FAQ"]
-	common.OptionMapRWMutex.RUnlock()
-	
-	if !exists || faqStr == "" {
-		return []map[string]interface{}{}
-	}
-	
-	var faq []map[string]interface{}
-	if err := json.Unmarshal([]byte(faqStr), &faq); err != nil {
-		return []map[string]interface{}{}
-	}
-	
-	return faq
-} 

+ 39 - 0
setting/console_setting/config.go

@@ -0,0 +1,39 @@
+package console_setting
+
+import "one-api/setting/config"
+
+type ConsoleSetting struct {
+    ApiInfo           string `json:"api_info"`           // 控制台 API 信息 (JSON 数组字符串)
+    UptimeKumaGroups  string `json:"uptime_kuma_groups"` // Uptime Kuma 分组配置 (JSON 数组字符串)
+    Announcements     string `json:"announcements"`      // 系统公告 (JSON 数组字符串)
+    FAQ               string `json:"faq"`                // 常见问题 (JSON 数组字符串)
+    ApiInfoEnabled        bool `json:"api_info_enabled"`        // 是否启用 API 信息面板
+    UptimeKumaEnabled     bool `json:"uptime_kuma_enabled"`     // 是否启用 Uptime Kuma 面板
+    AnnouncementsEnabled  bool `json:"announcements_enabled"`   // 是否启用系统公告面板
+    FAQEnabled            bool `json:"faq_enabled"`             // 是否启用常见问答面板
+}
+
+// 默认配置
+var defaultConsoleSetting = ConsoleSetting{
+    ApiInfo:          "",
+    UptimeKumaGroups: "",
+    Announcements:    "",
+    FAQ:              "",
+    ApiInfoEnabled:       true,
+    UptimeKumaEnabled:    true,
+    AnnouncementsEnabled: true,
+    FAQEnabled:           true,
+}
+
+// 全局实例
+var consoleSetting = defaultConsoleSetting
+
+func init() {
+    // 注册到全局配置管理器,键名为 console_setting
+    config.GlobalConfig.Register("console_setting", &consoleSetting)
+}
+
+// GetConsoleSetting 获取 ConsoleSetting 配置实例
+func GetConsoleSetting() *ConsoleSetting {
+    return &consoleSetting
+} 

+ 304 - 0
setting/console_setting/validation.go

@@ -0,0 +1,304 @@
+package console_setting
+
+import (
+    "encoding/json"
+    "fmt"
+    "net/url"
+    "regexp"
+    "strings"
+    "time"
+    "sort"
+)
+
+var (
+    urlRegex = regexp.MustCompile(`^https?://(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(?:\:[0-9]{1,5})?(?:/.*)?$`)
+    dangerousChars = []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
+    validColors = map[string]bool{
+        "blue": true, "green": true, "cyan": true, "purple": true, "pink": true,
+        "red": true, "orange": true, "amber": true, "yellow": true, "lime": true,
+        "light-green": true, "teal": true, "light-blue": true, "indigo": true,
+        "violet": true, "grey": true,
+    }
+    slugRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
+)
+
+func parseJSONArray(jsonStr string, typeName string) ([]map[string]interface{}, error) {
+    var list []map[string]interface{}
+    if err := json.Unmarshal([]byte(jsonStr), &list); err != nil {
+        return nil, fmt.Errorf("%s格式错误:%s", typeName, err.Error())
+    }
+    return list, nil
+}
+
+func validateURL(urlStr string, index int, itemType string) error {
+    if !urlRegex.MatchString(urlStr) {
+        return fmt.Errorf("第%d个%s的URL格式不正确", index, itemType)
+    }
+    if _, err := url.Parse(urlStr); err != nil {
+        return fmt.Errorf("第%d个%s的URL无法解析:%s", index, itemType, err.Error())
+    }
+    return nil
+}
+
+func checkDangerousContent(content string, index int, itemType string) error {
+    lower := strings.ToLower(content)
+    for _, d := range dangerousChars {
+        if strings.Contains(lower, d) {
+            return fmt.Errorf("第%d个%s包含不允许的内容", index, itemType)
+        }
+    }
+    return nil
+}
+
+func getJSONList(jsonStr string) []map[string]interface{} {
+    if jsonStr == "" {
+        return []map[string]interface{}{}
+    }
+    var list []map[string]interface{}
+    json.Unmarshal([]byte(jsonStr), &list)
+    return list
+}
+
+func ValidateConsoleSettings(settingsStr string, settingType string) error {
+    if settingsStr == "" {
+        return nil
+    }
+
+    switch settingType {
+    case "ApiInfo":
+        return validateApiInfo(settingsStr)
+    case "Announcements":
+        return validateAnnouncements(settingsStr)
+    case "FAQ":
+        return validateFAQ(settingsStr)
+    case "UptimeKumaGroups":
+        return validateUptimeKumaGroups(settingsStr)
+    default:
+        return fmt.Errorf("未知的设置类型:%s", settingType)
+    }
+}
+
+func validateApiInfo(apiInfoStr string) error {
+    apiInfoList, err := parseJSONArray(apiInfoStr, "API信息")
+    if err != nil {
+        return err
+    }
+
+    if len(apiInfoList) > 50 {
+        return fmt.Errorf("API信息数量不能超过50个")
+    }
+
+    for i, apiInfo := range apiInfoList {
+        urlStr, ok := apiInfo["url"].(string)
+        if !ok || urlStr == "" {
+            return fmt.Errorf("第%d个API信息缺少URL字段", i+1)
+        }
+        route, ok := apiInfo["route"].(string)
+        if !ok || route == "" {
+            return fmt.Errorf("第%d个API信息缺少线路描述字段", i+1)
+        }
+        description, ok := apiInfo["description"].(string)
+        if !ok || description == "" {
+            return fmt.Errorf("第%d个API信息缺少说明字段", i+1)
+        }
+        color, ok := apiInfo["color"].(string)
+        if !ok || color == "" {
+            return fmt.Errorf("第%d个API信息缺少颜色字段", i+1)
+        }
+
+        if err := validateURL(urlStr, i+1, "API信息"); err != nil {
+            return err
+        }
+
+        if len(urlStr) > 500 {
+            return fmt.Errorf("第%d个API信息的URL长度不能超过500字符", i+1)
+        }
+        if len(route) > 100 {
+            return fmt.Errorf("第%d个API信息的线路描述长度不能超过100字符", i+1)
+        }
+        if len(description) > 200 {
+            return fmt.Errorf("第%d个API信息的说明长度不能超过200字符", i+1)
+        }
+
+        if !validColors[color] {
+            return fmt.Errorf("第%d个API信息的颜色值不合法", i+1)
+        }
+
+        if err := checkDangerousContent(description, i+1, "API信息"); err != nil {
+            return err
+        }
+        if err := checkDangerousContent(route, i+1, "API信息"); err != nil {
+            return err
+        }
+    }
+    return nil
+}
+
+func GetApiInfo() []map[string]interface{} {
+    return getJSONList(GetConsoleSetting().ApiInfo)
+}
+
+func validateAnnouncements(announcementsStr string) error {
+    list, err := parseJSONArray(announcementsStr, "系统公告")
+    if err != nil {
+        return err
+    }
+    if len(list) > 100 {
+        return fmt.Errorf("系统公告数量不能超过100个")
+    }
+    validTypes := map[string]bool{
+        "default": true, "ongoing": true, "success": true, "warning": true, "error": true,
+    }
+    for i, ann := range list {
+        content, ok := ann["content"].(string)
+        if !ok || content == "" {
+            return fmt.Errorf("第%d个公告缺少内容字段", i+1)
+        }
+        publishDateAny, exists := ann["publishDate"]
+        if !exists {
+            return fmt.Errorf("第%d个公告缺少发布日期字段", i+1)
+        }
+        publishDateStr, ok := publishDateAny.(string)
+        if !ok || publishDateStr == "" {
+            return fmt.Errorf("第%d个公告的发布日期不能为空", i+1)
+        }
+        if _, err := time.Parse(time.RFC3339, publishDateStr); err != nil {
+            return fmt.Errorf("第%d个公告的发布日期格式错误", i+1)
+        }
+        if t, exists := ann["type"]; exists {
+            if typeStr, ok := t.(string); ok {
+                if !validTypes[typeStr] {
+                    return fmt.Errorf("第%d个公告的类型值不合法", i+1)
+                }
+            }
+        }
+        if len(content) > 500 {
+            return fmt.Errorf("第%d个公告的内容长度不能超过500字符", i+1)
+        }
+        if extra, exists := ann["extra"]; exists {
+            if extraStr, ok := extra.(string); ok && len(extraStr) > 200 {
+                return fmt.Errorf("第%d个公告的说明长度不能超过200字符", i+1)
+            }
+        }
+    }
+    return nil
+}
+
+func validateFAQ(faqStr string) error {
+    list, err := parseJSONArray(faqStr, "FAQ信息")
+    if err != nil {
+        return err
+    }
+    if len(list) > 100 {
+        return fmt.Errorf("FAQ数量不能超过100个")
+    }
+    for i, faq := range list {
+        question, ok := faq["question"].(string)
+        if !ok || question == "" {
+            return fmt.Errorf("第%d个FAQ缺少问题字段", i+1)
+        }
+        answer, ok := faq["answer"].(string)
+        if !ok || answer == "" {
+            return fmt.Errorf("第%d个FAQ缺少答案字段", i+1)
+        }
+        if len(question) > 200 {
+            return fmt.Errorf("第%d个FAQ的问题长度不能超过200字符", i+1)
+        }
+        if len(answer) > 1000 {
+            return fmt.Errorf("第%d个FAQ的答案长度不能超过1000字符", i+1)
+        }
+    }
+    return nil
+}
+
+func getPublishTime(item map[string]interface{}) time.Time {
+    if v, ok := item["publishDate"]; ok {
+        if s, ok2 := v.(string); ok2 {
+            if t, err := time.Parse(time.RFC3339, s); err == nil {
+                return t
+            }
+        }
+    }
+    return time.Time{}
+}
+
+func GetAnnouncements() []map[string]interface{} {
+    list := getJSONList(GetConsoleSetting().Announcements)
+    sort.SliceStable(list, func(i, j int) bool {
+        return getPublishTime(list[i]).After(getPublishTime(list[j]))
+    })
+    return list
+}
+
+func GetFAQ() []map[string]interface{} {
+    return getJSONList(GetConsoleSetting().FAQ)
+}
+
+func validateUptimeKumaGroups(groupsStr string) error {
+    groups, err := parseJSONArray(groupsStr, "Uptime Kuma分组配置")
+    if err != nil {
+        return err
+    }
+
+    if len(groups) > 20 {
+        return fmt.Errorf("Uptime Kuma分组数量不能超过20个")
+    }
+
+    nameSet := make(map[string]bool)
+
+    for i, group := range groups {
+        categoryName, ok := group["categoryName"].(string)
+        if !ok || categoryName == "" {
+            return fmt.Errorf("第%d个分组缺少分类名称字段", i+1)
+        }
+        if nameSet[categoryName] {
+            return fmt.Errorf("第%d个分组的分类名称与其他分组重复", i+1)
+        }
+        nameSet[categoryName] = true
+        urlStr, ok := group["url"].(string)
+        if !ok || urlStr == "" {
+            return fmt.Errorf("第%d个分组缺少URL字段", i+1)
+        }
+        slug, ok := group["slug"].(string)
+        if !ok || slug == "" {
+            return fmt.Errorf("第%d个分组缺少Slug字段", i+1)
+        }
+        description, ok := group["description"].(string)
+        if !ok {
+            description = ""
+        }
+
+        if err := validateURL(urlStr, i+1, "分组"); err != nil {
+            return err
+        }
+
+        if len(categoryName) > 50 {
+            return fmt.Errorf("第%d个分组的分类名称长度不能超过50字符", i+1)
+        }
+        if len(urlStr) > 500 {
+            return fmt.Errorf("第%d个分组的URL长度不能超过500字符", i+1)
+        }
+        if len(slug) > 100 {
+            return fmt.Errorf("第%d个分组的Slug长度不能超过100字符", i+1)
+        }
+        if len(description) > 200 {
+            return fmt.Errorf("第%d个分组的描述长度不能超过200字符", i+1)
+        }
+
+        if !slugRegex.MatchString(slug) {
+            return fmt.Errorf("第%d个分组的Slug只能包含字母、数字、下划线和连字符", i+1)
+        }
+
+        if err := checkDangerousContent(description, i+1, "分组"); err != nil {
+            return err
+        }
+        if err := checkDangerousContent(categoryName, i+1, "分组"); err != nil {
+            return err
+        }
+    }
+    return nil
+}
+
+func GetUptimeKumaGroups() []map[string]interface{} {
+    return getJSONList(GetConsoleSetting().UptimeKumaGroups)
+} 

+ 48 - 5
setting/group_ratio.go

@@ -14,10 +14,19 @@ var groupRatio = map[string]float64{
 }
 var groupRatioMutex sync.RWMutex
 
+var (
+	GroupGroupRatio = map[string]map[string]float64{
+		"vip": {
+			"edit_this": 0.9,
+		},
+	}
+	groupGroupRatioMutex sync.RWMutex
+)
+
 func GetGroupRatioCopy() map[string]float64 {
 	groupRatioMutex.RLock()
 	defer groupRatioMutex.RUnlock()
-	
+
 	groupRatioCopy := make(map[string]float64)
 	for k, v := range groupRatio {
 		groupRatioCopy[k] = v
@@ -28,7 +37,7 @@ func GetGroupRatioCopy() map[string]float64 {
 func ContainsGroupRatio(name string) bool {
 	groupRatioMutex.RLock()
 	defer groupRatioMutex.RUnlock()
-	
+
 	_, ok := groupRatio[name]
 	return ok
 }
@@ -36,7 +45,7 @@ func ContainsGroupRatio(name string) bool {
 func GroupRatio2JSONString() string {
 	groupRatioMutex.RLock()
 	defer groupRatioMutex.RUnlock()
-	
+
 	jsonBytes, err := json.Marshal(groupRatio)
 	if err != nil {
 		common.SysError("error marshalling model ratio: " + err.Error())
@@ -47,7 +56,7 @@ func GroupRatio2JSONString() string {
 func UpdateGroupRatioByJSONString(jsonStr string) error {
 	groupRatioMutex.Lock()
 	defer groupRatioMutex.Unlock()
-	
+
 	groupRatio = make(map[string]float64)
 	return json.Unmarshal([]byte(jsonStr), &groupRatio)
 }
@@ -55,7 +64,7 @@ func UpdateGroupRatioByJSONString(jsonStr string) error {
 func GetGroupRatio(name string) float64 {
 	groupRatioMutex.RLock()
 	defer groupRatioMutex.RUnlock()
-	
+
 	ratio, ok := groupRatio[name]
 	if !ok {
 		common.SysError("group ratio not found: " + name)
@@ -64,6 +73,40 @@ func GetGroupRatio(name string) float64 {
 	return ratio
 }
 
+func GetGroupGroupRatio(group, name string) (float64, bool) {
+	groupGroupRatioMutex.RLock()
+	defer groupGroupRatioMutex.RUnlock()
+
+	gp, ok := GroupGroupRatio[group]
+	if !ok {
+		return -1, false
+	}
+	ratio, ok := gp[name]
+	if !ok {
+		return -1, false
+	}
+	return ratio, true
+}
+
+func GroupGroupRatio2JSONString() string {
+	groupGroupRatioMutex.RLock()
+	defer groupGroupRatioMutex.RUnlock()
+
+	jsonBytes, err := json.Marshal(GroupGroupRatio)
+	if err != nil {
+		common.SysError("error marshalling group-group ratio: " + err.Error())
+	}
+	return string(jsonBytes)
+}
+
+func UpdateGroupGroupRatioByJSONString(jsonStr string) error {
+	groupGroupRatioMutex.Lock()
+	defer groupGroupRatioMutex.Unlock()
+
+	GroupGroupRatio = make(map[string]map[string]float64)
+	return json.Unmarshal([]byte(jsonStr), &GroupGroupRatio)
+}
+
 func CheckGroupRatio(jsonStr string) error {
 	checkGroupRatio := make(map[string]float64)
 	err := json.Unmarshal([]byte(jsonStr), &checkGroupRatio)

+ 17 - 2
setting/operation_setting/model-ratio.go

@@ -142,6 +142,11 @@ var defaultModelRatio = map[string]float64{
 	"gemini-2.5-flash-preview-04-17":            0.075,
 	"gemini-2.5-flash-preview-04-17-thinking":   0.075,
 	"gemini-2.5-flash-preview-04-17-nothinking": 0.075,
+	"gemini-2.5-flash-preview-05-20":            0.075,
+	"gemini-2.5-flash-preview-05-20-thinking":   0.075,
+	"gemini-2.5-flash-preview-05-20-nothinking": 0.075,
+	"gemini-2.5-flash-thinking-*":               0.075, // 用于为后续所有2.5 flash thinking budget 模型设置默认倍率
+	"gemini-2.5-pro-thinking-*":                 0.625, // 用于为后续所有2.5 pro thinking budget 模型设置默认倍率
 	"text-embedding-004":                        0.001,
 	"chatglm_turbo":                             0.3572,     // ¥0.005 / 1k tokens
 	"chatglm_pro":                               0.7143,     // ¥0.01 / 1k tokens
@@ -342,10 +347,20 @@ func UpdateModelRatioByJSONString(jsonStr string) error {
 	return json.Unmarshal([]byte(jsonStr), &modelRatioMap)
 }
 
+// 处理带有思考预算的模型名称,方便统一定价
+func handleThinkingBudgetModel(name, prefix, wildcard string) string {
+	if strings.HasPrefix(name, prefix) && strings.Contains(name, "-thinking-") {
+		return wildcard
+	}
+	return name
+}
+
 func GetModelRatio(name string) (float64, bool) {
 	modelRatioMapMutex.RLock()
 	defer modelRatioMapMutex.RUnlock()
 
+	name = handleThinkingBudgetModel(name, "gemini-2.5-flash", "gemini-2.5-flash-thinking-*")
+	name = handleThinkingBudgetModel(name, "gemini-2.5-pro", "gemini-2.5-pro-thinking-*")
 	if strings.HasPrefix(name, "gpt-4-gizmo") {
 		name = "gpt-4-gizmo-*"
 	}
@@ -470,9 +485,9 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) {
 			return 4, true
 		} else if strings.HasPrefix(name, "gemini-2.0") {
 			return 4, true
-		} else if strings.HasPrefix(name, "gemini-2.5-pro-preview") {
+		} else if strings.HasPrefix(name, "gemini-2.5-pro") { // 移除preview来增加兼容性,这里假设正式版的倍率和preview一致
 			return 8, true
-		} else if strings.HasPrefix(name, "gemini-2.5-flash-preview") {
+		} else if strings.HasPrefix(name, "gemini-2.5-flash") { // 同上
 			if strings.HasSuffix(name, "-nothinking") {
 				return 4, false
 			} else {

+ 7 - 0
setting/user_usable_group.go

@@ -50,3 +50,10 @@ func GroupInUserUsableGroups(groupName string) bool {
 	_, ok := userUsableGroups[groupName]
 	return ok
 }
+
+func GetUsableGroupDescription(groupName string) string {
+	if desc, ok := userUsableGroups[groupName]; ok {
+		return desc
+	}
+	return groupName
+}

+ 0 - 21
web/README.md

@@ -1,21 +0,0 @@
-# React Template
-
-## Basic Usages
-
-```shell
-# Runs the app in the development mode
-npm start
-
-# Builds the app for production to the `build` folder
-npm run build
-```
-
-If you want to change the default server, please set `REACT_APP_SERVER` environment variables before build,
-for example: `REACT_APP_SERVER=http://your.domain.com`.
-
-Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled.
-
-## Reference
-
-1. https://github.com/OIerDb-ng/OIerDb
-2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example

+ 0 - 2
web/package.json

@@ -37,8 +37,6 @@
     "remark-breaks": "^4.0.0",
     "remark-gfm": "^4.0.1",
     "remark-math": "^6.0.0",
-    "semantic-ui-offline": "^2.5.0",
-    "semantic-ui-react": "^2.1.3",
     "sse.js": "^2.6.0",
     "unist-util-visit": "^5.0.0",
     "use-debounce": "^10.0.4"

+ 0 - 5584
web/pnpm-lock.yaml

@@ -1,5584 +0,0 @@
-lockfileVersion: '9.0'
-
-settings:
-  autoInstallPeers: true
-  excludeLinksFromLockfile: false
-
-importers:
-  .:
-    dependencies:
-      '@douyinfe/semi-icons':
-        specifier: ^2.63.1
-        version: 2.77.1(react@18.3.1)
-      '@douyinfe/semi-ui':
-        specifier: ^2.69.1
-        version: 2.77.1(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@visactor/react-vchart':
-        specifier: ~1.8.8
-        version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@visactor/vchart':
-        specifier: ~1.8.8
-        version: 1.8.11
-      '@visactor/vchart-semi-theme':
-        specifier: ~1.8.8
-        version: 1.8.8(@visactor/vchart@1.8.11)
-      axios:
-        specifier: ^0.27.2
-        version: 0.27.2
-      dayjs:
-        specifier: ^1.11.11
-        version: 1.11.13
-      history:
-        specifier: ^5.3.0
-        version: 5.3.0
-      i18next:
-        specifier: ^23.16.8
-        version: 23.16.8
-      i18next-browser-languagedetector:
-        specifier: ^7.2.0
-        version: 7.2.2
-      marked:
-        specifier: ^4.1.1
-        version: 4.3.0
-      react:
-        specifier: ^18.2.0
-        version: 18.3.1
-      react-dom:
-        specifier: ^18.2.0
-        version: 18.3.1(react@18.3.1)
-      react-dropzone:
-        specifier: ^14.2.3
-        version: 14.3.8(react@18.3.1)
-      react-fireworks:
-        specifier: ^1.0.4
-        version: 1.0.4
-      react-i18next:
-        specifier: ^13.0.0
-        version: 13.5.0(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      react-router-dom:
-        specifier: ^6.3.0
-        version: 6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      react-telegram-login:
-        specifier: ^1.1.2
-        version: 1.1.2(react@18.3.1)
-      react-toastify:
-        specifier: ^9.0.8
-        version: 9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      react-turnstile:
-        specifier: ^1.0.5
-        version: 1.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      semantic-ui-offline:
-        specifier: ^2.5.0
-        version: 2.5.0
-      semantic-ui-react:
-        specifier: ^2.1.3
-        version: 2.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      sse:
-        specifier: github:mpetazzoni/sse.js
-        version: sse.js@https://codeload.github.com/mpetazzoni/sse.js/tar.gz/39b9b82aae95fd58d9d08b487845fe230f4b14e6
-    devDependencies:
-      '@so1ve/prettier-config':
-        specifier: ^3.1.0
-        version: 3.1.0(prettier@3.5.3)
-      '@vitejs/plugin-react':
-        specifier: ^4.2.1
-        version: 4.3.4(vite@5.4.16)
-      prettier:
-        specifier: ^3.0.0
-        version: 3.5.3
-      typescript:
-        specifier: 4.4.2
-        version: 4.4.2
-      vite:
-        specifier: ^5.2.0
-        version: 5.4.16
-
-packages:
-  '@ampproject/remapping@2.3.0':
-    resolution:
-      {
-        integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==,
-      }
-    engines: { node: '>=6.0.0' }
-
-  '@astrojs/compiler@2.11.0':
-    resolution:
-      {
-        integrity: sha512-zZOO7i+JhojO8qmlyR/URui6LyfHJY6m+L9nwyX5GiKD78YoRaZ5tzz6X0fkl+5bD3uwlDHayf6Oe8Fu36RKNg==,
-      }
-
-  '@babel/code-frame@7.26.2':
-    resolution:
-      {
-        integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/compat-data@7.26.8':
-    resolution:
-      {
-        integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/core@7.26.10':
-    resolution:
-      {
-        integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/generator@7.27.0':
-    resolution:
-      {
-        integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/helper-compilation-targets@7.27.0':
-    resolution:
-      {
-        integrity: sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/helper-module-imports@7.25.9':
-    resolution:
-      {
-        integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/helper-module-transforms@7.26.0':
-    resolution:
-      {
-        integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==,
-      }
-    engines: { node: '>=6.9.0' }
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/helper-plugin-utils@7.26.5':
-    resolution:
-      {
-        integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/helper-string-parser@7.25.9':
-    resolution:
-      {
-        integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/helper-validator-identifier@7.25.9':
-    resolution:
-      {
-        integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/helper-validator-option@7.25.9':
-    resolution:
-      {
-        integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/helpers@7.27.0':
-    resolution:
-      {
-        integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/parser@7.27.0':
-    resolution:
-      {
-        integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==,
-      }
-    engines: { node: '>=6.0.0' }
-    hasBin: true
-
-  '@babel/plugin-transform-react-jsx-self@7.25.9':
-    resolution:
-      {
-        integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==,
-      }
-    engines: { node: '>=6.9.0' }
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-react-jsx-source@7.25.9':
-    resolution:
-      {
-        integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==,
-      }
-    engines: { node: '>=6.9.0' }
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/runtime@7.27.0':
-    resolution:
-      {
-        integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/template@7.27.0':
-    resolution:
-      {
-        integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/traverse@7.27.0':
-    resolution:
-      {
-        integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@babel/types@7.27.0':
-    resolution:
-      {
-        integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  '@dnd-kit/accessibility@3.1.1':
-    resolution:
-      {
-        integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==,
-      }
-    peerDependencies:
-      react: '>=16.8.0'
-
-  '@dnd-kit/core@6.3.1':
-    resolution:
-      {
-        integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==,
-      }
-    peerDependencies:
-      react: '>=16.8.0'
-      react-dom: '>=16.8.0'
-
-  '@dnd-kit/sortable@7.0.2':
-    resolution:
-      {
-        integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==,
-      }
-    peerDependencies:
-      '@dnd-kit/core': ^6.0.7
-      react: '>=16.8.0'
-
-  '@dnd-kit/utilities@3.2.2':
-    resolution:
-      {
-        integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==,
-      }
-    peerDependencies:
-      react: '>=16.8.0'
-
-  '@douyinfe/semi-animation-react@2.77.1':
-    resolution:
-      {
-        integrity: sha512-imELR02pufgGFkZURfTd9oBUtZPYhHvXv9WsYoRvEoBM9U7yzxrR6Fb/Lc3TH+WHVJ2oZHH2S0APS5t1MceEOw==,
-      }
-
-  '@douyinfe/semi-animation-styled@2.77.1':
-    resolution:
-      {
-        integrity: sha512-FBRroqVJroel1CXmBgV58ulZHG2xUVInJF7k0FAag54noKKaToEobSxRjiTJ6JHne3ZDU1M6sBqpbzYJElFnPQ==,
-      }
-
-  '@douyinfe/semi-animation@2.77.1':
-    resolution:
-      {
-        integrity: sha512-Q1D7whvQe0D+mPov8hXeH/e1uR/iBhpGGcW1LCTL2pSVMEZEYGJLf2KeXTTiCIgRVWm0PRH3Sux7auJ64zg7vw==,
-      }
-
-  '@douyinfe/semi-foundation@2.77.1':
-    resolution:
-      {
-        integrity: sha512-DAXRy8ryLNzbKAiTAv+RrivGCoMU0asv2cO7PNV5aBq0ICB8XXn97FHyZo6Wb5NpqpyMhOaOr8Ro1bfpd0FeaA==,
-      }
-
-  '@douyinfe/semi-icons@2.77.1':
-    resolution:
-      {
-        integrity: sha512-IbGqYzbjzCoSd+//HlO/Gn1c3XmbulQwGys+JgDfQhYIbPeGyhQfLk56Q7ku3vJGC8BGy7dUmR9MbeTf1UQGtw==,
-      }
-    peerDependencies:
-      react: '>=16.0.0'
-
-  '@douyinfe/semi-illustrations@2.77.1':
-    resolution:
-      {
-        integrity: sha512-FlESLOPaY0SadiSIFcP4gqJUk+CYkd4rHK6YP9bfjmU26v7h1S02H7pGLLV1lS0WnY4j0ad4zqRV9tbXFvba9g==,
-      }
-    peerDependencies:
-      react: '>=16.0.0'
-
-  '@douyinfe/semi-json-viewer-core@2.77.1':
-    resolution:
-      {
-        integrity: sha512-LOW+7ga2OzFIL9pGKftwHfl1kKLTV3x6Cs857iyvq9GIF/GHbAboiHcKUy2OZIHfy66zvP+Focs+yhfZG7IcZw==,
-      }
-
-  '@douyinfe/semi-theme-default@2.77.1':
-    resolution:
-      {
-        integrity: sha512-Rug75C7jjSqmCP2L2tBI0K4dnXuo4GardzwSzdSjxDkiaIXwOwR5KE0K1FRbKWkQ7xmxbyRu4S6Pff+CDEJ/lA==,
-      }
-
-  '@douyinfe/semi-ui@2.77.1':
-    resolution:
-      {
-        integrity: sha512-eIy7kr9OleCwlNRby3VICSGScHM23Zt2u7TJpID68qN3WrfQowGaB4wQ/0k5bvpLzv463HQnVWFk5aak+v46yw==,
-      }
-    peerDependencies:
-      react: '>=16.0.0'
-      react-dom: '>=16.0.0'
-
-  '@esbuild/aix-ppc64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==,
-      }
-    engines: { node: '>=12' }
-    cpu: [ppc64]
-    os: [aix]
-
-  '@esbuild/android-arm64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==,
-      }
-    engines: { node: '>=12' }
-    cpu: [arm64]
-    os: [android]
-
-  '@esbuild/android-arm@0.21.5':
-    resolution:
-      {
-        integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==,
-      }
-    engines: { node: '>=12' }
-    cpu: [arm]
-    os: [android]
-
-  '@esbuild/android-x64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==,
-      }
-    engines: { node: '>=12' }
-    cpu: [x64]
-    os: [android]
-
-  '@esbuild/darwin-arm64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==,
-      }
-    engines: { node: '>=12' }
-    cpu: [arm64]
-    os: [darwin]
-
-  '@esbuild/darwin-x64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==,
-      }
-    engines: { node: '>=12' }
-    cpu: [x64]
-    os: [darwin]
-
-  '@esbuild/freebsd-arm64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==,
-      }
-    engines: { node: '>=12' }
-    cpu: [arm64]
-    os: [freebsd]
-
-  '@esbuild/freebsd-x64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==,
-      }
-    engines: { node: '>=12' }
-    cpu: [x64]
-    os: [freebsd]
-
-  '@esbuild/linux-arm64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==,
-      }
-    engines: { node: '>=12' }
-    cpu: [arm64]
-    os: [linux]
-
-  '@esbuild/linux-arm@0.21.5':
-    resolution:
-      {
-        integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==,
-      }
-    engines: { node: '>=12' }
-    cpu: [arm]
-    os: [linux]
-
-  '@esbuild/linux-ia32@0.21.5':
-    resolution:
-      {
-        integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==,
-      }
-    engines: { node: '>=12' }
-    cpu: [ia32]
-    os: [linux]
-
-  '@esbuild/linux-loong64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==,
-      }
-    engines: { node: '>=12' }
-    cpu: [loong64]
-    os: [linux]
-
-  '@esbuild/linux-mips64el@0.21.5':
-    resolution:
-      {
-        integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==,
-      }
-    engines: { node: '>=12' }
-    cpu: [mips64el]
-    os: [linux]
-
-  '@esbuild/linux-ppc64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==,
-      }
-    engines: { node: '>=12' }
-    cpu: [ppc64]
-    os: [linux]
-
-  '@esbuild/linux-riscv64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==,
-      }
-    engines: { node: '>=12' }
-    cpu: [riscv64]
-    os: [linux]
-
-  '@esbuild/linux-s390x@0.21.5':
-    resolution:
-      {
-        integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==,
-      }
-    engines: { node: '>=12' }
-    cpu: [s390x]
-    os: [linux]
-
-  '@esbuild/linux-x64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==,
-      }
-    engines: { node: '>=12' }
-    cpu: [x64]
-    os: [linux]
-
-  '@esbuild/netbsd-x64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==,
-      }
-    engines: { node: '>=12' }
-    cpu: [x64]
-    os: [netbsd]
-
-  '@esbuild/openbsd-x64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==,
-      }
-    engines: { node: '>=12' }
-    cpu: [x64]
-    os: [openbsd]
-
-  '@esbuild/sunos-x64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==,
-      }
-    engines: { node: '>=12' }
-    cpu: [x64]
-    os: [sunos]
-
-  '@esbuild/win32-arm64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==,
-      }
-    engines: { node: '>=12' }
-    cpu: [arm64]
-    os: [win32]
-
-  '@esbuild/win32-ia32@0.21.5':
-    resolution:
-      {
-        integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==,
-      }
-    engines: { node: '>=12' }
-    cpu: [ia32]
-    os: [win32]
-
-  '@esbuild/win32-x64@0.21.5':
-    resolution:
-      {
-        integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==,
-      }
-    engines: { node: '>=12' }
-    cpu: [x64]
-    os: [win32]
-
-  '@fluentui/react-component-event-listener@0.63.1':
-    resolution:
-      {
-        integrity: sha512-gSMdOh6tI3IJKZFqxfQwbTpskpME0CvxdxGM2tdglmf6ZPVDi0L4+KKIm+2dN8nzb8Ya1A8ZT+Ddq0KmZtwVQg==,
-      }
-    peerDependencies:
-      react: ^16.8.0 || ^17 || ^18
-      react-dom: ^16.8.0 || ^17 || ^18
-
-  '@fluentui/react-component-ref@0.63.1':
-    resolution:
-      {
-        integrity: sha512-8MkXX4+R3i80msdbD4rFpEB4WWq2UDvGwG386g3ckIWbekdvN9z2kWAd9OXhRGqB7QeOsoAGWocp6gAMCivRlw==,
-      }
-    peerDependencies:
-      react: ^16.8.0 || ^17 || ^18
-      react-dom: ^16.8.0 || ^17 || ^18
-
-  '@jridgewell/gen-mapping@0.3.8':
-    resolution:
-      {
-        integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==,
-      }
-    engines: { node: '>=6.0.0' }
-
-  '@jridgewell/resolve-uri@3.1.2':
-    resolution:
-      {
-        integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==,
-      }
-    engines: { node: '>=6.0.0' }
-
-  '@jridgewell/set-array@1.2.1':
-    resolution:
-      {
-        integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==,
-      }
-    engines: { node: '>=6.0.0' }
-
-  '@jridgewell/sourcemap-codec@1.5.0':
-    resolution:
-      {
-        integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==,
-      }
-
-  '@jridgewell/trace-mapping@0.3.25':
-    resolution:
-      {
-        integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==,
-      }
-
-  '@mdx-js/mdx@3.1.0':
-    resolution:
-      {
-        integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==,
-      }
-
-  '@popperjs/core@2.11.8':
-    resolution:
-      {
-        integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==,
-      }
-
-  '@remix-run/router@1.23.0':
-    resolution:
-      {
-        integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==,
-      }
-    engines: { node: '>=14.0.0' }
-
-  '@resvg/resvg-js-android-arm-eabi@2.4.1':
-    resolution:
-      {
-        integrity: sha512-AA6f7hS0FAPpvQMhBCf6f1oD1LdlqNXKCxAAPpKh6tR11kqV0YIB9zOlIYgITM14mq2YooLFl6XIbbvmY+jwUw==,
-      }
-    engines: { node: '>= 10' }
-    cpu: [arm]
-    os: [android]
-
-  '@resvg/resvg-js-android-arm64@2.4.1':
-    resolution:
-      {
-        integrity: sha512-/QleoRdPfsEuH9jUjilYcDtKK/BkmWcK+1LXM8L2nsnf/CI8EnFyv7ZzCj4xAIvZGAy9dTYr/5NZBcTwxG2HQg==,
-      }
-    engines: { node: '>= 10' }
-    cpu: [arm64]
-    os: [android]
-
-  '@resvg/resvg-js-darwin-arm64@2.4.1':
-    resolution:
-      {
-        integrity: sha512-U1oMNhea+kAXgiEXgzo7EbFGCD1Edq5aSlQoe6LMly6UjHzgx2W3N5kEXCwU/CgN5FiQhZr7PlSJSlcr7mdhfg==,
-      }
-    engines: { node: '>= 10' }
-    cpu: [arm64]
-    os: [darwin]
-
-  '@resvg/resvg-js-darwin-x64@2.4.1':
-    resolution:
-      {
-        integrity: sha512-avyVh6DpebBfHHtTQTZYSr6NG1Ur6TEilk1+H0n7V+g4F7x7WPOo8zL00ZhQCeRQ5H4f8WXNWIEKL8fwqcOkYw==,
-      }
-    engines: { node: '>= 10' }
-    cpu: [x64]
-    os: [darwin]
-
-  '@resvg/resvg-js-linux-arm-gnueabihf@2.4.1':
-    resolution:
-      {
-        integrity: sha512-isY/mdKoBWH4VB5v621co+8l101jxxYjuTkwOLsbW+5RK9EbLciPlCB02M99ThAHzI2MYxIUjXNmNgOW8btXvw==,
-      }
-    engines: { node: '>= 10' }
-    cpu: [arm]
-    os: [linux]
-
-  '@resvg/resvg-js-linux-arm64-gnu@2.4.1':
-    resolution:
-      {
-        integrity: sha512-uY5voSCrFI8TH95vIYBm5blpkOtltLxLRODyhKJhGfskOI7XkRw5/t1u0sWAGYD8rRSNX+CA+np86otKjubrNg==,
-      }
-    engines: { node: '>= 10' }
-    cpu: [arm64]
-    os: [linux]
-
-  '@resvg/resvg-js-linux-arm64-musl@2.4.1':
-    resolution:
-      {
-        integrity: sha512-6mT0+JBCsermKMdi/O2mMk3m7SqOjwi9TKAwSngRZ/nQoL3Z0Z5zV+572ztgbWr0GODB422uD8e9R9zzz38dRQ==,
-      }
-    engines: { node: '>= 10' }
-    cpu: [arm64]
-    os: [linux]
-
-  '@resvg/resvg-js-linux-x64-gnu@2.4.1':
-    resolution:
-      {
-        integrity: sha512-60KnrscLj6VGhkYOJEmmzPlqqfcw1keDh6U+vMcNDjPhV3B5vRSkpP/D/a8sfokyeh4VEacPSYkWGezvzS2/mg==,
-      }
-    engines: { node: '>= 10' }
-    cpu: [x64]
-    os: [linux]
-
-  '@resvg/resvg-js-linux-x64-musl@2.4.1':
-    resolution:
-      {
-        integrity: sha512-0AMyZSICC1D7ge115cOZQW8Pcad6PjWuZkBFF3FJuSxC6Dgok0MQnLTs2MfMdKBlAcwO9dXsf3bv9tJZj8pATA==,
-      }
-    engines: { node: '>= 10' }
-    cpu: [x64]
-    os: [linux]
-
-  '@resvg/resvg-js-win32-arm64-msvc@2.4.1':
-    resolution:
-      {
-        integrity: sha512-76XDFOFSa3d0QotmcNyChh2xHwk+JTFiEQBVxMlHpHMeq7hNrQJ1IpE1zcHSQvrckvkdfLboKRrlGB86B10Qjw==,
-      }
-    engines: { node: '>= 10' }
-    cpu: [arm64]
-    os: [win32]
-
-  '@resvg/resvg-js-win32-ia32-msvc@2.4.1':
-    resolution:
-      {
-        integrity: sha512-odyVFGrEWZIzzJ89KdaFtiYWaIJh9hJRW/frcEcG3agJ464VXkN/2oEVF5ulD+5mpGlug9qJg7htzHcKxDN8sg==,
-      }
-    engines: { node: '>= 10' }
-    cpu: [ia32]
-    os: [win32]
-
-  '@resvg/resvg-js-win32-x64-msvc@2.4.1':
-    resolution:
-      {
-        integrity: sha512-vY4kTLH2S3bP+puU5x7hlAxHv+ulFgcK6Zn3efKSr0M0KnZ9A3qeAjZteIpkowEFfUeMPNg2dvvoFRJA9zqxSw==,
-      }
-    engines: { node: '>= 10' }
-    cpu: [x64]
-    os: [win32]
-
-  '@resvg/resvg-js@2.4.1':
-    resolution:
-      {
-        integrity: sha512-wTOf1zerZX8qYcMmLZw3czR4paI4hXqPjShNwJRh5DeHxvgffUS5KM7XwxtbIheUW6LVYT5fhT2AJiP6mU7U4A==,
-      }
-    engines: { node: '>= 10' }
-
-  '@rollup/rollup-android-arm-eabi@4.39.0':
-    resolution:
-      {
-        integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==,
-      }
-    cpu: [arm]
-    os: [android]
-
-  '@rollup/rollup-android-arm64@4.39.0':
-    resolution:
-      {
-        integrity: sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==,
-      }
-    cpu: [arm64]
-    os: [android]
-
-  '@rollup/rollup-darwin-arm64@4.39.0':
-    resolution:
-      {
-        integrity: sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==,
-      }
-    cpu: [arm64]
-    os: [darwin]
-
-  '@rollup/rollup-darwin-x64@4.39.0':
-    resolution:
-      {
-        integrity: sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==,
-      }
-    cpu: [x64]
-    os: [darwin]
-
-  '@rollup/rollup-freebsd-arm64@4.39.0':
-    resolution:
-      {
-        integrity: sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==,
-      }
-    cpu: [arm64]
-    os: [freebsd]
-
-  '@rollup/rollup-freebsd-x64@4.39.0':
-    resolution:
-      {
-        integrity: sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==,
-      }
-    cpu: [x64]
-    os: [freebsd]
-
-  '@rollup/rollup-linux-arm-gnueabihf@4.39.0':
-    resolution:
-      {
-        integrity: sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==,
-      }
-    cpu: [arm]
-    os: [linux]
-
-  '@rollup/rollup-linux-arm-musleabihf@4.39.0':
-    resolution:
-      {
-        integrity: sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==,
-      }
-    cpu: [arm]
-    os: [linux]
-
-  '@rollup/rollup-linux-arm64-gnu@4.39.0':
-    resolution:
-      {
-        integrity: sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==,
-      }
-    cpu: [arm64]
-    os: [linux]
-
-  '@rollup/rollup-linux-arm64-musl@4.39.0':
-    resolution:
-      {
-        integrity: sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==,
-      }
-    cpu: [arm64]
-    os: [linux]
-
-  '@rollup/rollup-linux-loongarch64-gnu@4.39.0':
-    resolution:
-      {
-        integrity: sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==,
-      }
-    cpu: [loong64]
-    os: [linux]
-
-  '@rollup/rollup-linux-powerpc64le-gnu@4.39.0':
-    resolution:
-      {
-        integrity: sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==,
-      }
-    cpu: [ppc64]
-    os: [linux]
-
-  '@rollup/rollup-linux-riscv64-gnu@4.39.0':
-    resolution:
-      {
-        integrity: sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==,
-      }
-    cpu: [riscv64]
-    os: [linux]
-
-  '@rollup/rollup-linux-riscv64-musl@4.39.0':
-    resolution:
-      {
-        integrity: sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==,
-      }
-    cpu: [riscv64]
-    os: [linux]
-
-  '@rollup/rollup-linux-s390x-gnu@4.39.0':
-    resolution:
-      {
-        integrity: sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==,
-      }
-    cpu: [s390x]
-    os: [linux]
-
-  '@rollup/rollup-linux-x64-gnu@4.39.0':
-    resolution:
-      {
-        integrity: sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==,
-      }
-    cpu: [x64]
-    os: [linux]
-
-  '@rollup/rollup-linux-x64-musl@4.39.0':
-    resolution:
-      {
-        integrity: sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==,
-      }
-    cpu: [x64]
-    os: [linux]
-
-  '@rollup/rollup-win32-arm64-msvc@4.39.0':
-    resolution:
-      {
-        integrity: sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==,
-      }
-    cpu: [arm64]
-    os: [win32]
-
-  '@rollup/rollup-win32-ia32-msvc@4.39.0':
-    resolution:
-      {
-        integrity: sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==,
-      }
-    cpu: [ia32]
-    os: [win32]
-
-  '@rollup/rollup-win32-x64-msvc@4.39.0':
-    resolution:
-      {
-        integrity: sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==,
-      }
-    cpu: [x64]
-    os: [win32]
-
-  '@semantic-ui-react/event-stack@3.1.3':
-    resolution:
-      {
-        integrity: sha512-FdTmJyWvJaYinHrKRsMLDrz4tTMGdFfds299Qory53hBugiDvGC0tEJf+cHsi5igDwWb/CLOgOiChInHwq8URQ==,
-      }
-    peerDependencies:
-      react: ^16.0.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
-
-  '@so1ve/prettier-config@3.1.0':
-    resolution:
-      {
-        integrity: sha512-9GJ1yXKBC4DzqCTTaZoBf8zw7WWkVuXcccZt1Aqk4lj6ab/GiNUnjPGajUVYLjaqAEOKqM7jUSUfTjk2JTjCAg==,
-      }
-    peerDependencies:
-      prettier: ^3.0.0
-
-  '@so1ve/prettier-plugin-toml@3.1.0':
-    resolution:
-      {
-        integrity: sha512-8WZAGjAVNIJlkfWL6wHKxlUuEBY45fdd5qY5bR/Z6r/txgzKXk/r9qi1DTwc17gi/WcNuRrcRugecRT+mWbIYg==,
-      }
-    peerDependencies:
-      prettier: ^3.0.0
-
-  '@turf/boolean-clockwise@6.5.0':
-    resolution:
-      {
-        integrity: sha512-45+C7LC5RMbRWrxh3Z0Eihsc8db1VGBO5d9BLTOAwU4jR6SgsunTfRWR16X7JUwIDYlCVEmnjcXJNi/kIU3VIw==,
-      }
-
-  '@turf/clone@6.5.0':
-    resolution:
-      {
-        integrity: sha512-mzVtTFj/QycXOn6ig+annKrM6ZlimreKYz6f/GSERytOpgzodbQyOgkfwru100O1KQhhjSudKK4DsQ0oyi9cTw==,
-      }
-
-  '@turf/flatten@6.5.0':
-    resolution:
-      {
-        integrity: sha512-IBZVwoNLVNT6U/bcUUllubgElzpMsNoCw8tLqBw6dfYg9ObGmpEjf9BIYLr7a2Yn5ZR4l7YIj2T7kD5uJjZADQ==,
-      }
-
-  '@turf/helpers@6.5.0':
-    resolution:
-      {
-        integrity: sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==,
-      }
-
-  '@turf/invariant@6.5.0':
-    resolution:
-      {
-        integrity: sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==,
-      }
-
-  '@turf/meta@3.14.0':
-    resolution:
-      {
-        integrity: sha512-OtXqLQuR9hlQ/HkAF/OdzRea7E0eZK1ay8y8CBXkoO2R6v34CsDrWYLMSo0ZzMsaQDpKo76NPP2GGo+PyG1cSg==,
-      }
-
-  '@turf/meta@6.5.0':
-    resolution:
-      {
-        integrity: sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA==,
-      }
-
-  '@turf/rewind@6.5.0':
-    resolution:
-      {
-        integrity: sha512-IoUAMcHWotBWYwSYuYypw/LlqZmO+wcBpn8ysrBNbazkFNkLf3btSDZMkKJO/bvOzl55imr/Xj4fi3DdsLsbzQ==,
-      }
-
-  '@types/babel__core@7.20.5':
-    resolution:
-      {
-        integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==,
-      }
-
-  '@types/babel__generator@7.6.8':
-    resolution:
-      {
-        integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==,
-      }
-
-  '@types/babel__template@7.4.4':
-    resolution:
-      {
-        integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==,
-      }
-
-  '@types/babel__traverse@7.20.7':
-    resolution:
-      {
-        integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==,
-      }
-
-  '@types/debug@4.1.12':
-    resolution:
-      {
-        integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==,
-      }
-
-  '@types/estree-jsx@1.0.5':
-    resolution:
-      {
-        integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==,
-      }
-
-  '@types/estree@1.0.7':
-    resolution:
-      {
-        integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==,
-      }
-
-  '@types/hast@3.0.4':
-    resolution:
-      {
-        integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==,
-      }
-
-  '@types/mdast@4.0.4':
-    resolution:
-      {
-        integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==,
-      }
-
-  '@types/mdx@2.0.13':
-    resolution:
-      {
-        integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==,
-      }
-
-  '@types/ms@2.1.0':
-    resolution:
-      {
-        integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==,
-      }
-
-  '@types/parse-author@2.0.3':
-    resolution:
-      {
-        integrity: sha512-pgRW2K/GVQoogylrGJXDl7PBLW9A6T4OOc9Hy9MLT5f7vgufK2GQ8FcfAbjFHR5HjcN9ByzuCczAORk49REqoA==,
-      }
-
-  '@types/parse-json@4.0.2':
-    resolution:
-      {
-        integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==,
-      }
-
-  '@types/unist@2.0.11':
-    resolution:
-      {
-        integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==,
-      }
-
-  '@types/unist@3.0.3':
-    resolution:
-      {
-        integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==,
-      }
-
-  '@ungap/structured-clone@1.3.0':
-    resolution:
-      {
-        integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==,
-      }
-
-  '@visactor/react-vchart@1.8.11':
-    resolution:
-      {
-        integrity: sha512-wHnCex9gOpnttTtSu04ozKJhTveUk8Ln2KX/7PZyCJxqlXq+eWvW4zvM6Ja8T8kGXfXtFYVVNh9zBMQ7y2T/Sw==,
-      }
-    peerDependencies:
-      react: '>=16.0.0'
-      react-dom: '>=16.0.0'
-
-  '@visactor/vchart-semi-theme@1.8.8':
-    resolution:
-      {
-        integrity: sha512-lm57CX3r6Bm7iGBYYyWhDY+1BvkyhNVLEckKx2PnlPKpJHikKSIK2ACyI5SmHuSOOdYzhY2QK6ZfYa2NShJ83w==,
-      }
-    peerDependencies:
-      '@visactor/vchart': ~1.8.8
-
-  '@visactor/vchart-theme-utils@1.8.8':
-    resolution:
-      {
-        integrity: sha512-RdCey3/t0+82EYyFZvx210rgJJWti9rsgcL3ROZS7o9CtRW1CMj9u9LKLDNIcPLNcLNACFC0aoT03jpdD1BCpA==,
-      }
-    peerDependencies:
-      '@visactor/vchart': ~1.8.8
-
-  '@visactor/vchart@1.8.11':
-    resolution:
-      {
-        integrity: sha512-RdQ822J02GgAQNXvO1LiT0T3O6FjdgPdcm9hVBFyrpBBmuI8MH02IE7Y1kGe9NiFTH4tDwP0ixRgBmqNSGSLZQ==,
-      }
-
-  '@visactor/vdataset@0.17.5':
-    resolution:
-      {
-        integrity: sha512-zVBdLWHWrhldGc8JDjSYF9lvpFT4ZEFQDB0b6yvfSiHzHKHiSco+rWmUFvA7r4ObT6j2QWF1vZAV9To8Ml4vHw==,
-      }
-
-  '@visactor/vgrammar-coordinate@0.10.11':
-    resolution:
-      {
-        integrity: sha512-XSUvEkaf/NQHFafmTwqoIMZicp9fF3o6NB2FDpuWrK4DI1lTuip/0RkqrC+kBAjc5erjt0em0TiITyqXpp4G6w==,
-      }
-
-  '@visactor/vgrammar-core@0.10.11':
-    resolution:
-      {
-        integrity: sha512-VL9vcLPDg1LrHl7EOx0Ga9ATsoaChKIaCGzxjrPEjWiIS5VPU9Rs0jBKP+ch8BjamAoSuqL5mKd0L/RaUBqlaA==,
-      }
-
-  '@visactor/vgrammar-hierarchy@0.10.11':
-    resolution:
-      {
-        integrity: sha512-0r3k51pPlJHu63BduG3htsV/ul62aVcKJxFftRfvKkwGjm1KeHoOZEEAwIf78U2puio0BkLqVn2Ek2L4FYZaIg==,
-      }
-
-  '@visactor/vgrammar-projection@0.10.11':
-    resolution:
-      {
-        integrity: sha512-yEiKsxdfs5+g60wv5xZ1kyS/EDrAsUzAxCMpFFASVUYbQObHvW+elm+UPq2TBX6KZqAM0gsd1inzaLvfsCrLSg==,
-      }
-
-  '@visactor/vgrammar-sankey@0.10.11':
-    resolution:
-      {
-        integrity: sha512-BbJTPuyydsL/L5XtQv59Q82GgJeePY7Wleac798usx3GnDK0GAOrPsI3bubSsOESJ4pNk3V4HPGEQDG1vCPb4w==,
-      }
-
-  '@visactor/vgrammar-util@0.10.11':
-    resolution:
-      {
-        integrity: sha512-cJZLmKZvN95Y+yGhX+28+UpZu3bhYYlXDlHJNvXHyonI76ZYgtceyon2b3lI6XIsUsBGcD4Uo777s949X5os3g==,
-      }
-
-  '@visactor/vgrammar-wordcloud-shape@0.10.11':
-    resolution:
-      {
-        integrity: sha512-NsQOYJp+9WHnIApMvkcUOaajxIg5U/r6rD8LKnoXW/HqAN2TFYXcRR3Daqmk9rrpM5VztQimKOsA1yZWyzozrA==,
-      }
-
-  '@visactor/vgrammar-wordcloud@0.10.11':
-    resolution:
-      {
-        integrity: sha512-JWDqjGhr9JlYkKVBeEkiOqLQk7C1x1BtnsZ+E8oN541gzUqHwfS9qZyhwI3OyoSLewJlsSSPu1vXLKSQzLzKPA==,
-      }
-
-  '@visactor/vrender-components@0.17.17':
-    resolution:
-      {
-        integrity: sha512-7gYFQrozvBkyGF7s/JHXdWDZnATzymxzug63CZd4EB7A0OXKatVDImXRePqwzlPD3QamF7QMVWn0CuIx3gQ2gA==,
-      }
-
-  '@visactor/vrender-core@0.17.17':
-    resolution:
-      {
-        integrity: sha512-pAZGaimunDAWOBdFhzPh0auH5ryxAHr+MVoz+QdASG+6RZXy8D02l8v2QYu4+e4uorxe/s2ZkdNDm81SlNkoHQ==,
-      }
-
-  '@visactor/vrender-kits@0.17.17':
-    resolution:
-      {
-        integrity: sha512-noRP1hAHvPCv36nf2P6sZ930Tk+dJ8jpPWIUm1cFYmUNdcumgIS8Cug0RyeZ+saSqVt5FDTwIwifhOqupw5Zaw==,
-      }
-
-  '@visactor/vscale@0.17.5':
-    resolution:
-      {
-        integrity: sha512-2dkS1IlAJ/IdTp8JElbctOOv6lkHKBKPDm8KvwBo0NuGWQeYAebSeyN3QCdwKbj76gMlCub4zc+xWrS5YiA2zA==,
-      }
-
-  '@visactor/vutils-extension@1.8.11':
-    resolution:
-      {
-        integrity: sha512-Hknzpy3+xh4sdL0iSn5N93BHiMJF4FdwSwhHYEibRpriZmWKG6wBxsJ0Bll4d7oS4f+svxt8Sg2vRYKzQEcIxQ==,
-      }
-
-  '@visactor/vutils@0.17.5':
-    resolution:
-      {
-        integrity: sha512-HFN6Pk1Wc1RK842g02MeKOlvdri5L7/nqxMVTqxIvi0XMhHXpmoqN4+/9H+h8LmJpVohyrI/MT85TRBV/rManw==,
-      }
-
-  '@vitejs/plugin-react@4.3.4':
-    resolution:
-      {
-        integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==,
-      }
-    engines: { node: ^14.18.0 || >=16.0.0 }
-    peerDependencies:
-      vite: ^4.2.0 || ^5.0.0 || ^6.0.0
-
-  abs-svg-path@0.1.1:
-    resolution:
-      {
-        integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==,
-      }
-
-  acorn-jsx@5.3.2:
-    resolution:
-      {
-        integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==,
-      }
-    peerDependencies:
-      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
-
-  acorn@8.14.1:
-    resolution:
-      {
-        integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==,
-      }
-    engines: { node: '>=0.4.0' }
-    hasBin: true
-
-  array-source@0.0.4:
-    resolution:
-      {
-        integrity: sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw==,
-      }
-
-  astring@1.9.0:
-    resolution:
-      {
-        integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==,
-      }
-    hasBin: true
-
-  async-validator@3.5.2:
-    resolution:
-      {
-        integrity: sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ==,
-      }
-
-  asynckit@0.4.0:
-    resolution:
-      {
-        integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==,
-      }
-
-  attr-accept@2.2.5:
-    resolution:
-      {
-        integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==,
-      }
-    engines: { node: '>=4' }
-
-  author-regex@1.0.0:
-    resolution:
-      {
-        integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==,
-      }
-    engines: { node: '>=0.8' }
-
-  axios@0.27.2:
-    resolution:
-      {
-        integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==,
-      }
-
-  bail@2.0.2:
-    resolution:
-      {
-        integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==,
-      }
-
-  balanced-match@1.0.2:
-    resolution:
-      {
-        integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==,
-      }
-
-  bezier-easing@2.1.0:
-    resolution:
-      {
-        integrity: sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==,
-      }
-
-  brace-expansion@1.1.11:
-    resolution:
-      {
-        integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==,
-      }
-
-  browserslist@4.24.4:
-    resolution:
-      {
-        integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==,
-      }
-    engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 }
-    hasBin: true
-
-  buffer-from@1.1.2:
-    resolution:
-      {
-        integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==,
-      }
-
-  call-bind-apply-helpers@1.0.2:
-    resolution:
-      {
-        integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==,
-      }
-    engines: { node: '>= 0.4' }
-
-  callsites@3.1.0:
-    resolution:
-      {
-        integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==,
-      }
-    engines: { node: '>=6' }
-
-  caniuse-lite@1.0.30001709:
-    resolution:
-      {
-        integrity: sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==,
-      }
-
-  ccount@2.0.1:
-    resolution:
-      {
-        integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==,
-      }
-
-  character-entities-html4@2.1.0:
-    resolution:
-      {
-        integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==,
-      }
-
-  character-entities-legacy@3.0.0:
-    resolution:
-      {
-        integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==,
-      }
-
-  character-entities@2.0.2:
-    resolution:
-      {
-        integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==,
-      }
-
-  character-reference-invalid@2.0.1:
-    resolution:
-      {
-        integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==,
-      }
-
-  classnames@2.5.1:
-    resolution:
-      {
-        integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==,
-      }
-
-  clsx@1.2.1:
-    resolution:
-      {
-        integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==,
-      }
-    engines: { node: '>=6' }
-
-  collapse-white-space@2.1.0:
-    resolution:
-      {
-        integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==,
-      }
-
-  color-convert@2.0.1:
-    resolution:
-      {
-        integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==,
-      }
-    engines: { node: '>=7.0.0' }
-
-  color-name@1.1.4:
-    resolution:
-      {
-        integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==,
-      }
-
-  combined-stream@1.0.8:
-    resolution:
-      {
-        integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==,
-      }
-    engines: { node: '>= 0.8' }
-
-  comma-separated-tokens@2.0.3:
-    resolution:
-      {
-        integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==,
-      }
-
-  commander@2.20.3:
-    resolution:
-      {
-        integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==,
-      }
-
-  commander@4.1.1:
-    resolution:
-      {
-        integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==,
-      }
-    engines: { node: '>= 6' }
-
-  compute-scroll-into-view@1.0.20:
-    resolution:
-      {
-        integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==,
-      }
-
-  concat-map@0.0.1:
-    resolution:
-      {
-        integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==,
-      }
-
-  concat-stream@1.4.11:
-    resolution:
-      {
-        integrity: sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw==,
-      }
-    engines: { '0': node >= 0.8 }
-
-  concat-stream@2.0.0:
-    resolution:
-      {
-        integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==,
-      }
-    engines: { '0': node >= 6.0 }
-
-  convert-source-map@2.0.0:
-    resolution:
-      {
-        integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==,
-      }
-
-  copy-text-to-clipboard@2.2.0:
-    resolution:
-      {
-        integrity: sha512-WRvoIdnTs1rgPMkgA2pUOa/M4Enh2uzCwdKsOMYNAJiz/4ZvEJgmbF4OmninPmlFdAWisfeh0tH+Cpf7ni3RqQ==,
-      }
-    engines: { node: '>=6' }
-
-  core-util-is@1.0.3:
-    resolution:
-      {
-        integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==,
-      }
-
-  cosmiconfig@7.1.0:
-    resolution:
-      {
-        integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==,
-      }
-    engines: { node: '>=10' }
-
-  d3-array@1.2.4:
-    resolution:
-      {
-        integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==,
-      }
-
-  d3-dsv@2.0.0:
-    resolution:
-      {
-        integrity: sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==,
-      }
-    hasBin: true
-
-  d3-geo@1.12.1:
-    resolution:
-      {
-        integrity: sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==,
-      }
-
-  d3-hexbin@0.2.2:
-    resolution:
-      {
-        integrity: sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==,
-      }
-
-  d3-hierarchy@3.1.2:
-    resolution:
-      {
-        integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==,
-      }
-    engines: { node: '>=12' }
-
-  date-fns-tz@1.3.8:
-    resolution:
-      {
-        integrity: sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==,
-      }
-    peerDependencies:
-      date-fns: '>=2.0.0'
-
-  date-fns@2.30.0:
-    resolution:
-      {
-        integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==,
-      }
-    engines: { node: '>=0.11' }
-
-  dayjs@1.11.13:
-    resolution:
-      {
-        integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==,
-      }
-
-  debug@4.4.0:
-    resolution:
-      {
-        integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==,
-      }
-    engines: { node: '>=6.0' }
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-
-  decode-named-character-reference@1.1.0:
-    resolution:
-      {
-        integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==,
-      }
-
-  delayed-stream@1.0.0:
-    resolution:
-      {
-        integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==,
-      }
-    engines: { node: '>=0.4.0' }
-
-  dequal@2.0.3:
-    resolution:
-      {
-        integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==,
-      }
-    engines: { node: '>=6' }
-
-  devlop@1.1.0:
-    resolution:
-      {
-        integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==,
-      }
-
-  dunder-proto@1.0.1:
-    resolution:
-      {
-        integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==,
-      }
-    engines: { node: '>= 0.4' }
-
-  electron-to-chromium@1.5.130:
-    resolution:
-      {
-        integrity: sha512-Ou2u7L9j2XLZbhqzyX0jWDj6gA8D3jIfVzt4rikLf3cGBa0VdReuFimBKS9tQJA4+XpeCxj1NoWlfBXzbMa9IA==,
-      }
-
-  error-ex@1.3.2:
-    resolution:
-      {
-        integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==,
-      }
-
-  es-define-property@1.0.1:
-    resolution:
-      {
-        integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==,
-      }
-    engines: { node: '>= 0.4' }
-
-  es-errors@1.3.0:
-    resolution:
-      {
-        integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==,
-      }
-    engines: { node: '>= 0.4' }
-
-  es-object-atoms@1.1.1:
-    resolution:
-      {
-        integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==,
-      }
-    engines: { node: '>= 0.4' }
-
-  es-set-tostringtag@2.1.0:
-    resolution:
-      {
-        integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==,
-      }
-    engines: { node: '>= 0.4' }
-
-  esast-util-from-estree@2.0.0:
-    resolution:
-      {
-        integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==,
-      }
-
-  esast-util-from-js@2.0.1:
-    resolution:
-      {
-        integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==,
-      }
-
-  esbuild@0.21.5:
-    resolution:
-      {
-        integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==,
-      }
-    engines: { node: '>=12' }
-    hasBin: true
-
-  escalade@3.2.0:
-    resolution:
-      {
-        integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==,
-      }
-    engines: { node: '>=6' }
-
-  escape-string-regexp@5.0.0:
-    resolution:
-      {
-        integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==,
-      }
-    engines: { node: '>=12' }
-
-  estree-util-attach-comments@3.0.0:
-    resolution:
-      {
-        integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==,
-      }
-
-  estree-util-build-jsx@3.0.1:
-    resolution:
-      {
-        integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==,
-      }
-
-  estree-util-is-identifier-name@3.0.0:
-    resolution:
-      {
-        integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==,
-      }
-
-  estree-util-scope@1.0.0:
-    resolution:
-      {
-        integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==,
-      }
-
-  estree-util-to-js@2.0.0:
-    resolution:
-      {
-        integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==,
-      }
-
-  estree-util-visit@2.0.0:
-    resolution:
-      {
-        integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==,
-      }
-
-  estree-walker@3.0.3:
-    resolution:
-      {
-        integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==,
-      }
-
-  eventemitter3@4.0.7:
-    resolution:
-      {
-        integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==,
-      }
-
-  exenv@1.2.2:
-    resolution:
-      {
-        integrity: sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==,
-      }
-
-  extend@3.0.2:
-    resolution:
-      {
-        integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==,
-      }
-
-  fast-copy@3.0.2:
-    resolution:
-      {
-        integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==,
-      }
-
-  file-selector@2.1.2:
-    resolution:
-      {
-        integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==,
-      }
-    engines: { node: '>= 12' }
-
-  file-source@0.6.1:
-    resolution:
-      {
-        integrity: sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==,
-      }
-
-  follow-redirects@1.15.9:
-    resolution:
-      {
-        integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==,
-      }
-    engines: { node: '>=4.0' }
-    peerDependencies:
-      debug: '*'
-    peerDependenciesMeta:
-      debug:
-        optional: true
-
-  form-data@4.0.2:
-    resolution:
-      {
-        integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==,
-      }
-    engines: { node: '>= 6' }
-
-  fs-extra@10.1.0:
-    resolution:
-      {
-        integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==,
-      }
-    engines: { node: '>=12' }
-
-  fs-extra@4.0.3:
-    resolution:
-      {
-        integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==,
-      }
-
-  fs.realpath@1.0.0:
-    resolution:
-      {
-        integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==,
-      }
-
-  fsevents@2.3.3:
-    resolution:
-      {
-        integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==,
-      }
-    engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 }
-    os: [darwin]
-
-  function-bind@1.1.2:
-    resolution:
-      {
-        integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==,
-      }
-
-  gensync@1.0.0-beta.2:
-    resolution:
-      {
-        integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==,
-      }
-    engines: { node: '>=6.9.0' }
-
-  geobuf@3.0.2:
-    resolution:
-      {
-        integrity: sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==,
-      }
-    hasBin: true
-
-  geojson-dissolve@3.1.0:
-    resolution:
-      {
-        integrity: sha512-JXHfn+A3tU392HA703gJbjmuHaQOAE/C1KzbELCczFRFux+GdY6zt1nKb1VMBHp4LWeE7gUY2ql+g06vJqhiwQ==,
-      }
-
-  geojson-flatten@0.2.4:
-    resolution:
-      {
-        integrity: sha512-LiX6Jmot8adiIdZ/fthbcKKPOfWjTQchX/ggHnwMZ2e4b0I243N1ANUos0LvnzepTEsj0+D4fIJ5bKhBrWnAHA==,
-      }
-    hasBin: true
-
-  geojson-linestring-dissolve@0.0.1:
-    resolution:
-      {
-        integrity: sha512-Y8I2/Ea28R/Xeki7msBcpMvJL2TaPfaPKP8xqueJfQ9/jEhps+iOJxOR2XCBGgVb12Z6XnDb1CMbaPfLepsLaw==,
-      }
-
-  get-intrinsic@1.3.0:
-    resolution:
-      {
-        integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==,
-      }
-    engines: { node: '>= 0.4' }
-
-  get-proto@1.0.1:
-    resolution:
-      {
-        integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==,
-      }
-    engines: { node: '>= 0.4' }
-
-  get-stdin@6.0.0:
-    resolution:
-      {
-        integrity: sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==,
-      }
-    engines: { node: '>=4' }
-
-  glob@7.2.3:
-    resolution:
-      {
-        integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==,
-      }
-    deprecated: Glob versions prior to v9 are no longer supported
-
-  globals@11.12.0:
-    resolution:
-      {
-        integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==,
-      }
-    engines: { node: '>=4' }
-
-  gopd@1.2.0:
-    resolution:
-      {
-        integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==,
-      }
-    engines: { node: '>= 0.4' }
-
-  graceful-fs@4.2.11:
-    resolution:
-      {
-        integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==,
-      }
-
-  has-symbols@1.1.0:
-    resolution:
-      {
-        integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==,
-      }
-    engines: { node: '>= 0.4' }
-
-  has-tostringtag@1.0.2:
-    resolution:
-      {
-        integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==,
-      }
-    engines: { node: '>= 0.4' }
-
-  hasown@2.0.2:
-    resolution:
-      {
-        integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==,
-      }
-    engines: { node: '>= 0.4' }
-
-  hast-util-to-estree@3.1.3:
-    resolution:
-      {
-        integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==,
-      }
-
-  hast-util-to-jsx-runtime@2.3.6:
-    resolution:
-      {
-        integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==,
-      }
-
-  hast-util-whitespace@3.0.0:
-    resolution:
-      {
-        integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==,
-      }
-
-  history@5.3.0:
-    resolution:
-      {
-        integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==,
-      }
-
-  html-parse-stringify@3.0.1:
-    resolution:
-      {
-        integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==,
-      }
-
-  i18next-browser-languagedetector@7.2.2:
-    resolution:
-      {
-        integrity: sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==,
-      }
-
-  i18next@23.16.8:
-    resolution:
-      {
-        integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==,
-      }
-
-  iconv-lite@0.4.24:
-    resolution:
-      {
-        integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==,
-      }
-    engines: { node: '>=0.10.0' }
-
-  ieee754@1.2.1:
-    resolution:
-      {
-        integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==,
-      }
-
-  import-fresh@3.3.1:
-    resolution:
-      {
-        integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==,
-      }
-    engines: { node: '>=6' }
-
-  inflight@1.0.6:
-    resolution:
-      {
-        integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==,
-      }
-    deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
-
-  inherits@2.0.4:
-    resolution:
-      {
-        integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==,
-      }
-
-  inline-style-parser@0.2.4:
-    resolution:
-      {
-        integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==,
-      }
-
-  is-alphabetical@2.0.1:
-    resolution:
-      {
-        integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==,
-      }
-
-  is-alphanumerical@2.0.1:
-    resolution:
-      {
-        integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==,
-      }
-
-  is-arrayish@0.2.1:
-    resolution:
-      {
-        integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==,
-      }
-
-  is-decimal@2.0.1:
-    resolution:
-      {
-        integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==,
-      }
-
-  is-hexadecimal@2.0.1:
-    resolution:
-      {
-        integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==,
-      }
-
-  is-plain-obj@4.1.0:
-    resolution:
-      {
-        integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==,
-      }
-    engines: { node: '>=12' }
-
-  isarray@0.0.1:
-    resolution:
-      {
-        integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==,
-      }
-
-  jquery@3.7.1:
-    resolution:
-      {
-        integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==,
-      }
-
-  js-tokens@4.0.0:
-    resolution:
-      {
-        integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==,
-      }
-
-  jsesc@3.1.0:
-    resolution:
-      {
-        integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==,
-      }
-    engines: { node: '>=6' }
-    hasBin: true
-
-  json-parse-even-better-errors@2.3.1:
-    resolution:
-      {
-        integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==,
-      }
-
-  json5@2.2.3:
-    resolution:
-      {
-        integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==,
-      }
-    engines: { node: '>=6' }
-    hasBin: true
-
-  jsonc-parser@3.3.1:
-    resolution:
-      {
-        integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==,
-      }
-
-  jsonfile@4.0.0:
-    resolution:
-      {
-        integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==,
-      }
-
-  jsonfile@6.1.0:
-    resolution:
-      {
-        integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==,
-      }
-
-  keyboard-key@1.1.0:
-    resolution:
-      {
-        integrity: sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ==,
-      }
-
-  lines-and-columns@1.2.4:
-    resolution:
-      {
-        integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==,
-      }
-
-  lodash-es@4.17.21:
-    resolution:
-      {
-        integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==,
-      }
-
-  lodash@4.17.21:
-    resolution:
-      {
-        integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==,
-      }
-
-  longest-streak@3.1.0:
-    resolution:
-      {
-        integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==,
-      }
-
-  loose-envify@1.4.0:
-    resolution:
-      {
-        integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==,
-      }
-    hasBin: true
-
-  lottie-web@5.12.2:
-    resolution:
-      {
-        integrity: sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==,
-      }
-
-  lru-cache@5.1.1:
-    resolution:
-      {
-        integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==,
-      }
-
-  markdown-extensions@2.0.0:
-    resolution:
-      {
-        integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==,
-      }
-    engines: { node: '>=16' }
-
-  markdown-table@3.0.4:
-    resolution:
-      {
-        integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==,
-      }
-
-  marked@4.3.0:
-    resolution:
-      {
-        integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==,
-      }
-    engines: { node: '>= 12' }
-    hasBin: true
-
-  math-intrinsics@1.1.0:
-    resolution:
-      {
-        integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==,
-      }
-    engines: { node: '>= 0.4' }
-
-  mdast-util-find-and-replace@3.0.2:
-    resolution:
-      {
-        integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==,
-      }
-
-  mdast-util-from-markdown@2.0.2:
-    resolution:
-      {
-        integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==,
-      }
-
-  mdast-util-gfm-autolink-literal@2.0.1:
-    resolution:
-      {
-        integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==,
-      }
-
-  mdast-util-gfm-footnote@2.1.0:
-    resolution:
-      {
-        integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==,
-      }
-
-  mdast-util-gfm-strikethrough@2.0.0:
-    resolution:
-      {
-        integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==,
-      }
-
-  mdast-util-gfm-table@2.0.0:
-    resolution:
-      {
-        integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==,
-      }
-
-  mdast-util-gfm-task-list-item@2.0.0:
-    resolution:
-      {
-        integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==,
-      }
-
-  mdast-util-gfm@3.1.0:
-    resolution:
-      {
-        integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==,
-      }
-
-  mdast-util-mdx-expression@2.0.1:
-    resolution:
-      {
-        integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==,
-      }
-
-  mdast-util-mdx-jsx@3.2.0:
-    resolution:
-      {
-        integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==,
-      }
-
-  mdast-util-mdx@3.0.0:
-    resolution:
-      {
-        integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==,
-      }
-
-  mdast-util-mdxjs-esm@2.0.1:
-    resolution:
-      {
-        integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==,
-      }
-
-  mdast-util-phrasing@4.1.0:
-    resolution:
-      {
-        integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==,
-      }
-
-  mdast-util-to-hast@13.2.0:
-    resolution:
-      {
-        integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==,
-      }
-
-  mdast-util-to-markdown@2.1.2:
-    resolution:
-      {
-        integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==,
-      }
-
-  mdast-util-to-string@4.0.0:
-    resolution:
-      {
-        integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==,
-      }
-
-  memoize-one@5.2.1:
-    resolution:
-      {
-        integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==,
-      }
-
-  micromark-core-commonmark@2.0.3:
-    resolution:
-      {
-        integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==,
-      }
-
-  micromark-extension-gfm-autolink-literal@2.1.0:
-    resolution:
-      {
-        integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==,
-      }
-
-  micromark-extension-gfm-footnote@2.1.0:
-    resolution:
-      {
-        integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==,
-      }
-
-  micromark-extension-gfm-strikethrough@2.1.0:
-    resolution:
-      {
-        integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==,
-      }
-
-  micromark-extension-gfm-table@2.1.1:
-    resolution:
-      {
-        integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==,
-      }
-
-  micromark-extension-gfm-tagfilter@2.0.0:
-    resolution:
-      {
-        integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==,
-      }
-
-  micromark-extension-gfm-task-list-item@2.1.0:
-    resolution:
-      {
-        integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==,
-      }
-
-  micromark-extension-gfm@3.0.0:
-    resolution:
-      {
-        integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==,
-      }
-
-  micromark-extension-mdx-expression@3.0.1:
-    resolution:
-      {
-        integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==,
-      }
-
-  micromark-extension-mdx-jsx@3.0.2:
-    resolution:
-      {
-        integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==,
-      }
-
-  micromark-extension-mdx-md@2.0.0:
-    resolution:
-      {
-        integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==,
-      }
-
-  micromark-extension-mdxjs-esm@3.0.0:
-    resolution:
-      {
-        integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==,
-      }
-
-  micromark-extension-mdxjs@3.0.0:
-    resolution:
-      {
-        integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==,
-      }
-
-  micromark-factory-destination@2.0.1:
-    resolution:
-      {
-        integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==,
-      }
-
-  micromark-factory-label@2.0.1:
-    resolution:
-      {
-        integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==,
-      }
-
-  micromark-factory-mdx-expression@2.0.3:
-    resolution:
-      {
-        integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==,
-      }
-
-  micromark-factory-space@2.0.1:
-    resolution:
-      {
-        integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==,
-      }
-
-  micromark-factory-title@2.0.1:
-    resolution:
-      {
-        integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==,
-      }
-
-  micromark-factory-whitespace@2.0.1:
-    resolution:
-      {
-        integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==,
-      }
-
-  micromark-util-character@2.1.1:
-    resolution:
-      {
-        integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==,
-      }
-
-  micromark-util-chunked@2.0.1:
-    resolution:
-      {
-        integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==,
-      }
-
-  micromark-util-classify-character@2.0.1:
-    resolution:
-      {
-        integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==,
-      }
-
-  micromark-util-combine-extensions@2.0.1:
-    resolution:
-      {
-        integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==,
-      }
-
-  micromark-util-decode-numeric-character-reference@2.0.2:
-    resolution:
-      {
-        integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==,
-      }
-
-  micromark-util-decode-string@2.0.1:
-    resolution:
-      {
-        integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==,
-      }
-
-  micromark-util-encode@2.0.1:
-    resolution:
-      {
-        integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==,
-      }
-
-  micromark-util-events-to-acorn@2.0.3:
-    resolution:
-      {
-        integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==,
-      }
-
-  micromark-util-html-tag-name@2.0.1:
-    resolution:
-      {
-        integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==,
-      }
-
-  micromark-util-normalize-identifier@2.0.1:
-    resolution:
-      {
-        integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==,
-      }
-
-  micromark-util-resolve-all@2.0.1:
-    resolution:
-      {
-        integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==,
-      }
-
-  micromark-util-sanitize-uri@2.0.1:
-    resolution:
-      {
-        integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==,
-      }
-
-  micromark-util-subtokenize@2.1.0:
-    resolution:
-      {
-        integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==,
-      }
-
-  micromark-util-symbol@2.0.1:
-    resolution:
-      {
-        integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==,
-      }
-
-  micromark-util-types@2.0.2:
-    resolution:
-      {
-        integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==,
-      }
-
-  micromark@4.0.2:
-    resolution:
-      {
-        integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==,
-      }
-
-  mime-db@1.52.0:
-    resolution:
-      {
-        integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==,
-      }
-    engines: { node: '>= 0.6' }
-
-  mime-types@2.1.35:
-    resolution:
-      {
-        integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==,
-      }
-    engines: { node: '>= 0.6' }
-
-  minimatch@3.1.2:
-    resolution:
-      {
-        integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==,
-      }
-
-  minimist@1.2.0:
-    resolution:
-      {
-        integrity: sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw==,
-      }
-
-  minimist@1.2.6:
-    resolution:
-      {
-        integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==,
-      }
-
-  ms@2.1.3:
-    resolution:
-      {
-        integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==,
-      }
-
-  nanoid@3.3.11:
-    resolution:
-      {
-        integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==,
-      }
-    engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 }
-    hasBin: true
-
-  node-releases@2.0.19:
-    resolution:
-      {
-        integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==,
-      }
-
-  object-assign@4.1.1:
-    resolution:
-      {
-        integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==,
-      }
-    engines: { node: '>=0.10.0' }
-
-  once@1.4.0:
-    resolution:
-      {
-        integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==,
-      }
-
-  parent-module@1.0.1:
-    resolution:
-      {
-        integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==,
-      }
-    engines: { node: '>=6' }
-
-  parse-author@2.0.0:
-    resolution:
-      {
-        integrity: sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==,
-      }
-    engines: { node: '>=0.10.0' }
-
-  parse-entities@4.0.2:
-    resolution:
-      {
-        integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==,
-      }
-
-  parse-json@5.2.0:
-    resolution:
-      {
-        integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==,
-      }
-    engines: { node: '>=8' }
-
-  parse-svg-path@0.1.2:
-    resolution:
-      {
-        integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==,
-      }
-
-  path-browserify@1.0.1:
-    resolution:
-      {
-        integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==,
-      }
-
-  path-data-parser@0.1.0:
-    resolution:
-      {
-        integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==,
-      }
-
-  path-is-absolute@1.0.1:
-    resolution:
-      {
-        integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==,
-      }
-    engines: { node: '>=0.10.0' }
-
-  path-source@0.1.3:
-    resolution:
-      {
-        integrity: sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==,
-      }
-
-  path-type@4.0.0:
-    resolution:
-      {
-        integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==,
-      }
-    engines: { node: '>=8' }
-
-  pbf@3.3.0:
-    resolution:
-      {
-        integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==,
-      }
-    hasBin: true
-
-  picocolors@1.1.1:
-    resolution:
-      {
-        integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==,
-      }
-
-  point-at-length@1.1.0:
-    resolution:
-      {
-        integrity: sha512-nNHDk9rNEh/91o2Y8kHLzBLNpLf80RYd2gCun9ss+V0ytRSf6XhryBTx071fesktjbachRmGuUbId+JQmzhRXw==,
-      }
-
-  points-on-curve@0.2.0:
-    resolution:
-      {
-        integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==,
-      }
-
-  points-on-path@0.2.1:
-    resolution:
-      {
-        integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==,
-      }
-
-  postcss@8.5.3:
-    resolution:
-      {
-        integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==,
-      }
-    engines: { node: ^10 || ^12 || >=14 }
-
-  prettier-package-json@2.8.0:
-    resolution:
-      {
-        integrity: sha512-WxtodH/wWavfw3MR7yK/GrS4pASEQ+iSTkdtSxPJWvqzG55ir5nvbLt9rw5AOiEcqqPCRM92WCtR1rk3TG3JSQ==,
-      }
-    hasBin: true
-
-  prettier-plugin-astro@0.14.1:
-    resolution:
-      {
-        integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==,
-      }
-    engines: { node: ^14.15.0 || >=16.0.0 }
-
-  prettier-plugin-curly-and-jsdoc@3.1.0:
-    resolution:
-      {
-        integrity: sha512-4QMOHnLlkP2jTRWS0MFH6j+cuOiXLvXOqCLKbtwwVd8PPyq8NenW5AAwfwqiTNHBQG/DmzViPphRrwgN0XkUVQ==,
-      }
-    peerDependencies:
-      prettier: ^3.0.0
-
-  prettier-plugin-pkgsort@0.2.1:
-    resolution:
-      {
-        integrity: sha512-/k5MIw84EhgoH7dmq4+6ozHjJ0VYbxbw17g4C+WPGHODkLivGwJoA6U1YPR/KObyRDMQJHXAfXKu++9smg7Jyw==,
-      }
-    peerDependencies:
-      prettier: ^3.0.0
-
-  prettier@3.5.3:
-    resolution:
-      {
-        integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==,
-      }
-    engines: { node: '>=14' }
-    hasBin: true
-
-  prismjs@1.30.0:
-    resolution:
-      {
-        integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==,
-      }
-    engines: { node: '>=6' }
-
-  prop-types@15.8.1:
-    resolution:
-      {
-        integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==,
-      }
-
-  property-information@7.0.0:
-    resolution:
-      {
-        integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==,
-      }
-
-  protocol-buffers-schema@3.6.0:
-    resolution:
-      {
-        integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==,
-      }
-
-  react-dom@18.3.1:
-    resolution:
-      {
-        integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==,
-      }
-    peerDependencies:
-      react: ^18.3.1
-
-  react-draggable@4.4.6:
-    resolution:
-      {
-        integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==,
-      }
-    peerDependencies:
-      react: '>= 16.3.0'
-      react-dom: '>= 16.3.0'
-
-  react-dropzone@14.3.8:
-    resolution:
-      {
-        integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==,
-      }
-    engines: { node: '>= 10.13' }
-    peerDependencies:
-      react: '>= 16.8 || 18.0.0'
-
-  react-fast-compare@3.2.2:
-    resolution:
-      {
-        integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==,
-      }
-
-  react-fireworks@1.0.4:
-    resolution:
-      {
-        integrity: sha512-jj1a+HTicB4pR6g2lqhVyAox0GTE0TOrZK2XaJFRYOwltgQWeYErZxnvU9+zH/blY+Hpmu9IKyb39OD3KcCMJw==,
-      }
-
-  react-i18next@13.5.0:
-    resolution:
-      {
-        integrity: sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==,
-      }
-    peerDependencies:
-      i18next: '>= 23.2.3'
-      react: '>= 16.8.0'
-      react-dom: '*'
-      react-native: '*'
-    peerDependenciesMeta:
-      react-dom:
-        optional: true
-      react-native:
-        optional: true
-
-  react-is@16.13.1:
-    resolution:
-      {
-        integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==,
-      }
-
-  react-is@18.3.1:
-    resolution:
-      {
-        integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==,
-      }
-
-  react-popper@2.3.0:
-    resolution:
-      {
-        integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==,
-      }
-    peerDependencies:
-      '@popperjs/core': ^2.0.0
-      react: ^16.8.0 || ^17 || ^18
-      react-dom: ^16.8.0 || ^17 || ^18
-
-  react-refresh@0.14.2:
-    resolution:
-      {
-        integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==,
-      }
-    engines: { node: '>=0.10.0' }
-
-  react-resizable@3.0.5:
-    resolution:
-      {
-        integrity: sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==,
-      }
-    peerDependencies:
-      react: '>= 16.3'
-
-  react-router-dom@6.30.0:
-    resolution:
-      {
-        integrity: sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==,
-      }
-    engines: { node: '>=14.0.0' }
-    peerDependencies:
-      react: '>=16.8'
-      react-dom: '>=16.8'
-
-  react-router@6.30.0:
-    resolution:
-      {
-        integrity: sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==,
-      }
-    engines: { node: '>=14.0.0' }
-    peerDependencies:
-      react: '>=16.8'
-
-  react-telegram-login@1.1.2:
-    resolution:
-      {
-        integrity: sha512-pDP+bvfaklWgnK5O6yvZnIwgky0nnYUU6Zhk0EjdMSkPsLQoOzZRsXIoZnbxyBXhi7346bsxMH+EwwJPTxClDw==,
-      }
-    peerDependencies:
-      react: ^16.13.1
-
-  react-toastify@9.1.3:
-    resolution:
-      {
-        integrity: sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==,
-      }
-    peerDependencies:
-      react: '>=16'
-      react-dom: '>=16'
-
-  react-turnstile@1.1.4:
-    resolution:
-      {
-        integrity: sha512-oluyRWADdsufCt5eMqacW4gfw8/csr6Tk+fmuaMx0PWMKP1SX1iCviLvD2D5w92eAzIYDHi/krUWGHhlfzxTpQ==,
-      }
-    peerDependencies:
-      react: '>= 16.13.1'
-      react-dom: '>= 16.13.1'
-
-  react-window@1.8.11:
-    resolution:
-      {
-        integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==,
-      }
-    engines: { node: '>8.0.0' }
-    peerDependencies:
-      react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
-      react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
-
-  react@18.3.1:
-    resolution:
-      {
-        integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==,
-      }
-    engines: { node: '>=0.10.0' }
-
-  readable-stream@1.1.14:
-    resolution:
-      {
-        integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==,
-      }
-
-  readable-stream@3.6.2:
-    resolution:
-      {
-        integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==,
-      }
-    engines: { node: '>= 6' }
-
-  recma-build-jsx@1.0.0:
-    resolution:
-      {
-        integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==,
-      }
-
-  recma-jsx@1.0.0:
-    resolution:
-      {
-        integrity: sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==,
-      }
-
-  recma-parse@1.0.0:
-    resolution:
-      {
-        integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==,
-      }
-
-  recma-stringify@1.0.0:
-    resolution:
-      {
-        integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==,
-      }
-
-  regenerator-runtime@0.14.1:
-    resolution:
-      {
-        integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==,
-      }
-
-  rehype-recma@1.0.0:
-    resolution:
-      {
-        integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==,
-      }
-
-  remark-gfm@4.0.1:
-    resolution:
-      {
-        integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==,
-      }
-
-  remark-mdx@3.1.0:
-    resolution:
-      {
-        integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==,
-      }
-
-  remark-parse@11.0.0:
-    resolution:
-      {
-        integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==,
-      }
-
-  remark-rehype@11.1.2:
-    resolution:
-      {
-        integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==,
-      }
-
-  remark-stringify@11.0.0:
-    resolution:
-      {
-        integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==,
-      }
-
-  resolve-from@4.0.0:
-    resolution:
-      {
-        integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==,
-      }
-    engines: { node: '>=4' }
-
-  resolve-protobuf-schema@2.1.0:
-    resolution:
-      {
-        integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==,
-      }
-
-  rollup@4.39.0:
-    resolution:
-      {
-        integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==,
-      }
-    engines: { node: '>=18.0.0', npm: '>=8.0.0' }
-    hasBin: true
-
-  roughjs@4.5.2:
-    resolution:
-      {
-        integrity: sha512-2xSlLDKdsWyFxrveYWk9YQ/Y9UfK38EAMRNkYkMqYBJvPX8abCa9PN0x3w02H8Oa6/0bcZICJU+U95VumPqseg==,
-      }
-
-  rw@1.3.3:
-    resolution:
-      {
-        integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==,
-      }
-
-  s.color@0.0.15:
-    resolution:
-      {
-        integrity: sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==,
-      }
-
-  safe-buffer@5.2.1:
-    resolution:
-      {
-        integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==,
-      }
-
-  safer-buffer@2.1.2:
-    resolution:
-      {
-        integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==,
-      }
-
-  sass-formatter@0.7.9:
-    resolution:
-      {
-        integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==,
-      }
-
-  scheduler@0.23.2:
-    resolution:
-      {
-        integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==,
-      }
-
-  scroll-into-view-if-needed@2.2.31:
-    resolution:
-      {
-        integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==,
-      }
-
-  semantic-ui-offline@2.5.0:
-    resolution:
-      {
-        integrity: sha512-Fldx3SfaVtWx5EeCb/5EiJwYkzrGbtsAwVs02xLkeV5z5l8GJmplWEVOeJVjbEpmyiwPWp7cA48JwT5RjbWBVA==,
-      }
-
-  semantic-ui-react@2.1.5:
-    resolution:
-      {
-        integrity: sha512-nIqmmUNpFHfovEb+RI2w3E2/maZQutd8UIWyRjf1SLse+XF51hI559xbz/sLN3O6RpLjr/echLOOXwKCirPy3Q==,
-      }
-    peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
-
-  semver@6.3.1:
-    resolution:
-      {
-        integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==,
-      }
-    hasBin: true
-
-  shallowequal@1.1.0:
-    resolution:
-      {
-        integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==,
-      }
-
-  shapefile@0.6.6:
-    resolution:
-      {
-        integrity: sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==,
-      }
-    hasBin: true
-
-  simple-statistics@7.8.8:
-    resolution:
-      {
-        integrity: sha512-CUtP0+uZbcbsFpqEyvNDYjJCl+612fNgjT8GaVuvMG7tBuJg8gXGpsP5M7X658zy0IcepWOZ6nPBu1Qb9ezA1w==,
-      }
-
-  simplify-geojson@1.0.5:
-    resolution:
-      {
-        integrity: sha512-02l1W4UipP5ivNVq6kX15mAzCRIV1oI3tz0FUEyOsNiv1ltuFDjbNhO+nbv/xhbDEtKqWLYuzpWhUsJrjR/ypA==,
-      }
-    hasBin: true
-
-  simplify-geometry@0.0.2:
-    resolution:
-      {
-        integrity: sha512-ZEyrplkqgCqDlL7V8GbbYgTLlcnNF+MWWUdy8s8ZeJru50bnI71rDew/I+HG36QS2mPOYAq1ZjwNXxHJ8XOVBw==,
-      }
-
-  slice-source@0.4.1:
-    resolution:
-      {
-        integrity: sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==,
-      }
-
-  sort-object-keys@1.1.3:
-    resolution:
-      {
-        integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==,
-      }
-
-  sort-order@1.1.2:
-    resolution:
-      {
-        integrity: sha512-Q8tOrwB1TSv9fNUXym9st3TZJODtmcOIi2JWCkVNQPrRg17KPwlpwweTEb7pMwUIFMTAgx2/JsQQXEPFzYQj3A==,
-      }
-
-  source-map-js@1.2.1:
-    resolution:
-      {
-        integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==,
-      }
-    engines: { node: '>=0.10.0' }
-
-  source-map@0.7.4:
-    resolution:
-      {
-        integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==,
-      }
-    engines: { node: '>= 8' }
-
-  space-separated-tokens@2.0.2:
-    resolution:
-      {
-        integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==,
-      }
-
-  sse.js@https://codeload.github.com/mpetazzoni/sse.js/tar.gz/39b9b82aae95fd58d9d08b487845fe230f4b14e6:
-    resolution:
-      {
-        tarball: https://codeload.github.com/mpetazzoni/sse.js/tar.gz/39b9b82aae95fd58d9d08b487845fe230f4b14e6,
-      }
-    version: 2.6.0
-
-  stream-source@0.3.5:
-    resolution:
-      {
-        integrity: sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g==,
-      }
-
-  string_decoder@0.10.31:
-    resolution:
-      {
-        integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==,
-      }
-
-  string_decoder@1.3.0:
-    resolution:
-      {
-        integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==,
-      }
-
-  stringify-entities@4.0.4:
-    resolution:
-      {
-        integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==,
-      }
-
-  style-to-js@1.1.16:
-    resolution:
-      {
-        integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==,
-      }
-
-  style-to-object@1.0.8:
-    resolution:
-      {
-        integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==,
-      }
-
-  suf-log@2.5.3:
-    resolution:
-      {
-        integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==,
-      }
-
-  text-encoding@0.6.4:
-    resolution:
-      {
-        integrity: sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==,
-      }
-    deprecated: no longer maintained
-
-  topojson-client@3.1.0:
-    resolution:
-      {
-        integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==,
-      }
-    hasBin: true
-
-  topojson-server@3.0.1:
-    resolution:
-      {
-        integrity: sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw==,
-      }
-    hasBin: true
-
-  trim-lines@3.0.1:
-    resolution:
-      {
-        integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==,
-      }
-
-  trough@2.2.0:
-    resolution:
-      {
-        integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==,
-      }
-
-  tslib@2.8.1:
-    resolution:
-      {
-        integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==,
-      }
-
-  typedarray@0.0.6:
-    resolution:
-      {
-        integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==,
-      }
-
-  typedarray@0.0.7:
-    resolution:
-      {
-        integrity: sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ==,
-      }
-
-  typescript@4.4.2:
-    resolution:
-      {
-        integrity: sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==,
-      }
-    engines: { node: '>=4.2.0' }
-    hasBin: true
-
-  unified@11.0.5:
-    resolution:
-      {
-        integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==,
-      }
-
-  unist-util-is@6.0.0:
-    resolution:
-      {
-        integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==,
-      }
-
-  unist-util-position-from-estree@2.0.0:
-    resolution:
-      {
-        integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==,
-      }
-
-  unist-util-position@5.0.0:
-    resolution:
-      {
-        integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==,
-      }
-
-  unist-util-stringify-position@4.0.0:
-    resolution:
-      {
-        integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==,
-      }
-
-  unist-util-visit-parents@6.0.1:
-    resolution:
-      {
-        integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==,
-      }
-
-  unist-util-visit@5.0.0:
-    resolution:
-      {
-        integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==,
-      }
-
-  universalify@0.1.2:
-    resolution:
-      {
-        integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==,
-      }
-    engines: { node: '>= 4.0.0' }
-
-  universalify@2.0.1:
-    resolution:
-      {
-        integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==,
-      }
-    engines: { node: '>= 10.0.0' }
-
-  update-browserslist-db@1.1.3:
-    resolution:
-      {
-        integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==,
-      }
-    hasBin: true
-    peerDependencies:
-      browserslist: '>= 4.21.0'
-
-  util-deprecate@1.0.2:
-    resolution:
-      {
-        integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==,
-      }
-
-  utility-types@3.11.0:
-    resolution:
-      {
-        integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==,
-      }
-    engines: { node: '>= 4' }
-
-  vfile-message@4.0.2:
-    resolution:
-      {
-        integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==,
-      }
-
-  vfile@6.0.3:
-    resolution:
-      {
-        integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==,
-      }
-
-  vite@5.4.16:
-    resolution:
-      {
-        integrity: sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==,
-      }
-    engines: { node: ^18.0.0 || >=20.0.0 }
-    hasBin: true
-    peerDependencies:
-      '@types/node': ^18.0.0 || >=20.0.0
-      less: '*'
-      lightningcss: ^1.21.0
-      sass: '*'
-      sass-embedded: '*'
-      stylus: '*'
-      sugarss: '*'
-      terser: ^5.4.0
-    peerDependenciesMeta:
-      '@types/node':
-        optional: true
-      less:
-        optional: true
-      lightningcss:
-        optional: true
-      sass:
-        optional: true
-      sass-embedded:
-        optional: true
-      stylus:
-        optional: true
-      sugarss:
-        optional: true
-      terser:
-        optional: true
-
-  void-elements@3.1.0:
-    resolution:
-      {
-        integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==,
-      }
-    engines: { node: '>=0.10.0' }
-
-  warning@4.0.3:
-    resolution:
-      {
-        integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==,
-      }
-
-  wrappy@1.0.2:
-    resolution:
-      {
-        integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==,
-      }
-
-  yallist@3.1.1:
-    resolution:
-      {
-        integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==,
-      }
-
-  yaml@1.10.2:
-    resolution:
-      {
-        integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==,
-      }
-    engines: { node: '>= 6' }
-
-  zwitch@2.0.4:
-    resolution:
-      {
-        integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==,
-      }
-
-snapshots:
-  '@ampproject/remapping@2.3.0':
-    dependencies:
-      '@jridgewell/gen-mapping': 0.3.8
-      '@jridgewell/trace-mapping': 0.3.25
-
-  '@astrojs/compiler@2.11.0': {}
-
-  '@babel/code-frame@7.26.2':
-    dependencies:
-      '@babel/helper-validator-identifier': 7.25.9
-      js-tokens: 4.0.0
-      picocolors: 1.1.1
-
-  '@babel/compat-data@7.26.8': {}
-
-  '@babel/core@7.26.10':
-    dependencies:
-      '@ampproject/remapping': 2.3.0
-      '@babel/code-frame': 7.26.2
-      '@babel/generator': 7.27.0
-      '@babel/helper-compilation-targets': 7.27.0
-      '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10)
-      '@babel/helpers': 7.27.0
-      '@babel/parser': 7.27.0
-      '@babel/template': 7.27.0
-      '@babel/traverse': 7.27.0
-      '@babel/types': 7.27.0
-      convert-source-map: 2.0.0
-      debug: 4.4.0
-      gensync: 1.0.0-beta.2
-      json5: 2.2.3
-      semver: 6.3.1
-    transitivePeerDependencies:
-      - supports-color
-
-  '@babel/generator@7.27.0':
-    dependencies:
-      '@babel/parser': 7.27.0
-      '@babel/types': 7.27.0
-      '@jridgewell/gen-mapping': 0.3.8
-      '@jridgewell/trace-mapping': 0.3.25
-      jsesc: 3.1.0
-
-  '@babel/helper-compilation-targets@7.27.0':
-    dependencies:
-      '@babel/compat-data': 7.26.8
-      '@babel/helper-validator-option': 7.25.9
-      browserslist: 4.24.4
-      lru-cache: 5.1.1
-      semver: 6.3.1
-
-  '@babel/helper-module-imports@7.25.9':
-    dependencies:
-      '@babel/traverse': 7.27.0
-      '@babel/types': 7.27.0
-    transitivePeerDependencies:
-      - supports-color
-
-  '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)':
-    dependencies:
-      '@babel/core': 7.26.10
-      '@babel/helper-module-imports': 7.25.9
-      '@babel/helper-validator-identifier': 7.25.9
-      '@babel/traverse': 7.27.0
-    transitivePeerDependencies:
-      - supports-color
-
-  '@babel/helper-plugin-utils@7.26.5': {}
-
-  '@babel/helper-string-parser@7.25.9': {}
-
-  '@babel/helper-validator-identifier@7.25.9': {}
-
-  '@babel/helper-validator-option@7.25.9': {}
-
-  '@babel/helpers@7.27.0':
-    dependencies:
-      '@babel/template': 7.27.0
-      '@babel/types': 7.27.0
-
-  '@babel/parser@7.27.0':
-    dependencies:
-      '@babel/types': 7.27.0
-
-  '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.10)':
-    dependencies:
-      '@babel/core': 7.26.10
-      '@babel/helper-plugin-utils': 7.26.5
-
-  '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.10)':
-    dependencies:
-      '@babel/core': 7.26.10
-      '@babel/helper-plugin-utils': 7.26.5
-
-  '@babel/runtime@7.27.0':
-    dependencies:
-      regenerator-runtime: 0.14.1
-
-  '@babel/template@7.27.0':
-    dependencies:
-      '@babel/code-frame': 7.26.2
-      '@babel/parser': 7.27.0
-      '@babel/types': 7.27.0
-
-  '@babel/traverse@7.27.0':
-    dependencies:
-      '@babel/code-frame': 7.26.2
-      '@babel/generator': 7.27.0
-      '@babel/parser': 7.27.0
-      '@babel/template': 7.27.0
-      '@babel/types': 7.27.0
-      debug: 4.4.0
-      globals: 11.12.0
-    transitivePeerDependencies:
-      - supports-color
-
-  '@babel/types@7.27.0':
-    dependencies:
-      '@babel/helper-string-parser': 7.25.9
-      '@babel/helper-validator-identifier': 7.25.9
-
-  '@dnd-kit/accessibility@3.1.1(react@18.3.1)':
-    dependencies:
-      react: 18.3.1
-      tslib: 2.8.1
-
-  '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
-    dependencies:
-      '@dnd-kit/accessibility': 3.1.1(react@18.3.1)
-      '@dnd-kit/utilities': 3.2.2(react@18.3.1)
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-      tslib: 2.8.1
-
-  '@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
-    dependencies:
-      '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@dnd-kit/utilities': 3.2.2(react@18.3.1)
-      react: 18.3.1
-      tslib: 2.8.1
-
-  '@dnd-kit/utilities@3.2.2(react@18.3.1)':
-    dependencies:
-      react: 18.3.1
-      tslib: 2.8.1
-
-  '@douyinfe/semi-animation-react@2.77.1':
-    dependencies:
-      '@douyinfe/semi-animation': 2.77.1
-      '@douyinfe/semi-animation-styled': 2.77.1
-      classnames: 2.5.1
-
-  '@douyinfe/semi-animation-styled@2.77.1': {}
-
-  '@douyinfe/semi-animation@2.77.1':
-    dependencies:
-      bezier-easing: 2.1.0
-
-  '@douyinfe/semi-foundation@2.77.1(acorn@8.14.1)':
-    dependencies:
-      '@douyinfe/semi-animation': 2.77.1
-      '@douyinfe/semi-json-viewer-core': 2.77.1
-      '@mdx-js/mdx': 3.1.0(acorn@8.14.1)
-      async-validator: 3.5.2
-      classnames: 2.5.1
-      date-fns: 2.30.0
-      date-fns-tz: 1.3.8(date-fns@2.30.0)
-      fast-copy: 3.0.2
-      lodash: 4.17.21
-      lottie-web: 5.12.2
-      memoize-one: 5.2.1
-      prismjs: 1.30.0
-      remark-gfm: 4.0.1
-      scroll-into-view-if-needed: 2.2.31
-    transitivePeerDependencies:
-      - acorn
-      - supports-color
-
-  '@douyinfe/semi-icons@2.77.1(react@18.3.1)':
-    dependencies:
-      classnames: 2.5.1
-      react: 18.3.1
-
-  '@douyinfe/semi-illustrations@2.77.1(react@18.3.1)':
-    dependencies:
-      react: 18.3.1
-
-  '@douyinfe/semi-json-viewer-core@2.77.1':
-    dependencies:
-      jsonc-parser: 3.3.1
-
-  '@douyinfe/semi-theme-default@2.77.1': {}
-
-  '@douyinfe/semi-ui@2.77.1(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
-    dependencies:
-      '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
-      '@dnd-kit/utilities': 3.2.2(react@18.3.1)
-      '@douyinfe/semi-animation': 2.77.1
-      '@douyinfe/semi-animation-react': 2.77.1
-      '@douyinfe/semi-foundation': 2.77.1(acorn@8.14.1)
-      '@douyinfe/semi-icons': 2.77.1(react@18.3.1)
-      '@douyinfe/semi-illustrations': 2.77.1(react@18.3.1)
-      '@douyinfe/semi-theme-default': 2.77.1
-      async-validator: 3.5.2
-      classnames: 2.5.1
-      copy-text-to-clipboard: 2.2.0
-      date-fns: 2.30.0
-      date-fns-tz: 1.3.8(date-fns@2.30.0)
-      fast-copy: 3.0.2
-      jsonc-parser: 3.3.1
-      lodash: 4.17.21
-      prop-types: 15.8.1
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-      react-resizable: 3.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      react-window: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      scroll-into-view-if-needed: 2.2.31
-      utility-types: 3.11.0
-    transitivePeerDependencies:
-      - acorn
-      - supports-color
-
-  '@esbuild/aix-ppc64@0.21.5':
-    optional: true
-
-  '@esbuild/android-arm64@0.21.5':
-    optional: true
-
-  '@esbuild/android-arm@0.21.5':
-    optional: true
-
-  '@esbuild/android-x64@0.21.5':
-    optional: true
-
-  '@esbuild/darwin-arm64@0.21.5':
-    optional: true
-
-  '@esbuild/darwin-x64@0.21.5':
-    optional: true
-
-  '@esbuild/freebsd-arm64@0.21.5':
-    optional: true
-
-  '@esbuild/freebsd-x64@0.21.5':
-    optional: true
-
-  '@esbuild/linux-arm64@0.21.5':
-    optional: true
-
-  '@esbuild/linux-arm@0.21.5':
-    optional: true
-
-  '@esbuild/linux-ia32@0.21.5':
-    optional: true
-
-  '@esbuild/linux-loong64@0.21.5':
-    optional: true
-
-  '@esbuild/linux-mips64el@0.21.5':
-    optional: true
-
-  '@esbuild/linux-ppc64@0.21.5':
-    optional: true
-
-  '@esbuild/linux-riscv64@0.21.5':
-    optional: true
-
-  '@esbuild/linux-s390x@0.21.5':
-    optional: true
-
-  '@esbuild/linux-x64@0.21.5':
-    optional: true
-
-  '@esbuild/netbsd-x64@0.21.5':
-    optional: true
-
-  '@esbuild/openbsd-x64@0.21.5':
-    optional: true
-
-  '@esbuild/sunos-x64@0.21.5':
-    optional: true
-
-  '@esbuild/win32-arm64@0.21.5':
-    optional: true
-
-  '@esbuild/win32-ia32@0.21.5':
-    optional: true
-
-  '@esbuild/win32-x64@0.21.5':
-    optional: true
-
-  '@fluentui/react-component-event-listener@0.63.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
-    dependencies:
-      '@babel/runtime': 7.27.0
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-
-  '@fluentui/react-component-ref@0.63.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
-    dependencies:
-      '@babel/runtime': 7.27.0
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-      react-is: 16.13.1
-
-  '@jridgewell/gen-mapping@0.3.8':
-    dependencies:
-      '@jridgewell/set-array': 1.2.1
-      '@jridgewell/sourcemap-codec': 1.5.0
-      '@jridgewell/trace-mapping': 0.3.25
-
-  '@jridgewell/resolve-uri@3.1.2': {}
-
-  '@jridgewell/set-array@1.2.1': {}
-
-  '@jridgewell/sourcemap-codec@1.5.0': {}
-
-  '@jridgewell/trace-mapping@0.3.25':
-    dependencies:
-      '@jridgewell/resolve-uri': 3.1.2
-      '@jridgewell/sourcemap-codec': 1.5.0
-
-  '@mdx-js/mdx@3.1.0(acorn@8.14.1)':
-    dependencies:
-      '@types/estree': 1.0.7
-      '@types/estree-jsx': 1.0.5
-      '@types/hast': 3.0.4
-      '@types/mdx': 2.0.13
-      collapse-white-space: 2.1.0
-      devlop: 1.1.0
-      estree-util-is-identifier-name: 3.0.0
-      estree-util-scope: 1.0.0
-      estree-walker: 3.0.3
-      hast-util-to-jsx-runtime: 2.3.6
-      markdown-extensions: 2.0.0
-      recma-build-jsx: 1.0.0
-      recma-jsx: 1.0.0(acorn@8.14.1)
-      recma-stringify: 1.0.0
-      rehype-recma: 1.0.0
-      remark-mdx: 3.1.0
-      remark-parse: 11.0.0
-      remark-rehype: 11.1.2
-      source-map: 0.7.4
-      unified: 11.0.5
-      unist-util-position-from-estree: 2.0.0
-      unist-util-stringify-position: 4.0.0
-      unist-util-visit: 5.0.0
-      vfile: 6.0.3
-    transitivePeerDependencies:
-      - acorn
-      - supports-color
-
-  '@popperjs/core@2.11.8': {}
-
-  '@remix-run/router@1.23.0': {}
-
-  '@resvg/resvg-js-android-arm-eabi@2.4.1':
-    optional: true
-
-  '@resvg/resvg-js-android-arm64@2.4.1':
-    optional: true
-
-  '@resvg/resvg-js-darwin-arm64@2.4.1':
-    optional: true
-
-  '@resvg/resvg-js-darwin-x64@2.4.1':
-    optional: true
-
-  '@resvg/resvg-js-linux-arm-gnueabihf@2.4.1':
-    optional: true
-
-  '@resvg/resvg-js-linux-arm64-gnu@2.4.1':
-    optional: true
-
-  '@resvg/resvg-js-linux-arm64-musl@2.4.1':
-    optional: true
-
-  '@resvg/resvg-js-linux-x64-gnu@2.4.1':
-    optional: true
-
-  '@resvg/resvg-js-linux-x64-musl@2.4.1':
-    optional: true
-
-  '@resvg/resvg-js-win32-arm64-msvc@2.4.1':
-    optional: true
-
-  '@resvg/resvg-js-win32-ia32-msvc@2.4.1':
-    optional: true
-
-  '@resvg/resvg-js-win32-x64-msvc@2.4.1':
-    optional: true
-
-  '@resvg/resvg-js@2.4.1':
-    optionalDependencies:
-      '@resvg/resvg-js-android-arm-eabi': 2.4.1
-      '@resvg/resvg-js-android-arm64': 2.4.1
-      '@resvg/resvg-js-darwin-arm64': 2.4.1
-      '@resvg/resvg-js-darwin-x64': 2.4.1
-      '@resvg/resvg-js-linux-arm-gnueabihf': 2.4.1
-      '@resvg/resvg-js-linux-arm64-gnu': 2.4.1
-      '@resvg/resvg-js-linux-arm64-musl': 2.4.1
-      '@resvg/resvg-js-linux-x64-gnu': 2.4.1
-      '@resvg/resvg-js-linux-x64-musl': 2.4.1
-      '@resvg/resvg-js-win32-arm64-msvc': 2.4.1
-      '@resvg/resvg-js-win32-ia32-msvc': 2.4.1
-      '@resvg/resvg-js-win32-x64-msvc': 2.4.1
-
-  '@rollup/rollup-android-arm-eabi@4.39.0':
-    optional: true
-
-  '@rollup/rollup-android-arm64@4.39.0':
-    optional: true
-
-  '@rollup/rollup-darwin-arm64@4.39.0':
-    optional: true
-
-  '@rollup/rollup-darwin-x64@4.39.0':
-    optional: true
-
-  '@rollup/rollup-freebsd-arm64@4.39.0':
-    optional: true
-
-  '@rollup/rollup-freebsd-x64@4.39.0':
-    optional: true
-
-  '@rollup/rollup-linux-arm-gnueabihf@4.39.0':
-    optional: true
-
-  '@rollup/rollup-linux-arm-musleabihf@4.39.0':
-    optional: true
-
-  '@rollup/rollup-linux-arm64-gnu@4.39.0':
-    optional: true
-
-  '@rollup/rollup-linux-arm64-musl@4.39.0':
-    optional: true
-
-  '@rollup/rollup-linux-loongarch64-gnu@4.39.0':
-    optional: true
-
-  '@rollup/rollup-linux-powerpc64le-gnu@4.39.0':
-    optional: true
-
-  '@rollup/rollup-linux-riscv64-gnu@4.39.0':
-    optional: true
-
-  '@rollup/rollup-linux-riscv64-musl@4.39.0':
-    optional: true
-
-  '@rollup/rollup-linux-s390x-gnu@4.39.0':
-    optional: true
-
-  '@rollup/rollup-linux-x64-gnu@4.39.0':
-    optional: true
-
-  '@rollup/rollup-linux-x64-musl@4.39.0':
-    optional: true
-
-  '@rollup/rollup-win32-arm64-msvc@4.39.0':
-    optional: true
-
-  '@rollup/rollup-win32-ia32-msvc@4.39.0':
-    optional: true
-
-  '@rollup/rollup-win32-x64-msvc@4.39.0':
-    optional: true
-
-  '@semantic-ui-react/event-stack@3.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
-    dependencies:
-      exenv: 1.2.2
-      prop-types: 15.8.1
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-
-  '@so1ve/prettier-config@3.1.0(prettier@3.5.3)':
-    dependencies:
-      '@so1ve/prettier-plugin-toml': 3.1.0(prettier@3.5.3)
-      prettier: 3.5.3
-      prettier-plugin-astro: 0.14.1
-      prettier-plugin-curly-and-jsdoc: 3.1.0(prettier@3.5.3)
-      prettier-plugin-pkgsort: 0.2.1(prettier@3.5.3)
-
-  '@so1ve/prettier-plugin-toml@3.1.0(prettier@3.5.3)':
-    dependencies:
-      prettier: 3.5.3
-
-  '@turf/boolean-clockwise@6.5.0':
-    dependencies:
-      '@turf/helpers': 6.5.0
-      '@turf/invariant': 6.5.0
-
-  '@turf/clone@6.5.0':
-    dependencies:
-      '@turf/helpers': 6.5.0
-
-  '@turf/flatten@6.5.0':
-    dependencies:
-      '@turf/helpers': 6.5.0
-      '@turf/meta': 6.5.0
-
-  '@turf/helpers@6.5.0': {}
-
-  '@turf/invariant@6.5.0':
-    dependencies:
-      '@turf/helpers': 6.5.0
-
-  '@turf/meta@3.14.0': {}
-
-  '@turf/meta@6.5.0':
-    dependencies:
-      '@turf/helpers': 6.5.0
-
-  '@turf/rewind@6.5.0':
-    dependencies:
-      '@turf/boolean-clockwise': 6.5.0
-      '@turf/clone': 6.5.0
-      '@turf/helpers': 6.5.0
-      '@turf/invariant': 6.5.0
-      '@turf/meta': 6.5.0
-
-  '@types/babel__core@7.20.5':
-    dependencies:
-      '@babel/parser': 7.27.0
-      '@babel/types': 7.27.0
-      '@types/babel__generator': 7.6.8
-      '@types/babel__template': 7.4.4
-      '@types/babel__traverse': 7.20.7
-
-  '@types/babel__generator@7.6.8':
-    dependencies:
-      '@babel/types': 7.27.0
-
-  '@types/babel__template@7.4.4':
-    dependencies:
-      '@babel/parser': 7.27.0
-      '@babel/types': 7.27.0
-
-  '@types/babel__traverse@7.20.7':
-    dependencies:
-      '@babel/types': 7.27.0
-
-  '@types/debug@4.1.12':
-    dependencies:
-      '@types/ms': 2.1.0
-
-  '@types/estree-jsx@1.0.5':
-    dependencies:
-      '@types/estree': 1.0.7
-
-  '@types/estree@1.0.7': {}
-
-  '@types/hast@3.0.4':
-    dependencies:
-      '@types/unist': 3.0.3
-
-  '@types/mdast@4.0.4':
-    dependencies:
-      '@types/unist': 3.0.3
-
-  '@types/mdx@2.0.13': {}
-
-  '@types/ms@2.1.0': {}
-
-  '@types/parse-author@2.0.3': {}
-
-  '@types/parse-json@4.0.2': {}
-
-  '@types/unist@2.0.11': {}
-
-  '@types/unist@3.0.3': {}
-
-  '@ungap/structured-clone@1.3.0': {}
-
-  '@visactor/react-vchart@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
-    dependencies:
-      '@visactor/vchart': 1.8.11
-      '@visactor/vgrammar-core': 0.10.11
-      '@visactor/vrender-core': 0.17.17
-      '@visactor/vrender-kits': 0.17.17
-      '@visactor/vutils': 0.17.5
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-      react-is: 18.3.1
-
-  '@visactor/vchart-semi-theme@1.8.8(@visactor/vchart@1.8.11)':
-    dependencies:
-      '@visactor/vchart': 1.8.11
-      '@visactor/vchart-theme-utils': 1.8.8(@visactor/vchart@1.8.11)
-
-  '@visactor/vchart-theme-utils@1.8.8(@visactor/vchart@1.8.11)':
-    dependencies:
-      '@visactor/vchart': 1.8.11
-
-  '@visactor/vchart@1.8.11':
-    dependencies:
-      '@visactor/vdataset': 0.17.5
-      '@visactor/vgrammar-core': 0.10.11
-      '@visactor/vgrammar-hierarchy': 0.10.11
-      '@visactor/vgrammar-projection': 0.10.11
-      '@visactor/vgrammar-sankey': 0.10.11
-      '@visactor/vgrammar-util': 0.10.11
-      '@visactor/vgrammar-wordcloud': 0.10.11
-      '@visactor/vgrammar-wordcloud-shape': 0.10.11
-      '@visactor/vrender-components': 0.17.17
-      '@visactor/vrender-core': 0.17.17
-      '@visactor/vrender-kits': 0.17.17
-      '@visactor/vscale': 0.17.5
-      '@visactor/vutils': 0.17.5
-      '@visactor/vutils-extension': 1.8.11
-
-  '@visactor/vdataset@0.17.5':
-    dependencies:
-      '@turf/flatten': 6.5.0
-      '@turf/helpers': 6.5.0
-      '@turf/rewind': 6.5.0
-      '@visactor/vutils': 0.17.5
-      d3-dsv: 2.0.0
-      d3-geo: 1.12.1
-      d3-hexbin: 0.2.2
-      d3-hierarchy: 3.1.2
-      eventemitter3: 4.0.7
-      geobuf: 3.0.2
-      geojson-dissolve: 3.1.0
-      path-browserify: 1.0.1
-      pbf: 3.3.0
-      point-at-length: 1.1.0
-      simple-statistics: 7.8.8
-      simplify-geojson: 1.0.5
-      topojson-client: 3.1.0
-
-  '@visactor/vgrammar-coordinate@0.10.11':
-    dependencies:
-      '@visactor/vgrammar-util': 0.10.11
-      '@visactor/vutils': 0.17.5
-
-  '@visactor/vgrammar-core@0.10.11':
-    dependencies:
-      '@visactor/vdataset': 0.17.5
-      '@visactor/vgrammar-coordinate': 0.10.11
-      '@visactor/vgrammar-util': 0.10.11
-      '@visactor/vrender-components': 0.17.17
-      '@visactor/vrender-core': 0.17.17
-      '@visactor/vrender-kits': 0.17.17
-      '@visactor/vscale': 0.17.5
-      '@visactor/vutils': 0.17.5
-
-  '@visactor/vgrammar-hierarchy@0.10.11':
-    dependencies:
-      '@visactor/vgrammar-core': 0.10.11
-      '@visactor/vgrammar-util': 0.10.11
-      '@visactor/vrender-core': 0.17.17
-      '@visactor/vrender-kits': 0.17.17
-      '@visactor/vutils': 0.17.5
-
-  '@visactor/vgrammar-projection@0.10.11':
-    dependencies:
-      '@visactor/vgrammar-core': 0.10.11
-      '@visactor/vgrammar-util': 0.10.11
-      '@visactor/vutils': 0.17.5
-      d3-geo: 1.12.1
-
-  '@visactor/vgrammar-sankey@0.10.11':
-    dependencies:
-      '@visactor/vgrammar-core': 0.10.11
-      '@visactor/vgrammar-util': 0.10.11
-      '@visactor/vrender-core': 0.17.17
-      '@visactor/vrender-kits': 0.17.17
-      '@visactor/vutils': 0.17.5
-
-  '@visactor/vgrammar-util@0.10.11':
-    dependencies:
-      '@visactor/vutils': 0.17.5
-
-  '@visactor/vgrammar-wordcloud-shape@0.10.11':
-    dependencies:
-      '@visactor/vgrammar-core': 0.10.11
-      '@visactor/vgrammar-util': 0.10.11
-      '@visactor/vrender-core': 0.17.17
-      '@visactor/vrender-kits': 0.17.17
-      '@visactor/vscale': 0.17.5
-      '@visactor/vutils': 0.17.5
-
-  '@visactor/vgrammar-wordcloud@0.10.11':
-    dependencies:
-      '@visactor/vgrammar-core': 0.10.11
-      '@visactor/vgrammar-util': 0.10.11
-      '@visactor/vrender-core': 0.17.17
-      '@visactor/vrender-kits': 0.17.17
-      '@visactor/vutils': 0.17.5
-
-  '@visactor/vrender-components@0.17.17':
-    dependencies:
-      '@visactor/vrender-core': 0.17.17
-      '@visactor/vrender-kits': 0.17.17
-      '@visactor/vscale': 0.17.5
-      '@visactor/vutils': 0.17.5
-
-  '@visactor/vrender-core@0.17.17':
-    dependencies:
-      '@visactor/vutils': 0.17.5
-      color-convert: 2.0.1
-
-  '@visactor/vrender-kits@0.17.17':
-    dependencies:
-      '@resvg/resvg-js': 2.4.1
-      '@visactor/vrender-core': 0.17.17
-      '@visactor/vutils': 0.17.5
-      roughjs: 4.5.2
-
-  '@visactor/vscale@0.17.5':
-    dependencies:
-      '@visactor/vutils': 0.17.5
-
-  '@visactor/vutils-extension@1.8.11':
-    dependencies:
-      '@visactor/vrender-core': 0.17.17
-      '@visactor/vrender-kits': 0.17.17
-      '@visactor/vscale': 0.17.5
-      '@visactor/vutils': 0.17.5
-
-  '@visactor/vutils@0.17.5':
-    dependencies:
-      '@turf/helpers': 6.5.0
-      '@turf/invariant': 6.5.0
-      eventemitter3: 4.0.7
-
-  '@vitejs/plugin-react@4.3.4(vite@5.4.16)':
-    dependencies:
-      '@babel/core': 7.26.10
-      '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10)
-      '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10)
-      '@types/babel__core': 7.20.5
-      react-refresh: 0.14.2
-      vite: 5.4.16
-    transitivePeerDependencies:
-      - supports-color
-
-  abs-svg-path@0.1.1: {}
-
-  acorn-jsx@5.3.2(acorn@8.14.1):
-    dependencies:
-      acorn: 8.14.1
-
-  acorn@8.14.1: {}
-
-  array-source@0.0.4: {}
-
-  astring@1.9.0: {}
-
-  async-validator@3.5.2: {}
-
-  asynckit@0.4.0: {}
-
-  attr-accept@2.2.5: {}
-
-  author-regex@1.0.0: {}
-
-  axios@0.27.2:
-    dependencies:
-      follow-redirects: 1.15.9
-      form-data: 4.0.2
-    transitivePeerDependencies:
-      - debug
-
-  bail@2.0.2: {}
-
-  balanced-match@1.0.2: {}
-
-  bezier-easing@2.1.0: {}
-
-  brace-expansion@1.1.11:
-    dependencies:
-      balanced-match: 1.0.2
-      concat-map: 0.0.1
-
-  browserslist@4.24.4:
-    dependencies:
-      caniuse-lite: 1.0.30001709
-      electron-to-chromium: 1.5.130
-      node-releases: 2.0.19
-      update-browserslist-db: 1.1.3(browserslist@4.24.4)
-
-  buffer-from@1.1.2: {}
-
-  call-bind-apply-helpers@1.0.2:
-    dependencies:
-      es-errors: 1.3.0
-      function-bind: 1.1.2
-
-  callsites@3.1.0: {}
-
-  caniuse-lite@1.0.30001709: {}
-
-  ccount@2.0.1: {}
-
-  character-entities-html4@2.1.0: {}
-
-  character-entities-legacy@3.0.0: {}
-
-  character-entities@2.0.2: {}
-
-  character-reference-invalid@2.0.1: {}
-
-  classnames@2.5.1: {}
-
-  clsx@1.2.1: {}
-
-  collapse-white-space@2.1.0: {}
-
-  color-convert@2.0.1:
-    dependencies:
-      color-name: 1.1.4
-
-  color-name@1.1.4: {}
-
-  combined-stream@1.0.8:
-    dependencies:
-      delayed-stream: 1.0.0
-
-  comma-separated-tokens@2.0.3: {}
-
-  commander@2.20.3: {}
-
-  commander@4.1.1: {}
-
-  compute-scroll-into-view@1.0.20: {}
-
-  concat-map@0.0.1: {}
-
-  concat-stream@1.4.11:
-    dependencies:
-      inherits: 2.0.4
-      readable-stream: 1.1.14
-      typedarray: 0.0.7
-
-  concat-stream@2.0.0:
-    dependencies:
-      buffer-from: 1.1.2
-      inherits: 2.0.4
-      readable-stream: 3.6.2
-      typedarray: 0.0.6
-
-  convert-source-map@2.0.0: {}
-
-  copy-text-to-clipboard@2.2.0: {}
-
-  core-util-is@1.0.3: {}
-
-  cosmiconfig@7.1.0:
-    dependencies:
-      '@types/parse-json': 4.0.2
-      import-fresh: 3.3.1
-      parse-json: 5.2.0
-      path-type: 4.0.0
-      yaml: 1.10.2
-
-  d3-array@1.2.4: {}
-
-  d3-dsv@2.0.0:
-    dependencies:
-      commander: 2.20.3
-      iconv-lite: 0.4.24
-      rw: 1.3.3
-
-  d3-geo@1.12.1:
-    dependencies:
-      d3-array: 1.2.4
-
-  d3-hexbin@0.2.2: {}
-
-  d3-hierarchy@3.1.2: {}
-
-  date-fns-tz@1.3.8(date-fns@2.30.0):
-    dependencies:
-      date-fns: 2.30.0
-
-  date-fns@2.30.0:
-    dependencies:
-      '@babel/runtime': 7.27.0
-
-  dayjs@1.11.13: {}
-
-  debug@4.4.0:
-    dependencies:
-      ms: 2.1.3
-
-  decode-named-character-reference@1.1.0:
-    dependencies:
-      character-entities: 2.0.2
-
-  delayed-stream@1.0.0: {}
-
-  dequal@2.0.3: {}
-
-  devlop@1.1.0:
-    dependencies:
-      dequal: 2.0.3
-
-  dunder-proto@1.0.1:
-    dependencies:
-      call-bind-apply-helpers: 1.0.2
-      es-errors: 1.3.0
-      gopd: 1.2.0
-
-  electron-to-chromium@1.5.130: {}
-
-  error-ex@1.3.2:
-    dependencies:
-      is-arrayish: 0.2.1
-
-  es-define-property@1.0.1: {}
-
-  es-errors@1.3.0: {}
-
-  es-object-atoms@1.1.1:
-    dependencies:
-      es-errors: 1.3.0
-
-  es-set-tostringtag@2.1.0:
-    dependencies:
-      es-errors: 1.3.0
-      get-intrinsic: 1.3.0
-      has-tostringtag: 1.0.2
-      hasown: 2.0.2
-
-  esast-util-from-estree@2.0.0:
-    dependencies:
-      '@types/estree-jsx': 1.0.5
-      devlop: 1.1.0
-      estree-util-visit: 2.0.0
-      unist-util-position-from-estree: 2.0.0
-
-  esast-util-from-js@2.0.1:
-    dependencies:
-      '@types/estree-jsx': 1.0.5
-      acorn: 8.14.1
-      esast-util-from-estree: 2.0.0
-      vfile-message: 4.0.2
-
-  esbuild@0.21.5:
-    optionalDependencies:
-      '@esbuild/aix-ppc64': 0.21.5
-      '@esbuild/android-arm': 0.21.5
-      '@esbuild/android-arm64': 0.21.5
-      '@esbuild/android-x64': 0.21.5
-      '@esbuild/darwin-arm64': 0.21.5
-      '@esbuild/darwin-x64': 0.21.5
-      '@esbuild/freebsd-arm64': 0.21.5
-      '@esbuild/freebsd-x64': 0.21.5
-      '@esbuild/linux-arm': 0.21.5
-      '@esbuild/linux-arm64': 0.21.5
-      '@esbuild/linux-ia32': 0.21.5
-      '@esbuild/linux-loong64': 0.21.5
-      '@esbuild/linux-mips64el': 0.21.5
-      '@esbuild/linux-ppc64': 0.21.5
-      '@esbuild/linux-riscv64': 0.21.5
-      '@esbuild/linux-s390x': 0.21.5
-      '@esbuild/linux-x64': 0.21.5
-      '@esbuild/netbsd-x64': 0.21.5
-      '@esbuild/openbsd-x64': 0.21.5
-      '@esbuild/sunos-x64': 0.21.5
-      '@esbuild/win32-arm64': 0.21.5
-      '@esbuild/win32-ia32': 0.21.5
-      '@esbuild/win32-x64': 0.21.5
-
-  escalade@3.2.0: {}
-
-  escape-string-regexp@5.0.0: {}
-
-  estree-util-attach-comments@3.0.0:
-    dependencies:
-      '@types/estree': 1.0.7
-
-  estree-util-build-jsx@3.0.1:
-    dependencies:
-      '@types/estree-jsx': 1.0.5
-      devlop: 1.1.0
-      estree-util-is-identifier-name: 3.0.0
-      estree-walker: 3.0.3
-
-  estree-util-is-identifier-name@3.0.0: {}
-
-  estree-util-scope@1.0.0:
-    dependencies:
-      '@types/estree': 1.0.7
-      devlop: 1.1.0
-
-  estree-util-to-js@2.0.0:
-    dependencies:
-      '@types/estree-jsx': 1.0.5
-      astring: 1.9.0
-      source-map: 0.7.4
-
-  estree-util-visit@2.0.0:
-    dependencies:
-      '@types/estree-jsx': 1.0.5
-      '@types/unist': 3.0.3
-
-  estree-walker@3.0.3:
-    dependencies:
-      '@types/estree': 1.0.7
-
-  eventemitter3@4.0.7: {}
-
-  exenv@1.2.2: {}
-
-  extend@3.0.2: {}
-
-  fast-copy@3.0.2: {}
-
-  file-selector@2.1.2:
-    dependencies:
-      tslib: 2.8.1
-
-  file-source@0.6.1:
-    dependencies:
-      stream-source: 0.3.5
-
-  follow-redirects@1.15.9: {}
-
-  form-data@4.0.2:
-    dependencies:
-      asynckit: 0.4.0
-      combined-stream: 1.0.8
-      es-set-tostringtag: 2.1.0
-      mime-types: 2.1.35
-
-  fs-extra@10.1.0:
-    dependencies:
-      graceful-fs: 4.2.11
-      jsonfile: 6.1.0
-      universalify: 2.0.1
-
-  fs-extra@4.0.3:
-    dependencies:
-      graceful-fs: 4.2.11
-      jsonfile: 4.0.0
-      universalify: 0.1.2
-
-  fs.realpath@1.0.0: {}
-
-  fsevents@2.3.3:
-    optional: true
-
-  function-bind@1.1.2: {}
-
-  gensync@1.0.0-beta.2: {}
-
-  geobuf@3.0.2:
-    dependencies:
-      concat-stream: 2.0.0
-      pbf: 3.3.0
-      shapefile: 0.6.6
-
-  geojson-dissolve@3.1.0:
-    dependencies:
-      '@turf/meta': 3.14.0
-      geojson-flatten: 0.2.4
-      geojson-linestring-dissolve: 0.0.1
-      topojson-client: 3.1.0
-      topojson-server: 3.0.1
-
-  geojson-flatten@0.2.4:
-    dependencies:
-      get-stdin: 6.0.0
-      minimist: 1.2.0
-
-  geojson-linestring-dissolve@0.0.1: {}
-
-  get-intrinsic@1.3.0:
-    dependencies:
-      call-bind-apply-helpers: 1.0.2
-      es-define-property: 1.0.1
-      es-errors: 1.3.0
-      es-object-atoms: 1.1.1
-      function-bind: 1.1.2
-      get-proto: 1.0.1
-      gopd: 1.2.0
-      has-symbols: 1.1.0
-      hasown: 2.0.2
-      math-intrinsics: 1.1.0
-
-  get-proto@1.0.1:
-    dependencies:
-      dunder-proto: 1.0.1
-      es-object-atoms: 1.1.1
-
-  get-stdin@6.0.0: {}
-
-  glob@7.2.3:
-    dependencies:
-      fs.realpath: 1.0.0
-      inflight: 1.0.6
-      inherits: 2.0.4
-      minimatch: 3.1.2
-      once: 1.4.0
-      path-is-absolute: 1.0.1
-
-  globals@11.12.0: {}
-
-  gopd@1.2.0: {}
-
-  graceful-fs@4.2.11: {}
-
-  has-symbols@1.1.0: {}
-
-  has-tostringtag@1.0.2:
-    dependencies:
-      has-symbols: 1.1.0
-
-  hasown@2.0.2:
-    dependencies:
-      function-bind: 1.1.2
-
-  hast-util-to-estree@3.1.3:
-    dependencies:
-      '@types/estree': 1.0.7
-      '@types/estree-jsx': 1.0.5
-      '@types/hast': 3.0.4
-      comma-separated-tokens: 2.0.3
-      devlop: 1.1.0
-      estree-util-attach-comments: 3.0.0
-      estree-util-is-identifier-name: 3.0.0
-      hast-util-whitespace: 3.0.0
-      mdast-util-mdx-expression: 2.0.1
-      mdast-util-mdx-jsx: 3.2.0
-      mdast-util-mdxjs-esm: 2.0.1
-      property-information: 7.0.0
-      space-separated-tokens: 2.0.2
-      style-to-js: 1.1.16
-      unist-util-position: 5.0.0
-      zwitch: 2.0.4
-    transitivePeerDependencies:
-      - supports-color
-
-  hast-util-to-jsx-runtime@2.3.6:
-    dependencies:
-      '@types/estree': 1.0.7
-      '@types/hast': 3.0.4
-      '@types/unist': 3.0.3
-      comma-separated-tokens: 2.0.3
-      devlop: 1.1.0
-      estree-util-is-identifier-name: 3.0.0
-      hast-util-whitespace: 3.0.0
-      mdast-util-mdx-expression: 2.0.1
-      mdast-util-mdx-jsx: 3.2.0
-      mdast-util-mdxjs-esm: 2.0.1
-      property-information: 7.0.0
-      space-separated-tokens: 2.0.2
-      style-to-js: 1.1.16
-      unist-util-position: 5.0.0
-      vfile-message: 4.0.2
-    transitivePeerDependencies:
-      - supports-color
-
-  hast-util-whitespace@3.0.0:
-    dependencies:
-      '@types/hast': 3.0.4
-
-  history@5.3.0:
-    dependencies:
-      '@babel/runtime': 7.27.0
-
-  html-parse-stringify@3.0.1:
-    dependencies:
-      void-elements: 3.1.0
-
-  i18next-browser-languagedetector@7.2.2:
-    dependencies:
-      '@babel/runtime': 7.27.0
-
-  i18next@23.16.8:
-    dependencies:
-      '@babel/runtime': 7.27.0
-
-  iconv-lite@0.4.24:
-    dependencies:
-      safer-buffer: 2.1.2
-
-  ieee754@1.2.1: {}
-
-  import-fresh@3.3.1:
-    dependencies:
-      parent-module: 1.0.1
-      resolve-from: 4.0.0
-
-  inflight@1.0.6:
-    dependencies:
-      once: 1.4.0
-      wrappy: 1.0.2
-
-  inherits@2.0.4: {}
-
-  inline-style-parser@0.2.4: {}
-
-  is-alphabetical@2.0.1: {}
-
-  is-alphanumerical@2.0.1:
-    dependencies:
-      is-alphabetical: 2.0.1
-      is-decimal: 2.0.1
-
-  is-arrayish@0.2.1: {}
-
-  is-decimal@2.0.1: {}
-
-  is-hexadecimal@2.0.1: {}
-
-  is-plain-obj@4.1.0: {}
-
-  isarray@0.0.1: {}
-
-  jquery@3.7.1: {}
-
-  js-tokens@4.0.0: {}
-
-  jsesc@3.1.0: {}
-
-  json-parse-even-better-errors@2.3.1: {}
-
-  json5@2.2.3: {}
-
-  jsonc-parser@3.3.1: {}
-
-  jsonfile@4.0.0:
-    optionalDependencies:
-      graceful-fs: 4.2.11
-
-  jsonfile@6.1.0:
-    dependencies:
-      universalify: 2.0.1
-    optionalDependencies:
-      graceful-fs: 4.2.11
-
-  keyboard-key@1.1.0: {}
-
-  lines-and-columns@1.2.4: {}
-
-  lodash-es@4.17.21: {}
-
-  lodash@4.17.21: {}
-
-  longest-streak@3.1.0: {}
-
-  loose-envify@1.4.0:
-    dependencies:
-      js-tokens: 4.0.0
-
-  lottie-web@5.12.2: {}
-
-  lru-cache@5.1.1:
-    dependencies:
-      yallist: 3.1.1
-
-  markdown-extensions@2.0.0: {}
-
-  markdown-table@3.0.4: {}
-
-  marked@4.3.0: {}
-
-  math-intrinsics@1.1.0: {}
-
-  mdast-util-find-and-replace@3.0.2:
-    dependencies:
-      '@types/mdast': 4.0.4
-      escape-string-regexp: 5.0.0
-      unist-util-is: 6.0.0
-      unist-util-visit-parents: 6.0.1
-
-  mdast-util-from-markdown@2.0.2:
-    dependencies:
-      '@types/mdast': 4.0.4
-      '@types/unist': 3.0.3
-      decode-named-character-reference: 1.1.0
-      devlop: 1.1.0
-      mdast-util-to-string: 4.0.0
-      micromark: 4.0.2
-      micromark-util-decode-numeric-character-reference: 2.0.2
-      micromark-util-decode-string: 2.0.1
-      micromark-util-normalize-identifier: 2.0.1
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-      unist-util-stringify-position: 4.0.0
-    transitivePeerDependencies:
-      - supports-color
-
-  mdast-util-gfm-autolink-literal@2.0.1:
-    dependencies:
-      '@types/mdast': 4.0.4
-      ccount: 2.0.1
-      devlop: 1.1.0
-      mdast-util-find-and-replace: 3.0.2
-      micromark-util-character: 2.1.1
-
-  mdast-util-gfm-footnote@2.1.0:
-    dependencies:
-      '@types/mdast': 4.0.4
-      devlop: 1.1.0
-      mdast-util-from-markdown: 2.0.2
-      mdast-util-to-markdown: 2.1.2
-      micromark-util-normalize-identifier: 2.0.1
-    transitivePeerDependencies:
-      - supports-color
-
-  mdast-util-gfm-strikethrough@2.0.0:
-    dependencies:
-      '@types/mdast': 4.0.4
-      mdast-util-from-markdown: 2.0.2
-      mdast-util-to-markdown: 2.1.2
-    transitivePeerDependencies:
-      - supports-color
-
-  mdast-util-gfm-table@2.0.0:
-    dependencies:
-      '@types/mdast': 4.0.4
-      devlop: 1.1.0
-      markdown-table: 3.0.4
-      mdast-util-from-markdown: 2.0.2
-      mdast-util-to-markdown: 2.1.2
-    transitivePeerDependencies:
-      - supports-color
-
-  mdast-util-gfm-task-list-item@2.0.0:
-    dependencies:
-      '@types/mdast': 4.0.4
-      devlop: 1.1.0
-      mdast-util-from-markdown: 2.0.2
-      mdast-util-to-markdown: 2.1.2
-    transitivePeerDependencies:
-      - supports-color
-
-  mdast-util-gfm@3.1.0:
-    dependencies:
-      mdast-util-from-markdown: 2.0.2
-      mdast-util-gfm-autolink-literal: 2.0.1
-      mdast-util-gfm-footnote: 2.1.0
-      mdast-util-gfm-strikethrough: 2.0.0
-      mdast-util-gfm-table: 2.0.0
-      mdast-util-gfm-task-list-item: 2.0.0
-      mdast-util-to-markdown: 2.1.2
-    transitivePeerDependencies:
-      - supports-color
-
-  mdast-util-mdx-expression@2.0.1:
-    dependencies:
-      '@types/estree-jsx': 1.0.5
-      '@types/hast': 3.0.4
-      '@types/mdast': 4.0.4
-      devlop: 1.1.0
-      mdast-util-from-markdown: 2.0.2
-      mdast-util-to-markdown: 2.1.2
-    transitivePeerDependencies:
-      - supports-color
-
-  mdast-util-mdx-jsx@3.2.0:
-    dependencies:
-      '@types/estree-jsx': 1.0.5
-      '@types/hast': 3.0.4
-      '@types/mdast': 4.0.4
-      '@types/unist': 3.0.3
-      ccount: 2.0.1
-      devlop: 1.1.0
-      mdast-util-from-markdown: 2.0.2
-      mdast-util-to-markdown: 2.1.2
-      parse-entities: 4.0.2
-      stringify-entities: 4.0.4
-      unist-util-stringify-position: 4.0.0
-      vfile-message: 4.0.2
-    transitivePeerDependencies:
-      - supports-color
-
-  mdast-util-mdx@3.0.0:
-    dependencies:
-      mdast-util-from-markdown: 2.0.2
-      mdast-util-mdx-expression: 2.0.1
-      mdast-util-mdx-jsx: 3.2.0
-      mdast-util-mdxjs-esm: 2.0.1
-      mdast-util-to-markdown: 2.1.2
-    transitivePeerDependencies:
-      - supports-color
-
-  mdast-util-mdxjs-esm@2.0.1:
-    dependencies:
-      '@types/estree-jsx': 1.0.5
-      '@types/hast': 3.0.4
-      '@types/mdast': 4.0.4
-      devlop: 1.1.0
-      mdast-util-from-markdown: 2.0.2
-      mdast-util-to-markdown: 2.1.2
-    transitivePeerDependencies:
-      - supports-color
-
-  mdast-util-phrasing@4.1.0:
-    dependencies:
-      '@types/mdast': 4.0.4
-      unist-util-is: 6.0.0
-
-  mdast-util-to-hast@13.2.0:
-    dependencies:
-      '@types/hast': 3.0.4
-      '@types/mdast': 4.0.4
-      '@ungap/structured-clone': 1.3.0
-      devlop: 1.1.0
-      micromark-util-sanitize-uri: 2.0.1
-      trim-lines: 3.0.1
-      unist-util-position: 5.0.0
-      unist-util-visit: 5.0.0
-      vfile: 6.0.3
-
-  mdast-util-to-markdown@2.1.2:
-    dependencies:
-      '@types/mdast': 4.0.4
-      '@types/unist': 3.0.3
-      longest-streak: 3.1.0
-      mdast-util-phrasing: 4.1.0
-      mdast-util-to-string: 4.0.0
-      micromark-util-classify-character: 2.0.1
-      micromark-util-decode-string: 2.0.1
-      unist-util-visit: 5.0.0
-      zwitch: 2.0.4
-
-  mdast-util-to-string@4.0.0:
-    dependencies:
-      '@types/mdast': 4.0.4
-
-  memoize-one@5.2.1: {}
-
-  micromark-core-commonmark@2.0.3:
-    dependencies:
-      decode-named-character-reference: 1.1.0
-      devlop: 1.1.0
-      micromark-factory-destination: 2.0.1
-      micromark-factory-label: 2.0.1
-      micromark-factory-space: 2.0.1
-      micromark-factory-title: 2.0.1
-      micromark-factory-whitespace: 2.0.1
-      micromark-util-character: 2.1.1
-      micromark-util-chunked: 2.0.1
-      micromark-util-classify-character: 2.0.1
-      micromark-util-html-tag-name: 2.0.1
-      micromark-util-normalize-identifier: 2.0.1
-      micromark-util-resolve-all: 2.0.1
-      micromark-util-subtokenize: 2.1.0
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-extension-gfm-autolink-literal@2.1.0:
-    dependencies:
-      micromark-util-character: 2.1.1
-      micromark-util-sanitize-uri: 2.0.1
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-extension-gfm-footnote@2.1.0:
-    dependencies:
-      devlop: 1.1.0
-      micromark-core-commonmark: 2.0.3
-      micromark-factory-space: 2.0.1
-      micromark-util-character: 2.1.1
-      micromark-util-normalize-identifier: 2.0.1
-      micromark-util-sanitize-uri: 2.0.1
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-extension-gfm-strikethrough@2.1.0:
-    dependencies:
-      devlop: 1.1.0
-      micromark-util-chunked: 2.0.1
-      micromark-util-classify-character: 2.0.1
-      micromark-util-resolve-all: 2.0.1
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-extension-gfm-table@2.1.1:
-    dependencies:
-      devlop: 1.1.0
-      micromark-factory-space: 2.0.1
-      micromark-util-character: 2.1.1
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-extension-gfm-tagfilter@2.0.0:
-    dependencies:
-      micromark-util-types: 2.0.2
-
-  micromark-extension-gfm-task-list-item@2.1.0:
-    dependencies:
-      devlop: 1.1.0
-      micromark-factory-space: 2.0.1
-      micromark-util-character: 2.1.1
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-extension-gfm@3.0.0:
-    dependencies:
-      micromark-extension-gfm-autolink-literal: 2.1.0
-      micromark-extension-gfm-footnote: 2.1.0
-      micromark-extension-gfm-strikethrough: 2.1.0
-      micromark-extension-gfm-table: 2.1.1
-      micromark-extension-gfm-tagfilter: 2.0.0
-      micromark-extension-gfm-task-list-item: 2.1.0
-      micromark-util-combine-extensions: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-extension-mdx-expression@3.0.1:
-    dependencies:
-      '@types/estree': 1.0.7
-      devlop: 1.1.0
-      micromark-factory-mdx-expression: 2.0.3
-      micromark-factory-space: 2.0.1
-      micromark-util-character: 2.1.1
-      micromark-util-events-to-acorn: 2.0.3
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-extension-mdx-jsx@3.0.2:
-    dependencies:
-      '@types/estree': 1.0.7
-      devlop: 1.1.0
-      estree-util-is-identifier-name: 3.0.0
-      micromark-factory-mdx-expression: 2.0.3
-      micromark-factory-space: 2.0.1
-      micromark-util-character: 2.1.1
-      micromark-util-events-to-acorn: 2.0.3
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-      vfile-message: 4.0.2
-
-  micromark-extension-mdx-md@2.0.0:
-    dependencies:
-      micromark-util-types: 2.0.2
-
-  micromark-extension-mdxjs-esm@3.0.0:
-    dependencies:
-      '@types/estree': 1.0.7
-      devlop: 1.1.0
-      micromark-core-commonmark: 2.0.3
-      micromark-util-character: 2.1.1
-      micromark-util-events-to-acorn: 2.0.3
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-      unist-util-position-from-estree: 2.0.0
-      vfile-message: 4.0.2
-
-  micromark-extension-mdxjs@3.0.0:
-    dependencies:
-      acorn: 8.14.1
-      acorn-jsx: 5.3.2(acorn@8.14.1)
-      micromark-extension-mdx-expression: 3.0.1
-      micromark-extension-mdx-jsx: 3.0.2
-      micromark-extension-mdx-md: 2.0.0
-      micromark-extension-mdxjs-esm: 3.0.0
-      micromark-util-combine-extensions: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-factory-destination@2.0.1:
-    dependencies:
-      micromark-util-character: 2.1.1
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-factory-label@2.0.1:
-    dependencies:
-      devlop: 1.1.0
-      micromark-util-character: 2.1.1
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-factory-mdx-expression@2.0.3:
-    dependencies:
-      '@types/estree': 1.0.7
-      devlop: 1.1.0
-      micromark-factory-space: 2.0.1
-      micromark-util-character: 2.1.1
-      micromark-util-events-to-acorn: 2.0.3
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-      unist-util-position-from-estree: 2.0.0
-      vfile-message: 4.0.2
-
-  micromark-factory-space@2.0.1:
-    dependencies:
-      micromark-util-character: 2.1.1
-      micromark-util-types: 2.0.2
-
-  micromark-factory-title@2.0.1:
-    dependencies:
-      micromark-factory-space: 2.0.1
-      micromark-util-character: 2.1.1
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-factory-whitespace@2.0.1:
-    dependencies:
-      micromark-factory-space: 2.0.1
-      micromark-util-character: 2.1.1
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-util-character@2.1.1:
-    dependencies:
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-util-chunked@2.0.1:
-    dependencies:
-      micromark-util-symbol: 2.0.1
-
-  micromark-util-classify-character@2.0.1:
-    dependencies:
-      micromark-util-character: 2.1.1
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-util-combine-extensions@2.0.1:
-    dependencies:
-      micromark-util-chunked: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-util-decode-numeric-character-reference@2.0.2:
-    dependencies:
-      micromark-util-symbol: 2.0.1
-
-  micromark-util-decode-string@2.0.1:
-    dependencies:
-      decode-named-character-reference: 1.1.0
-      micromark-util-character: 2.1.1
-      micromark-util-decode-numeric-character-reference: 2.0.2
-      micromark-util-symbol: 2.0.1
-
-  micromark-util-encode@2.0.1: {}
-
-  micromark-util-events-to-acorn@2.0.3:
-    dependencies:
-      '@types/estree': 1.0.7
-      '@types/unist': 3.0.3
-      devlop: 1.1.0
-      estree-util-visit: 2.0.0
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-      vfile-message: 4.0.2
-
-  micromark-util-html-tag-name@2.0.1: {}
-
-  micromark-util-normalize-identifier@2.0.1:
-    dependencies:
-      micromark-util-symbol: 2.0.1
-
-  micromark-util-resolve-all@2.0.1:
-    dependencies:
-      micromark-util-types: 2.0.2
-
-  micromark-util-sanitize-uri@2.0.1:
-    dependencies:
-      micromark-util-character: 2.1.1
-      micromark-util-encode: 2.0.1
-      micromark-util-symbol: 2.0.1
-
-  micromark-util-subtokenize@2.1.0:
-    dependencies:
-      devlop: 1.1.0
-      micromark-util-chunked: 2.0.1
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-
-  micromark-util-symbol@2.0.1: {}
-
-  micromark-util-types@2.0.2: {}
-
-  micromark@4.0.2:
-    dependencies:
-      '@types/debug': 4.1.12
-      debug: 4.4.0
-      decode-named-character-reference: 1.1.0
-      devlop: 1.1.0
-      micromark-core-commonmark: 2.0.3
-      micromark-factory-space: 2.0.1
-      micromark-util-character: 2.1.1
-      micromark-util-chunked: 2.0.1
-      micromark-util-combine-extensions: 2.0.1
-      micromark-util-decode-numeric-character-reference: 2.0.2
-      micromark-util-encode: 2.0.1
-      micromark-util-normalize-identifier: 2.0.1
-      micromark-util-resolve-all: 2.0.1
-      micromark-util-sanitize-uri: 2.0.1
-      micromark-util-subtokenize: 2.1.0
-      micromark-util-symbol: 2.0.1
-      micromark-util-types: 2.0.2
-    transitivePeerDependencies:
-      - supports-color
-
-  mime-db@1.52.0: {}
-
-  mime-types@2.1.35:
-    dependencies:
-      mime-db: 1.52.0
-
-  minimatch@3.1.2:
-    dependencies:
-      brace-expansion: 1.1.11
-
-  minimist@1.2.0: {}
-
-  minimist@1.2.6: {}
-
-  ms@2.1.3: {}
-
-  nanoid@3.3.11: {}
-
-  node-releases@2.0.19: {}
-
-  object-assign@4.1.1: {}
-
-  once@1.4.0:
-    dependencies:
-      wrappy: 1.0.2
-
-  parent-module@1.0.1:
-    dependencies:
-      callsites: 3.1.0
-
-  parse-author@2.0.0:
-    dependencies:
-      author-regex: 1.0.0
-
-  parse-entities@4.0.2:
-    dependencies:
-      '@types/unist': 2.0.11
-      character-entities-legacy: 3.0.0
-      character-reference-invalid: 2.0.1
-      decode-named-character-reference: 1.1.0
-      is-alphanumerical: 2.0.1
-      is-decimal: 2.0.1
-      is-hexadecimal: 2.0.1
-
-  parse-json@5.2.0:
-    dependencies:
-      '@babel/code-frame': 7.26.2
-      error-ex: 1.3.2
-      json-parse-even-better-errors: 2.3.1
-      lines-and-columns: 1.2.4
-
-  parse-svg-path@0.1.2: {}
-
-  path-browserify@1.0.1: {}
-
-  path-data-parser@0.1.0: {}
-
-  path-is-absolute@1.0.1: {}
-
-  path-source@0.1.3:
-    dependencies:
-      array-source: 0.0.4
-      file-source: 0.6.1
-
-  path-type@4.0.0: {}
-
-  pbf@3.3.0:
-    dependencies:
-      ieee754: 1.2.1
-      resolve-protobuf-schema: 2.1.0
-
-  picocolors@1.1.1: {}
-
-  point-at-length@1.1.0:
-    dependencies:
-      abs-svg-path: 0.1.1
-      isarray: 0.0.1
-      parse-svg-path: 0.1.2
-
-  points-on-curve@0.2.0: {}
-
-  points-on-path@0.2.1:
-    dependencies:
-      path-data-parser: 0.1.0
-      points-on-curve: 0.2.0
-
-  postcss@8.5.3:
-    dependencies:
-      nanoid: 3.3.11
-      picocolors: 1.1.1
-      source-map-js: 1.2.1
-
-  prettier-package-json@2.8.0:
-    dependencies:
-      '@types/parse-author': 2.0.3
-      commander: 4.1.1
-      cosmiconfig: 7.1.0
-      fs-extra: 10.1.0
-      glob: 7.2.3
-      minimatch: 3.1.2
-      parse-author: 2.0.0
-      sort-object-keys: 1.1.3
-      sort-order: 1.1.2
-
-  prettier-plugin-astro@0.14.1:
-    dependencies:
-      '@astrojs/compiler': 2.11.0
-      prettier: 3.5.3
-      sass-formatter: 0.7.9
-
-  prettier-plugin-curly-and-jsdoc@3.1.0(prettier@3.5.3):
-    dependencies:
-      prettier: 3.5.3
-
-  prettier-plugin-pkgsort@0.2.1(prettier@3.5.3):
-    dependencies:
-      prettier: 3.5.3
-      prettier-package-json: 2.8.0
-
-  prettier@3.5.3: {}
-
-  prismjs@1.30.0: {}
-
-  prop-types@15.8.1:
-    dependencies:
-      loose-envify: 1.4.0
-      object-assign: 4.1.1
-      react-is: 16.13.1
-
-  property-information@7.0.0: {}
-
-  protocol-buffers-schema@3.6.0: {}
-
-  react-dom@18.3.1(react@18.3.1):
-    dependencies:
-      loose-envify: 1.4.0
-      react: 18.3.1
-      scheduler: 0.23.2
-
-  react-draggable@4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
-    dependencies:
-      clsx: 1.2.1
-      prop-types: 15.8.1
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-
-  react-dropzone@14.3.8(react@18.3.1):
-    dependencies:
-      attr-accept: 2.2.5
-      file-selector: 2.1.2
-      prop-types: 15.8.1
-      react: 18.3.1
-
-  react-fast-compare@3.2.2: {}
-
-  react-fireworks@1.0.4: {}
-
-  react-i18next@13.5.0(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
-    dependencies:
-      '@babel/runtime': 7.27.0
-      html-parse-stringify: 3.0.1
-      i18next: 23.16.8
-      react: 18.3.1
-    optionalDependencies:
-      react-dom: 18.3.1(react@18.3.1)
-
-  react-is@16.13.1: {}
-
-  react-is@18.3.1: {}
-
-  react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
-    dependencies:
-      '@popperjs/core': 2.11.8
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-      react-fast-compare: 3.2.2
-      warning: 4.0.3
-
-  react-refresh@0.14.2: {}
-
-  react-resizable@3.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
-    dependencies:
-      prop-types: 15.8.1
-      react: 18.3.1
-      react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-    transitivePeerDependencies:
-      - react-dom
-
-  react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
-    dependencies:
-      '@remix-run/router': 1.23.0
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-      react-router: 6.30.0(react@18.3.1)
-
-  react-router@6.30.0(react@18.3.1):
-    dependencies:
-      '@remix-run/router': 1.23.0
-      react: 18.3.1
-
-  react-telegram-login@1.1.2(react@18.3.1):
-    dependencies:
-      react: 18.3.1
-
-  react-toastify@9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
-    dependencies:
-      clsx: 1.2.1
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-
-  react-turnstile@1.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
-    dependencies:
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-
-  react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
-    dependencies:
-      '@babel/runtime': 7.27.0
-      memoize-one: 5.2.1
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-
-  react@18.3.1:
-    dependencies:
-      loose-envify: 1.4.0
-
-  readable-stream@1.1.14:
-    dependencies:
-      core-util-is: 1.0.3
-      inherits: 2.0.4
-      isarray: 0.0.1
-      string_decoder: 0.10.31
-
-  readable-stream@3.6.2:
-    dependencies:
-      inherits: 2.0.4
-      string_decoder: 1.3.0
-      util-deprecate: 1.0.2
-
-  recma-build-jsx@1.0.0:
-    dependencies:
-      '@types/estree': 1.0.7
-      estree-util-build-jsx: 3.0.1
-      vfile: 6.0.3
-
-  recma-jsx@1.0.0(acorn@8.14.1):
-    dependencies:
-      acorn-jsx: 5.3.2(acorn@8.14.1)
-      estree-util-to-js: 2.0.0
-      recma-parse: 1.0.0
-      recma-stringify: 1.0.0
-      unified: 11.0.5
-    transitivePeerDependencies:
-      - acorn
-
-  recma-parse@1.0.0:
-    dependencies:
-      '@types/estree': 1.0.7
-      esast-util-from-js: 2.0.1
-      unified: 11.0.5
-      vfile: 6.0.3
-
-  recma-stringify@1.0.0:
-    dependencies:
-      '@types/estree': 1.0.7
-      estree-util-to-js: 2.0.0
-      unified: 11.0.5
-      vfile: 6.0.3
-
-  regenerator-runtime@0.14.1: {}
-
-  rehype-recma@1.0.0:
-    dependencies:
-      '@types/estree': 1.0.7
-      '@types/hast': 3.0.4
-      hast-util-to-estree: 3.1.3
-    transitivePeerDependencies:
-      - supports-color
-
-  remark-gfm@4.0.1:
-    dependencies:
-      '@types/mdast': 4.0.4
-      mdast-util-gfm: 3.1.0
-      micromark-extension-gfm: 3.0.0
-      remark-parse: 11.0.0
-      remark-stringify: 11.0.0
-      unified: 11.0.5
-    transitivePeerDependencies:
-      - supports-color
-
-  remark-mdx@3.1.0:
-    dependencies:
-      mdast-util-mdx: 3.0.0
-      micromark-extension-mdxjs: 3.0.0
-    transitivePeerDependencies:
-      - supports-color
-
-  remark-parse@11.0.0:
-    dependencies:
-      '@types/mdast': 4.0.4
-      mdast-util-from-markdown: 2.0.2
-      micromark-util-types: 2.0.2
-      unified: 11.0.5
-    transitivePeerDependencies:
-      - supports-color
-
-  remark-rehype@11.1.2:
-    dependencies:
-      '@types/hast': 3.0.4
-      '@types/mdast': 4.0.4
-      mdast-util-to-hast: 13.2.0
-      unified: 11.0.5
-      vfile: 6.0.3
-
-  remark-stringify@11.0.0:
-    dependencies:
-      '@types/mdast': 4.0.4
-      mdast-util-to-markdown: 2.1.2
-      unified: 11.0.5
-
-  resolve-from@4.0.0: {}
-
-  resolve-protobuf-schema@2.1.0:
-    dependencies:
-      protocol-buffers-schema: 3.6.0
-
-  rollup@4.39.0:
-    dependencies:
-      '@types/estree': 1.0.7
-    optionalDependencies:
-      '@rollup/rollup-android-arm-eabi': 4.39.0
-      '@rollup/rollup-android-arm64': 4.39.0
-      '@rollup/rollup-darwin-arm64': 4.39.0
-      '@rollup/rollup-darwin-x64': 4.39.0
-      '@rollup/rollup-freebsd-arm64': 4.39.0
-      '@rollup/rollup-freebsd-x64': 4.39.0
-      '@rollup/rollup-linux-arm-gnueabihf': 4.39.0
-      '@rollup/rollup-linux-arm-musleabihf': 4.39.0
-      '@rollup/rollup-linux-arm64-gnu': 4.39.0
-      '@rollup/rollup-linux-arm64-musl': 4.39.0
-      '@rollup/rollup-linux-loongarch64-gnu': 4.39.0
-      '@rollup/rollup-linux-powerpc64le-gnu': 4.39.0
-      '@rollup/rollup-linux-riscv64-gnu': 4.39.0
-      '@rollup/rollup-linux-riscv64-musl': 4.39.0
-      '@rollup/rollup-linux-s390x-gnu': 4.39.0
-      '@rollup/rollup-linux-x64-gnu': 4.39.0
-      '@rollup/rollup-linux-x64-musl': 4.39.0
-      '@rollup/rollup-win32-arm64-msvc': 4.39.0
-      '@rollup/rollup-win32-ia32-msvc': 4.39.0
-      '@rollup/rollup-win32-x64-msvc': 4.39.0
-      fsevents: 2.3.3
-
-  roughjs@4.5.2:
-    dependencies:
-      path-data-parser: 0.1.0
-      points-on-curve: 0.2.0
-      points-on-path: 0.2.1
-
-  rw@1.3.3: {}
-
-  s.color@0.0.15: {}
-
-  safe-buffer@5.2.1: {}
-
-  safer-buffer@2.1.2: {}
-
-  sass-formatter@0.7.9:
-    dependencies:
-      suf-log: 2.5.3
-
-  scheduler@0.23.2:
-    dependencies:
-      loose-envify: 1.4.0
-
-  scroll-into-view-if-needed@2.2.31:
-    dependencies:
-      compute-scroll-into-view: 1.0.20
-
-  semantic-ui-offline@2.5.0:
-    dependencies:
-      fs-extra: 4.0.3
-      jquery: 3.7.1
-
-  semantic-ui-react@2.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
-    dependencies:
-      '@babel/runtime': 7.27.0
-      '@fluentui/react-component-event-listener': 0.63.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@fluentui/react-component-ref': 0.63.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@popperjs/core': 2.11.8
-      '@semantic-ui-react/event-stack': 3.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      clsx: 1.2.1
-      keyboard-key: 1.1.0
-      lodash: 4.17.21
-      lodash-es: 4.17.21
-      prop-types: 15.8.1
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-      react-is: 18.3.1
-      react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      shallowequal: 1.1.0
-
-  semver@6.3.1: {}
-
-  shallowequal@1.1.0: {}
-
-  shapefile@0.6.6:
-    dependencies:
-      array-source: 0.0.4
-      commander: 2.20.3
-      path-source: 0.1.3
-      slice-source: 0.4.1
-      stream-source: 0.3.5
-      text-encoding: 0.6.4
-
-  simple-statistics@7.8.8: {}
-
-  simplify-geojson@1.0.5:
-    dependencies:
-      concat-stream: 1.4.11
-      minimist: 1.2.6
-      simplify-geometry: 0.0.2
-
-  simplify-geometry@0.0.2: {}
-
-  slice-source@0.4.1: {}
-
-  sort-object-keys@1.1.3: {}
-
-  sort-order@1.1.2: {}
-
-  source-map-js@1.2.1: {}
-
-  source-map@0.7.4: {}
-
-  space-separated-tokens@2.0.2: {}
-
-  sse.js@https://codeload.github.com/mpetazzoni/sse.js/tar.gz/39b9b82aae95fd58d9d08b487845fe230f4b14e6:
-    {}
-
-  stream-source@0.3.5: {}
-
-  string_decoder@0.10.31: {}
-
-  string_decoder@1.3.0:
-    dependencies:
-      safe-buffer: 5.2.1
-
-  stringify-entities@4.0.4:
-    dependencies:
-      character-entities-html4: 2.1.0
-      character-entities-legacy: 3.0.0
-
-  style-to-js@1.1.16:
-    dependencies:
-      style-to-object: 1.0.8
-
-  style-to-object@1.0.8:
-    dependencies:
-      inline-style-parser: 0.2.4
-
-  suf-log@2.5.3:
-    dependencies:
-      s.color: 0.0.15
-
-  text-encoding@0.6.4: {}
-
-  topojson-client@3.1.0:
-    dependencies:
-      commander: 2.20.3
-
-  topojson-server@3.0.1:
-    dependencies:
-      commander: 2.20.3
-
-  trim-lines@3.0.1: {}
-
-  trough@2.2.0: {}
-
-  tslib@2.8.1: {}
-
-  typedarray@0.0.6: {}
-
-  typedarray@0.0.7: {}
-
-  typescript@4.4.2: {}
-
-  unified@11.0.5:
-    dependencies:
-      '@types/unist': 3.0.3
-      bail: 2.0.2
-      devlop: 1.1.0
-      extend: 3.0.2
-      is-plain-obj: 4.1.0
-      trough: 2.2.0
-      vfile: 6.0.3
-
-  unist-util-is@6.0.0:
-    dependencies:
-      '@types/unist': 3.0.3
-
-  unist-util-position-from-estree@2.0.0:
-    dependencies:
-      '@types/unist': 3.0.3
-
-  unist-util-position@5.0.0:
-    dependencies:
-      '@types/unist': 3.0.3
-
-  unist-util-stringify-position@4.0.0:
-    dependencies:
-      '@types/unist': 3.0.3
-
-  unist-util-visit-parents@6.0.1:
-    dependencies:
-      '@types/unist': 3.0.3
-      unist-util-is: 6.0.0
-
-  unist-util-visit@5.0.0:
-    dependencies:
-      '@types/unist': 3.0.3
-      unist-util-is: 6.0.0
-      unist-util-visit-parents: 6.0.1
-
-  universalify@0.1.2: {}
-
-  universalify@2.0.1: {}
-
-  update-browserslist-db@1.1.3(browserslist@4.24.4):
-    dependencies:
-      browserslist: 4.24.4
-      escalade: 3.2.0
-      picocolors: 1.1.1
-
-  util-deprecate@1.0.2: {}
-
-  utility-types@3.11.0: {}
-
-  vfile-message@4.0.2:
-    dependencies:
-      '@types/unist': 3.0.3
-      unist-util-stringify-position: 4.0.0
-
-  vfile@6.0.3:
-    dependencies:
-      '@types/unist': 3.0.3
-      vfile-message: 4.0.2
-
-  vite@5.4.16:
-    dependencies:
-      esbuild: 0.21.5
-      postcss: 8.5.3
-      rollup: 4.39.0
-    optionalDependencies:
-      fsevents: 2.3.3
-
-  void-elements@3.1.0: {}
-
-  warning@4.0.3:
-    dependencies:
-      loose-envify: 1.4.0
-
-  wrappy@1.0.2: {}
-
-  yallist@3.1.1: {}
-
-  yaml@1.10.2: {}
-
-  zwitch@2.0.4: {}

+ 1 - 5
web/src/components/layout/NoticeModal.js

@@ -64,11 +64,7 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
     return (
       <div
         dangerouslySetInnerHTML={{ __html: noticeContent }}
-        className="max-h-[60vh] overflow-y-auto pr-2"
-        style={{
-          scrollbarWidth: 'thin',
-          scrollbarColor: 'var(--semi-color-tertiary) transparent'
-        }}
+        className="notice-content-scroll max-h-[60vh] overflow-y-auto pr-2"
       />
     );
   };

+ 57 - 3
web/src/components/settings/DashboardSetting.js

@@ -1,6 +1,6 @@
-import React, { useEffect, useState } from 'react';
-import { Card, Spin } from '@douyinfe/semi-ui';
-import { API, showError } from '../../helpers';
+import React, { useEffect, useState, useMemo } from 'react';
+import { Card, Spin, Button, Modal } from '@douyinfe/semi-ui';
+import { API, showError, showSuccess } from '../../helpers';
 import SettingsAPIInfo from '../../pages/Setting/Dashboard/SettingsAPIInfo.js';
 import SettingsAnnouncements from '../../pages/Setting/Dashboard/SettingsAnnouncements.js';
 import SettingsFAQ from '../../pages/Setting/Dashboard/SettingsFAQ.js';
@@ -8,6 +8,16 @@ import SettingsUptimeKuma from '../../pages/Setting/Dashboard/SettingsUptimeKuma
 
 const DashboardSetting = () => {
   let [inputs, setInputs] = useState({
+    'console_setting.api_info': '',
+    'console_setting.announcements': '',
+    'console_setting.faq': '',
+    'console_setting.uptime_kuma_groups': '',
+    'console_setting.api_info_enabled': '',
+    'console_setting.announcements_enabled': '',
+    'console_setting.faq_enabled': '',
+    'console_setting.uptime_kuma_enabled': '',
+
+    // 用于迁移检测的旧键,下个版本会删除
     ApiInfo: '',
     Announcements: '',
     FAQ: '',
@@ -16,6 +26,7 @@ const DashboardSetting = () => {
   });
 
   let [loading, setLoading] = useState(false);
+  const [showMigrateModal, setShowMigrateModal] = useState(false); // 下个版本会删除
 
   const getOptions = async () => {
     const res = await API.get('/api/option/');
@@ -49,9 +60,52 @@ const DashboardSetting = () => {
     onRefresh();
   }, []);
 
+  // 用于迁移检测的旧键,下个版本会删除
+  const hasLegacyData = useMemo(() => {
+    const legacyKeys = ['ApiInfo', 'Announcements', 'FAQ', 'UptimeKumaUrl', 'UptimeKumaSlug'];
+    return legacyKeys.some(k => inputs[k]);
+  }, [inputs]);
+
+  useEffect(() => {
+    if (hasLegacyData) {
+      setShowMigrateModal(true);
+    }
+  }, [hasLegacyData]);
+
+  const handleMigrate = async () => {
+    try {
+      setLoading(true);
+      await API.post('/api/option/migrate_console_setting');
+      showSuccess('旧配置迁移完成');
+      await onRefresh();
+      setShowMigrateModal(false);
+    } catch (err) {
+      console.error(err);
+      showError('迁移失败: ' + (err.message || '未知错误'));
+    } finally {
+      setLoading(false);
+    }
+  };
+
   return (
     <>
       <Spin spinning={loading} size='large'>
+        {/* 用于迁移检测的旧键模态框,下个版本会删除 */}
+        <Modal
+          title="配置迁移确认"
+          visible={showMigrateModal}
+          onOk={handleMigrate}
+          onCancel={() => setShowMigrateModal(false)}
+          confirmLoading={loading}
+          okText="确认迁移"
+          cancelText="取消"
+        >
+          <p>检测到旧版本的配置数据,是否要迁移到新的配置格式?</p>
+          <p style={{ color: '#f57c00', marginTop: '10px' }}>
+            <strong>注意:</strong>迁移过程中会自动处理数据格式转换,迁移完成后旧配置将被清除,请在迁移前在数据库中备份好旧配置。
+          </p>
+        </Modal>
+
         {/* API信息管理 */}
         <Card style={{ marginTop: '10px' }}>
           <SettingsAPIInfo options={inputs} refresh={onRefresh} />

+ 7 - 1
web/src/components/settings/OperationSetting.js

@@ -30,6 +30,9 @@ const OperationSetting = () => {
     CompletionRatio: '',
     ModelPrice: '',
     GroupRatio: '',
+    GroupGroupRatio: '',
+    AutoGroups: '',
+    DefaultUseAutoGroup: false,
     UserUsableGroups: '',
     TopUpLink: '',
     'general_setting.docs_link': '',
@@ -74,6 +77,8 @@ const OperationSetting = () => {
         if (
           item.key === 'ModelRatio' ||
           item.key === 'GroupRatio' ||
+          item.key === 'GroupGroupRatio' ||
+          item.key === 'AutoGroups' ||
           item.key === 'UserUsableGroups' ||
           item.key === 'CompletionRatio' ||
           item.key === 'ModelPrice' ||
@@ -83,7 +88,8 @@ const OperationSetting = () => {
         }
         if (
           item.key.endsWith('Enabled') ||
-          ['DefaultCollapseSidebar'].includes(item.key)
+          ['DefaultCollapseSidebar'].includes(item.key) ||
+          ['DefaultUseAutoGroup'].includes(item.key)
         ) {
           newInputs[item.key] = item.value === 'true' ? true : false;
         } else {

+ 52 - 9
web/src/components/settings/PersonalSetting.js

@@ -103,6 +103,7 @@ const PersonalSetting = () => {
     webhookSecret: '',
     notificationEmail: '',
     acceptUnsetModelRatioModel: false,
+    recordIpLog: false,
   });
   const [modelsLoading, setModelsLoading] = useState(true);
   const [showWebhookDocs, setShowWebhookDocs] = useState(true);
@@ -147,6 +148,7 @@ const PersonalSetting = () => {
         notificationEmail: settings.notification_email || '',
         acceptUnsetModelRatioModel:
           settings.accept_unset_model_ratio_model || false,
+        recordIpLog: settings.record_ip_log || false,
       });
     }
   }, [userState?.user?.setting]);
@@ -346,7 +348,7 @@ const PersonalSetting = () => {
   const handleNotificationSettingChange = (type, value) => {
     setNotificationSettings((prev) => ({
       ...prev,
-      [type]: value.target ? value.target.value : value, // 处理 Radio 事件对象
+      [type]: value.target ? value.target.value !== undefined ? value.target.value : value.target.checked : value, // handle checkbox properly
     }));
   };
 
@@ -362,16 +364,17 @@ const PersonalSetting = () => {
         notification_email: notificationSettings.notificationEmail,
         accept_unset_model_ratio_model:
           notificationSettings.acceptUnsetModelRatioModel,
+        record_ip_log: notificationSettings.recordIpLog,
       });
 
       if (res.data.success) {
-        showSuccess(t('通知设置已更新'));
+        showSuccess(t('设置保存成功'));
         await getUserData();
       } else {
         showError(res.data.message);
       }
     } catch (error) {
-      showError(t('更新通知设置失败'));
+      showError(t('设置保存失败'));
     }
   };
 
@@ -1063,7 +1066,7 @@ const PersonalSetting = () => {
                       tab={
                         <div className="flex items-center">
                           <Bell size={16} className="mr-2" />
-                          {t('通知设置')}
+                          {t('其他设置')}
                         </div>
                       }
                       itemKey='notification'
@@ -1228,28 +1231,68 @@ const PersonalSetting = () => {
                           <TabPane
                             tab={t('价格设置')}
                             itemKey='price'
+                          >
+                            <div className="py-4">
+                              <div className="space-y-4">
+                                {/* 接受未设置价格模型 */}
+                                <div className="bg-white rounded-xl">
+                                  <div className="flex items-start">
+                                    <div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center mt-1">
+                                      <Shield size={20} className="text-slate-600" />
+                                    </div>
+                                    <div className="flex-1">
+                                      <div className="flex items-center justify-between">
+                                        <div>
+                                          <Typography.Text strong className="block mb-2">
+                                            {t('接受未设置价格模型')}
+                                          </Typography.Text>
+                                          <div className="text-gray-500 text-sm">
+                                            {t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
+                                          </div>
+                                        </div>
+                                        <Checkbox
+                                          checked={notificationSettings.acceptUnsetModelRatioModel}
+                                          onChange={(e) =>
+                                            handleNotificationSettingChange(
+                                              'acceptUnsetModelRatioModel',
+                                              e.target.checked,
+                                            )
+                                          }
+                                          className="ml-4"
+                                        />
+                                      </div>
+                                    </div>
+                                  </div>
+                                </div>
+                              </div>
+                            </div>
+                          </TabPane>
+
+                          <TabPane
+                            tab={t('IP记录')}
+                            itemKey='ip'
                           >
                             <div className="py-4">
                               <div className="bg-white rounded-xl">
                                 <div className="flex items-start">
                                   <div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center mt-1">
-                                    <Shield size={20} className="text-slate-600" />
+                                    <ShieldCheck size={20} className="text-slate-600" />
                                   </div>
                                   <div className="flex-1">
                                     <div className="flex items-center justify-between">
                                       <div>
                                         <Typography.Text strong className="block mb-2">
-                                          {t('接受未设置价格模型')}
+                                          {t('记录请求与错误日志 IP')}
                                         </Typography.Text>
                                         <div className="text-gray-500 text-sm">
-                                          {t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
+                                          {t('开启后,仅“消费”和“错误”日志将记录您的客户端 IP 地址')}
                                         </div>
                                       </div>
                                       <Checkbox
-                                        checked={notificationSettings.acceptUnsetModelRatioModel}
+                                        checked={notificationSettings.recordIpLog}
                                         onChange={(e) =>
                                           handleNotificationSettingChange(
-                                            'acceptUnsetModelRatioModel',
+                                            'recordIpLog',
                                             e.target.checked,
                                           )
                                         }

+ 14 - 33
web/src/components/table/ChannelsTable.js

@@ -865,32 +865,22 @@ const ChannelsTable = () => {
         tagChannelDates.response_time = tagChannelDates.response_time / 2;
       }
     }
-    // data.key = '' + data.id
     setChannels(channelDates);
-    if (channelDates.length >= pageSize) {
-      setChannelCount(channelDates.length + pageSize);
-    } else {
-      setChannelCount(channelDates.length);
-    }
   };
 
-  const loadChannels = async (startIdx, pageSize, idSort, enableTagMode) => {
+  const loadChannels = async (page, pageSize, idSort, enableTagMode) => {
     setLoading(true);
     const res = await API.get(
-      `/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}`,
+      `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}`,
     );
     if (res === undefined) {
       return;
     }
     const { success, message, data } = res.data;
     if (success) {
-      if (startIdx === 0) {
-        setChannelFormat(data, enableTagMode);
-      } else {
-        let newChannels = [...channels];
-        newChannels.splice(startIdx * pageSize, data.length, ...data);
-        setChannelFormat(newChannels, enableTagMode);
-      }
+      const { items, total } = data;
+      setChannelFormat(items, enableTagMode);
+      setChannelCount(total);
     } else {
       showError(message);
     }
@@ -903,7 +893,6 @@ const ChannelsTable = () => {
     channelToCopy.created_time = null;
     channelToCopy.balance = 0;
     channelToCopy.used_quota = 0;
-    // 删除可能导致类型不匹配的字段
     delete channelToCopy.test_time;
     delete channelToCopy.response_time;
     if (!channelToCopy) {
@@ -927,7 +916,7 @@ const ChannelsTable = () => {
   const refresh = async () => {
     const { searchKeyword, searchGroup, searchModel } = getFormValues();
     if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
-      await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
+      await loadChannels(activePage, pageSize, idSort, enableTagMode);
     } else {
       await searchChannels(enableTagMode);
     }
@@ -944,7 +933,7 @@ const ChannelsTable = () => {
     setPageSize(localPageSize);
     setEnableTagMode(localEnableTagMode);
     setEnableBatchDelete(localEnableBatchDelete);
-    loadChannels(0, localPageSize, localIdSort, localEnableTagMode)
+    loadChannels(1, localPageSize, localIdSort, localEnableTagMode)
       .then()
       .catch((reason) => {
         showError(reason);
@@ -1052,7 +1041,6 @@ const ChannelsTable = () => {
     try {
       if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
         await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
-        // setActivePage(1);
         return;
       }
 
@@ -1191,24 +1179,18 @@ const ChannelsTable = () => {
     }
   };
 
-  let pageData = channels.slice(
-    (activePage - 1) * pageSize,
-    activePage * pageSize,
-  );
+  let pageData = channels;
 
   const handlePageChange = (page) => {
     setActivePage(page);
-    if (page === Math.ceil(channels.length / pageSize) + 1) {
-      // In this case we have to load more data and then append them.
-      loadChannels(page - 1, pageSize, idSort, enableTagMode).then((r) => { });
-    }
+    loadChannels(page, pageSize, idSort, enableTagMode).then(() => { });
   };
 
   const handlePageSizeChange = async (size) => {
     localStorage.setItem('page-size', size + '');
     setPageSize(size);
     setActivePage(1);
-    loadChannels(0, size, idSort, enableTagMode)
+    loadChannels(1, size, idSort, enableTagMode)
       .then()
       .catch((reason) => {
         showError(reason);
@@ -1218,8 +1200,6 @@ const ChannelsTable = () => {
   const fetchGroups = async () => {
     try {
       let res = await API.get(`/api/group/`);
-      // add 'all' option
-      // res.data.data.unshift('all');
       if (res === undefined) {
         return;
       }
@@ -1514,7 +1494,7 @@ const ChannelsTable = () => {
               onChange={(v) => {
                 localStorage.setItem('id-sort', v + '');
                 setIdSort(v);
-                loadChannels(0, pageSize, v, enableTagMode);
+                loadChannels(activePage, pageSize, v, enableTagMode);
               }}
             />
           </div>
@@ -1541,7 +1521,8 @@ const ChannelsTable = () => {
               onChange={(v) => {
                 localStorage.setItem('enable-tag-mode', v + '');
                 setEnableTagMode(v);
-                loadChannels(0, pageSize, idSort, v);
+                setActivePage(1);
+                loadChannels(1, pageSize, idSort, v);
               }}
             />
           </div>
@@ -1703,7 +1684,7 @@ const ChannelsTable = () => {
             formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
               start: page.currentStart,
               end: page.currentEnd,
-              total: channels.length,
+              total: channelCount,
             }),
             onPageSizeChange: (size) => {
               handlePageSizeChange(size);

+ 127 - 58
web/src/components/table/LogsTable.js

@@ -20,7 +20,7 @@ import {
   renderQuota,
   stringToColor,
   getLogOther,
-  renderModelTag
+  renderModelTag,
 } from '../../helpers';
 
 import {
@@ -39,15 +39,15 @@ import {
   Card,
   Typography,
   Divider,
-  Form
+  Form,
 } from '@douyinfe/semi-ui';
 import {
   IllustrationNoResult,
-  IllustrationNoResultDark
+  IllustrationNoResultDark,
 } from '@douyinfe/semi-illustrations';
 import { ITEMS_PER_PAGE } from '../../constants';
 import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
-import { IconSetting, IconSearch } from '@douyinfe/semi-icons';
+import { IconSetting, IconSearch, IconHelpCircle } from '@douyinfe/semi-icons';
 import { Route } from 'lucide-react';
 
 const { Text } = Typography;
@@ -192,7 +192,7 @@ const LogsTable = () => {
     if (!modelMapped) {
       return renderModelTag(record.model_name, {
         onClick: (event) => {
-          copyText(event, record.model_name).then((r) => { });
+          copyText(event, record.model_name).then((r) => {});
         },
       });
     } else {
@@ -209,7 +209,7 @@ const LogsTable = () => {
                       </Text>
                       {renderModelTag(record.model_name, {
                         onClick: (event) => {
-                          copyText(event, record.model_name).then((r) => { });
+                          copyText(event, record.model_name).then((r) => {});
                         },
                       })}
                     </div>
@@ -220,7 +220,7 @@ const LogsTable = () => {
                       {renderModelTag(other.upstream_model_name, {
                         onClick: (event) => {
                           copyText(event, other.upstream_model_name).then(
-                            (r) => { },
+                            (r) => {},
                           );
                         },
                       })}
@@ -231,7 +231,7 @@ const LogsTable = () => {
             >
               {renderModelTag(record.model_name, {
                 onClick: (event) => {
-                  copyText(event, record.model_name).then((r) => { });
+                  copyText(event, record.model_name).then((r) => {});
                 },
                 suffixIcon: (
                   <Route
@@ -260,6 +260,7 @@ const LogsTable = () => {
     COMPLETION: 'completion',
     COST: 'cost',
     RETRY: 'retry',
+    IP: 'ip',
     DETAILS: 'details',
   };
 
@@ -301,6 +302,7 @@ const LogsTable = () => {
       [COLUMN_KEYS.COMPLETION]: true,
       [COLUMN_KEYS.COST]: true,
       [COLUMN_KEYS.RETRY]: isAdminUser,
+      [COLUMN_KEYS.IP]: true,
       [COLUMN_KEYS.DETAILS]: true,
     };
   };
@@ -485,6 +487,9 @@ const LogsTable = () => {
       title: t('用时/首字'),
       dataIndex: 'use_time',
       render: (text, record, index) => {
+        if (!(record.type === 2 || record.type === 5)) {
+          return <></>;
+        }
         if (record.is_stream) {
           let other = getLogOther(record.other);
           return (
@@ -545,12 +550,45 @@ const LogsTable = () => {
         );
       },
     },
+    {
+      key: COLUMN_KEYS.IP,
+      title: (
+        <div className="flex items-center gap-1">
+          {t('IP')}
+          <Tooltip content={t('只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录')}>
+            <IconHelpCircle className="text-gray-400 cursor-help" />
+          </Tooltip>
+        </div>
+      ),
+      dataIndex: 'ip',
+      render: (text, record, index) => {
+        return (record.type === 2 || record.type === 5) && text ? (
+          <Tooltip content={text}>
+            <Tag
+              color='orange'
+              size='large'
+              shape='circle'
+              onClick={(event) => {
+                copyText(event, text);
+              }}
+            >
+              {text}
+            </Tag>
+          </Tooltip>
+        ) : (
+          <></>
+        );
+      },
+    },
     {
       key: COLUMN_KEYS.RETRY,
       title: t('重试'),
       dataIndex: 'retry',
       className: isAdmin() ? 'tableShow' : 'tableHiddle',
       render: (text, record, index) => {
+        if (!(record.type === 2 || record.type === 5)) {
+          return <></>;
+        }
         let content = t('渠道') + `:${record.channel}`;
         if (record.other !== '') {
           let other = JSON.parse(record.other);
@@ -598,21 +636,23 @@ const LogsTable = () => {
         }
         let content = other?.claude
           ? renderClaudeModelPriceSimple(
-            other.model_ratio,
-            other.model_price,
-            other.group_ratio,
-            other.cache_tokens || 0,
-            other.cache_ratio || 1.0,
-            other.cache_creation_tokens || 0,
-            other.cache_creation_ratio || 1.0,
-          )
+              other.model_ratio,
+              other.model_price,
+              other.group_ratio,
+              other?.user_group_ratio,
+              other.cache_tokens || 0,
+              other.cache_ratio || 1.0,
+              other.cache_creation_tokens || 0,
+              other.cache_creation_ratio || 1.0,
+            )
           : renderModelPriceSimple(
-            other.model_ratio,
-            other.model_price,
-            other.group_ratio,
-            other.cache_tokens || 0,
-            other.cache_ratio || 1.0,
-          );
+              other.model_ratio,
+              other.model_price,
+              other.group_ratio,
+              other?.user_group_ratio,
+              other.cache_tokens || 0,
+              other.cache_ratio || 1.0,
+            );
         return (
           <Paragraph
             ellipsis={{
@@ -742,7 +782,7 @@ const LogsTable = () => {
     group: '',
     dateRange: [
       timestamp2string(getTodayStartTimestamp()),
-      timestamp2string(now.getTime() / 1000 + 3600)
+      timestamp2string(now.getTime() / 1000 + 3600),
     ],
     logType: '0',
   };
@@ -763,7 +803,11 @@ const LogsTable = () => {
     let start_timestamp = timestamp2string(getTodayStartTimestamp());
     let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
 
-    if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) {
+    if (
+      formValues.dateRange &&
+      Array.isArray(formValues.dateRange) &&
+      formValues.dateRange.length === 2
+    ) {
       start_timestamp = formValues.dateRange[0];
       end_timestamp = formValues.dateRange[1];
     }
@@ -941,27 +985,27 @@ const LogsTable = () => {
           key: t('日志详情'),
           value: other?.claude
             ? renderClaudeLogContent(
-              other?.model_ratio,
-              other.completion_ratio,
-              other.model_price,
-              other.group_ratio,
-              other.cache_ratio || 1.0,
-              other.cache_creation_ratio || 1.0,
-            )
+                other?.model_ratio,
+                other.completion_ratio,
+                other.model_price,
+                other.group_ratio,
+                other?.user_group_ratio,
+                other.cache_ratio || 1.0,
+                other.cache_creation_ratio || 1.0,
+              )
             : renderLogContent(
-              other?.model_ratio,
-              other.completion_ratio,
-              other.model_price,
-              other.group_ratio,
-              other?.user_group_ratio,
-              false,
-              1.0,
-              undefined,
-              other.web_search || false,
-              other.web_search_call_count || 0,
-              other.file_search || false,
-              other.file_search_call_count || 0,
-            ),
+                other?.model_ratio,
+                other.completion_ratio,
+                other.model_price,
+                other.group_ratio,
+                other?.user_group_ratio,
+                false,
+                1.0,
+                other.web_search || false,
+                other.web_search_call_count || 0,
+                other.file_search || false,
+                other.file_search_call_count || 0,
+              ),
         });
       }
       if (logs[i].type === 2) {
@@ -992,6 +1036,7 @@ const LogsTable = () => {
             other?.audio_ratio,
             other?.audio_completion_ratio,
             other?.group_ratio,
+            other?.user_group_ratio,
             other?.cache_tokens || 0,
             other?.cache_ratio || 1.0,
           );
@@ -1003,6 +1048,7 @@ const LogsTable = () => {
             other.model_price,
             other.completion_ratio,
             other.group_ratio,
+            other?.user_group_ratio,
             other.cache_tokens || 0,
             other.cache_ratio || 1.0,
             other.cache_creation_tokens || 0,
@@ -1016,6 +1062,7 @@ const LogsTable = () => {
             other?.model_price,
             other?.completion_ratio,
             other?.group_ratio,
+            other?.user_group_ratio,
             other?.cache_tokens || 0,
             other?.cache_ratio || 1.0,
             other?.image || false,
@@ -1066,7 +1113,12 @@ const LogsTable = () => {
     } = getFormValues();
 
     // 使用传入的 logType 或者表单中的 logType 或者状态中的 logType
-    const currentLogType = customLogType !== null ? customLogType : formLogType !== undefined ? formLogType : logType;
+    const currentLogType =
+      customLogType !== null
+        ? customLogType
+        : formLogType !== undefined
+          ? formLogType
+          : logType;
 
     let localStartTimestamp = Date.parse(start_timestamp) / 1000;
     let localEndTimestamp = Date.parse(end_timestamp) / 1000;
@@ -1093,7 +1145,7 @@ const LogsTable = () => {
 
   const handlePageChange = (page) => {
     setActivePage(page);
-    loadLogs(page, pageSize).then((r) => { }); // 不传入logType,让其从表单获取最新值
+    loadLogs(page, pageSize).then((r) => {}); // 不传入logType,让其从表单获取最新值
   };
 
   const handlePageSizeChange = async (size) => {
@@ -1208,9 +1260,9 @@ const LogsTable = () => {
               getFormApi={(api) => setFormApi(api)}
               onSubmit={refresh}
               allowEmpty={true}
-              autoComplete="off"
-              layout="vertical"
-              trigger="change"
+              autoComplete='off'
+              layout='vertical'
+              trigger='change'
               stopValidateWithError={false}
             >
               <div className='flex flex-col gap-4'>
@@ -1294,12 +1346,24 @@ const LogsTable = () => {
                         }, 0);
                       }}
                     >
-                      <Form.Select.Option value='0'>{t('全部')}</Form.Select.Option>
-                      <Form.Select.Option value='1'>{t('充值')}</Form.Select.Option>
-                      <Form.Select.Option value='2'>{t('消费')}</Form.Select.Option>
-                      <Form.Select.Option value='3'>{t('管理')}</Form.Select.Option>
-                      <Form.Select.Option value='4'>{t('系统')}</Form.Select.Option>
-                      <Form.Select.Option value='5'>{t('错误')}</Form.Select.Option>
+                      <Form.Select.Option value='0'>
+                        {t('全部')}
+                      </Form.Select.Option>
+                      <Form.Select.Option value='1'>
+                        {t('充值')}
+                      </Form.Select.Option>
+                      <Form.Select.Option value='2'>
+                        {t('消费')}
+                      </Form.Select.Option>
+                      <Form.Select.Option value='3'>
+                        {t('管理')}
+                      </Form.Select.Option>
+                      <Form.Select.Option value='4'>
+                        {t('系统')}
+                      </Form.Select.Option>
+                      <Form.Select.Option value='5'>
+                        {t('错误')}
+                      </Form.Select.Option>
                     </Form.Select>
                   </div>
 
@@ -1351,7 +1415,8 @@ const LogsTable = () => {
           {...(hasExpandableRows() && {
             expandedRowRender: expandRowRender,
             expandRowByClick: true,
-            rowExpandable: (record) => expandData[record.key] && expandData[record.key].length > 0
+            rowExpandable: (record) =>
+              expandData[record.key] && expandData[record.key].length > 0,
           })}
           dataSource={logs}
           rowKey='key'
@@ -1361,8 +1426,12 @@ const LogsTable = () => {
           size='middle'
           empty={
             <Empty
-              image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
-              darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+              image={
+                <IllustrationNoResult style={{ width: 150, height: 150 }} />
+              }
+              darkModeImage={
+                <IllustrationNoResultDark style={{ width: 150, height: 150 }} />
+              }
               description={t('搜索无结果')}
               style={{ padding: 30 }}
             />

+ 27 - 45
web/src/components/table/MjLogsTable.js

@@ -601,7 +601,7 @@ const LogsTable = () => {
   const [logs, setLogs] = useState([]);
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
-  const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
+  const [logCount, setLogCount] = useState(0);
   const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
   const [isModalOpenurl, setIsModalOpenurl] = useState(false);
   const [showBanner, setShowBanner] = useState(false);
@@ -649,69 +649,53 @@ const LogsTable = () => {
     };
   };
 
-  const setLogsFormat = (logs) => {
-    for (let i = 0; i < logs.length; i++) {
-      logs[i].timestamp2string = timestamp2string(logs[i].created_at);
-      logs[i].key = '' + logs[i].id;
-    }
-    // data.key = '' + data.id
-    setLogs(logs);
-    setLogCount(logs.length + pageSize);
-    // console.log(logCount);
+  const enrichLogs = (items) => {
+    return items.map((log) => ({
+      ...log,
+      timestamp2string: timestamp2string(log.created_at),
+      key: '' + log.id,
+    }));
   };
 
-  const loadLogs = async (startIdx, pageSize = ITEMS_PER_PAGE) => {
-    setLoading(true);
+  const syncPageData = (payload) => {
+    const items = enrichLogs(payload.items || []);
+    setLogs(items);
+    setLogCount(payload.total || 0);
+    setActivePage(payload.page || 1);
+    setPageSize(payload.page_size || pageSize);
+  };
 
-    let url = '';
+  const loadLogs = async (page = 1, size = pageSize) => {
+    setLoading(true);
     const { channel_id, mj_id, start_timestamp, end_timestamp } = getFormValues();
     let localStartTimestamp = Date.parse(start_timestamp);
     let localEndTimestamp = Date.parse(end_timestamp);
-    if (isAdminUser) {
-      url = `/api/mj/?p=${startIdx}&page_size=${pageSize}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
-    } else {
-      url = `/api/mj/self/?p=${startIdx}&page_size=${pageSize}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
-    }
+    const url = isAdminUser
+      ? `/api/mj/?p=${page}&page_size=${size}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
+      : `/api/mj/self/?p=${page}&page_size=${size}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
     const res = await API.get(url);
     const { success, message, data } = res.data;
     if (success) {
-      if (startIdx === 0) {
-        setLogsFormat(data);
-      } else {
-        let newLogs = [...logs];
-        newLogs.splice(startIdx * pageSize, data.length, ...data);
-        setLogsFormat(newLogs);
-      }
+      syncPageData(data);
     } else {
       showError(message);
     }
     setLoading(false);
   };
 
-  const pageData = logs.slice(
-    (activePage - 1) * pageSize,
-    activePage * pageSize,
-  );
+  const pageData = logs;
 
   const handlePageChange = (page) => {
-    setActivePage(page);
-    if (page === Math.ceil(logs.length / pageSize) + 1) {
-      // In this case we have to load more data and then append them.
-      loadLogs(page - 1, pageSize).then((r) => { });
-    }
+    loadLogs(page, pageSize).then();
   };
 
   const handlePageSizeChange = async (size) => {
     localStorage.setItem('mj-page-size', size + '');
-    setPageSize(size);
-    setActivePage(1);
-    await loadLogs(0, size);
+    await loadLogs(1, size);
   };
 
   const refresh = async () => {
-    // setLoading(true);
-    setActivePage(1);
-    await loadLogs(0, pageSize);
+    await loadLogs(1, pageSize);
   };
 
   const copyText = async (text) => {
@@ -726,7 +710,7 @@ const LogsTable = () => {
   useEffect(() => {
     const localPageSize = parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
     setPageSize(localPageSize);
-    loadLogs(0, localPageSize).then();
+    loadLogs(1, localPageSize).then();
   }, []);
 
   useEffect(() => {
@@ -936,7 +920,7 @@ const LogsTable = () => {
         >
           <Table
             columns={getVisibleColumns()}
-            dataSource={pageData}
+            dataSource={logs}
             rowKey='key'
             loading={loading}
             scroll={{ x: 'max-content' }}
@@ -962,9 +946,7 @@ const LogsTable = () => {
               total: logCount,
               pageSizeOptions: [10, 20, 50, 100],
               showSizeChanger: true,
-              onPageSizeChange: (size) => {
-                handlePageSizeChange(size);
-              },
+              onPageSizeChange: handlePageSizeChange,
               onPageChange: handlePageChange,
             }}
           />

+ 79 - 36
web/src/components/table/RedemptionsTable.js

@@ -13,7 +13,8 @@ import {
   XCircle,
   Minus,
   HelpCircle,
-  Coins
+  Coins,
+  Ticket
 } from 'lucide-react';
 
 import { ITEMS_PER_PAGE } from '../../constants';
@@ -58,7 +59,16 @@ function renderTimestamp(timestamp) {
 const RedemptionsTable = () => {
   const { t } = useTranslation();
 
-  const renderStatus = (status) => {
+  const isExpired = (rec) => {
+    return rec.status === 1 && rec.expired_time !== 0 && rec.expired_time < Math.floor(Date.now() / 1000);
+  };
+
+  const renderStatus = (status, record) => {
+    if (isExpired(record)) {
+      return (
+        <Tag color='orange' size='large' shape='circle' prefixIcon={<Minus size={14} />}>{t('已过期')}</Tag>
+      );
+    }
     switch (status) {
       case 1:
         return (
@@ -101,7 +111,7 @@ const RedemptionsTable = () => {
       dataIndex: 'status',
       key: 'status',
       render: (text, record, index) => {
-        return <div>{renderStatus(text)}</div>;
+        return <div>{renderStatus(text, record)}</div>;
       },
     },
     {
@@ -124,6 +134,13 @@ const RedemptionsTable = () => {
         return <div>{renderTimestamp(text)}</div>;
       },
     },
+    {
+      title: t('过期时间'),
+      dataIndex: 'expired_time',
+      render: (text) => {
+        return <div>{text === 0 ? t('永不过期') : renderTimestamp(text)}</div>;
+      },
+    },
     {
       title: t('兑换人ID'),
       dataIndex: 'used_user_id',
@@ -157,8 +174,7 @@ const RedemptionsTable = () => {
           }
         ];
 
-        // 动态添加启用/禁用按钮
-        if (record.status === 1) {
+        if (record.status === 1 && !isExpired(record)) {
           moreMenuItems.push({
             node: 'item',
             name: t('禁用'),
@@ -168,7 +184,7 @@ const RedemptionsTable = () => {
               manageRedemption(record.id, 'disable', record);
             },
           });
-        } else {
+        } else if (!isExpired(record)) {
           moreMenuItems.push({
             node: 'item',
             name: t('启用'),
@@ -435,7 +451,7 @@ const RedemptionsTable = () => {
   };
 
   const handleRow = (record, index) => {
-    if (record.status !== 1) {
+    if (record.status !== 1 || isExpired(record)) {
       return {
         style: {
           background: 'var(--semi-color-disabled-border)',
@@ -450,7 +466,7 @@ const RedemptionsTable = () => {
     <div className="flex flex-col w-full">
       <div className="mb-2">
         <div className="flex items-center text-orange-500">
-          <IconEyeOpened className="mr-2" />
+          <Ticket size={16} className="mr-2" />
           <Text>{t('兑换码可以批量生成和分发,适合用于推广活动或批量充值。')}</Text>
         </div>
       </div>
@@ -458,39 +474,66 @@ const RedemptionsTable = () => {
       <Divider margin="12px" />
 
       <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
-        <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
+        <div className="flex flex-col sm:flex-row gap-2 w-full md:w-auto order-2 md:order-1">
+          <div className="flex gap-2 w-full sm:w-auto">
+            <Button
+              theme='light'
+              type='primary'
+              icon={<IconPlus />}
+              className="!rounded-full w-full sm:w-auto"
+              onClick={() => {
+                setEditingRedemption({
+                  id: undefined,
+                });
+                setShowEdit(true);
+              }}
+            >
+              {t('添加兑换码')}
+            </Button>
+            <Button
+              type='warning'
+              icon={<IconCopy />}
+              className="!rounded-full w-full sm:w-auto"
+              onClick={async () => {
+                if (selectedKeys.length === 0) {
+                  showError(t('请至少选择一个兑换码!'));
+                  return;
+                }
+                let keys = '';
+                for (let i = 0; i < selectedKeys.length; i++) {
+                  keys +=
+                    selectedKeys[i].name + '    ' + selectedKeys[i].key + '\n';
+                }
+                await copyText(keys);
+              }}
+            >
+              {t('复制所选兑换码到剪贴板')}
+            </Button>
+          </div>
           <Button
-            theme='light'
-            type='primary'
-            icon={<IconPlus />}
-            className="!rounded-full w-full md:w-auto"
+            type='danger'
+            icon={<IconDelete />}
+            className="!rounded-full w-full sm:w-auto"
             onClick={() => {
-              setEditingRedemption({
-                id: undefined,
+              Modal.confirm({
+                title: t('确定清除所有失效兑换码?'),
+                content: t('将删除已使用、已禁用及过期的兑换码,此操作不可撤销。'),
+                onOk: async () => {
+                  setLoading(true);
+                  const res = await API.delete('/api/redemption/invalid');
+                  const { success, message, data } = res.data;
+                  if (success) {
+                    showSuccess(t('已删除 {{count}} 条失效兑换码', { count: data }));
+                    await refresh();
+                  } else {
+                    showError(message);
+                  }
+                  setLoading(false);
+                },
               });
-              setShowEdit(true);
-            }}
-          >
-            {t('添加兑换码')}
-          </Button>
-          <Button
-            type='warning'
-            icon={<IconCopy />}
-            className="!rounded-full w-full md:w-auto"
-            onClick={async () => {
-              if (selectedKeys.length === 0) {
-                showError(t('请至少选择一个兑换码!'));
-                return;
-              }
-              let keys = '';
-              for (let i = 0; i < selectedKeys.length; i++) {
-                keys +=
-                  selectedKeys[i].name + '    ' + selectedKeys[i].key + '\n';
-              }
-              await copyText(keys);
             }}
           >
-            {t('复制所选兑换码到剪贴板')}
+            {t('清除失效兑换码')}
           </Button>
         </div>
 

+ 35 - 51
web/src/components/table/TaskLogsTable.js

@@ -451,10 +451,16 @@ const LogsTable = () => {
     return allColumns.filter((column) => visibleColumns[column.key]);
   };
 
-  const [logs, setLogs] = useState([]);
-  const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
-  const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
+  const [logCount, setLogCount] = useState(0);
+  const [logs, setLogs] = useState([]);
+  const [loading, setLoading] = useState(false);
+
+  useEffect(() => {
+    const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
+    setPageSize(localPageSize);
+    loadLogs(1, localPageSize).then();
+  }, []);
 
   let now = new Date();
   // 初始化start_timestamp为前一天
@@ -494,67 +500,53 @@ const LogsTable = () => {
     };
   };
 
-  const setLogsFormat = (logs) => {
-    for (let i = 0; i < logs.length; i++) {
-      logs[i].timestamp2string = timestamp2string(logs[i].created_at);
-      logs[i].key = '' + logs[i].id;
-    }
-    // data.key = '' + data.id
-    setLogs(logs);
-    setLogCount(logs.length + ITEMS_PER_PAGE);
-    // console.log(logCount);
+  const enrichLogs = (items) => {
+    return items.map((log) => ({
+      ...log,
+      timestamp2string: timestamp2string(log.created_at),
+      key: '' + log.id,
+    }));
   };
 
-  const loadLogs = async (startIdx, pageSize = ITEMS_PER_PAGE) => {
-    setLoading(true);
+  const syncPageData = (payload) => {
+    const items = enrichLogs(payload.items || []);
+    setLogs(items);
+    setLogCount(payload.total || 0);
+    setActivePage(payload.page || 1);
+    setPageSize(payload.page_size || pageSize);
+  };
 
-    let url = '';
+  const loadLogs = async (page = 1, size = pageSize) => {
+    setLoading(true);
     const { channel_id, task_id, start_timestamp, end_timestamp } = getFormValues();
     let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
     let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000);
-    if (isAdminUser) {
-      url = `/api/task/?p=${startIdx}&page_size=${pageSize}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
-    } else {
-      url = `/api/task/self?p=${startIdx}&page_size=${pageSize}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
-    }
+    let url = isAdminUser
+      ? `/api/task/?p=${page}&page_size=${size}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
+      : `/api/task/self?p=${page}&page_size=${size}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
     const res = await API.get(url);
-    let { success, message, data } = res.data;
+    const { success, message, data } = res.data;
     if (success) {
-      if (startIdx === 0) {
-        setLogsFormat(data);
-      } else {
-        let newLogs = [...logs];
-        newLogs.splice(startIdx * pageSize, data.length, ...data);
-        setLogsFormat(newLogs);
-      }
+      syncPageData(data);
     } else {
       showError(message);
     }
     setLoading(false);
   };
 
-  const pageData = logs.slice(
-    (activePage - 1) * pageSize,
-    activePage * pageSize,
-  );
+  const pageData = logs;
 
   const handlePageChange = (page) => {
-    setActivePage(page);
-    if (page === Math.ceil(logs.length / pageSize) + 1) {
-      loadLogs(page - 1, pageSize).then((r) => { });
-    }
+    loadLogs(page, pageSize).then();
   };
 
   const handlePageSizeChange = async (size) => {
     localStorage.setItem('task-page-size', size + '');
-    setPageSize(size);
-    setActivePage(1);
-    await loadLogs(0, size);
+    await loadLogs(1, size);
   };
 
   const refresh = async () => {
-    setActivePage(1);
-    await loadLogs(0, pageSize);
+    await loadLogs(1, pageSize);
   };
 
   const copyText = async (text) => {
@@ -565,12 +557,6 @@ const LogsTable = () => {
     }
   };
 
-  useEffect(() => {
-    const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
-    setPageSize(localPageSize);
-    loadLogs(0, localPageSize).then();
-  }, []);
-
   // 列选择器模态框
   const renderColumnSelector = () => {
     return (
@@ -763,7 +749,7 @@ const LogsTable = () => {
         >
           <Table
             columns={getVisibleColumns()}
-            dataSource={pageData}
+            dataSource={logs}
             rowKey='key'
             loading={loading}
             scroll={{ x: 'max-content' }}
@@ -789,9 +775,7 @@ const LogsTable = () => {
               total: logCount,
               pageSizeOptions: [10, 20, 50, 100],
               showSizeChanger: true,
-              onPageSizeChange: (size) => {
-                handlePageSizeChange(size);
-              },
+              onPageSizeChange: handlePageSizeChange,
               onPageChange: handlePageChange,
             }}
           />

+ 42 - 40
web/src/components/table/TokensTable.js

@@ -14,6 +14,7 @@ import { ITEMS_PER_PAGE } from '../../constants';
 import {
   Button,
   Card,
+  Divider,
   Dropdown,
   Empty,
   Form,
@@ -21,7 +22,8 @@ import {
   Space,
   SplitButtonGroup,
   Table,
-  Tag
+  Tag,
+  Typography
 } from '@douyinfe/semi-ui';
 import {
   IllustrationNoResult,
@@ -36,7 +38,8 @@ import {
   Gauge,
   HelpCircle,
   Infinity,
-  Coins
+  Coins,
+  Key
 } from 'lucide-react';
 
 import {
@@ -54,6 +57,8 @@ import {
 import EditToken from '../../pages/Token/EditToken';
 import { useTranslation } from 'react-i18next';
 
+const { Text } = Typography;
+
 function renderTimestamp(timestamp) {
   return <>{timestamp2string(timestamp)}</>;
 }
@@ -408,31 +413,20 @@ const TokensTable = () => {
     }, 500);
   };
 
-  const setTokensFormat = (tokens) => {
-    setTokens(tokens);
-    if (tokens.length >= pageSize) {
-      setTokenCount(tokens.length + pageSize);
-    } else {
-      setTokenCount(tokens.length);
-    }
+  // 将后端返回的数据写入状态
+  const syncPageData = (payload) => {
+    setTokens(payload.items || []);
+    setTokenCount(payload.total || 0);
+    setActivePage(payload.page || 1);
+    setPageSize(payload.page_size || pageSize);
   };
 
-  let pageData = tokens.slice(
-    (activePage - 1) * pageSize,
-    activePage * pageSize,
-  );
-  const loadTokens = async (startIdx) => {
+  const loadTokens = async (page = 1, size = pageSize) => {
     setLoading(true);
-    const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}`);
+    const res = await API.get(`/api/token/?p=${page}&size=${size}`);
     const { success, message, data } = res.data;
     if (success) {
-      if (startIdx === 0) {
-        setTokensFormat(data);
-      } else {
-        let newTokens = [...tokens];
-        newTokens.splice(startIdx * pageSize, data.length, ...data);
-        setTokensFormat(newTokens);
-      }
+      syncPageData(data);
     } else {
       showError(message);
     }
@@ -440,7 +434,7 @@ const TokensTable = () => {
   };
 
   const refresh = async () => {
-    await loadTokens(activePage - 1);
+    await loadTokens(1);
   };
 
   const copyText = async (text) => {
@@ -473,7 +467,7 @@ const TokensTable = () => {
   };
 
   useEffect(() => {
-    loadTokens(0)
+    loadTokens(1)
       .then()
       .catch((reason) => {
         showError(reason);
@@ -487,7 +481,7 @@ const TokensTable = () => {
 
       if (idx > -1) {
         newDataSource.splice(idx, 1);
-        setTokensFormat(newDataSource);
+        setTokens(newDataSource);
       }
     }
   };
@@ -518,7 +512,7 @@ const TokensTable = () => {
       } else {
         record.status = token.status;
       }
-      setTokensFormat(newTokens);
+      setTokens(newTokens);
     } else {
       showError(message);
     }
@@ -528,8 +522,7 @@ const TokensTable = () => {
   const searchTokens = async () => {
     const { searchKeyword, searchToken } = getFormValues();
     if (searchKeyword === '' && searchToken === '') {
-      await loadTokens(0);
-      setActivePage(1);
+      await loadTokens(1);
       return;
     }
     setSearching(true);
@@ -538,7 +531,8 @@ const TokensTable = () => {
     );
     const { success, message, data } = res.data;
     if (success) {
-      setTokensFormat(data);
+      setTokens(data);
+      setTokenCount(data.length);
       setActivePage(1);
     } else {
       showError(message);
@@ -561,10 +555,12 @@ const TokensTable = () => {
   };
 
   const handlePageChange = (page) => {
-    setActivePage(page);
-    if (page === Math.ceil(tokens.length / pageSize) + 1) {
-      loadTokens(page - 1).then((r) => { });
-    }
+    loadTokens(page, pageSize).then();
+  };
+
+  const handlePageSizeChange = async (size) => {
+    setPageSize(size);
+    await loadTokens(1, size);
   };
 
   const rowSelection = {
@@ -589,6 +585,15 @@ const TokensTable = () => {
 
   const renderHeader = () => (
     <div className="flex flex-col w-full">
+      <div className="mb-2">
+        <div className="flex items-center text-blue-500">
+          <Key size={16} className="mr-2" />
+          <Text>{t('令牌用于API访问认证,可以设置额度限制和模型权限。')}</Text>
+        </div>
+      </div>
+
+      <Divider margin="12px" />
+
       <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
         <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
           <Button
@@ -663,7 +668,7 @@ const TokensTable = () => {
               <Button
                 type="primary"
                 htmlType="submit"
-                loading={searching}
+                loading={loading || searching}
                 className="!rounded-full flex-1 md:flex-initial md:w-auto"
               >
                 {t('查询')}
@@ -707,7 +712,7 @@ const TokensTable = () => {
       >
         <Table
           columns={columns}
-          dataSource={pageData}
+          dataSource={tokens}
           scroll={{ x: 'max-content' }}
           pagination={{
             currentPage: activePage,
@@ -719,12 +724,9 @@ const TokensTable = () => {
               t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
                 start: page.currentStart,
                 end: page.currentEnd,
-                total: tokens.length,
+                total: tokenCount,
               }),
-            onPageSizeChange: (size) => {
-              setPageSize(size);
-              setActivePage(1);
-            },
+            onPageSizeChange: handlePageSizeChange,
             onPageChange: handlePageChange,
           }}
           loading={loading}

+ 22 - 0
web/src/components/table/UsersTable.js

@@ -26,6 +26,7 @@ import {
   Space,
   Table,
   Tag,
+  Tooltip,
   Typography
 } from '@douyinfe/semi-ui';
 import {
@@ -110,6 +111,27 @@ const UsersTable = () => {
     {
       title: t('用户名'),
       dataIndex: 'username',
+      render: (text, record) => {
+        const remark = record.remark;
+        if (!remark) {
+          return <span>{text}</span>;
+        }
+        const maxLen = 10;
+        const displayRemark = remark.length > maxLen ? remark.slice(0, maxLen) + '…' : remark;
+        return (
+          <Space spacing={2}>
+            <span>{text}</span>
+            <Tooltip content={remark} position="top" showArrow>
+              <Tag color='white' size='large' shape='circle' className="!text-xs">
+                <div className="flex items-center gap-1">
+                  <div className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: '#10b981' }} />
+                  {displayRemark}
+                </div>
+              </Tag>
+            </Tooltip>
+          </Space>
+        );
+      },
     },
     {
       title: t('分组'),

+ 1 - 0
web/src/helpers/api.js

@@ -83,6 +83,7 @@ export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled
   const payload = {
     model: inputs.model,
     messages: processedMessages,
+    group: inputs.group, 
     stream: inputs.stream,
   };
 

+ 124 - 80
web/src/helpers/render.js

@@ -30,7 +30,7 @@ import {
   Dify,
   Coze,
   SiliconCloud,
-  FastGPT
+  FastGPT,
 } from '@lobehub/icons';
 
 import {
@@ -46,7 +46,7 @@ import {
   Gift,
   User,
   Settings,
-  CircleUser
+  CircleUser,
 } from 'lucide-react';
 
 // 侧边栏图标颜色映射
@@ -315,7 +315,6 @@ export const getModelCategories = (() => {
   };
 })();
 
-
 /**
  * 根据渠道类型返回对应的厂商图标
  * @param {number} channelType - 渠道类型值
@@ -868,6 +867,30 @@ export function renderQuota(quota, digits = 2) {
   return renderNumber(quota);
 }
 
+function isValidGroupRatio(ratio) {
+  return Number.isFinite(ratio) && ratio !== -1;
+}
+
+/**
+ * Helper function to get effective ratio and label
+ * @param {number} groupRatio - The default group ratio
+ * @param {number} user_group_ratio - The user-specific group ratio  
+ * @returns {Object} - Object containing { ratio, label, useUserGroupRatio }
+ */
+function getEffectiveRatio(groupRatio, user_group_ratio) {
+  const useUserGroupRatio = isValidGroupRatio(user_group_ratio);
+  const ratioLabel = useUserGroupRatio
+    ? i18next.t('专属倍率')
+    : i18next.t('分组倍率');
+  const effectiveRatio = useUserGroupRatio ? user_group_ratio : groupRatio;
+  
+  return {
+    ratio: effectiveRatio,
+    label: ratioLabel,
+    useUserGroupRatio: useUserGroupRatio
+  };
+}
+
 export function renderModelPrice(
   inputTokens,
   completionTokens,
@@ -875,6 +898,7 @@ export function renderModelPrice(
   modelPrice = -1,
   completionRatio,
   groupRatio,
+  user_group_ratio,
   cacheTokens = 0,
   cacheRatio = 1.0,
   image = false,
@@ -890,13 +914,17 @@ export function renderModelPrice(
   audioInputTokens = 0,
   audioInputPrice = 0,
 ) {
+  const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(groupRatio, user_group_ratio);
+  groupRatio = effectiveGroupRatio;
+
   if (modelPrice !== -1) {
     return i18next.t(
-      '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
+      '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}',
       {
         price: modelPrice,
         ratio: groupRatio,
         total: modelPrice * groupRatio,
+        ratioType: ratioLabel,
       },
     );
   } else {
@@ -1033,11 +1061,12 @@ export function renderModelPrice(
 
               // 构建输出部分描述
               const outputDesc = i18next.t(
-                '输出 {{completion}} tokens / 1M tokens * ${{compPrice}}) * 分组倍率 {{ratio}}',
+                '输出 {{completion}} tokens / 1M tokens * ${{compPrice}}) * {{ratioType}} {{ratio}}',
                 {
                   completion: completionTokens,
                   compPrice: completionRatioPrice,
                   ratio: groupRatio,
+                  ratioType: ratioLabel,
                 },
               );
 
@@ -1045,23 +1074,25 @@ export function renderModelPrice(
               const extraServices = [
                 webSearch && webSearchCallCount > 0
                   ? i18next.t(
-                    ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}',
-                    {
-                      count: webSearchCallCount,
-                      price: webSearchPrice,
-                      ratio: groupRatio,
-                    },
-                  )
+                      ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
+                      {
+                        count: webSearchCallCount,
+                        price: webSearchPrice,
+                        ratio: groupRatio,
+                        ratioType: ratioLabel,
+                      },
+                    )
                   : '',
                 fileSearch && fileSearchCallCount > 0
                   ? i18next.t(
-                    ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}',
-                    {
-                      count: fileSearchCallCount,
-                      price: fileSearchPrice,
-                      ratio: groupRatio,
-                    },
-                  )
+                      ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
+                      {
+                        count: fileSearchCallCount,
+                        price: fileSearchPrice,
+                        ratio: groupRatio,
+                        ratioType: ratioLabel,
+                      },
+                    )
                   : '',
               ].join('');
 
@@ -1091,16 +1122,12 @@ export function renderLogContent(
   user_group_ratio,
   image = false,
   imageRatio = 1.0,
-  useUserGroupRatio = undefined,
   webSearch = false,
   webSearchCallCount = 0,
   fileSearch = false,
   fileSearchCallCount = 0,
 ) {
-  const ratioLabel = useUserGroupRatio
-    ? i18next.t('专属倍率')
-    : i18next.t('分组倍率');
-  const ratio = useUserGroupRatio ? user_group_ratio : groupRatio;
+  const { ratio, label: ratioLabel, useUserGroupRatio: useUserGroupRatio } = getEffectiveRatio(groupRatio, user_group_ratio);
 
   if (modelPrice !== -1) {
     return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
@@ -1149,14 +1176,18 @@ export function renderModelPriceSimple(
   modelRatio,
   modelPrice = -1,
   groupRatio,
+  user_group_ratio,
   cacheTokens = 0,
   cacheRatio = 1.0,
   image = false,
   imageRatio = 1.0,
 ) {
+  const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(groupRatio, user_group_ratio);
+  groupRatio = effectiveGroupRatio;
   if (modelPrice !== -1) {
-    return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
+    return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', {
       price: modelPrice,
+      ratioType: ratioLabel,
       ratio: groupRatio,
     });
   } else {
@@ -1191,8 +1222,9 @@ export function renderModelPriceSimple(
         },
       );
     } else {
-      return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
+      return i18next.t('模型: {{ratio}} * {{ratioType}}:{{groupRatio}}', {
         ratio: modelRatio,
+        ratioType: ratioLabel,
         groupRatio: groupRatio,
       });
     }
@@ -1210,17 +1242,21 @@ export function renderAudioModelPrice(
   audioRatio,
   audioCompletionRatio,
   groupRatio,
+  user_group_ratio,
   cacheTokens = 0,
   cacheRatio = 1.0,
 ) {
+  const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(groupRatio, user_group_ratio);
+  groupRatio = effectiveGroupRatio;
   // 1 ratio = $0.002 / 1K tokens
   if (modelPrice !== -1) {
     return i18next.t(
-      '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
+      '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}',
       {
         price: modelPrice,
         ratio: groupRatio,
         total: modelPrice * groupRatio,
+        ratioType: ratioLabel,
       },
     );
   } else {
@@ -1245,10 +1281,10 @@ export function renderAudioModelPrice(
     let audioPrice =
       (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
       (audioCompletionTokens / 1000000) *
-      inputRatioPrice *
-      audioRatio *
-      audioCompletionRatio *
-      groupRatio;
+        inputRatioPrice *
+        audioRatio *
+        audioCompletionRatio *
+        groupRatio;
     let price = textPrice + audioPrice;
     return (
       <>
@@ -1304,27 +1340,27 @@ export function renderAudioModelPrice(
           <p>
             {cacheTokens > 0
               ? i18next.t(
-                '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
-                {
-                  nonCacheInput: inputTokens - cacheTokens,
-                  cacheInput: cacheTokens,
-                  cachePrice: inputRatioPrice * cacheRatio,
-                  price: inputRatioPrice,
-                  completion: completionTokens,
-                  compPrice: completionRatioPrice,
-                  total: textPrice.toFixed(6),
-                },
-              )
+                  '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
+                  {
+                    nonCacheInput: inputTokens - cacheTokens,
+                    cacheInput: cacheTokens,
+                    cachePrice: inputRatioPrice * cacheRatio,
+                    price: inputRatioPrice,
+                    completion: completionTokens,
+                    compPrice: completionRatioPrice,
+                    total: textPrice.toFixed(6),
+                  },
+                )
               : i18next.t(
-                '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
-                {
-                  input: inputTokens,
-                  price: inputRatioPrice,
-                  completion: completionTokens,
-                  compPrice: completionRatioPrice,
-                  total: textPrice.toFixed(6),
-                },
-              )}
+                  '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
+                  {
+                    input: inputTokens,
+                    price: inputRatioPrice,
+                    completion: completionTokens,
+                    compPrice: completionRatioPrice,
+                    total: textPrice.toFixed(6),
+                  },
+                )}
           </p>
           <p>
             {i18next.t(
@@ -1374,12 +1410,14 @@ export function renderClaudeModelPrice(
   modelPrice = -1,
   completionRatio,
   groupRatio,
+  user_group_ratio,
   cacheTokens = 0,
   cacheRatio = 1.0,
   cacheCreationTokens = 0,
   cacheCreationRatio = 1.0,
 ) {
-  const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
+  const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(groupRatio, user_group_ratio);
+  groupRatio = effectiveGroupRatio;
 
   if (modelPrice !== -1) {
     return i18next.t(
@@ -1461,33 +1499,35 @@ export function renderClaudeModelPrice(
           <p>
             {cacheTokens > 0 || cacheCreationTokens > 0
               ? i18next.t(
-                '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
-                {
-                  nonCacheInput: nonCachedTokens,
-                  cacheInput: cacheTokens,
-                  cacheRatio: cacheRatio,
-                  cacheCreationInput: cacheCreationTokens,
-                  cacheCreationRatio: cacheCreationRatio,
-                  cachePrice: cacheRatioPrice,
-                  cacheCreationPrice: cacheCreationRatioPrice,
-                  price: inputRatioPrice,
-                  completion: completionTokens,
-                  compPrice: completionRatioPrice,
-                  ratio: groupRatio,
-                  total: price.toFixed(6),
-                },
-              )
+                  '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
+                  {
+                    nonCacheInput: nonCachedTokens,
+                    cacheInput: cacheTokens,
+                    cacheRatio: cacheRatio,
+                    cacheCreationInput: cacheCreationTokens,
+                    cacheCreationRatio: cacheCreationRatio,
+                    cachePrice: cacheRatioPrice,
+                    cacheCreationPrice: cacheCreationRatioPrice,
+                    price: inputRatioPrice,
+                    completion: completionTokens,
+                    compPrice: completionRatioPrice,
+                    ratio: groupRatio,
+                    ratioType: ratioLabel,
+                    total: price.toFixed(6),
+                  },
+                )
               : i18next.t(
-                '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
-                {
-                  input: inputTokens,
-                  price: inputRatioPrice,
-                  completion: completionTokens,
-                  compPrice: completionRatioPrice,
-                  ratio: groupRatio,
-                  total: price.toFixed(6),
-                },
-              )}
+                  '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
+                  {
+                    input: inputTokens,
+                    price: inputRatioPrice,
+                    completion: completionTokens,
+                    compPrice: completionRatioPrice,
+                    ratio: groupRatio,
+                    ratioType: ratioLabel,
+                    total: price.toFixed(6),
+                  },
+                )}
           </p>
           <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
         </article>
@@ -1501,10 +1541,12 @@ export function renderClaudeLogContent(
   completionRatio,
   modelPrice = -1,
   groupRatio,
+  user_group_ratio,
   cacheRatio = 1.0,
   cacheCreationRatio = 1.0,
 ) {
-  const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
+  const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(groupRatio, user_group_ratio);
+  groupRatio = effectiveGroupRatio;
 
   if (modelPrice !== -1) {
     return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
@@ -1531,12 +1573,14 @@ export function renderClaudeModelPriceSimple(
   modelRatio,
   modelPrice = -1,
   groupRatio,
+  user_group_ratio,
   cacheTokens = 0,
   cacheRatio = 1.0,
   cacheCreationTokens = 0,
   cacheCreationRatio = 1.0,
 ) {
-  const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组');
+  const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(groupRatio, user_group_ratio);
+  groupRatio = effectiveGroupRatio;
 
   if (modelPrice !== -1) {
     return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', {

+ 6 - 7
web/src/helpers/token.js

@@ -6,14 +6,13 @@ import { API } from './api';
  */
 export async function fetchTokenKeys() {
   try {
-    const response = await API.get('/api/token/?p=0&size=100');
+    const response = await API.get('/api/token/?p=1&size=10');
     const { success, data } = response.data;
-    if (success) {
-      const activeTokens = data.filter((token) => token.status === 1);
-      return activeTokens.map((token) => token.key);
-    } else {
-      throw new Error('Failed to fetch token keys');
-    }
+    if (!success) throw new Error('Failed to fetch token keys');
+
+    const tokenItems = Array.isArray(data) ? data : data.items || [];
+    const activeTokens = tokenItems.filter((token) => token.status === 1);
+    return activeTokens.map((token) => token.key);
   } catch (error) {
     console.error('Error fetching token keys:', error);
     return [];

+ 30 - 11
web/src/i18n/locales/en.json

@@ -1373,6 +1373,12 @@
   "示例": "Example",
   "缺省 MaxTokens": "Default MaxTokens",
   "启用Claude思考适配(-thinking后缀)": "Enable Claude thinking adaptation (-thinking suffix)",
+  "和Claude不同,默认情况下Gemini的思考模型会自动决定要不要思考,就算不开启适配模型也可以正常使用,": "Unlike Claude, Gemini's thinking model automatically decides whether to think by default, and can be used normally even without enabling the adaptation model.",
+  "如果您需要计费,推荐设置无后缀模型价格按思考价格设置。": "If you need billing, it is recommended to set the no-suffix model price according to the thinking price.",
+  "支持使用 gemini-2.5-pro-preview-06-05-thinking-128 格式来精确传递思考预算。": "Supports using gemini-2.5-pro-preview-06-05-thinking-128 format to precisely pass thinking budget.",
+  "启用Gemini思考后缀适配": "Enable Gemini thinking suffix adaptation",
+  "适配-thinking、-thinking-预算数字和-nothinking后缀": "Adapt -thinking, -thinking-budgetNumber, and -nothinking suffixes",
+  "思考预算占比": "Thinking budget ratio",
   "Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Claude thinking adaptation BudgetTokens = MaxTokens * BudgetTokens percentage",
   "思考适配 BudgetTokens 百分比": "Thinking adaptation BudgetTokens percentage",
   "0.1-1之间的小数": "Decimal between 0.1 and 1",
@@ -1598,6 +1604,7 @@
   "请联系管理员在系统设置中配置API信息": "Please contact the administrator to configure API information in the system settings.",
   "请联系管理员在系统设置中配置公告信息": "Please contact the administrator to configure notice information in the system settings.",
   "请联系管理员在系统设置中配置常见问答": "Please contact the administrator to configure FAQ information in the system settings.",
+  "请联系管理员在系统设置中配置Uptime": "Please contact the administrator to configure Uptime in the system settings.",
   "确定要删除此API信息吗?": "Are you sure you want to delete this API information?",
   "测速": "Speed Test",
   "批量删除": "Batch Delete",
@@ -1628,16 +1635,15 @@
   "常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "FAQ management, providing answers to common questions for users (maximum 50, display latest 20 on the front end)",
   "暂无常见问答": "No FAQ",
   "显示最新20条": "Display latest 20",
-  "Uptime Kuma 服务地址": "Uptime Kuma service address",
-  "状态页面 Slug": "Status page slug",
-  "请输入 Uptime Kuma 服务的完整地址,例如:https://uptime.example.com": "Please enter the complete address of Uptime Kuma, for example: https://uptime.example.com",
-  "请输入状态页面的 slug 标识符,例如:my-status": "Please enter the slug identifier for the status page, for example: my-status",
-  "Uptime Kuma 服务地址不能为空": "Uptime Kuma service address cannot be empty",
-  "请输入有效的 URL 地址": "Please enter a valid URL address",
-  "状态页面 Slug 不能为空": "Status page slug cannot be empty",
-  "Slug 只能包含字母、数字、下划线和连字符": "Slug can only contain letters, numbers, underscores, and hyphens",
-  "请输入 Uptime Kuma 服务地址": "Please enter the Uptime Kuma service address",
-  "请输入状态页面 Slug": "Please enter the status page slug",
+  "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Uptime Kuma monitoring category management, you can configure multiple monitoring categories for service status display (maximum 20)",
+  "添加分类": "Add Category",
+  "分类名称": "Category Name",
+  "Uptime Kuma地址": "Uptime Kuma Address",
+  "状态页面Slug": "Status Page Slug",
+  "请输入分类名称,如:OpenAI、Claude等": "Please enter the category name, such as: OpenAI, Claude, etc.",
+  "请输入Uptime Kuma服务地址,如:https://status.example.com": "Please enter the Uptime Kuma service address, such as: https://status.example.com",
+  "请输入状态页面的Slug,如:my-status": "Please enter the slug for the status page, such as: my-status",
+  "确定要删除此分类吗?": "Are you sure you want to delete this category?",
   "配置": "Configure",
   "服务监控地址,用于展示服务状态信息": "service monitoring address for displaying status information",
   "服务可用性": "Service Status",
@@ -1646,5 +1652,18 @@
   "高延迟": "High latency",
   "维护中": "Maintenance",
   "暂无监控数据": "No monitoring data",
-  "请联系管理员在系统设置中配置Uptime": "Please contact the administrator to configure Uptime in the system settings."
+  "IP记录": "IP Record",
+  "记录请求与错误日志 IP": "Record request and error log IP",
+  "开启后,仅“消费”和“错误”日志将记录您的客户端 IP 地址": "After enabling, only \"consumption\" and \"error\" logs will record your client IP address",
+  "只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录": "Only when the user sets IP recording, the IP recording of request and error type logs will be performed",
+  "设置保存成功": "Settings saved successfully",
+  "设置保存失败": "Settings save failed",
+  "已新增 {{count}} 个模型:{{list}}": "Added {{count}} models: {{list}}",
+  "未发现新增模型": "No new models were added",
+  "令牌用于API访问认证,可以设置额度限制和模型权限。": "Tokens are used for API access authentication, and can set quota limits and model permissions.",
+  "清除失效兑换码": "Clear invalid redemption codes",
+  "确定清除所有失效兑换码?": "Are you sure you want to clear all invalid redemption codes?",
+  "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "This will delete all used, disabled, and expired redemption codes, this operation cannot be undone.",
+  "选择过期时间(可选,留空为永久)": "Select expiration time (optional, leave blank for permanent)",
+  "请输入备注(仅管理员可见)": "Please enter a remark (only visible to administrators)"
 }

+ 11 - 9
web/src/index.css

@@ -375,23 +375,25 @@ code {
 }
 
 /* 隐藏卡片内容区域的滚动条 */
-.card-content-scroll::-webkit-scrollbar,
-.model-settings-scroll::-webkit-scrollbar,
-.thinking-content-scroll::-webkit-scrollbar,
-.custom-request-textarea .semi-input::-webkit-scrollbar,
-.custom-request-textarea textarea::-webkit-scrollbar {
-  display: none;
-}
-
 .card-content-scroll,
 .model-settings-scroll,
 .thinking-content-scroll,
 .custom-request-textarea .semi-input,
-.custom-request-textarea textarea {
+.custom-request-textarea textarea,
+.notice-content-scroll {
   -ms-overflow-style: none;
   scrollbar-width: none;
 }
 
+.card-content-scroll::-webkit-scrollbar,
+.model-settings-scroll::-webkit-scrollbar,
+.thinking-content-scroll::-webkit-scrollbar,
+.custom-request-textarea .semi-input::-webkit-scrollbar,
+.custom-request-textarea textarea::-webkit-scrollbar,
+.notice-content-scroll::-webkit-scrollbar {
+  display: none;
+}
+
 /* 图片列表滚动条样式 */
 .image-list-scroll::-webkit-scrollbar {
   width: 6px;

+ 13 - 6
web/src/pages/Channel/EditChannel.js

@@ -385,7 +385,7 @@ const EditChannel = (props) => {
 
     let localModels = [...inputs.models];
     let localModelOptions = [...modelOptions];
-    let hasError = false;
+    const addedModels = [];
 
     modelArray.forEach((model) => {
       if (model && !localModels.includes(model)) {
@@ -395,17 +395,24 @@ const EditChannel = (props) => {
           text: model,
           value: model,
         });
-      } else if (model) {
-        showError(t('某些模型已存在!'));
-        hasError = true;
+        addedModels.push(model);
       }
     });
 
-    if (hasError) return;
-
     setModelOptions(localModelOptions);
     setCustomModel('');
     handleInputChange('models', localModels);
+
+    if (addedModels.length > 0) {
+      showSuccess(
+        t('已新增 {{count}} 个模型:{{list}}', {
+          count: addedModels.length,
+          list: addedModels.join(', '),
+        })
+      );
+    } else {
+      showInfo(t('未发现新增模型'));
+    }
   };
 
   return (

+ 13 - 6
web/src/pages/Channel/EditTagModal.js

@@ -229,7 +229,7 @@ const EditTagModal = (props) => {
 
     let localModels = [...inputs.models];
     let localModelOptions = [...modelOptions];
-    let hasError = false;
+    const addedModels = [];
 
     modelArray.forEach((model) => {
       // 检查模型是否已存在,且模型名称非空
@@ -241,18 +241,25 @@ const EditTagModal = (props) => {
           text: model,
           value: model,
         });
-      } else if (model) {
-        showError('某些模型已存在!');
-        hasError = true;
+        addedModels.push(model);
       }
     });
 
-    if (hasError) return; // 如果有错误则终止操作
-
     // 更新状态值
     setModelOptions(localModelOptions);
     setCustomModel('');
     handleInputChange('models', localModels);
+
+    if (addedModels.length > 0) {
+      showSuccess(
+        t('已新增 {{count}} 个模型:{{list}}', {
+          count: addedModels.length,
+          list: addedModels.join(', '),
+        })
+      );
+    } else {
+      showInfo(t('未发现新增模型'));
+    }
   };
 
   return (

+ 312 - 198
web/src/pages/Detail/index.js

@@ -16,7 +16,8 @@ import {
   Tag,
   Timeline,
   Collapse,
-  Progress
+  Progress,
+  Divider
 } from '@douyinfe/semi-ui';
 import {
   IconRefresh,
@@ -86,10 +87,21 @@ const Detail = (props) => {
   const ICON_BUTTON_CLASS = "text-white hover:bg-opacity-80 !rounded-full";
   const FLEX_CENTER_GAP2 = "flex items-center gap-2";
 
+  const ILLUSTRATION_SIZE = { width: 96, height: 96 };
+
   // ========== Constants ==========
   let now = new Date();
   const isAdminUser = isAdmin();
 
+  // ========== Panel enable flags ==========
+  const apiInfoEnabled = statusState?.status?.api_info_enabled ?? true;
+  const announcementsEnabled = statusState?.status?.announcements_enabled ?? true;
+  const faqEnabled = statusState?.status?.faq_enabled ?? true;
+  const uptimeEnabled = statusState?.status?.uptime_kuma_enabled ?? true;
+
+  const hasApiInfoPanel = apiInfoEnabled;
+  const hasInfoPanels = announcementsEnabled || faqEnabled || uptimeEnabled;
+
   // ========== Helper Functions ==========
   const getDefaultTime = useCallback(() => {
     return localStorage.getItem('data_export_default_time') || 'hour';
@@ -206,6 +218,7 @@ const Detail = (props) => {
   const announcementScrollRef = useRef(null);
   const faqScrollRef = useRef(null);
   const uptimeScrollRef = useRef(null);
+  const uptimeTabScrollRefs = useRef({});
 
   // ========== Additional State for scroll hints ==========
   const [showAnnouncementScrollHint, setShowAnnouncementScrollHint] = useState(false);
@@ -215,6 +228,7 @@ const Detail = (props) => {
   // ========== Uptime data ==========
   const [uptimeData, setUptimeData] = useState([]);
   const [uptimeLoading, setUptimeLoading] = useState(false);
+  const [activeUptimeTab, setActiveUptimeTab] = useState('');
 
   // ========== Props Destructuring ==========
   const { username, model_name, start_timestamp, end_timestamp, channel } = inputs;
@@ -570,6 +584,9 @@ const Detail = (props) => {
       const { success, message, data } = res.data;
       if (success) {
         setUptimeData(data || []);
+        if (data && data.length > 0 && !activeUptimeTab) {
+          setActiveUptimeTab(data[0].categoryName);
+        }
       } else {
         showError(message);
       }
@@ -578,7 +595,7 @@ const Detail = (props) => {
     } finally {
       setUptimeLoading(false);
     }
-  }, []);
+  }, [activeUptimeTab]);
 
   const refresh = useCallback(async () => {
     await Promise.all([loadQuotaData(), loadUptimeData()]);
@@ -635,10 +652,18 @@ const Detail = (props) => {
       checkApiScrollable();
       checkCardScrollable(announcementScrollRef, setShowAnnouncementScrollHint);
       checkCardScrollable(faqScrollRef, setShowFaqScrollHint);
-      checkCardScrollable(uptimeScrollRef, setShowUptimeScrollHint);
+
+      if (uptimeData.length === 1) {
+        checkCardScrollable(uptimeScrollRef, setShowUptimeScrollHint);
+      } else if (uptimeData.length > 1 && activeUptimeTab) {
+        const activeTabRef = uptimeTabScrollRefs.current[activeUptimeTab];
+        if (activeTabRef) {
+          checkCardScrollable(activeTabRef, setShowUptimeScrollHint);
+        }
+      }
     }, 100);
     return () => clearTimeout(timer);
-  }, [uptimeData]);
+  }, [uptimeData, activeUptimeTab]);
 
   const getUserData = async () => {
     let res = await API.get(`/api/user/self`);
@@ -874,7 +899,6 @@ const Detail = (props) => {
 
   const announcementData = useMemo(() => {
     const announcements = statusState?.status?.announcements || [];
-    // 处理后台配置的公告数据,自动生成相对时间
     return announcements.map(item => ({
       ...item,
       time: getRelativeTime(item.publishDate)
@@ -885,6 +909,67 @@ const Detail = (props) => {
     return statusState?.status?.faq || [];
   }, [statusState?.status?.faq]);
 
+  const renderMonitorList = useCallback((monitors) => {
+    if (!monitors || monitors.length === 0) {
+      return (
+        <div className="flex justify-center items-center py-4">
+          <Empty
+            image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
+            darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
+            title={t('暂无监控数据')}
+          />
+        </div>
+      );
+    }
+
+    const grouped = {};
+    monitors.forEach((m) => {
+      const g = m.group || '';
+      if (!grouped[g]) grouped[g] = [];
+      grouped[g].push(m);
+    });
+
+    const renderItem = (monitor, idx) => (
+      <div key={idx} className="p-2 hover:bg-white rounded-lg transition-colors">
+        <div className="flex items-center justify-between mb-1">
+          <div className="flex items-center gap-2">
+            <div
+              className="w-2 h-2 rounded-full flex-shrink-0"
+              style={{ backgroundColor: getUptimeStatusColor(monitor.status) }}
+            />
+            <span className="text-sm font-medium text-gray-900">{monitor.name}</span>
+          </div>
+          <span className="text-xs text-gray-500">{((monitor.uptime || 0) * 100).toFixed(2)}%</span>
+        </div>
+        <div className="flex items-center gap-2">
+          <span className="text-xs text-gray-500">{getUptimeStatusText(monitor.status)}</span>
+          <div className="flex-1">
+            <Progress
+              percent={(monitor.uptime || 0) * 100}
+              showInfo={false}
+              aria-label={`${monitor.name} uptime`}
+              stroke={getUptimeStatusColor(monitor.status)}
+            />
+          </div>
+        </div>
+      </div>
+    );
+
+    return Object.entries(grouped).map(([gname, list]) => (
+      <div key={gname || 'default'} className="mb-2">
+        {gname && (
+          <>
+            <div className="text-md font-semibold text-gray-500 px-2 py-1">
+              {gname}
+            </div>
+            <Divider />
+          </>
+        )}
+        {list.map(renderItem)}
+      </div>
+    ));
+  }, [t, getUptimeStatusColor, getUptimeStatusText]);
+
   // ========== Hooks - Effects ==========
   useEffect(() => {
     getUserData();
@@ -1015,10 +1100,10 @@ const Detail = (props) => {
         </div>
 
         <div className="mb-4">
-          <div className={`grid grid-cols-1 gap-4 ${!statusState?.status?.self_use_mode_enabled ? 'lg:grid-cols-4' : ''}`}>
+          <div className={`grid grid-cols-1 gap-4 ${hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}>
             <Card
               {...CARD_PROPS}
-              className={`shadow-sm !rounded-2xl ${!statusState?.status?.self_use_mode_enabled ? 'lg:col-span-3' : ''}`}
+              className={`shadow-sm !rounded-2xl ${hasApiInfoPanel ? 'lg:col-span-3' : ''}`}
               title={
                 <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
                   <div className={FLEX_CENTER_GAP2}>
@@ -1061,7 +1146,7 @@ const Detail = (props) => {
               </div>
             </Card>
 
-            {!statusState?.status?.self_use_mode_enabled && (
+            {hasApiInfoPanel && (
               <Card
                 {...CARD_PROPS}
                 className="bg-gray-50 border-0 !rounded-2xl"
@@ -1118,11 +1203,10 @@ const Detail = (props) => {
                     ) : (
                       <div className="flex justify-center items-center py-8">
                         <Empty
-                          image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
-                          darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
+                          image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
+                          darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
                           title={t('暂无API信息')}
                           description={t('请联系管理员在系统设置中配置API信息')}
-                          style={{ padding: '12px' }}
                         />
                       </div>
                     )}
@@ -1138,220 +1222,250 @@ const Detail = (props) => {
         </div>
 
         {/* 系统公告和常见问答卡片 */}
-        {!statusState?.status?.self_use_mode_enabled && (
+        {hasInfoPanels && (
           <div className="mb-4">
             <div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
               {/* 公告卡片 */}
-              <Card
-                {...CARD_PROPS}
-                className="shadow-sm !rounded-2xl lg:col-span-2"
-                title={
-                  <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
-                    <div className="flex items-center gap-2">
-                      <Bell size={16} />
-                      {t('系统公告')}
-                      <Tag size="small" color="grey" shape="circle">
-                        {t('显示最新20条')}
-                      </Tag>
+              {announcementsEnabled && (
+                <Card
+                  {...CARD_PROPS}
+                  className="shadow-sm !rounded-2xl lg:col-span-2"
+                  title={
+                    <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
+                      <div className="flex items-center gap-2">
+                        <Bell size={16} />
+                        {t('系统公告')}
+                        <Tag size="small" color="grey" shape="circle">
+                          {t('显示最新20条')}
+                        </Tag>
+                      </div>
+                      {/* 图例 */}
+                      <div className="flex flex-wrap gap-3 text-xs">
+                        {announcementLegendData.map((legend, index) => (
+                          <div key={index} className="flex items-center gap-1">
+                            <div
+                              className="w-2 h-2 rounded-full"
+                              style={{
+                                backgroundColor: legend.color === 'grey' ? '#8b9aa7' :
+                                  legend.color === 'blue' ? '#3b82f6' :
+                                    legend.color === 'green' ? '#10b981' :
+                                      legend.color === 'orange' ? '#f59e0b' :
+                                        legend.color === 'red' ? '#ef4444' : '#8b9aa7'
+                              }}
+                            />
+                            <span className="text-gray-600">{legend.label}</span>
+                          </div>
+                        ))}
+                      </div>
                     </div>
-                    {/* 图例 */}
-                    <div className="flex flex-wrap gap-3 text-xs">
-                      {announcementLegendData.map((legend, index) => (
-                        <div key={index} className="flex items-center gap-1">
-                          <div
-                            className="w-2 h-2 rounded-full"
-                            style={{
-                              backgroundColor: legend.color === 'grey' ? '#8b9aa7' :
-                                legend.color === 'blue' ? '#3b82f6' :
-                                  legend.color === 'green' ? '#10b981' :
-                                    legend.color === 'orange' ? '#f59e0b' :
-                                      legend.color === 'red' ? '#ef4444' : '#8b9aa7'
-                            }}
+                  }
+                >
+                  <div className="card-content-container">
+                    <div
+                      ref={announcementScrollRef}
+                      className="p-2 max-h-96 overflow-y-auto card-content-scroll"
+                      onScroll={() => handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)}
+                    >
+                      {announcementData.length > 0 ? (
+                        <Timeline
+                          mode="alternate"
+                          dataSource={announcementData}
+                        />
+                      ) : (
+                        <div className="flex justify-center items-center py-8">
+                          <Empty
+                            image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
+                            darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
+                            title={t('暂无系统公告')}
+                            description={t('请联系管理员在系统设置中配置公告信息')}
                           />
-                          <span className="text-gray-600">{legend.label}</span>
                         </div>
-                      ))}
+                      )}
                     </div>
+                    <div
+                      className="card-content-fade-indicator"
+                      style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
+                    />
                   </div>
-                }
-              >
-                <div className="card-content-container">
-                  <div
-                    ref={announcementScrollRef}
-                    className="p-2 max-h-96 overflow-y-auto card-content-scroll"
-                    onScroll={() => handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)}
-                  >
-                    {announcementData.length > 0 ? (
-                      <Timeline
-                        mode="alternate"
-                        dataSource={announcementData}
-                      />
-                    ) : (
-                      <div className="flex justify-center items-center py-8">
-                        <Empty
-                          image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
-                          darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
-                          title={t('暂无系统公告')}
-                          description={t('请联系管理员在系统设置中配置公告信息')}
-                          style={{ padding: '12px' }}
-                        />
-                      </div>
-                    )}
-                  </div>
-                  <div
-                    className="card-content-fade-indicator"
-                    style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
-                  />
-                </div>
-              </Card>
+                </Card>
+              )}
 
               {/* 常见问答卡片 */}
-              <Card
-                {...CARD_PROPS}
-                className="shadow-sm !rounded-2xl lg:col-span-1"
-                title={
-                  <div className={FLEX_CENTER_GAP2}>
-                    <HelpCircle size={16} />
-                    {t('常见问答')}
-                  </div>
-                }
-              >
-                <div className="card-content-container">
-                  <div
-                    ref={faqScrollRef}
-                    className="p-2 max-h-96 overflow-y-auto card-content-scroll"
-                    onScroll={() => handleCardScroll(faqScrollRef, setShowFaqScrollHint)}
-                  >
-                    {faqData.length > 0 ? (
-                      <Collapse
-                        accordion
-                        expandIcon={<IconPlus />}
-                        collapseIcon={<IconMinus />}
-                      >
-                        {faqData.map((item, index) => (
-                          <Collapse.Panel
-                            key={index}
-                            header={item.title}
-                            itemKey={index.toString()}
-                          >
-                            <p>{item.content}</p>
-                          </Collapse.Panel>
-                        ))}
-                      </Collapse>
-                    ) : (
-                      <div className="flex justify-center items-center py-8">
-                        <Empty
-                          image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
-                          darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
-                          title={t('暂无常见问答')}
-                          description={t('请联系管理员在系统设置中配置常见问答')}
-                          style={{ padding: '12px' }}
-                        />
-                      </div>
-                    )}
-                  </div>
-                  <div
-                    className="card-content-fade-indicator"
-                    style={{ opacity: showFaqScrollHint ? 1 : 0 }}
-                  />
-                </div>
-              </Card>
-
-              {/* 服务可用性卡片 */}
-              <Card
-                {...CARD_PROPS}
-                className="shadow-sm !rounded-2xl lg:col-span-1"
-                title={
-                  <div className="flex items-center justify-between w-full gap-2">
-                    <div className="flex items-center gap-2">
-                      <Gauge size={16} />
-                      {t('服务可用性')}
+              {faqEnabled && (
+                <Card
+                  {...CARD_PROPS}
+                  className="shadow-sm !rounded-2xl lg:col-span-1"
+                  title={
+                    <div className={FLEX_CENTER_GAP2}>
+                      <HelpCircle size={16} />
+                      {t('常见问答')}
                     </div>
-                    <IconButton
-                      icon={<IconRefresh />}
-                      onClick={loadUptimeData}
-                      loading={uptimeLoading}
-                      size="small"
-                      theme="borderless"
-                      className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
-                    />
-                  </div>
-                }
-                footer={uptimeData.length > 0 ? (
-                  <Card
-                    shadows="always"
-                    bordered={false}
-                    className="!rounded-full backdrop-blur"
-                  >
-                    <div className="flex flex-wrap gap-3 text-xs justify-center">
-                      {uptimeLegendData.map((legend, index) => (
-                        <div key={index} className="flex items-center gap-1">
-                          <div
-                            className="w-2 h-2 rounded-full"
-                            style={{ backgroundColor: legend.color }}
+                  }
+                  bodyStyle={{ padding: 0 }}
+                >
+                  <div className="card-content-container">
+                    <div
+                      ref={faqScrollRef}
+                      className="p-2 max-h-96 overflow-y-auto card-content-scroll"
+                      onScroll={() => handleCardScroll(faqScrollRef, setShowFaqScrollHint)}
+                    >
+                      {faqData.length > 0 ? (
+                        <Collapse
+                          accordion
+                          expandIcon={<IconPlus />}
+                          collapseIcon={<IconMinus />}
+                        >
+                          {faqData.map((item, index) => (
+                            <Collapse.Panel
+                              key={index}
+                              header={item.question}
+                              itemKey={index.toString()}
+                            >
+                              <p>{item.answer}</p>
+                            </Collapse.Panel>
+                          ))}
+                        </Collapse>
+                      ) : (
+                        <div className="flex justify-center items-center py-8">
+                          <Empty
+                            image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
+                            darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
+                            title={t('暂无常见问答')}
+                            description={t('请联系管理员在系统设置中配置常见问答')}
                           />
-                          <span className="text-gray-600">{legend.label}</span>
                         </div>
-                      ))}
+                      )}
                     </div>
-                  </Card>
-                ) : null}
-                footerStyle={uptimeData.length > 0 ? { marginTop: '-100px' } : undefined}
-              >
-                <div className="card-content-container">
-                  <Spin spinning={uptimeLoading}>
                     <div
-                      ref={uptimeScrollRef}
-                      className="p-2 max-h-96 overflow-y-auto card-content-scroll"
-                      onScroll={() => handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)}
-                    >
+                      className="card-content-fade-indicator"
+                      style={{ opacity: showFaqScrollHint ? 1 : 0 }}
+                    />
+                  </div>
+                </Card>
+              )}
+
+              {/* 服务可用性卡片 */}
+              {uptimeEnabled && (
+                <Card
+                  {...CARD_PROPS}
+                  className="shadow-sm !rounded-2xl lg:col-span-1 flex flex-col"
+                  title={
+                    <div className="flex items-center justify-between w-full gap-2">
+                      <div className="flex items-center gap-2">
+                        <Gauge size={16} />
+                        {t('服务可用性')}
+                      </div>
+                      <IconButton
+                        icon={<IconRefresh />}
+                        onClick={loadUptimeData}
+                        loading={uptimeLoading}
+                        size="small"
+                        theme="borderless"
+                        className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
+                      />
+                    </div>
+                  }
+                  bodyStyle={{ padding: 0 }}
+                >
+                  {/* 内容区域 */}
+                  <div className="flex-1 relative">
+                    <Spin spinning={uptimeLoading}>
                       {uptimeData.length > 0 ? (
-                        uptimeData.map((monitor, idx) => (
-                          <div key={idx} className="p-2 hover:bg-white rounded-lg transition-colors">
-                            <div className="flex items-center justify-between mb-1">
-                              <div className="flex items-center gap-2">
-                                <div
-                                  className="w-2 h-2 rounded-full flex-shrink-0"
-                                  style={{
-                                    backgroundColor: getUptimeStatusColor(monitor.status)
-                                  }}
-                                />
-                                <span className="text-sm font-medium text-gray-900">{monitor.name}</span>
-                              </div>
-                              <span className="text-xs text-gray-500">{((monitor.uptime || 0) * 100).toFixed(2)}%</span>
-                            </div>
-                            <div className="flex items-center gap-2">
-                              <span className="text-xs text-gray-500">{getUptimeStatusText(monitor.status)}</span>
-                              <div className="flex-1">
-                                <Progress
-                                  percent={(monitor.uptime || 0) * 100}
-                                  showInfo={false}
-                                  aria-label={`${monitor.name} uptime`}
-                                  stroke={getUptimeStatusColor(monitor.status)}
-                                />
-                              </div>
+                        uptimeData.length === 1 ? (
+                          <div className="card-content-container">
+                            <div
+                              ref={uptimeScrollRef}
+                              className="p-2 max-h-[24rem] overflow-y-auto card-content-scroll"
+                              onScroll={() => handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)}
+                            >
+                              {renderMonitorList(uptimeData[0].monitors)}
                             </div>
+                            <div
+                              className="card-content-fade-indicator"
+                              style={{ opacity: showUptimeScrollHint ? 1 : 0 }}
+                            />
                           </div>
-                        ))
+                        ) : (
+                          <Tabs
+                            type="card"
+                            collapsible
+                            activeKey={activeUptimeTab}
+                            onChange={setActiveUptimeTab}
+                            size="small"
+                          >
+                            {uptimeData.map((group, groupIdx) => {
+                              if (!uptimeTabScrollRefs.current[group.categoryName]) {
+                                uptimeTabScrollRefs.current[group.categoryName] = React.createRef();
+                              }
+                              const tabScrollRef = uptimeTabScrollRefs.current[group.categoryName];
+
+                              return (
+                                <TabPane
+                                  tab={
+                                    <span className="flex items-center gap-2">
+                                      <Gauge size={14} />
+                                      {group.categoryName}
+                                      <Tag
+                                        color={activeUptimeTab === group.categoryName ? 'red' : 'grey'}
+                                        size='small'
+                                        shape='circle'
+                                      >
+                                        {group.monitors ? group.monitors.length : 0}
+                                      </Tag>
+                                    </span>
+                                  }
+                                  itemKey={group.categoryName}
+                                  key={groupIdx}
+                                >
+                                  <div className="card-content-container">
+                                    <div
+                                      ref={tabScrollRef}
+                                      className="p-2 max-h-[21.5rem] overflow-y-auto card-content-scroll"
+                                      onScroll={() => handleCardScroll(tabScrollRef, setShowUptimeScrollHint)}
+                                    >
+                                      {renderMonitorList(group.monitors)}
+                                    </div>
+                                    <div
+                                      className="card-content-fade-indicator"
+                                      style={{ opacity: activeUptimeTab === group.categoryName ? showUptimeScrollHint ? 1 : 0 : 0 }}
+                                    />
+                                  </div>
+                                </TabPane>
+                              );
+                            })}
+                          </Tabs>
+                        )
                       ) : (
                         <div className="flex justify-center items-center py-8">
                           <Empty
-                            image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
-                            darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
+                            image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
+                            darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
                             title={t('暂无监控数据')}
                             description={t('请联系管理员在系统设置中配置Uptime')}
-                            style={{ padding: '12px' }}
                           />
                         </div>
                       )}
+                    </Spin>
+                  </div>
+
+                  {/* 固定在底部的图例 */}
+                  {uptimeData.length > 0 && (
+                    <div className="p-3 mt-auto bg-gray-50 rounded-b-2xl">
+                      <div className="flex flex-wrap gap-3 text-xs justify-center">
+                        {uptimeLegendData.map((legend, index) => (
+                          <div key={index} className="flex items-center gap-1">
+                            <div
+                              className="w-2 h-2 rounded-full"
+                              style={{ backgroundColor: legend.color }}
+                            />
+                            <span className="text-gray-600">{legend.label}</span>
+                          </div>
+                        ))}
+                      </div>
                     </div>
-                  </Spin>
-                  <div
-                    className="card-content-fade-indicator"
-                    style={{ opacity: showUptimeScrollHint ? 1 : 0 }}
-                  />
-                </div>
-              </Card>
+                  )}
+                </Card>
+              )}
             </div>
           </div>
         )}

+ 26 - 1
web/src/pages/Redemption/EditRedemption.js

@@ -20,6 +20,8 @@ import {
   Typography,
   Card,
   Tag,
+  Form,
+  DatePicker,
 } from '@douyinfe/semi-ui';
 import {
   IconCreditCard,
@@ -40,9 +42,10 @@ const EditRedemption = (props) => {
     name: '',
     quota: 100000,
     count: 1,
+    expired_time: 0,
   };
   const [inputs, setInputs] = useState(originInputs);
-  const { name, quota, count } = inputs;
+  const { name, quota, count, expired_time } = inputs;
 
   const handleCancel = () => {
     props.handleClose();
@@ -85,6 +88,9 @@ const EditRedemption = (props) => {
     localInputs.count = parseInt(localInputs.count);
     localInputs.quota = parseInt(localInputs.quota);
     localInputs.name = name;
+    if (localInputs.expired_time === null || localInputs.expired_time === undefined) {
+      localInputs.expired_time = 0;
+    }
     let res;
     if (isEdit) {
       res = await API.put(`/api/redemption/`, {
@@ -220,6 +226,25 @@ const EditRedemption = (props) => {
                     required={!isEdit}
                   />
                 </div>
+                <div>
+                  <Text strong className="block mb-2">{t('过期时间')}</Text>
+                  <DatePicker
+                    type="dateTime"
+                    placeholder={t('选择过期时间(可选,留空为永久)')}
+                    showClear
+                    value={expired_time ? new Date(expired_time * 1000) : null}
+                    onChange={(value) => {
+                      if (value === null || value === undefined) {
+                        handleInputChange('expired_time', 0);
+                      } else {
+                        const timestamp = Math.floor(value.getTime() / 1000);
+                        handleInputChange('expired_time', timestamp);
+                      }
+                    }}
+                    size="large"
+                    className="!rounded-lg w-full"
+                  />
+                </div>
               </div>
             </Card>
 

+ 48 - 6
web/src/pages/Setting/Dashboard/SettingsAPIInfo.js

@@ -9,7 +9,8 @@ import {
   Divider,
   Avatar,
   Modal,
-  Tag
+  Tag,
+  Switch
 } from '@douyinfe/semi-ui';
 import {
   IllustrationNoResult,
@@ -48,6 +49,9 @@ const SettingsAPIInfo = ({ options, refresh }) => {
   const [pageSize, setPageSize] = useState(10);
   const [selectedRowKeys, setSelectedRowKeys] = useState([]);
 
+  // 面板启用状态 state
+  const [panelEnabled, setPanelEnabled] = useState(true);
+
   const colorOptions = [
     { value: 'blue', label: 'blue' },
     { value: 'green', label: 'green' },
@@ -85,7 +89,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
     try {
       setLoading(true);
       const apiInfoJson = JSON.stringify(apiInfoList);
-      await updateOption('ApiInfo', apiInfoJson);
+      await updateOption('console_setting.api_info', apiInfoJson);
       setHasChanges(false);
     } catch (error) {
       console.error('API信息更新失败', error);
@@ -185,10 +189,35 @@ const SettingsAPIInfo = ({ options, refresh }) => {
   };
 
   useEffect(() => {
-    if (options.ApiInfo !== undefined) {
-      parseApiInfo(options.ApiInfo);
+    const apiInfoStr = options['console_setting.api_info'] ?? options.ApiInfo;
+    if (apiInfoStr !== undefined) {
+      parseApiInfo(apiInfoStr);
+    }
+  }, [options['console_setting.api_info'], options.ApiInfo]);
+
+  useEffect(() => {
+    const enabledStr = options['console_setting.api_info_enabled'];
+    setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
+  }, [options['console_setting.api_info_enabled']]);
+
+  const handleToggleEnabled = async (checked) => {
+    const newValue = checked ? 'true' : 'false';
+    try {
+      const res = await API.put('/api/option/', {
+        key: 'console_setting.api_info_enabled',
+        value: newValue,
+      });
+      if (res.data.success) {
+        setPanelEnabled(checked);
+        showSuccess(t('设置已保存'));
+        refresh?.();
+      } else {
+        showError(res.data.message);
+      }
+    } catch (err) {
+      showError(err.message);
     }
-  }, [options.ApiInfo]);
+  };
 
   const columns = [
     {
@@ -324,6 +353,15 @@ const SettingsAPIInfo = ({ options, refresh }) => {
             {t('保存设置')}
           </Button>
         </div>
+
+        {/* 启用开关 */}
+        <div className="order-1 md:order-2 flex items-center gap-2">
+          <Switch
+            checked={panelEnabled}
+            onChange={handleToggleEnabled}
+          />
+          <Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
+        </div>
       </div>
     </div>
   );
@@ -367,7 +405,11 @@ const SettingsAPIInfo = ({ options, refresh }) => {
             total: apiInfoList.length,
             showSizeChanger: true,
             showQuickJumper: true,
-            showTotal: (total, range) => t(`共 ${total} 条记录,显示第 ${range[0]}-${range[1]} 条`),
+            formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
+              start: page.currentStart,
+              end: page.currentEnd,
+              total: apiInfoList.length,
+            }),
             pageSizeOptions: ['5', '10', '20', '50'],
             onChange: (page, size) => {
               setCurrentPage(page);

+ 45 - 6
web/src/pages/Setting/Dashboard/SettingsAnnouncements.js

@@ -8,7 +8,8 @@ import {
   Empty,
   Divider,
   Modal,
-  Tag
+  Tag,
+  Switch
 } from '@douyinfe/semi-ui';
 import {
   IllustrationNoResult,
@@ -47,6 +48,9 @@ const SettingsAnnouncements = ({ options, refresh }) => {
   const [pageSize, setPageSize] = useState(10);
   const [selectedRowKeys, setSelectedRowKeys] = useState([]);
 
+  // 面板启用状态
+  const [panelEnabled, setPanelEnabled] = useState(true);
+
   const typeOptions = [
     { value: 'default', label: t('默认') },
     { value: 'ongoing', label: t('进行中') },
@@ -176,7 +180,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
     try {
       setLoading(true);
       const announcementsJson = JSON.stringify(announcementsList);
-      await updateOption('Announcements', announcementsJson);
+      await updateOption('console_setting.announcements', announcementsJson);
       setHasChanges(false);
     } catch (error) {
       console.error('系统公告更新失败', error);
@@ -288,10 +292,35 @@ const SettingsAnnouncements = ({ options, refresh }) => {
   };
 
   useEffect(() => {
-    if (options.Announcements !== undefined) {
-      parseAnnouncements(options.Announcements);
+    const annStr = options['console_setting.announcements'] ?? options.Announcements;
+    if (annStr !== undefined) {
+      parseAnnouncements(annStr);
     }
-  }, [options.Announcements]);
+  }, [options['console_setting.announcements'], options.Announcements]);
+
+  useEffect(() => {
+    const enabledStr = options['console_setting.announcements_enabled'];
+    setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
+  }, [options['console_setting.announcements_enabled']]);
+
+  const handleToggleEnabled = async (checked) => {
+    const newValue = checked ? 'true' : 'false';
+    try {
+      const res = await API.put('/api/option/', {
+        key: 'console_setting.announcements_enabled',
+        value: newValue,
+      });
+      if (res.data.success) {
+        setPanelEnabled(checked);
+        showSuccess(t('设置已保存'));
+        refresh?.();
+      } else {
+        showError(res.data.message);
+      }
+    } catch (err) {
+      showError(err.message);
+    }
+  };
 
   const handleBatchDelete = () => {
     if (selectedRowKeys.length === 0) {
@@ -349,6 +378,12 @@ const SettingsAnnouncements = ({ options, refresh }) => {
             {t('保存设置')}
           </Button>
         </div>
+
+        {/* 启用开关 */}
+        <div className="order-1 md:order-2 flex items-center gap-2">
+          <Switch checked={panelEnabled} onChange={handleToggleEnabled} />
+          <Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
+        </div>
       </div>
     </div>
   );
@@ -392,7 +427,11 @@ const SettingsAnnouncements = ({ options, refresh }) => {
             total: announcementsList.length,
             showSizeChanger: true,
             showQuickJumper: true,
-            showTotal: (total, range) => t(`共 ${total} 条记录,显示第 ${range[0]}-${range[1]} 条`),
+            formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
+              start: page.currentStart,
+              end: page.currentEnd,
+              total: announcementsList.length,
+            }),
             pageSizeOptions: ['5', '10', '20', '50'],
             onChange: (page, size) => {
               setCurrentPage(page);

+ 59 - 21
web/src/pages/Setting/Dashboard/SettingsFAQ.js

@@ -7,7 +7,8 @@ import {
   Typography,
   Empty,
   Divider,
-  Modal
+  Modal,
+  Switch
 } from '@douyinfe/semi-ui';
 import {
   IllustrationNoResult,
@@ -37,18 +38,21 @@ const SettingsFAQ = ({ options, refresh }) => {
   const [loading, setLoading] = useState(false);
   const [hasChanges, setHasChanges] = useState(false);
   const [faqForm, setFaqForm] = useState({
-    title: '',
-    content: ''
+    question: '',
+    answer: ''
   });
   const [currentPage, setCurrentPage] = useState(1);
   const [pageSize, setPageSize] = useState(10);
   const [selectedRowKeys, setSelectedRowKeys] = useState([]);
 
+  // 面板启用状态
+  const [panelEnabled, setPanelEnabled] = useState(true);
+
   const columns = [
     {
       title: t('问题标题'),
-      dataIndex: 'title',
-      key: 'title',
+      dataIndex: 'question',
+      key: 'question',
       render: (text) => (
         <div style={{
           maxWidth: '300px',
@@ -61,8 +65,8 @@ const SettingsFAQ = ({ options, refresh }) => {
     },
     {
       title: t('回答内容'),
-      dataIndex: 'content',
-      key: 'content',
+      dataIndex: 'answer',
+      key: 'answer',
       render: (text) => (
         <div style={{
           maxWidth: '400px',
@@ -124,7 +128,7 @@ const SettingsFAQ = ({ options, refresh }) => {
     try {
       setLoading(true);
       const faqJson = JSON.stringify(faqList);
-      await updateOption('FAQ', faqJson);
+      await updateOption('console_setting.faq', faqJson);
       setHasChanges(false);
     } catch (error) {
       console.error('常见问答更新失败', error);
@@ -137,8 +141,8 @@ const SettingsFAQ = ({ options, refresh }) => {
   const handleAddFaq = () => {
     setEditingFaq(null);
     setFaqForm({
-      title: '',
-      content: ''
+      question: '',
+      answer: ''
     });
     setShowFaqModal(true);
   };
@@ -146,8 +150,8 @@ const SettingsFAQ = ({ options, refresh }) => {
   const handleEditFaq = (faq) => {
     setEditingFaq(faq);
     setFaqForm({
-      title: faq.title,
-      content: faq.content
+      question: faq.question,
+      answer: faq.answer
     });
     setShowFaqModal(true);
   };
@@ -169,7 +173,7 @@ const SettingsFAQ = ({ options, refresh }) => {
   };
 
   const handleSaveFaq = async () => {
-    if (!faqForm.title || !faqForm.content) {
+    if (!faqForm.question || !faqForm.answer) {
       showError('请填写完整的问答信息');
       return;
     }
@@ -226,10 +230,34 @@ const SettingsFAQ = ({ options, refresh }) => {
   };
 
   useEffect(() => {
-    if (options.FAQ !== undefined) {
-      parseFAQ(options.FAQ);
+    if (options['console_setting.faq'] !== undefined) {
+      parseFAQ(options['console_setting.faq']);
     }
-  }, [options.FAQ]);
+  }, [options['console_setting.faq']]);
+
+  useEffect(() => {
+    const enabledStr = options['console_setting.faq_enabled'];
+    setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
+  }, [options['console_setting.faq_enabled']]);
+
+  const handleToggleEnabled = async (checked) => {
+    const newValue = checked ? 'true' : 'false';
+    try {
+      const res = await API.put('/api/option/', {
+        key: 'console_setting.faq_enabled',
+        value: newValue,
+      });
+      if (res.data.success) {
+        setPanelEnabled(checked);
+        showSuccess(t('设置已保存'));
+        refresh?.();
+      } else {
+        showError(res.data.message);
+      }
+    } catch (err) {
+      showError(err.message);
+    }
+  };
 
   const handleBatchDelete = () => {
     if (selectedRowKeys.length === 0) {
@@ -287,6 +315,12 @@ const SettingsFAQ = ({ options, refresh }) => {
             {t('保存设置')}
           </Button>
         </div>
+
+        {/* 启用开关 */}
+        <div className="order-1 md:order-2 flex items-center gap-2">
+          <Switch checked={panelEnabled} onChange={handleToggleEnabled} />
+          <Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
+        </div>
       </div>
     </div>
   );
@@ -330,7 +364,11 @@ const SettingsFAQ = ({ options, refresh }) => {
             total: faqList.length,
             showSizeChanger: true,
             showQuickJumper: true,
-            showTotal: (total, range) => t(`共 ${total} 条记录,显示第 ${range[0]}-${range[1]} 条`),
+            formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
+              start: page.currentStart,
+              end: page.currentEnd,
+              total: faqList.length,
+            }),
             pageSizeOptions: ['5', '10', '20', '50'],
             onChange: (page, size) => {
               setCurrentPage(page);
@@ -368,21 +406,21 @@ const SettingsFAQ = ({ options, refresh }) => {
       >
         <Form layout='vertical' initValues={faqForm} key={editingFaq ? editingFaq.id : 'new'}>
           <Form.Input
-            field='title'
+            field='question'
             label={t('问题标题')}
             placeholder={t('请输入问题标题')}
             maxLength={200}
             rules={[{ required: true, message: t('请输入问题标题') }]}
-            onChange={(value) => setFaqForm({ ...faqForm, title: value })}
+            onChange={(value) => setFaqForm({ ...faqForm, question: value })}
           />
           <Form.TextArea
-            field='content'
+            field='answer'
             label={t('回答内容')}
             placeholder={t('请输入回答内容')}
             maxCount={1000}
             rows={6}
             rules={[{ required: true, message: t('请输入回答内容') }]}
-            onChange={(value) => setFaqForm({ ...faqForm, content: value })}
+            onChange={(value) => setFaqForm({ ...faqForm, answer: value })}
           />
         </Form>
       </Modal>

+ 422 - 126
web/src/pages/Setting/Dashboard/SettingsUptimeKuma.js

@@ -1,12 +1,23 @@
-import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
+import React, { useEffect, useState } from 'react';
 import {
-  Form,
   Button,
+  Space,
+  Table,
+  Form,
   Typography,
-  Row,
-  Col,
+  Empty,
+  Divider,
+  Modal,
+  Switch
 } from '@douyinfe/semi-ui';
 import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
+import {
+  Plus,
+  Edit,
+  Trash2,
   Save,
   Activity
 } from 'lucide-react';
@@ -18,168 +29,453 @@ const { Text } = Typography;
 const SettingsUptimeKuma = ({ options, refresh }) => {
   const { t } = useTranslation();
 
+  const [uptimeGroupsList, setUptimeGroupsList] = useState([]);
+  const [showUptimeModal, setShowUptimeModal] = useState(false);
+  const [showDeleteModal, setShowDeleteModal] = useState(false);
+  const [deletingGroup, setDeletingGroup] = useState(null);
+  const [editingGroup, setEditingGroup] = useState(null);
+  const [modalLoading, setModalLoading] = useState(false);
   const [loading, setLoading] = useState(false);
-  const formApiRef = useRef(null);
+  const [hasChanges, setHasChanges] = useState(false);
+  const [uptimeForm, setUptimeForm] = useState({
+    categoryName: '',
+    url: '',
+    slug: '',
+  });
+  const [currentPage, setCurrentPage] = useState(1);
+  const [pageSize, setPageSize] = useState(10);
+  const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+  const [panelEnabled, setPanelEnabled] = useState(true);
 
-  const initValues = useMemo(() => ({
-    uptimeKumaUrl: options?.UptimeKumaUrl || '',
-    uptimeKumaSlug: options?.UptimeKumaSlug || ''
-  }), [options?.UptimeKumaUrl, options?.UptimeKumaSlug]);
+  const columns = [
+    {
+      title: t('分类名称'),
+      dataIndex: 'categoryName',
+      key: 'categoryName',
+      render: (text) => (
+        <div style={{
+          fontWeight: 'bold',
+          color: 'var(--semi-color-text-0)'
+        }}>
+          {text}
+        </div>
+      )
+    },
+    {
+      title: t('Uptime Kuma地址'),
+      dataIndex: 'url',
+      key: 'url',
+      render: (text) => (
+        <div style={{
+          maxWidth: '300px',
+          wordBreak: 'break-all',
+          fontFamily: 'monospace',
+          color: 'var(--semi-color-primary)'
+        }}>
+          {text}
+        </div>
+      )
+    },
+    {
+      title: t('状态页面Slug'),
+      dataIndex: 'slug',
+      key: 'slug',
+      render: (text) => (
+        <div style={{
+          fontFamily: 'monospace',
+          color: 'var(--semi-color-text-1)'
+        }}>
+          {text}
+        </div>
+      )
+    },
+    {
+      title: t('操作'),
+      key: 'action',
+      fixed: 'right',
+      width: 150,
+      render: (text, record) => (
+        <Space>
+          <Button
+            icon={<Edit size={14} />}
+            theme='light'
+            type='tertiary'
+            size='small'
+            className="!rounded-full"
+            onClick={() => handleEditGroup(record)}
+          >
+            {t('编辑')}
+          </Button>
+          <Button
+            icon={<Trash2 size={14} />}
+            type='danger'
+            theme='light'
+            size='small'
+            className="!rounded-full"
+            onClick={() => handleDeleteGroup(record)}
+          >
+            {t('删除')}
+          </Button>
+        </Space>
+      )
+    }
+  ];
 
-  useEffect(() => {
-    if (formApiRef.current) {
-      formApiRef.current.setValues(initValues, { isOverride: true });
+  const updateOption = async (key, value) => {
+    const res = await API.put('/api/option/', {
+      key,
+      value,
+    });
+    const { success, message } = res.data;
+    if (success) {
+      showSuccess('Uptime Kuma配置已更新');
+      if (refresh) refresh();
+    } else {
+      showError(message);
     }
-  }, [initValues]);
+  };
 
-  const handleSave = async () => {
-    const api = formApiRef.current;
-    if (!api) {
-      showError(t('表单未初始化'));
+  const submitUptimeGroups = async () => {
+    try {
+      setLoading(true);
+      const groupsJson = JSON.stringify(uptimeGroupsList);
+      await updateOption('console_setting.uptime_kuma_groups', groupsJson);
+      setHasChanges(false);
+    } catch (error) {
+      console.error('Uptime Kuma配置更新失败', error);
+      showError('Uptime Kuma配置更新失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleAddGroup = () => {
+    setEditingGroup(null);
+    setUptimeForm({
+      categoryName: '',
+      url: '',
+      slug: '',
+    });
+    setShowUptimeModal(true);
+  };
+
+  const handleEditGroup = (group) => {
+    setEditingGroup(group);
+    setUptimeForm({
+      categoryName: group.categoryName,
+      url: group.url,
+      slug: group.slug,
+    });
+    setShowUptimeModal(true);
+  };
+
+  const handleDeleteGroup = (group) => {
+    setDeletingGroup(group);
+    setShowDeleteModal(true);
+  };
+
+  const confirmDeleteGroup = () => {
+    if (deletingGroup) {
+      const newList = uptimeGroupsList.filter(item => item.id !== deletingGroup.id);
+      setUptimeGroupsList(newList);
+      setHasChanges(true);
+      showSuccess('分类已删除,请及时点击“保存设置”进行保存');
+    }
+    setShowDeleteModal(false);
+    setDeletingGroup(null);
+  };
+
+  const handleSaveGroup = async () => {
+    if (!uptimeForm.categoryName || !uptimeForm.url || !uptimeForm.slug) {
+      showError('请填写完整的分类信息');
       return;
     }
 
     try {
-      setLoading(true);
-      const { uptimeKumaUrl, uptimeKumaSlug } = await api.validate();
+      new URL(uptimeForm.url);
+    } catch (error) {
+      showError('请输入有效的URL地址');
+      return;
+    }
+
+    if (!/^[a-zA-Z0-9_-]+$/.test(uptimeForm.slug)) {
+      showError('Slug只能包含字母、数字、下划线和连字符');
+      return;
+    }
 
-      const trimmedUrl = (uptimeKumaUrl || '').trim();
-      const trimmedSlug = (uptimeKumaSlug || '').trim();
+    try {
+      setModalLoading(true);
 
-      if (trimmedUrl === options?.UptimeKumaUrl && trimmedSlug === options?.UptimeKumaSlug) {
-        showSuccess(t('无需保存,配置未变动'));
-        return;
+      let newList;
+      if (editingGroup) {
+        newList = uptimeGroupsList.map(item =>
+          item.id === editingGroup.id
+            ? { ...item, ...uptimeForm }
+            : item
+        );
+      } else {
+        const newId = Math.max(...uptimeGroupsList.map(item => item.id), 0) + 1;
+        const newGroup = {
+          id: newId,
+          ...uptimeForm
+        };
+        newList = [...uptimeGroupsList, newGroup];
       }
 
-      const [urlRes, slugRes] = await Promise.all([
-        trimmedUrl === options?.UptimeKumaUrl ? Promise.resolve({ data: { success: true } }) : API.put('/api/option/', {
-          key: 'UptimeKumaUrl',
-          value: trimmedUrl
-        }),
-        trimmedSlug === options?.UptimeKumaSlug ? Promise.resolve({ data: { success: true } }) : API.put('/api/option/', {
-          key: 'UptimeKumaSlug',
-          value: trimmedSlug
-        })
-      ]);
-
-      if (!urlRes.data.success) throw new Error(urlRes.data.message || t('URL 保存失败'));
-      if (!slugRes.data.success) throw new Error(slugRes.data.message || t('Slug 保存失败'));
-
-      showSuccess(t('Uptime Kuma 设置保存成功'));
-      refresh?.();
-    } catch (err) {
-      console.error(err);
-      showError(err.message || t('保存失败,请重试'));
+      setUptimeGroupsList(newList);
+      setHasChanges(true);
+      setShowUptimeModal(false);
+      showSuccess(editingGroup ? '分类已更新,请及时点击“保存设置”进行保存' : '分类已添加,请及时点击“保存设置”进行保存');
+    } catch (error) {
+      showError('操作失败: ' + error.message);
     } finally {
-      setLoading(false);
+      setModalLoading(false);
+    }
+  };
+
+  const parseUptimeGroups = (groupsStr) => {
+    if (!groupsStr) {
+      setUptimeGroupsList([]);
+      return;
+    }
+
+    try {
+      const parsed = JSON.parse(groupsStr);
+      const list = Array.isArray(parsed) ? parsed : [];
+      const listWithIds = list.map((item, index) => ({
+        ...item,
+        id: item.id || index + 1
+      }));
+      setUptimeGroupsList(listWithIds);
+    } catch (error) {
+      console.error('解析Uptime Kuma配置失败:', error);
+      setUptimeGroupsList([]);
     }
   };
 
-  const isValidUrl = useCallback((string) => {
+  useEffect(() => {
+    const groupsStr = options['console_setting.uptime_kuma_groups'];
+    if (groupsStr !== undefined) {
+      parseUptimeGroups(groupsStr);
+    }
+  }, [options['console_setting.uptime_kuma_groups']]);
+
+  useEffect(() => {
+    const enabledStr = options['console_setting.uptime_kuma_enabled'];
+    setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
+  }, [options['console_setting.uptime_kuma_enabled']]);
+
+  const handleToggleEnabled = async (checked) => {
+    const newValue = checked ? 'true' : 'false';
     try {
-      new URL(string);
-      return true;
-    } catch (_) {
-      return false;
+      const res = await API.put('/api/option/', {
+        key: 'console_setting.uptime_kuma_enabled',
+        value: newValue,
+      });
+      if (res.data.success) {
+        setPanelEnabled(checked);
+        showSuccess(t('设置已保存'));
+        refresh?.();
+      } else {
+        showError(res.data.message);
+      }
+    } catch (err) {
+      showError(err.message);
+    }
+  };
+
+  const handleBatchDelete = () => {
+    if (selectedRowKeys.length === 0) {
+      showError('请先选择要删除的分类');
+      return;
     }
-  }, []);
+
+    const newList = uptimeGroupsList.filter(item => !selectedRowKeys.includes(item.id));
+    setUptimeGroupsList(newList);
+    setSelectedRowKeys([]);
+    setHasChanges(true);
+    showSuccess(`已删除 ${selectedRowKeys.length} 个分类,请及时点击“保存设置”进行保存`);
+  };
 
   const renderHeader = () => (
     <div className="flex flex-col w-full">
-      <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4 mb-2">
+      <div className="mb-2">
         <div className="flex items-center text-blue-500">
           <Activity size={16} className="mr-2" />
-          <Text>
-            {t('配置')}&nbsp;
-            <a
-              href="https://github.com/louislam/uptime-kuma"
-              target="_blank"
-              rel="noopener noreferrer"
-              className="text-blue-600 hover:underline"
-            >
-              Uptime&nbsp;Kuma
-            </a>
-            &nbsp;{t('服务监控地址,用于展示服务状态信息')}
-          </Text>
+          <Text>{t('Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)')}</Text>
         </div>
+      </div>
 
-        <div className="flex gap-2">
+      <Divider margin="12px" />
+
+      <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
+        <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
           <Button
-            icon={<Save size={14} />}
-            theme='solid'
+            theme='light'
             type='primary'
-            onClick={handleSave}
+            icon={<Plus size={14} />}
+            className="!rounded-full w-full md:w-auto"
+            onClick={handleAddGroup}
+          >
+            {t('添加分类')}
+          </Button>
+          <Button
+            icon={<Trash2 size={14} />}
+            type='danger'
+            theme='light'
+            onClick={handleBatchDelete}
+            disabled={selectedRowKeys.length === 0}
+            className="!rounded-full w-full md:w-auto"
+          >
+            {t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
+          </Button>
+          <Button
+            icon={<Save size={14} />}
+            onClick={submitUptimeGroups}
             loading={loading}
-            className="!rounded-full"
+            disabled={!hasChanges}
+            type='secondary'
+            className="!rounded-full w-full md:w-auto"
           >
             {t('保存设置')}
           </Button>
         </div>
+
+        {/* 启用开关 */}
+        <div className="order-1 md:order-2 flex items-center gap-2">
+          <Switch checked={panelEnabled} onChange={handleToggleEnabled} />
+          <Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
+        </div>
       </div>
     </div>
   );
 
+  const getCurrentPageData = () => {
+    const startIndex = (currentPage - 1) * pageSize;
+    const endIndex = startIndex + pageSize;
+    return uptimeGroupsList.slice(startIndex, endIndex);
+  };
+
+  const rowSelection = {
+    selectedRowKeys,
+    onChange: (selectedRowKeys, selectedRows) => {
+      setSelectedRowKeys(selectedRowKeys);
+    },
+    onSelect: (record, selected, selectedRows) => {
+      console.log(`选择行: ${selected}`, record);
+    },
+    onSelectAll: (selected, selectedRows) => {
+      console.log(`全选: ${selected}`, selectedRows);
+    },
+    getCheckboxProps: (record) => ({
+      disabled: false,
+      name: record.id,
+    }),
+  };
+
   return (
-    <Form.Section text={renderHeader()}>
-      <Form
-        layout="vertical"
-        autoScrollToError
-        initValues={initValues}
-        getFormApi={(api) => {
-          formApiRef.current = api;
+    <>
+      <Form.Section text={renderHeader()}>
+        <Table
+          columns={columns}
+          dataSource={getCurrentPageData()}
+          rowSelection={rowSelection}
+          rowKey="id"
+          scroll={{ x: 'max-content' }}
+          pagination={{
+            currentPage: currentPage,
+            pageSize: pageSize,
+            total: uptimeGroupsList.length,
+            showSizeChanger: true,
+            showQuickJumper: true,
+            formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
+              start: page.currentStart,
+              end: page.currentEnd,
+              total: uptimeGroupsList.length,
+            }),
+            pageSizeOptions: ['5', '10', '20', '50'],
+            onChange: (page, size) => {
+              setCurrentPage(page);
+              setPageSize(size);
+            },
+            onShowSizeChange: (current, size) => {
+              setCurrentPage(1);
+              setPageSize(size);
+            }
+          }}
+          size='middle'
+          loading={loading}
+          empty={
+            <Empty
+              image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
+              darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+              description={t('暂无监控数据')}
+              style={{ padding: 30 }}
+            />
+          }
+          className="rounded-xl overflow-hidden"
+        />
+      </Form.Section>
+
+      <Modal
+        title={editingGroup ? t('编辑分类') : t('添加分类')}
+        visible={showUptimeModal}
+        onOk={handleSaveGroup}
+        onCancel={() => setShowUptimeModal(false)}
+        okText={t('保存')}
+        cancelText={t('取消')}
+        className="rounded-xl"
+        confirmLoading={modalLoading}
+        width={600}
+      >
+        <Form layout='vertical' initValues={uptimeForm} key={editingGroup ? editingGroup.id : 'new'}>
+          <Form.Input
+            field='categoryName'
+            label={t('分类名称')}
+            placeholder={t('请输入分类名称,如:OpenAI、Claude等')}
+            maxLength={50}
+            rules={[{ required: true, message: t('请输入分类名称') }]}
+            onChange={(value) => setUptimeForm({ ...uptimeForm, categoryName: value })}
+          />
+          <Form.Input
+            field='url'
+            label={t('Uptime Kuma地址')}
+            placeholder={t('请输入Uptime Kuma服务地址,如:https://status.example.com')}
+            maxLength={500}
+            rules={[{ required: true, message: t('请输入Uptime Kuma地址') }]}
+            onChange={(value) => setUptimeForm({ ...uptimeForm, url: value })}
+          />
+          <Form.Input
+            field='slug'
+            label={t('状态页面Slug')}
+            placeholder={t('请输入状态页面的Slug,如:my-status')}
+            maxLength={100}
+            rules={[{ required: true, message: t('请输入状态页面Slug') }]}
+            onChange={(value) => setUptimeForm({ ...uptimeForm, slug: value })}
+          />
+        </Form>
+      </Modal>
+
+      <Modal
+        title={t('确认删除')}
+        visible={showDeleteModal}
+        onOk={confirmDeleteGroup}
+        onCancel={() => {
+          setShowDeleteModal(false);
+          setDeletingGroup(null);
+        }}
+        okText={t('确认删除')}
+        cancelText={t('取消')}
+        type="warning"
+        className="rounded-xl"
+        okButtonProps={{
+          type: 'danger',
+          theme: 'solid'
         }}
       >
-        <Row gutter={[24, 24]}>
-          <Col xs={24} md={12}>
-            <Form.Input
-              showClear
-              field="uptimeKumaUrl"
-              label={{ text: t("Uptime Kuma 服务地址") }}
-              placeholder={t("请输入 Uptime Kuma 服务地址")}
-              style={{ fontFamily: 'monospace' }}
-              helpText={t("请输入 Uptime Kuma 服务的完整地址,例如:https://uptime.example.com")}
-              rules={[
-                {
-                  validator: (_, value) => {
-                    const url = (value || '').trim();
-
-                    if (url && !isValidUrl(url)) {
-                      return Promise.reject(t('请输入有效的 URL 地址'));
-                    }
-
-                    return Promise.resolve();
-                  }
-                }
-              ]}
-            />
-          </Col>
-
-          <Col xs={24} md={12}>
-            <Form.Input
-              showClear
-              field="uptimeKumaSlug"
-              label={{ text: t("状态页面 Slug") }}
-              placeholder={t("请输入状态页面 Slug")}
-              style={{ fontFamily: 'monospace' }}
-              helpText={t("请输入状态页面的 slug 标识符,例如:my-status")}
-              rules={[
-                {
-                  validator: (_, value) => {
-                    const slug = (value || '').trim();
-
-                    if (slug && !/^[a-zA-Z0-9_-]+$/.test(slug)) {
-                      return Promise.reject(t('Slug 只能包含字母、数字、下划线和连字符'));
-                    }
-
-                    return Promise.resolve();
-                  }
-                }
-              ]}
-            />
-          </Col>
-        </Row>
-      </Form>
-    </Form.Section>
+        <Text>{t('确定要删除此分类吗?')}</Text>
+      </Modal>
+    </>
   );
 };
 

+ 4 - 3
web/src/pages/Setting/Model/SettingGeminiModel.js

@@ -173,7 +173,8 @@ export default function SettingGeminiModel(props) {
                 <Text>
                   {t(
                     "和Claude不同,默认情况下Gemini的思考模型会自动决定要不要思考,就算不开启适配模型也可以正常使用," +
-                    "如果您需要计费,推荐设置无后缀模型价格按思考价格设置"
+                    "如果您需要计费,推荐设置无后缀模型价格按思考价格设置。" +
+                    "支持使用 gemini-2.5-pro-preview-06-05-thinking-128 格式来精确传递思考预算。"
                   )}
                 </Text>
               </Col>
@@ -183,7 +184,7 @@ export default function SettingGeminiModel(props) {
                 <Form.Switch
                   label={t('启用Gemini思考后缀适配')}
                   field={'gemini.thinking_adapter_enabled'}
-                  extraText={"适配-thinking和-nothinking后缀"}
+                  extraText={t('适配 -thinking、-thinking-预算数字 和 -nothinking 后缀')}
                   onChange={(value) =>
                     setInputs({
                       ...inputs,
@@ -205,7 +206,7 @@ export default function SettingGeminiModel(props) {
             <Row>
               <Col xs={24} sm={12} md={8} lg={8} xl={8}>
                 <Form.InputNumber
-                  label={t('请求模型带-thinking后缀的BudgetTokens数(超出24576的部分将被忽略)')}
+                  label={t('思考预算占比')}
                   field={'gemini.thinking_adapter_budget_tokens_percentage'}
                   initValue={''}
                   extraText={t('0.1-1之间的小数')}

+ 86 - 0
web/src/pages/Setting/Operation/GroupRatioSettings.js

@@ -16,6 +16,9 @@ export default function GroupRatioSettings(props) {
   const [inputs, setInputs] = useState({
     GroupRatio: '',
     UserUsableGroups: '',
+    GroupGroupRatio: '',
+    AutoGroups: '',
+    DefaultUseAutoGroup: false,
   });
   const refForm = useRef();
   const [inputsRow, setInputsRow] = useState(inputs);
@@ -99,6 +102,9 @@ export default function GroupRatioSettings(props) {
               <Form.TextArea
                 label={t('分组倍率')}
                 placeholder={t('为一个 JSON 文本,键为分组名称,值为倍率')}
+                extraText={t(
+                  '分组倍率设置,可以在此处新增分组或修改现有分组的倍率,格式为 JSON 字符串,例如:{"vip": 0.5, "test": 1},表示 vip 分组的倍率为 0.5,test 分组的倍率为 1',
+                )}
                 field={'GroupRatio'}
                 autosize={{ minRows: 6, maxRows: 12 }}
                 trigger='blur'
@@ -120,6 +126,9 @@ export default function GroupRatioSettings(props) {
               <Form.TextArea
                 label={t('用户可选分组')}
                 placeholder={t('为一个 JSON 文本,键为分组名称,值为分组描述')}
+                extraText={t(
+                  '用户新建令牌时可选的分组,格式为 JSON 字符串,例如:{"vip": "VIP 用户", "test": "测试"},表示用户可以选择 vip 分组和 test 分组',
+                )}
                 field={'UserUsableGroups'}
                 autosize={{ minRows: 6, maxRows: 12 }}
                 trigger='blur'
@@ -136,6 +145,83 @@ export default function GroupRatioSettings(props) {
               />
             </Col>
           </Row>
+          <Row gutter={16}>
+            <Col xs={24} sm={16}>
+              <Form.TextArea
+                label={t('分组特殊倍率')}
+                placeholder={t('为一个 JSON 文本')}
+                extraText={t(
+                  '键为分组名称,值为另一个 JSON 对象,键为分组名称,值为该分组的用户的特殊分组倍率,例如:{"vip": {"default": 0.5, "test": 1}},表示 vip 分组的用户在使用default分组的令牌时倍率为0.5,使用test分组时倍率为1',
+                )}
+                field={'GroupGroupRatio'}
+                autosize={{ minRows: 6, maxRows: 12 }}
+                trigger='blur'
+                stopValidateWithError
+                rules={[
+                  {
+                    validator: (rule, value) => verifyJSON(value),
+                    message: t('不是合法的 JSON 字符串'),
+                  },
+                ]}
+                onChange={(value) =>
+                  setInputs({ ...inputs, GroupGroupRatio: value })
+                }
+              />
+            </Col>
+          </Row>
+          <Row gutter={16}>
+            <Col xs={24} sm={16}>
+              <Form.TextArea
+                label={t('自动分组auto,从第一个开始选择')}
+                placeholder={t('为一个 JSON 文本')}
+                field={'AutoGroups'}
+                autosize={{ minRows: 6, maxRows: 12 }}
+                trigger='blur'
+                stopValidateWithError
+                rules={[
+                  {
+                    validator: (rule, value) => {
+                      if (!value || value.trim() === '') {
+                        return true; // Allow empty values
+                      }
+                      
+                      // First check if it's valid JSON
+                      try {
+                        const parsed = JSON.parse(value);
+                        
+                        // Check if it's an array
+                        if (!Array.isArray(parsed)) {
+                          return false;
+                        }
+                        
+                        // Check if every element is a string
+                        return parsed.every(item => typeof item === 'string');
+                      } catch (error) {
+                        return false;
+                      }
+                    },
+                    message: t('必须是有效的 JSON 字符串数组,例如:["g1","g2"]'),
+                  },
+                ]}
+                onChange={(value) =>
+                  setInputs({ ...inputs, AutoGroups: value })
+                }
+              />
+            </Col>
+          </Row>
+          <Row gutter={16}>
+            <Col span={16}>
+              <Form.Switch
+                label={t(
+                  '创建令牌默认选择auto分组,初始令牌也将设为auto(否则留空,为用户默认分组)',
+                )}
+                field={'DefaultUseAutoGroup'}
+                onChange={(value) =>
+                  setInputs({ ...inputs, DefaultUseAutoGroup: value })
+                }
+              />
+            </Col>
+          </Row>
         </Form.Section>
       </Form>
       <Button onClick={onSubmit}>{t('保存分组倍率设置')}</Button>

+ 233 - 129
web/src/pages/Token/EditToken.js

@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useContext } from 'react';
 import { useNavigate } from 'react-router-dom';
 import {
   API,
@@ -7,7 +7,7 @@ import {
   showSuccess,
   timestamp2string,
   renderGroupOption,
-  renderQuotaWithPrompt
+  renderQuotaWithPrompt,
 } from '../../helpers';
 import {
   AutoComplete,
@@ -37,11 +37,13 @@ import {
   IconPlusCircle,
 } from '@douyinfe/semi-icons';
 import { useTranslation } from 'react-i18next';
+import { StatusContext } from '../../context/Status';
 
 const { Text, Title } = Typography;
 
 const EditToken = (props) => {
   const { t } = useTranslation();
+  const [statusState, statusDispatch] = useContext(StatusContext);
   const [isEdit, setIsEdit] = useState(false);
   const [loading, setLoading] = useState(isEdit);
   const originInputs = {
@@ -119,7 +121,19 @@ const EditToken = (props) => {
         value: group,
         ratio: info.ratio,
       }));
+      if (statusState?.status?.default_use_auto_group) {
+        // if contain auto, add it to the first position
+        if (localGroupOptions.some((group) => group.value === 'auto')) {
+          // 排序
+          localGroupOptions.sort((a, b) => (a.value === 'auto' ? -1 : 1));
+        } else {
+          localGroupOptions.unshift({ label: t('自动选择'), value: 'auto' });
+        }
+      }
       setGroups(localGroupOptions);
+      if (statusState?.status?.default_use_auto_group) {
+        setInputs({ ...inputs, group: 'auto' });
+      }
     } else {
       showError(t(message));
     }
@@ -268,32 +282,37 @@ const EditToken = (props) => {
       placement={isEdit ? 'right' : 'left'}
       title={
         <Space>
-          {isEdit ?
-            <Tag color="blue" shape="circle">{t('更新')}</Tag> :
-            <Tag color="green" shape="circle">{t('新建')}</Tag>
-          }
-          <Title heading={4} className="m-0">
+          {isEdit ? (
+            <Tag color='blue' shape='circle'>
+              {t('更新')}
+            </Tag>
+          ) : (
+            <Tag color='green' shape='circle'>
+              {t('新建')}
+            </Tag>
+          )}
+          <Title heading={4} className='m-0'>
             {isEdit ? t('更新令牌信息') : t('创建新的令牌')}
           </Title>
         </Space>
       }
       headerStyle={{
         borderBottom: '1px solid var(--semi-color-border)',
-        padding: '24px'
+        padding: '24px',
       }}
       bodyStyle={{
         backgroundColor: 'var(--semi-color-bg-0)',
-        padding: '0'
+        padding: '0',
       }}
       visible={props.visiable}
       width={isMobile() ? '100%' : 600}
       footer={
-        <div className="flex justify-end bg-white">
+        <div className='flex justify-end bg-white'>
           <Space>
             <Button
-              theme="solid"
-              size="large"
-              className="!rounded-full"
+              theme='solid'
+              size='large'
+              className='!rounded-full'
               onClick={submit}
               icon={<IconSave />}
               loading={loading}
@@ -301,10 +320,10 @@ const EditToken = (props) => {
               {t('提交')}
             </Button>
             <Button
-              theme="light"
-              size="large"
-              className="!rounded-full"
-              type="primary"
+              theme='light'
+              size='large'
+              className='!rounded-full'
+              type='primary'
               onClick={handleCancel}
               icon={<IconClose />}
             >
@@ -317,87 +336,107 @@ const EditToken = (props) => {
       onCancel={() => handleCancel()}
     >
       <Spin spinning={loading}>
-        <div className="p-6">
-          <Card className="!rounded-2xl shadow-sm border-0 mb-6">
-            <div className="flex items-center mb-4 p-6 rounded-xl" style={{
-              background: 'linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #3b82f6 100%)',
-              position: 'relative'
-            }}>
-              <div className="absolute inset-0 overflow-hidden">
-                <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
-                <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
+        <div className='p-6'>
+          <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
+            <div
+              className='flex items-center mb-4 p-6 rounded-xl'
+              style={{
+                background:
+                  'linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #3b82f6 100%)',
+                position: 'relative',
+              }}
+            >
+              <div className='absolute inset-0 overflow-hidden'>
+                <div className='absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full'></div>
+                <div className='absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full'></div>
               </div>
-              <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
-                <IconPlusCircle size="large" style={{ color: '#ffffff' }} />
+              <div className='w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative'>
+                <IconPlusCircle size='large' style={{ color: '#ffffff' }} />
               </div>
-              <div className="relative">
-                <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('基本信息')}</Text>
-                <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置令牌的基本信息')}</div>
+              <div className='relative'>
+                <Text
+                  style={{ color: '#ffffff' }}
+                  className='text-lg font-medium'
+                >
+                  {t('基本信息')}
+                </Text>
+                <div
+                  style={{ color: '#ffffff' }}
+                  className='text-sm opacity-80'
+                >
+                  {t('设置令牌的基本信息')}
+                </div>
               </div>
             </div>
 
-            <div className="space-y-4">
+            <div className='space-y-4'>
               <div>
-                <Text strong className="block mb-2">{t('名称')}</Text>
+                <Text strong className='block mb-2'>
+                  {t('名称')}
+                </Text>
                 <Input
                   placeholder={t('请输入名称')}
                   onChange={(value) => handleInputChange('name', value)}
                   value={name}
-                  autoComplete="new-password"
-                  size="large"
-                  className="!rounded-lg"
+                  autoComplete='new-password'
+                  size='large'
+                  className='!rounded-lg'
                   showClear
                   required
                 />
               </div>
 
               <div>
-                <Text strong className="block mb-2">{t('过期时间')}</Text>
-                <div className="mb-2">
+                <Text strong className='block mb-2'>
+                  {t('过期时间')}
+                </Text>
+                <div className='mb-2'>
                   <DatePicker
                     placeholder={t('请选择过期时间')}
-                    onChange={(value) => handleInputChange('expired_time', value)}
+                    onChange={(value) =>
+                      handleInputChange('expired_time', value)
+                    }
                     value={expired_time}
-                    autoComplete="new-password"
-                    type="dateTime"
-                    className="w-full !rounded-lg"
-                    size="large"
+                    autoComplete='new-password'
+                    type='dateTime'
+                    className='w-full !rounded-lg'
+                    size='large'
                     prefix={<IconCalendar />}
                   />
                 </div>
 
-                <div className="flex flex-wrap gap-2">
+                <div className='flex flex-wrap gap-2'>
                   <Button
-                    theme="light"
-                    type="primary"
+                    theme='light'
+                    type='primary'
                     onClick={() => setExpiredTime(0, 0, 0, 0)}
-                    className="!rounded-full"
+                    className='!rounded-full'
                   >
                     {t('永不过期')}
                   </Button>
                   <Button
-                    theme="light"
-                    type="tertiary"
+                    theme='light'
+                    type='tertiary'
                     onClick={() => setExpiredTime(0, 0, 1, 0)}
-                    className="!rounded-full"
+                    className='!rounded-full'
                     icon={<IconClock />}
                   >
                     {t('一小时')}
                   </Button>
                   <Button
-                    theme="light"
-                    type="tertiary"
+                    theme='light'
+                    type='tertiary'
                     onClick={() => setExpiredTime(0, 1, 0, 0)}
-                    className="!rounded-full"
+                    className='!rounded-full'
                     icon={<IconCalendar />}
                   >
                     {t('一天')}
                   </Button>
                   <Button
-                    theme="light"
-                    type="tertiary"
+                    theme='light'
+                    type='tertiary'
                     onClick={() => setExpiredTime(1, 0, 0, 0)}
-                    className="!rounded-full"
+                    className='!rounded-full'
                     icon={<IconCalendar />}
                   >
                     {t('一个月')}
@@ -407,44 +446,62 @@ const EditToken = (props) => {
             </div>
           </Card>
 
-          <Card className="!rounded-2xl shadow-sm border-0 mb-6">
-            <div className="flex items-center mb-4 p-6 rounded-xl" style={{
-              background: 'linear-gradient(135deg, #065f46 0%, #059669 50%, #10b981 100%)',
-              position: 'relative'
-            }}>
-              <div className="absolute inset-0 overflow-hidden">
-                <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
-                <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
+          <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
+            <div
+              className='flex items-center mb-4 p-6 rounded-xl'
+              style={{
+                background:
+                  'linear-gradient(135deg, #065f46 0%, #059669 50%, #10b981 100%)',
+                position: 'relative',
+              }}
+            >
+              <div className='absolute inset-0 overflow-hidden'>
+                <div className='absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full'></div>
+                <div className='absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full'></div>
               </div>
-              <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
-                <IconCreditCard size="large" style={{ color: '#ffffff' }} />
+              <div className='w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative'>
+                <IconCreditCard size='large' style={{ color: '#ffffff' }} />
               </div>
-              <div className="relative">
-                <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('额度设置')}</Text>
-                <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置令牌可用额度和数量')}</div>
+              <div className='relative'>
+                <Text
+                  style={{ color: '#ffffff' }}
+                  className='text-lg font-medium'
+                >
+                  {t('额度设置')}
+                </Text>
+                <div
+                  style={{ color: '#ffffff' }}
+                  className='text-sm opacity-80'
+                >
+                  {t('设置令牌可用额度和数量')}
+                </div>
               </div>
             </div>
 
             <Banner
-              type="warning"
-              description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')}
-              className="mb-4 !rounded-lg"
+              type='warning'
+              description={t(
+                '注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。',
+              )}
+              className='mb-4 !rounded-lg'
             />
 
-            <div className="space-y-4">
+            <div className='space-y-4'>
               <div>
-                <div className="flex justify-between mb-2">
+                <div className='flex justify-between mb-2'>
                   <Text strong>{t('额度')}</Text>
-                  <Text type="tertiary">{renderQuotaWithPrompt(remain_quota)}</Text>
+                  <Text type='tertiary'>
+                    {renderQuotaWithPrompt(remain_quota)}
+                  </Text>
                 </div>
                 <AutoComplete
                   placeholder={t('请输入额度')}
                   onChange={(value) => handleInputChange('remain_quota', value)}
                   value={remain_quota}
-                  autoComplete="new-password"
-                  type="number"
-                  size="large"
-                  className="w-full !rounded-lg"
+                  autoComplete='new-password'
+                  type='number'
+                  size='large'
+                  className='w-full !rounded-lg'
                   prefix={<IconCreditCard />}
                   data={[
                     { value: 500000, label: '1$' },
@@ -460,16 +517,18 @@ const EditToken = (props) => {
 
               {!isEdit && (
                 <div>
-                  <Text strong className="block mb-2">{t('新建数量')}</Text>
+                  <Text strong className='block mb-2'>
+                    {t('新建数量')}
+                  </Text>
                   <AutoComplete
                     placeholder={t('请选择或输入创建令牌的数量')}
                     onChange={(value) => handleTokenCountChange(value)}
                     onSelect={(value) => handleTokenCountChange(value)}
                     value={tokenCount.toString()}
-                    autoComplete="off"
-                    type="number"
-                    className="w-full !rounded-lg"
-                    size="large"
+                    autoComplete='off'
+                    type='number'
+                    className='w-full !rounded-lg'
+                    size='large'
                     prefix={<IconPlusCircle />}
                     data={[
                       { value: 10, label: t('10个') },
@@ -482,12 +541,12 @@ const EditToken = (props) => {
                 </div>
               )}
 
-              <div className="flex justify-end">
+              <div className='flex justify-end'>
                 <Button
-                  theme="light"
-                  type={unlimited_quota ? "danger" : "warning"}
+                  theme='light'
+                  type={unlimited_quota ? 'danger' : 'warning'}
                   onClick={setUnlimitedQuota}
-                  className="!rounded-full"
+                  className='!rounded-full'
                 >
                   {unlimited_quota ? t('取消无限额度') : t('设为无限额度')}
                 </Button>
@@ -495,92 +554,137 @@ const EditToken = (props) => {
             </div>
           </Card>
 
-          <Card className="!rounded-2xl shadow-sm border-0 mb-6">
-            <div className="flex items-center mb-4 p-6 rounded-xl" style={{
-              background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
-              position: 'relative'
-            }}>
-              <div className="absolute inset-0 overflow-hidden">
-                <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
-                <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
+          <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
+            <div
+              className='flex items-center mb-4 p-6 rounded-xl'
+              style={{
+                background:
+                  'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
+                position: 'relative',
+              }}
+            >
+              <div className='absolute inset-0 overflow-hidden'>
+                <div className='absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full'></div>
+                <div className='absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full'></div>
               </div>
-              <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
-                <IconLink size="large" style={{ color: '#ffffff' }} />
+              <div className='w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative'>
+                <IconLink size='large' style={{ color: '#ffffff' }} />
               </div>
-              <div className="relative">
-                <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('访问限制')}</Text>
-                <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置令牌的访问限制')}</div>
+              <div className='relative'>
+                <Text
+                  style={{ color: '#ffffff' }}
+                  className='text-lg font-medium'
+                >
+                  {t('访问限制')}
+                </Text>
+                <div
+                  style={{ color: '#ffffff' }}
+                  className='text-sm opacity-80'
+                >
+                  {t('设置令牌的访问限制')}
+                </div>
               </div>
             </div>
 
-            <div className="space-y-4">
+            <div className='space-y-4'>
               <div>
-                <Text strong className="block mb-2">{t('IP白名单')}</Text>
+                <Text strong className='block mb-2'>
+                  {t('IP白名单')}
+                </Text>
                 <TextArea
                   placeholder={t('允许的IP,一行一个,不填写则不限制')}
                   onChange={(value) => handleInputChange('allow_ips', value)}
                   value={inputs.allow_ips}
                   style={{ fontFamily: 'JetBrains Mono, Consolas' }}
-                  className="!rounded-lg"
+                  className='!rounded-lg'
                   rows={4}
                 />
-                <Text type="tertiary" className="mt-1 block text-xs">{t('请勿过度信任此功能,IP可能被伪造')}</Text>
+                <Text type='tertiary' className='mt-1 block text-xs'>
+                  {t('请勿过度信任此功能,IP可能被伪造')}
+                </Text>
               </div>
 
               <div>
-                <div className="flex items-center mb-2">
+                <div className='flex items-center mb-2'>
                   <Checkbox
                     checked={model_limits_enabled}
-                    onChange={(e) => handleInputChange('model_limits_enabled', e.target.checked)}
+                    onChange={(e) =>
+                      handleInputChange(
+                        'model_limits_enabled',
+                        e.target.checked,
+                      )
+                    }
                   >
                     <Text strong>{t('模型限制')}</Text>
                   </Checkbox>
                 </div>
                 <Select
-                  placeholder={model_limits_enabled ? t('请选择该渠道所支持的模型') : t('勾选启用模型限制后可选择')}
+                  placeholder={
+                    model_limits_enabled
+                      ? t('请选择该渠道所支持的模型')
+                      : t('勾选启用模型限制后可选择')
+                  }
                   onChange={(value) => handleInputChange('model_limits', value)}
                   value={inputs.model_limits}
                   multiple
-                  size="large"
-                  className="w-full !rounded-lg"
+                  size='large'
+                  className='w-full !rounded-lg'
                   prefix={<IconServer />}
                   optionList={models}
                   disabled={!model_limits_enabled}
                   maxTagCount={3}
                 />
-                <Text type="tertiary" className="mt-1 block text-xs">{t('非必要,不建议启用模型限制')}</Text>
+                <Text type='tertiary' className='mt-1 block text-xs'>
+                  {t('非必要,不建议启用模型限制')}
+                </Text>
               </div>
             </div>
           </Card>
 
-          <Card className="!rounded-2xl shadow-sm border-0">
-            <div className="flex items-center mb-4 p-6 rounded-xl" style={{
-              background: 'linear-gradient(135deg, #92400e 0%, #d97706 50%, #f59e0b 100%)',
-              position: 'relative'
-            }}>
-              <div className="absolute inset-0 overflow-hidden">
-                <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
-                <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
+          <Card className='!rounded-2xl shadow-sm border-0'>
+            <div
+              className='flex items-center mb-4 p-6 rounded-xl'
+              style={{
+                background:
+                  'linear-gradient(135deg, #92400e 0%, #d97706 50%, #f59e0b 100%)',
+                position: 'relative',
+              }}
+            >
+              <div className='absolute inset-0 overflow-hidden'>
+                <div className='absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full'></div>
+                <div className='absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full'></div>
               </div>
-              <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
-                <IconUserGroup size="large" style={{ color: '#ffffff' }} />
+              <div className='w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative'>
+                <IconUserGroup size='large' style={{ color: '#ffffff' }} />
               </div>
-              <div className="relative">
-                <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('分组信息')}</Text>
-                <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置令牌的分组')}</div>
+              <div className='relative'>
+                <Text
+                  style={{ color: '#ffffff' }}
+                  className='text-lg font-medium'
+                >
+                  {t('分组信息')}
+                </Text>
+                <div
+                  style={{ color: '#ffffff' }}
+                  className='text-sm opacity-80'
+                >
+                  {t('设置令牌的分组')}
+                </div>
               </div>
             </div>
 
             <div>
-              <Text strong className="block mb-2">{t('令牌分组')}</Text>
+              <Text strong className='block mb-2'>
+                {t('令牌分组')}
+              </Text>
               {groups.length > 0 ? (
                 <Select
                   placeholder={t('令牌分组,默认为用户的分组')}
                   onChange={(value) => handleInputChange('group', value)}
                   renderOptionItem={renderGroupOption}
                   value={inputs.group}
-                  size="large"
-                  className="w-full !rounded-lg"
+                  size='large'
+                  className='w-full !rounded-lg'
                   prefix={<IconUserGroup />}
                   optionList={groups}
                 />
@@ -588,8 +692,8 @@ const EditToken = (props) => {
                 <Select
                   placeholder={t('管理员未设置用户可选分组')}
                   disabled={true}
-                  size="large"
-                  className="w-full !rounded-lg"
+                  size='large'
+                  className='w-full !rounded-lg'
                   prefix={<IconUserGroup />}
                 />
               )}

+ 17 - 1
web/src/pages/User/AddUser.js

@@ -16,6 +16,7 @@ import {
   IconClose,
   IconKey,
   IconUserAdd,
+  IconEdit,
 } from '@douyinfe/semi-icons';
 import { useTranslation } from 'react-i18next';
 
@@ -27,10 +28,11 @@ const AddUser = (props) => {
     username: '',
     display_name: '',
     password: '',
+    remark: '',
   };
   const [inputs, setInputs] = useState(originInputs);
   const [loading, setLoading] = useState(false);
-  const { username, display_name, password } = inputs;
+  const { username, display_name, password, remark } = inputs;
 
   const handleInputChange = (name, value) => {
     setInputs((inputs) => ({ ...inputs, [name]: value }));
@@ -175,6 +177,20 @@ const AddUser = (props) => {
                     required
                   />
                 </div>
+
+                <div>
+                  <Text strong className="block mb-2">{t('备注')}</Text>
+                  <Input
+                    placeholder={t('请输入备注(仅管理员可见)')}
+                    onChange={(value) => handleInputChange('remark', value)}
+                    value={remark}
+                    autoComplete="off"
+                    size="large"
+                    className="!rounded-lg"
+                    prefix={<IconEdit />}
+                    showClear
+                  />
+                </div>
               </div>
             </Card>
           </div>

+ 17 - 0
web/src/pages/User/EditUser.js

@@ -22,6 +22,7 @@ import {
   IconLink,
   IconUserGroup,
   IconPlus,
+  IconEdit,
 } from '@douyinfe/semi-icons';
 import { useTranslation } from 'react-i18next';
 
@@ -42,6 +43,7 @@ const EditUser = (props) => {
     email: '',
     quota: 0,
     group: 'default',
+    remark: '',
   });
   const [groupOptions, setGroupOptions] = useState([]);
   const {
@@ -55,6 +57,7 @@ const EditUser = (props) => {
     email,
     quota,
     group,
+    remark,
   } = inputs;
   const handleInputChange = (name, value) => {
     setInputs((inputs) => ({ ...inputs, [name]: value }));
@@ -247,6 +250,20 @@ const EditUser = (props) => {
                     showClear
                   />
                 </div>
+
+                <div>
+                  <Text strong className="block mb-2">{t('备注')}</Text>
+                  <Input
+                    placeholder={t('请输入备注(仅管理员可见)')}
+                    onChange={(value) => handleInputChange('remark', value)}
+                    value={remark}
+                    autoComplete="off"
+                    size="large"
+                    className="!rounded-lg"
+                    prefix={<IconEdit />}
+                    showClear
+                  />
+                </div>
               </div>
             </Card>
 

Неке датотеке нису приказане због велике количине промена