diff --git a/README.md b/README.md index e9d1c154..6ba3574c 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ GoReportCard + + CodeRabbit Pull Request Reviews +

@@ -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的前端美化版本 ## 帮助支持 diff --git a/common/database.go b/common/database.go index 3c0a944b..9cbaf46a 100644 --- a/common/database.go +++ b/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 diff --git a/common/redis.go b/common/redis.go index ba35331a..1efc217f 100644 --- a/common/redis.go +++ b/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 { diff --git a/common/utils.go b/common/utils.go index 587de537..d9db67d0 100644 --- a/common/utils.go +++ b/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") } + 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) - return strconv.ParseFloat(string(bytes.TrimSpace(output)), 64) + // ffmpeg -y -i filename -vcodec copy -acodec copy + 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) } diff --git a/constant/cache_key.go b/constant/cache_key.go index 27cb3b75..daedfd40 100644 --- a/constant/cache_key.go +++ b/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 ( diff --git a/constant/user_setting.go b/constant/user_setting.go index 055884f7..7e79035e 100644 --- a/constant/user_setting.go +++ b/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 ( diff --git a/controller/channel-test.go b/controller/channel-test.go index f9c7bf7b..d162d8cf 100644 --- a/controller/channel-test.go +++ b/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, "通道测试完成", "所有通道测试已完成") } diff --git a/controller/channel.go b/controller/channel.go index a4ef87c3..1cfb7906 100644 --- a/controller/channel.go +++ b/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 } diff --git a/controller/console_migrate.go b/controller/console_migrate.go new file mode 100644 index 00000000..d25f199b --- /dev/null +++ b/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"}) +} \ No newline at end of file diff --git a/controller/group.go b/controller/group.go index 2c725a4d..632b6cd5 100644 --- a/controller/group.go +++ b/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": "", diff --git a/controller/midjourney.go b/controller/midjourney.go index 21027d8f..56bdcb80 100644 --- a/controller/midjourney.go +++ b/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, + }, }) } diff --git a/controller/misc.go b/controller/misc.go index 8fa8e8f6..1caaf640 100644 --- a/controller/misc.go +++ b/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 } diff --git a/controller/model.go b/controller/model.go index df7e59a6..134217a3 100644 --- a/controller/model.go +++ b/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]) diff --git a/controller/option.go b/controller/option.go index b52012fd..79ba2ffe 100644 --- a/controller/option.go +++ b/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, diff --git a/controller/playground.go b/controller/playground.go index a2b54790..37a5c7b0 100644 --- a/controller/playground.go +++ b/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 } diff --git a/controller/pricing.go b/controller/pricing.go index 1cbfe731..e6a3e57f 100644 --- a/controller/pricing.go +++ b/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 + } + } } } diff --git a/controller/redemption.go b/controller/redemption.go index a7e09a8a..50620597 100644 --- a/controller/redemption.go +++ b/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 +} diff --git a/controller/relay.go b/controller/relay.go index 1a875dbc..c1c45114 100644 --- a/controller/relay.go +++ b/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 diff --git a/controller/setup.go b/controller/setup.go index 0a13bcf9..8943a1a0 100644 --- a/controller/setup.go +++ b/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{ diff --git a/controller/task.go b/controller/task.go index 65f79ead..34e14f3f 100644 --- a/controller/task.go +++ b/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, + }, }) } diff --git a/controller/token.go b/controller/token.go index a8803279..c57552c0 100644 --- a/controller/token.go +++ b/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 } diff --git a/controller/uptime_kuma.go b/controller/uptime_kuma.go index 6ceaa1f3..05d6297e 100644 --- a/controller/uptime_kuma.go +++ b/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"` -} +const ( + requestTimeout = 30 * time.Second + httpTimeout = 10 * time.Second + uptimeKeySuffix = "_24" + apiStatusPath = "/api/status-page/" + apiHeartbeatPath = "/api/status-page/heartbeat/" +) -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"` -} - -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, baseURL+apiHeartbeatPath+slug, &heartbeatData) }) - g.Go(func() error { - return getAndDecode(gCtx, client, heartbeatUrl, &heartbeatData) - }) + if g.Wait() != nil { + return result + } - 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(), - }) + for _, pg := range statusData.PublicGroupList { + if len(pg.MonitorList) == 0 { + continue } + + for _, m := range pg.MonitorList { + monitor := Monitor{ + Name: m.Name, + Group: pg.Name, + } + + monitorID := strconv.Itoa(m.ID) + + if uptime, exists := heartbeatData.UptimeList[monitorID+uptimeKeySuffix]; exists { + monitor.Uptime = uptime + } + + if heartbeats, exists := heartbeatData.HeartbeatList[monitorID]; exists && len(heartbeats) > 0 { + monitor.Status = heartbeats[0].Status + } + + result.Monitors = append(result.Monitors, monitor) + } + } + + 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 } - var monitors []MonitorStatus - for _, group := range statusData.PublicGroupList { - for _, monitor := range group.MonitorList { - monitorStatus := MonitorStatus{ - Name: monitor.Name, - Uptime: 0.0, - Status: 0, - } + ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout) + defer cancel() - uptimeKey := fmt.Sprintf("%d_24", monitor.ID) - if uptime, exists := heartbeatData.UptimeList[uptimeKey]; exists { - monitorStatus.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 - } - - monitors = append(monitors, monitorStatus) - } + 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 + }) } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": monitors, - }) + + g.Wait() + c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": results}) } \ No newline at end of file diff --git a/controller/user.go b/controller/user.go index fd53e743..e8ce3c3d 100644 --- a/controller/user.go +++ b/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相关设置 diff --git a/dto/claude.go b/dto/claude.go index 4d24bc70..98e09c78 100644 --- a/dto/claude.go +++ b/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 { diff --git a/dto/openai_request.go b/dto/openai_request.go index 10e10332..c8355e54 100644 --- a/dto/openai_request.go +++ b/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 { diff --git a/main.go b/main.go index c286650f..30ba8092 100644 --- a/main.go +++ b/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() diff --git a/middleware/distributor.go b/middleware/distributor.go index 1bfe1821..5d1c3641 100644 --- a/middleware/distributor.go +++ b/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)) diff --git a/model/ability.go b/model/ability.go index 38b0bd73..96a9ef6a 100644 --- a/model/ability.go +++ b/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() diff --git a/model/cache.go b/model/cache.go index e2f83e22..3e5eb4c4 100644 --- a/model/cache.go +++ b/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-*" } diff --git a/model/channel.go b/model/channel.go index ed7a0a7e..b5503eee 100644 --- a/model/channel.go +++ b/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 +} diff --git a/model/log.go b/model/log.go index 0a891fcd..b3fd1ad2 100644 --- a/model/log.go +++ b/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) diff --git a/model/main.go b/model/main.go index 61d6bb10..965bba93 100644 --- a/model/main.go +++ b/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 !common.UsingPostgreSQL { + return migrateDBFast() + } + err := DB.AutoMigrate( + &Channel{}, + &Token{}, + &User{}, + &Option{}, + &Redemption{}, + &Ability{}, + &Log{}, + &Midjourney{}, + &TopUp{}, + &QuotaData{}, + &Task{}, + &Setup{}, + ) if err != nil { return err } - err = DB.AutoMigrate(&Token{}) - 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(&User{}) - 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(&Option{}) - 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(&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 - } - err = DB.AutoMigrate(&QuotaData{}) - if err != nil { - return err - } - err = DB.AutoMigrate(&Task{}) - if err != nil { - return err - } - err = DB.AutoMigrate(&Setup{}) common.SysLog("database migrated") - //err = createRootAccountIfNeed() - return err + return nil } func migrateLOGDB() error { diff --git a/model/midjourney.go b/model/midjourney.go index 5f85abfd..e8140447 100644 --- a/model/midjourney.go +++ b/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 +} diff --git a/model/option.go b/model/option.go index 42949e8b..1391b203 100644 --- a/model/option.go +++ b/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": diff --git a/model/redemption.go b/model/redemption.go index 89c4ac8c..bf237668 100644 --- a/model/redemption.go +++ b/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 +} diff --git a/model/task.go b/model/task.go index df221edf..9e4177ba 100644 --- a/model/task.go +++ b/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 +} diff --git a/model/token.go b/model/token.go index 8587ea62..2ed2c09a 100644 --- a/model/token.go +++ b/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 +} diff --git a/model/token_cache.go b/model/token_cache.go index b2e0c951..a4b0beae 100644 --- a/model/token_cache.go +++ b/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 } diff --git a/model/user.go b/model/user.go index 1a3372aa..6a695457 100644 --- a/model/user.go +++ b/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 } diff --git a/model/user_cache.go b/model/user_cache.go index d74877bd..e673defc 100644 --- a/model/user_cache.go +++ b/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, ) } diff --git a/relay/channel/claude/relay-claude.go b/relay/channel/claude/relay-claude.go index cb2c75b1..ba20adea 100644 --- a/relay/channel/claude/relay-claude.go +++ b/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 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.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 { - 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) - } - } + // } 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 info.ShouldIncludeUsage { response := helper.GenerateFinalUsageResponse(claudeInfo.ResponseId, claudeInfo.Created, info.UpstreamModelName, *claudeInfo.Usage) err := helper.ObjectData(c, response) diff --git a/relay/channel/cohere/relay-cohere.go b/relay/channel/cohere/relay-cohere.go index 10c4328b..8a044bf2 100644 --- a/relay/channel/cohere/relay-cohere.go +++ b/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 := "" diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go index e6f66d5f..a81eb3a9 100644 --- a/relay/channel/gemini/adaptor.go +++ b/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- 格式 + 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") diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index e2288faf..635041d7 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/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, diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index cef958b2..ea24d811 100644 --- a/relay/channel/openai/adaptor.go +++ b/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 { diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go index 2e3d8df1..4dc0fc60 100644 --- a/relay/channel/openai/relay-openai.go +++ b/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) } diff --git a/relay/channel/palm/relay-palm.go b/relay/channel/palm/relay-palm.go index 5c398b5e..0c6f8641 100644 --- a/relay/channel/palm/relay-palm.go +++ b/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) diff --git a/relay/claude_handler.go b/relay/claude_handler.go index fb68a88a..e8805255 100644 --- a/relay/claude_handler.go +++ b/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 diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index f4fc3c1e..a842a58d 100644 --- a/relay/common/relay_info.go +++ b/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), diff --git a/relay/helper/price.go b/relay/helper/price.go index 89efa1da..326790b4 100644 --- a/relay/helper/price.go +++ b/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, diff --git a/relay/relay-gemini.go b/relay/relay-gemini.go index 93a2b7aa..21cf5e12 100644 --- a/relay/relay-gemini.go +++ b/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) diff --git a/relay/relay-image.go b/relay/relay-image.go index dc63cce8..197a8af6 100644 --- a/relay/relay-image.go +++ b/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) diff --git a/relay/relay-text.go b/relay/relay-text.go index a48a664a..24fb8155 100644 --- a/relay/relay-text.go +++ b/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 diff --git a/relay/websocket.go b/relay/websocket.go index c815eb71..571f3a82 100644 --- a/relay/websocket.go +++ b/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 } diff --git a/router/api-router.go b/router/api-router.go index 0ab8be7f..45930246 100644 --- a/router/api-router.go +++ b/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") diff --git a/service/channel.go b/service/channel.go index e3a76af4..746e9a34 100644 --- a/service/channel.go +++ b/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": diff --git a/service/convert.go b/service/convert.go index 53aefb62..df7acf0d 100644 --- a/service/convert.go +++ b/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 { diff --git a/service/error.go b/service/error.go index 1bf5992b..f3d8a17d 100644 --- a/service/error.go +++ b/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, diff --git a/service/file_decoder.go b/service/file_decoder.go index bbb188f8..c1d4fb0c 100644 --- a/service/file_decoder.go +++ b/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 + } +} diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 75457b97..1edc9073 100644 --- a/service/log_info_generate.go +++ b/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 diff --git a/service/quota.go b/service/quota.go index 43297b4a..8c7ed07e 100644 --- a/service/quota.go +++ b/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) } diff --git a/setting/auto_group.go b/setting/auto_group.go new file mode 100644 index 00000000..5a87ae56 --- /dev/null +++ b/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) +} diff --git a/setting/console.go b/setting/console.go deleted file mode 100644 index 94023666..00000000 --- a/setting/console.go +++ /dev/null @@ -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{" 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{" 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{" 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 -} \ No newline at end of file diff --git a/setting/console_setting/config.go b/setting/console_setting/config.go new file mode 100644 index 00000000..6327e558 --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/setting/console_setting/validation.go b/setting/console_setting/validation.go new file mode 100644 index 00000000..fda6453d --- /dev/null +++ b/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{" 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) +} \ No newline at end of file diff --git a/setting/group_ratio.go b/setting/group_ratio.go index 8b163625..28dbd167 100644 --- a/setting/group_ratio.go +++ b/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) diff --git a/setting/operation_setting/model-ratio.go b/setting/operation_setting/model-ratio.go index 700a7c4e..5155b2fc 100644 --- a/setting/operation_setting/model-ratio.go +++ b/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 { diff --git a/setting/user_usable_group.go b/setting/user_usable_group.go index 7082b683..fdf2f723 100644 --- a/setting/user_usable_group.go +++ b/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 +} diff --git a/web/README.md b/web/README.md deleted file mode 100644 index 07a1fd2a..00000000 --- a/web/README.md +++ /dev/null @@ -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 diff --git a/web/package.json b/web/package.json index df98db8e..a313e0f5 100644 --- a/web/package.json +++ b/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" diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml deleted file mode 100644 index c503b5bb..00000000 --- a/web/pnpm-lock.yaml +++ /dev/null @@ -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: {} diff --git a/web/src/components/layout/NoticeModal.js b/web/src/components/layout/NoticeModal.js index 9bb062a1..1bbaa554 100644 --- a/web/src/components/layout/NoticeModal.js +++ b/web/src/components/layout/NoticeModal.js @@ -64,11 +64,7 @@ const NoticeModal = ({ visible, onClose, isMobile }) => { return (
); }; diff --git a/web/src/components/settings/DashboardSetting.js b/web/src/components/settings/DashboardSetting.js index 649e0ffa..bf4a26a3 100644 --- a/web/src/components/settings/DashboardSetting.js +++ b/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 ( <> + {/* 用于迁移检测的旧键模态框,下个版本会删除 */} + setShowMigrateModal(false)} + confirmLoading={loading} + okText="确认迁移" + cancelText="取消" + > +

