From 469d3747af202db88a4f69cdb88b67b42df078bc Mon Sep 17 00:00:00 2001
From: Calcium-Ion
Date: Tue, 12 May 2026 16:47:02 +0800
Subject: [PATCH] fix: defaut ui triage (#4802)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix: theme-aware payment paths, auto-group validation, route guards, perf group filtering
- Add common.ThemeAwarePath to generate correct redirect URLs based on
active theme (default vs classic), replacing hardcoded /console/* paths
in 7 controllers and service/quota.go (#4765)
- Validate auto-group availability against getUserGroups before defaulting
form values; playground falls back to 'default' group when 'auto' is
unavailable (#4796, #4799)
- Enforce HeaderNavModules settings in rankings route (frontend + backend
API) and SidebarModulesAdmin in playground route to block direct URL
access when features are disabled (#4704, #4512)
- Filter perf_metrics API response to only include currently configured
groups, hiding stale data from deleted groups (#4790)
- Preserve query params (pay=success/fail) in /console/topup → /wallet
frontend redirect
* fix: update hero section text and localization strings for clarity
---
common/constants.go | 21 +++++
controller/perf_metrics.go | 14 ++++
controller/rankings.go | 40 ++++++++++
controller/return_path.go | 13 +++
controller/subscription_payment_epay.go | 15 ++--
controller/subscription_payment_stripe.go | 5 +-
controller/telegram.go | 2 +-
controller/topup.go | 3 +-
controller/topup_stripe.go | 5 +-
controller/topup_waffo.go | 3 +-
controller/topup_waffo_pancake.go | 3 +-
service/quota.go | 5 +-
service/return_path.go | 13 +++
.../home/components/sections/hero.tsx | 7 +-
.../components/api-keys-mutate-drawer.tsx | 29 ++++---
.../src/features/playground/constants.ts | 5 +-
web/default/src/features/playground/index.tsx | 25 +++---
web/default/src/i18n/locales/en.json | 2 +-
web/default/src/i18n/locales/fr.json | 2 +-
web/default/src/i18n/locales/ja.json | 2 +-
web/default/src/i18n/locales/ru.json | 2 +-
web/default/src/i18n/locales/vi.json | 2 +-
web/default/src/i18n/locales/zh.json | 2 +-
web/default/src/lib/nav-modules.ts | 80 +++++++++++++++++++
.../_authenticated/playground/index.tsx | 8 +-
web/default/src/routes/console/topup.tsx | 8 +-
web/default/src/routes/rankings/index.tsx | 16 +++-
27 files changed, 261 insertions(+), 71 deletions(-)
create mode 100644 controller/return_path.go
create mode 100644 service/return_path.go
create mode 100644 web/default/src/lib/nav-modules.ts
diff --git a/common/constants.go b/common/constants.go
index c4d2511e..1912e460 100644
--- a/common/constants.go
+++ b/common/constants.go
@@ -4,6 +4,7 @@ import (
"crypto/tls"
//"os"
//"strconv"
+ "strings"
"sync"
"sync/atomic"
"time"
@@ -36,6 +37,26 @@ func SetTheme(t string) {
}
}
+// ThemeAwarePath rewrites legacy /console/* paths to the default-theme
+// equivalents when the active theme is "default". For "classic" (or any
+// other theme) the path is returned unchanged. The function only touches
+// known prefixes so it is safe to call with arbitrary suffixes and query
+// strings.
+func ThemeAwarePath(suffix string) string {
+ if GetTheme() != "default" {
+ return suffix
+ }
+ switch {
+ case strings.HasPrefix(suffix, "/console/topup"):
+ return strings.Replace(suffix, "/console/topup", "/wallet", 1)
+ case strings.HasPrefix(suffix, "/console/log"):
+ return strings.Replace(suffix, "/console/log", "/usage-logs", 1)
+ case strings.HasPrefix(suffix, "/console/personal"):
+ return strings.Replace(suffix, "/console/personal", "/profile", 1)
+ }
+ return suffix
+}
+
// var ChatLink = ""
// var ChatLink2 = ""
var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
diff --git a/controller/perf_metrics.go b/controller/perf_metrics.go
index 2ffc2b91..51e8d9ec 100644
--- a/controller/perf_metrics.go
+++ b/controller/perf_metrics.go
@@ -5,6 +5,7 @@ import (
"strconv"
perfmetrics "github.com/QuantumNous/new-api/pkg/perf_metrics"
+ "github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/gin-gonic/gin"
)
@@ -62,8 +63,21 @@ func GetPerfMetrics(c *gin.Context) {
return
}
+ result.Groups = filterActiveGroups(result.Groups)
+
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": result,
})
}
+
+func filterActiveGroups(groups []perfmetrics.GroupResult) []perfmetrics.GroupResult {
+ activeGroups := ratio_setting.GetGroupRatioCopy()
+ filtered := make([]perfmetrics.GroupResult, 0, len(groups))
+ for _, g := range groups {
+ if _, ok := activeGroups[g.Group]; ok || g.Group == "auto" {
+ filtered = append(filtered, g)
+ }
+ }
+ return filtered
+}
diff --git a/controller/rankings.go b/controller/rankings.go
index 5a7fdaae..a3fdf2b5 100644
--- a/controller/rankings.go
+++ b/controller/rankings.go
@@ -3,11 +3,51 @@ package controller
import (
"net/http"
+ "github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
)
+func isRankingsEnabled() bool {
+ common.OptionMapRWMutex.RLock()
+ raw := common.OptionMap["HeaderNavModules"]
+ common.OptionMapRWMutex.RUnlock()
+
+ if raw == "" {
+ return true
+ }
+
+ var parsed map[string]interface{}
+ if err := common.Unmarshal([]byte(raw), &parsed); err != nil {
+ return true
+ }
+ rankings, ok := parsed["rankings"]
+ if !ok {
+ return true
+ }
+ switch v := rankings.(type) {
+ case bool:
+ return v
+ case map[string]interface{}:
+ if enabled, ok := v["enabled"]; ok {
+ if b, ok := enabled.(bool); ok {
+ return b
+ }
+ }
+ return true
+ }
+ return true
+}
+
func GetRankings(c *gin.Context) {
+ if !isRankingsEnabled() {
+ c.JSON(http.StatusForbidden, gin.H{
+ "success": false,
+ "message": "rankings is disabled",
+ })
+ return
+ }
+
result, err := service.GetRankingsSnapshot(c.DefaultQuery("period", "week"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
diff --git a/controller/return_path.go b/controller/return_path.go
new file mode 100644
index 00000000..28378b7e
--- /dev/null
+++ b/controller/return_path.go
@@ -0,0 +1,13 @@
+package controller
+
+import (
+ "strings"
+
+ "github.com/QuantumNous/new-api/common"
+ "github.com/QuantumNous/new-api/setting/system_setting"
+)
+
+func paymentReturnPath(suffix string) string {
+ base := strings.TrimRight(system_setting.ServerAddress, "/")
+ return base + common.ThemeAwarePath(suffix)
+}
diff --git a/controller/subscription_payment_epay.go b/controller/subscription_payment_epay.go
index 2567654f..ecd38472 100644
--- a/controller/subscription_payment_epay.go
+++ b/controller/subscription_payment_epay.go
@@ -12,7 +12,6 @@ import (
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting/operation_setting"
- "github.com/QuantumNous/new-api/setting/system_setting"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
)
@@ -173,7 +172,7 @@ func SubscriptionEpayReturn(c *gin.Context) {
if c.Request.Method == "POST" {
// POST 请求:从 POST body 解析参数
if err := c.Request.ParseForm(); err != nil {
- c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail")
+ c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=fail"))
return
}
params = lo.Reduce(lo.Keys(c.Request.PostForm), func(r map[string]string, t string, i int) map[string]string {
@@ -189,29 +188,29 @@ func SubscriptionEpayReturn(c *gin.Context) {
}
if len(params) == 0 {
- c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail")
+ c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=fail"))
return
}
client := GetEpayClient()
if client == nil {
- c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail")
+ c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=fail"))
return
}
verifyInfo, err := client.Verify(params)
if err != nil || !verifyInfo.VerifyStatus {
- c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail")
+ c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=fail"))
return
}
if verifyInfo.TradeStatus == epay.StatusTradeSuccess {
LockOrder(verifyInfo.ServiceTradeNo)
defer UnlockOrder(verifyInfo.ServiceTradeNo)
if err := model.CompleteSubscriptionOrder(verifyInfo.ServiceTradeNo, common.GetJsonString(verifyInfo), model.PaymentProviderEpay, verifyInfo.Type); err != nil {
- c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail")
+ c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=fail"))
return
}
- c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=success")
+ c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=success"))
return
}
- c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=pending")
+ c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=pending"))
}
diff --git a/controller/subscription_payment_stripe.go b/controller/subscription_payment_stripe.go
index a5ce4685..3efe7b38 100644
--- a/controller/subscription_payment_stripe.go
+++ b/controller/subscription_payment_stripe.go
@@ -10,7 +10,6 @@ import (
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting"
- "github.com/QuantumNous/new-api/setting/system_setting"
"github.com/gin-gonic/gin"
"github.com/stripe/stripe-go/v81"
"github.com/stripe/stripe-go/v81/checkout/session"
@@ -111,8 +110,8 @@ func genStripeSubscriptionLink(referenceId string, customerId string, email stri
params := &stripe.CheckoutSessionParams{
ClientReferenceID: stripe.String(referenceId),
- SuccessURL: stripe.String(system_setting.ServerAddress + "/console/topup"),
- CancelURL: stripe.String(system_setting.ServerAddress + "/console/topup"),
+ SuccessURL: stripe.String(paymentReturnPath("/console/topup")),
+ CancelURL: stripe.String(paymentReturnPath("/console/topup")),
LineItems: []*stripe.CheckoutSessionLineItemParams{
{
Price: stripe.String(priceId),
diff --git a/controller/telegram.go b/controller/telegram.go
index f16cdd66..b5918d8e 100644
--- a/controller/telegram.go
+++ b/controller/telegram.go
@@ -66,7 +66,7 @@ func TelegramBind(c *gin.Context) {
return
}
- c.Redirect(302, "/console/personal")
+ c.Redirect(302, common.ThemeAwarePath("/console/personal"))
}
func TelegramLogin(c *gin.Context) {
diff --git a/controller/topup.go b/controller/topup.go
index f2848671..208de2c7 100644
--- a/controller/topup.go
+++ b/controller/topup.go
@@ -14,7 +14,6 @@ import (
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/operation_setting"
- "github.com/QuantumNous/new-api/setting/system_setting"
"github.com/Calcium-Ion/go-epay/epay"
"github.com/gin-gonic/gin"
@@ -208,7 +207,7 @@ func RequestEpay(c *gin.Context) {
}
callBackAddress := service.GetCallbackAddress()
- returnUrl, _ := url.Parse(system_setting.ServerAddress + "/console/log")
+ returnUrl, _ := url.Parse(paymentReturnPath("/console/log"))
notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify")
tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo)
diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go
index ceee8ecd..bcae201e 100644
--- a/controller/topup_stripe.go
+++ b/controller/topup_stripe.go
@@ -15,7 +15,6 @@ import (
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/operation_setting"
- "github.com/QuantumNous/new-api/setting/system_setting"
"github.com/gin-gonic/gin"
"github.com/stripe/stripe-go/v81"
@@ -348,10 +347,10 @@ func genStripeLink(referenceId string, customerId string, email string, amount i
// Use custom URLs if provided, otherwise use defaults
if successURL == "" {
- successURL = system_setting.ServerAddress + "/console/log"
+ successURL = paymentReturnPath("/console/log")
}
if cancelURL == "" {
- cancelURL = system_setting.ServerAddress + "/console/topup"
+ cancelURL = paymentReturnPath("/console/topup")
}
params := &stripe.CheckoutSessionParams{
diff --git a/controller/topup_waffo.go b/controller/topup_waffo.go
index 1885c1de..344630f7 100644
--- a/controller/topup_waffo.go
+++ b/controller/topup_waffo.go
@@ -14,7 +14,6 @@ import (
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/operation_setting"
- "github.com/QuantumNous/new-api/setting/system_setting"
"github.com/gin-gonic/gin"
"github.com/thanhpk/randstr"
waffo "github.com/waffo-com/waffo-go"
@@ -237,7 +236,7 @@ func RequestWaffoPay(c *gin.Context) {
if setting.WaffoNotifyUrl != "" {
notifyUrl = setting.WaffoNotifyUrl
}
- returnUrl := system_setting.ServerAddress + "/console/topup?show_history=true"
+ returnUrl := paymentReturnPath("/console/topup?show_history=true")
if setting.WaffoReturnUrl != "" {
returnUrl = setting.WaffoReturnUrl
}
diff --git a/controller/topup_waffo_pancake.go b/controller/topup_waffo_pancake.go
index 09f15163..11c581fa 100644
--- a/controller/topup_waffo_pancake.go
+++ b/controller/topup_waffo_pancake.go
@@ -13,7 +13,6 @@ import (
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/operation_setting"
- "github.com/QuantumNous/new-api/setting/system_setting"
"github.com/gin-gonic/gin"
"github.com/shopspring/decimal"
"github.com/thanhpk/randstr"
@@ -107,7 +106,7 @@ func getWaffoPancakeReturnURL() string {
if strings.TrimSpace(setting.WaffoPancakeReturnURL) != "" {
return setting.WaffoPancakeReturnURL
}
- return strings.TrimRight(system_setting.ServerAddress, "/") + "/console/topup?show_history=true"
+ return paymentReturnPath("/console/topup?show_history=true")
}
func RequestWaffoPancakePay(c *gin.Context) {
diff --git a/service/quota.go b/service/quota.go
index 7364598a..e2ab25cf 100644
--- a/service/quota.go
+++ b/service/quota.go
@@ -17,7 +17,6 @@ import (
perfmetrics "github.com/QuantumNous/new-api/pkg/perf_metrics"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/setting/ratio_setting"
- "github.com/QuantumNous/new-api/setting/system_setting"
"github.com/QuantumNous/new-api/types"
"github.com/bytedance/gopkg/util/gopool"
@@ -467,7 +466,7 @@ func checkAndSendQuotaNotify(relayInfo *relaycommon.RelayInfo, quota int, preCon
}
if quotaTooLow {
prompt := "您的额度即将用尽"
- topUpLink := fmt.Sprintf("%s/console/topup", system_setting.ServerAddress)
+ topUpLink := PaymentReturnURL("/console/topup")
// 根据通知方式生成不同的内容格式
var content string
@@ -521,7 +520,7 @@ func checkAndSendSubscriptionQuotaNotify(relayInfo *relaycommon.RelayInfo) {
}
prompt := "您的订阅额度即将用尽"
- topUpLink := fmt.Sprintf("%s/console/topup", system_setting.ServerAddress)
+ topUpLink := PaymentReturnURL("/console/topup")
var content string
var values []interface{}
diff --git a/service/return_path.go b/service/return_path.go
new file mode 100644
index 00000000..c99e1fd3
--- /dev/null
+++ b/service/return_path.go
@@ -0,0 +1,13 @@
+package service
+
+import (
+ "strings"
+
+ "github.com/QuantumNous/new-api/common"
+ "github.com/QuantumNous/new-api/setting/system_setting"
+)
+
+func PaymentReturnURL(suffix string) string {
+ base := strings.TrimRight(system_setting.ServerAddress, "/")
+ return base + common.ThemeAwarePath(suffix)
+}
diff --git a/web/default/src/features/home/components/sections/hero.tsx b/web/default/src/features/home/components/sections/hero.tsx
index f5955972..34f6d915 100644
--- a/web/default/src/features/home/components/sections/hero.tsx
+++ b/web/default/src/features/home/components/sections/hero.tsx
@@ -19,7 +19,6 @@ For commercial licensing, please contact support@quantumnous.com
import { Link } from '@tanstack/react-router'
import { ArrowRight } from 'lucide-react'
import { useTranslation } from 'react-i18next'
-import { useSystemConfig } from '@/hooks/use-system-config'
import { Button } from '@/components/ui/button'
import { HeroTerminalDemo } from '../hero-terminal-demo'
@@ -30,7 +29,6 @@ interface HeroProps {
export function Hero(props: HeroProps) {
const { t } = useTranslation()
- const { systemName } = useSystemConfig()
return (