diff --git a/atsf_server/common/constants.go b/atsf_server/common/constants.go
index 9329d58a..db4bf8bd 100644
--- a/atsf_server/common/constants.go
+++ b/atsf_server/common/constants.go
@@ -72,19 +72,19 @@ var (
// All duration's unit is seconds
// Shouldn't larger then RateLimitKeyExpirationDuration
var (
- GlobalApiRateLimitNum = 60
+ GlobalApiRateLimitNum = 300
GlobalApiRateLimitDuration int64 = 3 * 60
- GlobalWebRateLimitNum = 60
+ GlobalWebRateLimitNum = 300
GlobalWebRateLimitDuration int64 = 3 * 60
- UploadRateLimitNum = 10
+ UploadRateLimitNum = 50
UploadRateLimitDuration int64 = 60
- DownloadRateLimitNum = 10
+ DownloadRateLimitNum = 50
DownloadRateLimitDuration int64 = 60
- CriticalRateLimitNum = 20
+ CriticalRateLimitNum = 100
CriticalRateLimitDuration int64 = 20 * 60
)
diff --git a/atsf_server/controller/option.go b/atsf_server/controller/option.go
index 23e03ca8..1d7638b7 100644
--- a/atsf_server/controller/option.go
+++ b/atsf_server/controller/option.go
@@ -4,11 +4,37 @@ import (
"atsflare/common"
"atsflare/model"
"encoding/json"
+ "fmt"
"github.com/gin-gonic/gin"
"net/http"
+ "strconv"
"strings"
)
+func validateRateLimitOption(key string, value string) error {
+ maxDurationSeconds := int(common.RateLimitKeyExpirationDuration.Seconds())
+
+ switch key {
+ case "GlobalApiRateLimitNum", "GlobalWebRateLimitNum", "UploadRateLimitNum", "DownloadRateLimitNum", "CriticalRateLimitNum":
+ intValue, err := strconv.Atoi(value)
+ if err != nil || intValue <= 0 {
+ return fmt.Errorf("%s 必须为大于 0 的整数", key)
+ }
+ return nil
+ case "GlobalApiRateLimitDuration", "GlobalWebRateLimitDuration", "UploadRateLimitDuration", "DownloadRateLimitDuration", "CriticalRateLimitDuration":
+ intValue, err := strconv.Atoi(value)
+ if err != nil || intValue <= 0 {
+ return fmt.Errorf("%s 必须为大于 0 的整数秒", key)
+ }
+ if intValue > maxDurationSeconds {
+ return fmt.Errorf("%s 不能大于 %d 秒", key, maxDurationSeconds)
+ }
+ return nil
+ default:
+ return nil
+ }
+}
+
// GetOptions godoc
// @Summary List editable options
// @Tags Options
@@ -81,6 +107,13 @@ func UpdateOption(c *gin.Context) {
return
}
}
+ if err = validateRateLimitOption(option.Key, option.Value); err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ return
+ }
err = model.UpdateOption(option.Key, option.Value)
if err != nil {
c.JSON(http.StatusOK, gin.H{
diff --git a/atsf_server/model/option.go b/atsf_server/model/option.go
index f1d903fa..aa21c5e4 100644
--- a/atsf_server/model/option.go
+++ b/atsf_server/model/option.go
@@ -55,6 +55,16 @@ func InitOptionMap() {
common.OptionMap["AgentSyncInterval"] = strconv.Itoa(common.AgentSyncInterval)
common.OptionMap["NodeOfflineThreshold"] = strconv.Itoa(int(common.NodeOfflineThreshold.Milliseconds()))
common.OptionMap["AgentUpdateRepo"] = common.AgentUpdateRepo
+ common.OptionMap["GlobalApiRateLimitNum"] = strconv.Itoa(common.GlobalApiRateLimitNum)
+ common.OptionMap["GlobalApiRateLimitDuration"] = strconv.FormatInt(common.GlobalApiRateLimitDuration, 10)
+ common.OptionMap["GlobalWebRateLimitNum"] = strconv.Itoa(common.GlobalWebRateLimitNum)
+ common.OptionMap["GlobalWebRateLimitDuration"] = strconv.FormatInt(common.GlobalWebRateLimitDuration, 10)
+ common.OptionMap["UploadRateLimitNum"] = strconv.Itoa(common.UploadRateLimitNum)
+ common.OptionMap["UploadRateLimitDuration"] = strconv.FormatInt(common.UploadRateLimitDuration, 10)
+ common.OptionMap["DownloadRateLimitNum"] = strconv.Itoa(common.DownloadRateLimitNum)
+ common.OptionMap["DownloadRateLimitDuration"] = strconv.FormatInt(common.DownloadRateLimitDuration, 10)
+ common.OptionMap["CriticalRateLimitNum"] = strconv.Itoa(common.CriticalRateLimitNum)
+ common.OptionMap["CriticalRateLimitDuration"] = strconv.FormatInt(common.CriticalRateLimitDuration, 10)
common.OptionMapRWMutex.Unlock()
options, _ := AllOption()
for _, option := range options {
@@ -165,5 +175,45 @@ func updateOptionMap(key string, value string) {
if value != "" {
common.AgentUpdateRepo = value
}
+ case "GlobalApiRateLimitNum":
+ if v, err := strconv.Atoi(value); err == nil && v > 0 {
+ common.GlobalApiRateLimitNum = v
+ }
+ case "GlobalApiRateLimitDuration":
+ if v, err := strconv.ParseInt(value, 10, 64); err == nil && v > 0 {
+ common.GlobalApiRateLimitDuration = v
+ }
+ case "GlobalWebRateLimitNum":
+ if v, err := strconv.Atoi(value); err == nil && v > 0 {
+ common.GlobalWebRateLimitNum = v
+ }
+ case "GlobalWebRateLimitDuration":
+ if v, err := strconv.ParseInt(value, 10, 64); err == nil && v > 0 {
+ common.GlobalWebRateLimitDuration = v
+ }
+ case "UploadRateLimitNum":
+ if v, err := strconv.Atoi(value); err == nil && v > 0 {
+ common.UploadRateLimitNum = v
+ }
+ case "UploadRateLimitDuration":
+ if v, err := strconv.ParseInt(value, 10, 64); err == nil && v > 0 {
+ common.UploadRateLimitDuration = v
+ }
+ case "DownloadRateLimitNum":
+ if v, err := strconv.Atoi(value); err == nil && v > 0 {
+ common.DownloadRateLimitNum = v
+ }
+ case "DownloadRateLimitDuration":
+ if v, err := strconv.ParseInt(value, 10, 64); err == nil && v > 0 {
+ common.DownloadRateLimitDuration = v
+ }
+ case "CriticalRateLimitNum":
+ if v, err := strconv.Atoi(value); err == nil && v > 0 {
+ common.CriticalRateLimitNum = v
+ }
+ case "CriticalRateLimitDuration":
+ if v, err := strconv.ParseInt(value, 10, 64); err == nil && v > 0 {
+ common.CriticalRateLimitDuration = v
+ }
}
}
diff --git a/atsf_server/router/api_phase2_test.go b/atsf_server/router/api_phase2_test.go
index 8eb4b387..74b52c2a 100644
--- a/atsf_server/router/api_phase2_test.go
+++ b/atsf_server/router/api_phase2_test.go
@@ -17,6 +17,75 @@ import (
"time"
)
+func TestPhase2RateLimitOptionsHotReload(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ common.RedisEnabled = false
+ setupTestDB(t)
+
+ oldGlobalApiRateLimitNum := common.GlobalApiRateLimitNum
+ oldGlobalApiRateLimitDuration := common.GlobalApiRateLimitDuration
+ oldCriticalRateLimitNum := common.CriticalRateLimitNum
+ oldCriticalRateLimitDuration := common.CriticalRateLimitDuration
+ t.Cleanup(func() {
+ common.GlobalApiRateLimitNum = oldGlobalApiRateLimitNum
+ common.GlobalApiRateLimitDuration = oldGlobalApiRateLimitDuration
+ common.CriticalRateLimitNum = oldCriticalRateLimitNum
+ common.CriticalRateLimitDuration = oldCriticalRateLimitDuration
+ })
+
+ engine := gin.New()
+ engine.Use(sessions.Sessions("session", cookie.NewStore([]byte("test-secret"))))
+ router.SetApiRouter(engine)
+
+ token := prepareRootToken(t)
+
+ performJSONRequest(t, engine, token, http.MethodPut, "/api/option/", map[string]any{
+ "key": "GlobalApiRateLimitNum",
+ "value": "450",
+ })
+ performJSONRequest(t, engine, token, http.MethodPut, "/api/option/", map[string]any{
+ "key": "GlobalApiRateLimitDuration",
+ "value": "240",
+ })
+ performJSONRequest(t, engine, token, http.MethodPut, "/api/option/", map[string]any{
+ "key": "CriticalRateLimitNum",
+ "value": "150",
+ })
+ performJSONRequest(t, engine, token, http.MethodPut, "/api/option/", map[string]any{
+ "key": "CriticalRateLimitDuration",
+ "value": "900",
+ })
+
+ if common.GlobalApiRateLimitNum != 450 {
+ t.Fatalf("expected GlobalApiRateLimitNum to be hot reloaded, got %d", common.GlobalApiRateLimitNum)
+ }
+ if common.GlobalApiRateLimitDuration != 240 {
+ t.Fatalf("expected GlobalApiRateLimitDuration to be hot reloaded, got %d", common.GlobalApiRateLimitDuration)
+ }
+ if common.CriticalRateLimitNum != 150 {
+ t.Fatalf("expected CriticalRateLimitNum to be hot reloaded, got %d", common.CriticalRateLimitNum)
+ }
+ if common.CriticalRateLimitDuration != 900 {
+ t.Fatalf("expected CriticalRateLimitDuration to be hot reloaded, got %d", common.CriticalRateLimitDuration)
+ }
+
+ resp := performJSONRequest(t, engine, token, http.MethodGet, "/api/option/", nil)
+ var options []model.Option
+ decodeResponseData(t, resp, &options)
+
+ optionMap := make(map[string]string, len(options))
+ for _, option := range options {
+ optionMap[option.Key] = option.Value
+ }
+
+ if optionMap["GlobalApiRateLimitNum"] != "450" {
+ t.Fatalf("expected option payload to include GlobalApiRateLimitNum=450, got %q", optionMap["GlobalApiRateLimitNum"])
+ }
+ if optionMap["CriticalRateLimitDuration"] != "900" {
+ t.Fatalf("expected option payload to include CriticalRateLimitDuration=900, got %q", optionMap["CriticalRateLimitDuration"])
+ }
+}
+
func TestPhase2AgentLifecycle(t *testing.T) {
gin.SetMode(gin.TestMode)
common.RedisEnabled = false
diff --git a/atsf_server/web/components/layout/dashboard-sidebar.tsx b/atsf_server/web/components/layout/dashboard-sidebar.tsx
index af9f2936..b3849076 100644
--- a/atsf_server/web/components/layout/dashboard-sidebar.tsx
+++ b/atsf_server/web/components/layout/dashboard-sidebar.tsx
@@ -1,5 +1,6 @@
'use client';
+import { useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
@@ -100,36 +101,40 @@ function SidebarNavItem({
item,
currentPath,
isSidebarCollapsed,
+ forceExpanded,
+ onNavigate,
depth = 0,
}: {
item: NavigationItem;
currentPath: string;
isSidebarCollapsed: boolean;
+ forceExpanded?: boolean;
+ onNavigate?: () => void;
depth?: number;
}) {
const active = isNavigationItemActive(currentPath, item);
const hasChildren = Boolean(item.children?.length);
+ const showLabel = forceExpanded || !isSidebarCollapsed;
return (
0 && 'ml-3 rounded-xl py-2.5',
+ 'flex min-h-[50px] items-center gap-3 rounded-2xl border px-3 py-2.5 transition-colors',
+ depth > 0 && 'ml-3 rounded-xl',
active
? 'border-[var(--border-strong)] bg-[var(--accent-soft)] text-[var(--foreground-primary)]'
: 'border-transparent text-[var(--foreground-secondary)] hover:border-[var(--border-default)] hover:bg-[var(--surface-muted)] hover:text-[var(--foreground-primary)]',
)}
>
-
+
- {!isSidebarCollapsed ? (
- {item.label}
- ) : null}
+ {showLabel ? {item.label} : null}
- {!isSidebarCollapsed && hasChildren ? (
+ {showLabel && hasChildren ? (
{item.children?.map((child) => (
))}
@@ -146,50 +153,101 @@ function SidebarNavItem({
);
}
-export function DashboardSidebar() {
- const pathname = usePathname();
- const currentPath = pathname ?? '/';
- const isSidebarCollapsed = useAppShellStore((state) => state.isSidebarCollapsed);
-
- return (
-