检测到旧版本的配置数据,是否要迁移到新的配置格式?

+

+ 注意:迁移过程中会自动处理数据格式转换,迁移完成后旧配置将被清除,请在迁移前在数据库中备份好旧配置。 +

+
+ {/* API信息管理 */} diff --git a/web/src/components/settings/OperationSetting.js b/web/src/components/settings/OperationSetting.js index 2dc0b88e..7bd9bf62 100644 --- a/web/src/components/settings/OperationSetting.js +++ b/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 { diff --git a/web/src/components/settings/PersonalSetting.js b/web/src/components/settings/PersonalSetting.js index 3228d184..36eb4e4d 100644 --- a/web/src/components/settings/PersonalSetting.js +++ b/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={
- {t('通知设置')} + {t('其他设置')}
} itemKey='notification' @@ -1228,28 +1231,68 @@ const PersonalSetting = () => { +
+
+ {/* 接受未设置价格模型 */} +
+
+
+ +
+
+
+
+ + {t('接受未设置价格模型')} + +
+ {t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')} +
+
+ + handleNotificationSettingChange( + 'acceptUnsetModelRatioModel', + e.target.checked, + ) + } + className="ml-4" + /> +
+
+
+
+
+
+
+ +
- +
- {t('接受未设置价格模型')} + {t('记录请求与错误日志 IP')}
- {t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')} + {t('开启后,仅“消费”和“错误”日志将记录您的客户端 IP 地址')}
handleNotificationSettingChange( - 'acceptUnsetModelRatioModel', + 'recordIpLog', e.target.checked, ) } diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index 6eeeab9a..f5a78490 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/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); }} />
@@ -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); }} />
@@ -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); diff --git a/web/src/components/table/LogsTable.js b/web/src/components/table/LogsTable.js index 6c8996a0..90e4a809 100644 --- a/web/src/components/table/LogsTable.js +++ b/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 = () => { {renderModelTag(record.model_name, { onClick: (event) => { - copyText(event, record.model_name).then((r) => { }); + copyText(event, record.model_name).then((r) => {}); }, })}
@@ -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: ( { 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: ( +
+ {t('IP')} + + + +
+ ), + dataIndex: 'ip', + render: (text, record, index) => { + return (record.type === 2 || record.type === 5) && text ? ( + + { + copyText(event, text); + }} + > + {text} + + + ) : ( + <> + ); + }, + }, { 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 ( { 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} >
@@ -1294,12 +1346,24 @@ const LogsTable = () => { }, 0); }} > - {t('全部')} - {t('充值')} - {t('消费')} - {t('管理')} - {t('系统')} - {t('错误')} + + {t('全部')} + + + {t('充值')} + + + {t('消费')} + + + {t('管理')} + + + {t('系统')} + + + {t('错误')} +
@@ -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={ } - darkModeImage={} + image={ + + } + darkModeImage={ + + } description={t('搜索无结果')} style={{ padding: 30 }} /> diff --git a/web/src/components/table/MjLogsTable.js b/web/src/components/table/MjLogsTable.js index 08376641..869db485 100644 --- a/web/src/components/table/MjLogsTable.js +++ b/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 = () => { > { total: logCount, pageSizeOptions: [10, 20, 50, 100], showSizeChanger: true, - onPageSizeChange: (size) => { - handlePageSizeChange(size); - }, + onPageSizeChange: handlePageSizeChange, onPageChange: handlePageChange, }} /> diff --git a/web/src/components/table/RedemptionsTable.js b/web/src/components/table/RedemptionsTable.js index c314bae2..e11a4657 100644 --- a/web/src/components/table/RedemptionsTable.js +++ b/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 ( + }>{t('已过期')} + ); + } switch (status) { case 1: return ( @@ -101,7 +111,7 @@ const RedemptionsTable = () => { dataIndex: 'status', key: 'status', render: (text, record, index) => { - return
{renderStatus(text)}
; + return
{renderStatus(text, record)}
; }, }, { @@ -124,6 +134,13 @@ const RedemptionsTable = () => { return
{renderTimestamp(text)}
; }, }, + { + title: t('过期时间'), + dataIndex: 'expired_time', + render: (text) => { + return
{text === 0 ? t('永不过期') : renderTimestamp(text)}
; + }, + }, { 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 = () => {
- + {t('兑换码可以批量生成和分发,适合用于推广活动或批量充值。')}
@@ -458,39 +474,66 @@ const RedemptionsTable = () => {
-
+
+
+ + +
-
diff --git a/web/src/components/table/TaskLogsTable.js b/web/src/components/table/TaskLogsTable.js index 4f802515..449b3d55 100644 --- a/web/src/components/table/TaskLogsTable.js +++ b/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 = () => { >
{ total: logCount, pageSizeOptions: [10, 20, 50, 100], showSizeChanger: true, - onPageSizeChange: (size) => { - handlePageSizeChange(size); - }, + onPageSizeChange: handlePageSizeChange, onPageChange: handlePageChange, }} /> diff --git a/web/src/components/table/TokensTable.js b/web/src/components/table/TokensTable.js index 4fc26c6c..bc6c7607 100644 --- a/web/src/components/table/TokensTable.js +++ b/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 = () => (
+
+
+ + {t('令牌用于API访问认证,可以设置额度限制和模型权限。')} +
+
+ + +
{ 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} diff --git a/web/src/components/table/UsersTable.js b/web/src/components/table/UsersTable.js index a027af59..d245c56f 100644 --- a/web/src/components/table/UsersTable.js +++ b/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 {text}; + } + const maxLen = 10; + const displayRemark = remark.length > maxLen ? remark.slice(0, maxLen) + '…' : remark; + return ( + + {text} + + +
+
+ {displayRemark} +
+ + + + ); + }, }, { title: t('分组'), diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js index aef01287..e00a5bdb 100644 --- a/web/src/helpers/api.js +++ b/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, }; diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 3d1bca0d..c9508203 100644 --- a/web/src/helpers/render.js +++ b/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(

{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), + }, + )}

{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(

{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), + }, + )}

{i18next.t('仅供参考,以实际扣费为准')}

@@ -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}}', { diff --git a/web/src/helpers/token.js b/web/src/helpers/token.js index ecdeaa3a..2c6e9f86 100644 --- a/web/src/helpers/token.js +++ b/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 []; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index f4bede2c..b456fff5 100644 --- a/web/src/i18n/locales/en.json +++ b/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)" } \ No newline at end of file diff --git a/web/src/index.css b/web/src/index.css index bb6c3b48..c1254fcc 100644 --- a/web/src/index.css +++ b/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; diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 04dc284b..8bfe5812 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/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 ( diff --git a/web/src/pages/Channel/EditTagModal.js b/web/src/pages/Channel/EditTagModal.js index 04089615..2195724c 100644 --- a/web/src/pages/Channel/EditTagModal.js +++ b/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 ( diff --git a/web/src/pages/Detail/index.js b/web/src/pages/Detail/index.js index 129d82ea..0fd18d16 100644 --- a/web/src/pages/Detail/index.js +++ b/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 ( +
+ } + darkModeImage={} + title={t('暂无监控数据')} + /> +
+ ); + } + + const grouped = {}; + monitors.forEach((m) => { + const g = m.group || ''; + if (!grouped[g]) grouped[g] = []; + grouped[g].push(m); + }); + + const renderItem = (monitor, idx) => ( +
+
+
+
+ {monitor.name} +
+ {((monitor.uptime || 0) * 100).toFixed(2)}% +
+
+ {getUptimeStatusText(monitor.status)} +
+ +
+
+
+ ); + + return Object.entries(grouped).map(([gname, list]) => ( +
+ {gname && ( + <> +
+ {gname} +
+ + + )} + {list.map(renderItem)} +
+ )); + }, [t, getUptimeStatusColor, getUptimeStatusText]); + // ========== Hooks - Effects ========== useEffect(() => { getUserData(); @@ -1015,10 +1100,10 @@ const Detail = (props) => {
-
+
@@ -1061,7 +1146,7 @@ const Detail = (props) => {
- {!statusState?.status?.self_use_mode_enabled && ( + {hasApiInfoPanel && ( { ) : (
} - darkModeImage={} + image={} + darkModeImage={} title={t('暂无API信息')} description={t('请联系管理员在系统设置中配置API信息')} - style={{ padding: '12px' }} />
)} @@ -1138,220 +1222,250 @@ const Detail = (props) => {
{/* 系统公告和常见问答卡片 */} - {!statusState?.status?.self_use_mode_enabled && ( + {hasInfoPanels && (
{/* 公告卡片 */} - -
- - {t('系统公告')} - - {t('显示最新20条')} - -
- {/* 图例 */} -
- {announcementLegendData.map((legend, index) => ( -
-
- {legend.label} -
- ))} -
-
- } - > -
-
handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)} - > - {announcementData.length > 0 ? ( - - ) : ( -
- } - darkModeImage={} - title={t('暂无系统公告')} - description={t('请联系管理员在系统设置中配置公告信息')} - style={{ padding: '12px' }} - /> + {announcementsEnabled && ( + +
+ + {t('系统公告')} + + {t('显示最新20条')} +
- )} -
-
-
- - - {/* 常见问答卡片 */} - - - {t('常见问答')} -
- } - > -
-
handleCardScroll(faqScrollRef, setShowFaqScrollHint)} - > - {faqData.length > 0 ? ( - } - collapseIcon={} - > - {faqData.map((item, index) => ( - -

{item.content}

-
- ))} -
- ) : ( -
- } - darkModeImage={} - title={t('暂无常见问答')} - description={t('请联系管理员在系统设置中配置常见问答')} - style={{ padding: '12px' }} - /> -
- )} -
-
-
- - - {/* 服务可用性卡片 */} - -
- - {t('服务可用性')} -
- } - onClick={loadUptimeData} - loading={uptimeLoading} - size="small" - theme="borderless" - className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full" - /> -
- } - footer={uptimeData.length > 0 ? ( - -
- {uptimeLegendData.map((legend, index) => ( -
-
- {legend.label} -
- ))} -
- - ) : null} - footerStyle={uptimeData.length > 0 ? { marginTop: '-100px' } : undefined} - > -
- -
handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)} - > - {uptimeData.length > 0 ? ( - uptimeData.map((monitor, idx) => ( -
-
-
-
- {monitor.name} -
- {((monitor.uptime || 0) * 100).toFixed(2)}% -
-
- {getUptimeStatusText(monitor.status)} -
- -
-
+ {/* 图例 */} +
+ {announcementLegendData.map((legend, index) => ( +
+
+ {legend.label}
- )) + ))} +
+
+ } + > +
+
handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)} + > + {announcementData.length > 0 ? ( + ) : (
} - darkModeImage={} - title={t('暂无监控数据')} - description={t('请联系管理员在系统设置中配置Uptime')} - style={{ padding: '12px' }} + image={} + darkModeImage={} + title={t('暂无系统公告')} + description={t('请联系管理员在系统设置中配置公告信息')} />
)}
- -
-
- +
+
+ + )} + + {/* 常见问答卡片 */} + {faqEnabled && ( + + + {t('常见问答')} +
+ } + bodyStyle={{ padding: 0 }} + > +
+
handleCardScroll(faqScrollRef, setShowFaqScrollHint)} + > + {faqData.length > 0 ? ( + } + collapseIcon={} + > + {faqData.map((item, index) => ( + +

{item.answer}

+
+ ))} +
+ ) : ( +
+ } + darkModeImage={} + title={t('暂无常见问答')} + description={t('请联系管理员在系统设置中配置常见问答')} + /> +
+ )} +
+
+
+ + )} + + {/* 服务可用性卡片 */} + {uptimeEnabled && ( + +
+ + {t('服务可用性')} +
+ } + onClick={loadUptimeData} + loading={uptimeLoading} + size="small" + theme="borderless" + className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full" + /> +
+ } + bodyStyle={{ padding: 0 }} + > + {/* 内容区域 */} +
+ + {uptimeData.length > 0 ? ( + uptimeData.length === 1 ? ( +
+
handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)} + > + {renderMonitorList(uptimeData[0].monitors)} +
+
+
+ ) : ( + + {uptimeData.map((group, groupIdx) => { + if (!uptimeTabScrollRefs.current[group.categoryName]) { + uptimeTabScrollRefs.current[group.categoryName] = React.createRef(); + } + const tabScrollRef = uptimeTabScrollRefs.current[group.categoryName]; + + return ( + + + {group.categoryName} + + {group.monitors ? group.monitors.length : 0} + + + } + itemKey={group.categoryName} + key={groupIdx} + > +
+
handleCardScroll(tabScrollRef, setShowUptimeScrollHint)} + > + {renderMonitorList(group.monitors)} +
+
+
+ + ); + })} + + ) + ) : ( +
+ } + darkModeImage={} + title={t('暂无监控数据')} + description={t('请联系管理员在系统设置中配置Uptime')} + /> +
+ )} + +
+ + {/* 固定在底部的图例 */} + {uptimeData.length > 0 && ( +
+
+ {uptimeLegendData.map((legend, index) => ( +
+
+ {legend.label} +
+ ))} +
+
+ )} + + )}
)} diff --git a/web/src/pages/Redemption/EditRedemption.js b/web/src/pages/Redemption/EditRedemption.js index e720f90c..a3664d0c 100644 --- a/web/src/pages/Redemption/EditRedemption.js +++ b/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} />
+
+ {t('过期时间')} + { + 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" + /> +
diff --git a/web/src/pages/Setting/Dashboard/SettingsAPIInfo.js b/web/src/pages/Setting/Dashboard/SettingsAPIInfo.js index 4e87c697..6a80b358 100644 --- a/web/src/pages/Setting/Dashboard/SettingsAPIInfo.js +++ b/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.ApiInfo]); + }, [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); + } + }; const columns = [ { @@ -324,6 +353,15 @@ const SettingsAPIInfo = ({ options, refresh }) => { {t('保存设置')}
+ + {/* 启用开关 */} +
+ + {panelEnabled ? t('已启用') : t('已禁用')} +
); @@ -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); diff --git a/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js b/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js index 3a9f71ca..f1b99f43 100644 --- a/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js +++ b/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('保存设置')}
+ + {/* 启用开关 */} +
+ + {panelEnabled ? t('已启用') : t('已禁用')} +
); @@ -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); diff --git a/web/src/pages/Setting/Dashboard/SettingsFAQ.js b/web/src/pages/Setting/Dashboard/SettingsFAQ.js index 3e1ff805..0e029e13 100644 --- a/web/src/pages/Setting/Dashboard/SettingsFAQ.js +++ b/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) => (
{ }, { title: t('回答内容'), - dataIndex: 'content', - key: 'content', + dataIndex: 'answer', + key: 'answer', render: (text) => (
{ 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('保存设置')}
+ + {/* 启用开关 */} +
+ + {panelEnabled ? t('已启用') : t('已禁用')} +
); @@ -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 }) => { >
setFaqForm({ ...faqForm, title: value })} + onChange={(value) => setFaqForm({ ...faqForm, question: value })} /> setFaqForm({ ...faqForm, content: value })} + onChange={(value) => setFaqForm({ ...faqForm, answer: value })} /> diff --git a/web/src/pages/Setting/Dashboard/SettingsUptimeKuma.js b/web/src/pages/Setting/Dashboard/SettingsUptimeKuma.js index 3a7b4896..0b1a2749 100644 --- a/web/src/pages/Setting/Dashboard/SettingsUptimeKuma.js +++ b/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]); - - useEffect(() => { - if (formApiRef.current) { - formApiRef.current.setValues(initValues, { isOverride: true }); + const columns = [ + { + title: t('分类名称'), + dataIndex: 'categoryName', + key: 'categoryName', + render: (text) => ( +
+ {text} +
+ ) + }, + { + title: t('Uptime Kuma地址'), + dataIndex: 'url', + key: 'url', + render: (text) => ( +
+ {text} +
+ ) + }, + { + title: t('状态页面Slug'), + dataIndex: 'slug', + key: 'slug', + render: (text) => ( +
+ {text} +
+ ) + }, + { + title: t('操作'), + key: 'action', + fixed: 'right', + width: 150, + render: (text, record) => ( + + + + + ) } - }, [initValues]); + ]; - const handleSave = async () => { - const api = formApiRef.current; - if (!api) { - showError(t('表单未初始化')); - return; + 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); } + }; + const submitUptimeGroups = async () => { try { setLoading(true); - const { uptimeKumaUrl, uptimeKumaSlug } = await api.validate(); - - const trimmedUrl = (uptimeKumaUrl || '').trim(); - const trimmedSlug = (uptimeKumaSlug || '').trim(); - - if (trimmedUrl === options?.UptimeKumaUrl && trimmedSlug === options?.UptimeKumaSlug) { - showSuccess(t('无需保存,配置未变动')); - return; - } - - 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('保存失败,请重试')); + 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 isValidUrl = useCallback((string) => { - try { - new URL(string); - return true; - } catch (_) { - return 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 { + new URL(uptimeForm.url); + } catch (error) { + showError('请输入有效的URL地址'); + return; + } + + if (!/^[a-zA-Z0-9_-]+$/.test(uptimeForm.slug)) { + showError('Slug只能包含字母、数字、下划线和连字符'); + return; + } + + try { + setModalLoading(true); + + 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]; + } + + setUptimeGroupsList(newList); + setHasChanges(true); + setShowUptimeModal(false); + showSuccess(editingGroup ? '分类已更新,请及时点击“保存设置”进行保存' : '分类已添加,请及时点击“保存设置”进行保存'); + } catch (error) { + showError('操作失败: ' + error.message); + } finally { + 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([]); + } + }; + + 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 { + 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 = () => (
-
+
- - {t('配置')}  - - Uptime Kuma - -  {t('服务监控地址,用于展示服务状态信息')} - + {t('Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)')}
+
-
+ + +
+
+ +
+ + {/* 启用开关 */} +
+ + {panelEnabled ? t('已启用') : t('已禁用')} +
); + 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 ( - -
{ - formApiRef.current = api; + <> + +
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={ + } + darkModeImage={} + description={t('暂无监控数据')} + style={{ padding: 30 }} + /> + } + className="rounded-xl overflow-hidden" + /> + + + setShowUptimeModal(false)} + okText={t('保存')} + cancelText={t('取消')} + className="rounded-xl" + confirmLoading={modalLoading} + width={600} + > + + setUptimeForm({ ...uptimeForm, categoryName: value })} + /> + setUptimeForm({ ...uptimeForm, url: value })} + /> + setUptimeForm({ ...uptimeForm, slug: value })} + /> + + + + { + setShowDeleteModal(false); + setDeletingGroup(null); + }} + okText={t('确认删除')} + cancelText={t('取消')} + type="warning" + className="rounded-xl" + okButtonProps={{ + type: 'danger', + theme: 'solid' }} > - - - { - const url = (value || '').trim(); - - if (url && !isValidUrl(url)) { - return Promise.reject(t('请输入有效的 URL 地址')); - } - - return Promise.resolve(); - } - } - ]} - /> - - - - { - const slug = (value || '').trim(); - - if (slug && !/^[a-zA-Z0-9_-]+$/.test(slug)) { - return Promise.reject(t('Slug 只能包含字母、数字、下划线和连字符')); - } - - return Promise.resolve(); - } - } - ]} - /> - - - - + {t('确定要删除此分类吗?')} + + ); }; diff --git a/web/src/pages/Setting/Model/SettingGeminiModel.js b/web/src/pages/Setting/Model/SettingGeminiModel.js index b802af1a..1d28ae92 100644 --- a/web/src/pages/Setting/Model/SettingGeminiModel.js +++ b/web/src/pages/Setting/Model/SettingGeminiModel.js @@ -173,7 +173,8 @@ export default function SettingGeminiModel(props) { {t( "和Claude不同,默认情况下Gemini的思考模型会自动决定要不要思考,就算不开启适配模型也可以正常使用," + - "如果您需要计费,推荐设置无后缀模型价格按思考价格设置" + "如果您需要计费,推荐设置无后缀模型价格按思考价格设置。" + + "支持使用 gemini-2.5-pro-preview-06-05-thinking-128 格式来精确传递思考预算。" )} @@ -183,7 +184,7 @@ export default function SettingGeminiModel(props) { setInputs({ ...inputs, @@ -205,7 +206,7 @@ export default function SettingGeminiModel(props) { + + + verifyJSON(value), + message: t('不是合法的 JSON 字符串'), + }, + ]} + onChange={(value) => + setInputs({ ...inputs, GroupGroupRatio: 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 }) + } + /> + + + + + + setInputs({ ...inputs, DefaultUseAutoGroup: value }) + } + /> + + diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js index 71f611bd..782562a3 100644 --- a/web/src/pages/Token/EditToken.js +++ b/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={ - {isEdit ? - {t('更新')} : - {t('新建')} - } - + {isEdit ? ( + <Tag color='blue' shape='circle'> + {t('更新')} + </Tag> + ) : ( + <Tag color='green' shape='circle'> + {t('新建')} + </Tag> + )} + <Title heading={4} className='m-0'> {isEdit ? t('更新令牌信息') : t('创建新的令牌')} } 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={ -
+
- -
-
-
-
+ +
+
+
+
-
- +
+
-
- {t('额度设置')} -
{t('设置令牌可用额度和数量')}
+
+ + {t('额度设置')} + +
+ {t('设置令牌可用额度和数量')} +
-
+
-
+
{t('额度')} - {renderQuotaWithPrompt(remain_quota)} + + {renderQuotaWithPrompt(remain_quota)} +
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={} data={[ { value: 500000, label: '1$' }, @@ -460,16 +517,18 @@ const EditToken = (props) => { {!isEdit && (
- {t('新建数量')} + + {t('新建数量')} + 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={} data={[ { value: 10, label: t('10个') }, @@ -482,12 +541,12 @@ const EditToken = (props) => {
)} -
+
@@ -495,92 +554,137 @@ const EditToken = (props) => {
- -
-
-
-
+ +
+
+
+
-
- +
+
-
- {t('访问限制')} -
{t('设置令牌的访问限制')}
+
+ + {t('访问限制')} + +
+ {t('设置令牌的访问限制')} +
-
+
- {t('IP白名单')} + + {t('IP白名单')} +