Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ token_estimator_test.go
skills-lock.json

.trae/
private/
private/

fixdoc.md
hero-rep.html
122 changes: 122 additions & 0 deletions common/frontend_theme.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package common

import (
"net/url"
"strings"

"github.com/gin-gonic/gin"
Expand All @@ -25,3 +26,124 @@ func SetFrontendThemeCookie(c *gin.Context, theme string) {
}
c.SetCookie(FrontendThemeCookieName, theme, FrontendThemeCookieMaxAge, "/", "", false, false)
}

var classicToDefaultMap = map[string]string{
"/console": "/dashboard",
"/console/personal": "/profile",
"/console/channel": "/channels",
"/console/token": "/keys",
"/console/log": "/usage-logs",
"/console/setting": "/system-settings",
"/console/topup": "/wallet",
"/console/redemption": "/redemption-codes",
"/console/user": "/users",
"/console/midjourney": "/usage-logs",
"/console/task": "/usage-logs",
"/console/models": "/models",
"/console/deployment": "/models/deployments",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the missing reverse mapping for deployment routes.

Line 43 adds /console/deployment -> /models/deployments, but there is no exact reverse entry. In classic theme, /models/deployments currently falls through to the generic /models/ prefix and rewrites to /console/models/deployments, which is inconsistent and can send users to the wrong route.

Suggested fix
 var defaultToClassicMap = map[string]string{
 	"/dashboard":        "/console",
 	"/profile":          "/console/personal",
 	"/channels":         "/console/channel",
 	"/keys":             "/console/token",
+	"/models/deployments": "/console/deployment",
 	"/models":           "/console/models",
 	"/usage-logs":       "/console/log",
 	"/system-settings":  "/console/setting",
 	"/playground":       "/console/playground",
 	"/wallet":           "/console/topup",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/frontend_theme.go` at line 43, The route map is missing an exact
reverse mapping for the deployment path so "/models/deployments" falls through
to the generic "/models/" rule; add an exact entry mapping "/models/deployments"
-> "/console/deployment" in the same routing map (the map that contains the
existing "/console/deployment": "/models/deployments" entry) so requests to
"/models/deployments" are rewritten back to the exact console deployment route
rather than the generic models prefix.

"/console/subscription": "/subscriptions",
"/console/playground": "/playground",
"/console/chat": "/playground",
}

var defaultToClassicMap = map[string]string{
"/dashboard": "/console",
"/profile": "/console/personal",
"/channels": "/console/channel",
"/keys": "/console/token",
"/models": "/console/models",
"/usage-logs": "/console/log",
"/system-settings": "/console/setting",
"/playground": "/console/playground",
"/wallet": "/console/topup",
"/subscriptions": "/console/subscription",
"/redemption-codes": "/console/redemption",
"/users": "/console/user",
"/availability": "/console",
}

var classicToDefaultPrefixes = []struct{ prefix, replacement string }{
{"/console/chat/", "/playground/"},
{"/console/setting/", "/system-settings/"},
{"/console/log/", "/usage-logs/"},
{"/console/channel/", "/channels/"},
{"/console/token/", "/keys/"},
{"/console/models/", "/models/"},
{"/console/topup/", "/wallet/"},
}

var defaultToClassicPrefixes = []struct{ prefix, replacement string }{
{"/dashboard/", "/console/"},
{"/system-settings/", "/console/setting/"},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{"/usage-logs/", "/console/log/"},
{"/channels/", "/console/channel/"},
{"/keys/", "/console/token/"},
{"/models/", "/console/models/"},
}

func normalizeMapPath(path string) string {
if path == "" {
return "/"
}
unescapedPath, err := url.PathUnescape(path)
if err == nil && unescapedPath != "" {
path = unescapedPath
}
path = strings.TrimSuffix(path, "/")
if path == "" {
path = "/"
}
return path
}

func MapFrontendPath(theme string, path string) string {
normalizedPath := normalizeMapPath(path)

if theme == "classic" {
if mapped, ok := defaultToClassicMap[normalizedPath]; ok {
return mapped
}
for _, p := range defaultToClassicPrefixes {
if strings.HasPrefix(normalizedPath, p.prefix) {
return p.replacement + normalizedPath[len(p.prefix):]
}
}
}

if theme == "default" {
if mapped, ok := classicToDefaultMap[normalizedPath]; ok {
return mapped
}
for _, p := range classicToDefaultPrefixes {
if strings.HasPrefix(normalizedPath, p.prefix) {
return p.replacement + normalizedPath[len(p.prefix):]
}
}
}

return ""
}

func GetThemeAwarePath(c *gin.Context, classicPath string) string {
theme := GetTheme()
themeCookie, err := c.Cookie(FrontendThemeCookieName)
if err == nil {
normalized := NormalizeFrontendTheme(themeCookie)
if normalized != "" {
theme = normalized
}
}
if theme == "default" {
path := classicPath
query := ""
if idx := strings.Index(classicPath, "?"); idx != -1 {
path = classicPath[:idx]
query = classicPath[idx:]
}
mapped := MapFrontendPath("default", path)
if mapped != "" {
return mapped + query
}
}
return classicPath
}
3 changes: 3 additions & 0 deletions controller/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/QuantumNous/new-api/oauth"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/console_setting"
"github.com/QuantumNous/new-api/setting/langfuse_setting"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/setting/system_setting"

Expand Down Expand Up @@ -120,6 +121,8 @@ func GetStatus(c *gin.Context) {
"privacy_policy_enabled": legalSetting.PrivacyPolicy != "",
"checkin_enabled": operation_setting.GetCheckinSetting().Enabled,
}
langfuseCfg := langfuse_setting.GetLangfuseSetting()
data["langfuse_trace_content"] = langfuseCfg.Enabled && langfuseCfg.TraceContent

// 根据启用状态注入可选内容
if cs.ApiInfoEnabled {
Expand Down
14 changes: 7 additions & 7 deletions controller/subscription_payment_epay.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,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, system_setting.ServerAddress+common.GetThemeAwarePath(c, "/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 {
Expand All @@ -189,29 +189,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, system_setting.ServerAddress+common.GetThemeAwarePath(c, "/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, system_setting.ServerAddress+common.GetThemeAwarePath(c, "/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, system_setting.ServerAddress+common.GetThemeAwarePath(c, "/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, system_setting.ServerAddress+common.GetThemeAwarePath(c, "/console/topup?pay=fail"))
return
}
c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=success")
c.Redirect(http.StatusFound, system_setting.ServerAddress+common.GetThemeAwarePath(c, "/console/topup?pay=success"))
return
}
c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=pending")
c.Redirect(http.StatusFound, system_setting.ServerAddress+common.GetThemeAwarePath(c, "/console/topup?pay=pending"))
}
8 changes: 4 additions & 4 deletions controller/subscription_payment_stripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func SubscriptionRequestStripePay(c *gin.Context) {
reference := fmt.Sprintf("sub-stripe-ref-%d-%d-%s", user.Id, time.Now().UnixMilli(), randstr.String(4))
referenceId := "sub_ref_" + common.Sha1([]byte(reference))

payLink, err := genStripeSubscriptionLink(referenceId, user.StripeCustomer, user.Email, plan.StripePriceId)
payLink, err := genStripeSubscriptionLink(c, referenceId, user.StripeCustomer, user.Email, plan.StripePriceId)
if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Stripe 订阅支付链接创建失败 trade_no=%s plan_id=%d error=%q", referenceId, plan.Id, err.Error()))
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "拉起支付失败"})
Expand Down Expand Up @@ -106,13 +106,13 @@ func SubscriptionRequestStripePay(c *gin.Context) {
})
}

func genStripeSubscriptionLink(referenceId string, customerId string, email string, priceId string) (string, error) {
func genStripeSubscriptionLink(c *gin.Context, referenceId string, customerId string, email string, priceId string) (string, error) {
stripe.Key = setting.StripeApiSecret

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(system_setting.ServerAddress + common.GetThemeAwarePath(c, "/console/topup")),
CancelURL: stripe.String(system_setting.ServerAddress + common.GetThemeAwarePath(c, "/console/topup")),
LineItems: []*stripe.CheckoutSessionLineItemParams{
{
Price: stripe.String(priceId),
Expand Down
2 changes: 1 addition & 1 deletion controller/telegram.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func TelegramBind(c *gin.Context) {
return
}

c.Redirect(302, "/console/personal")
c.Redirect(302, common.GetThemeAwarePath(c, "/console/personal"))
}

func TelegramLogin(c *gin.Context) {
Expand Down
2 changes: 1 addition & 1 deletion controller/topup.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func RequestEpay(c *gin.Context) {
}

callBackAddress := service.GetCallbackAddress()
returnUrl, _ := url.Parse(system_setting.ServerAddress + "/console/log")
returnUrl, _ := url.Parse(system_setting.ServerAddress + common.GetThemeAwarePath(c, "/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)
Expand Down
8 changes: 4 additions & 4 deletions controller/topup_stripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) {
reference := fmt.Sprintf("new-api-ref-%d-%d-%s", user.Id, time.Now().UnixMilli(), randstr.String(4))
referenceId := "ref_" + common.Sha1([]byte(reference))

payLink, err := genStripeLink(referenceId, user.StripeCustomer, user.Email, req.Amount, req.SuccessURL, req.CancelURL)
payLink, err := genStripeLink(c, referenceId, user.StripeCustomer, user.Email, req.Amount, req.SuccessURL, req.CancelURL)
if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Stripe 创建 Checkout Session 失败 user_id=%d trade_no=%s amount=%d error=%q", id, referenceId, req.Amount, err.Error()))
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "拉起支付失败"})
Expand Down Expand Up @@ -339,7 +339,7 @@ func sessionExpired(ctx context.Context, event stripe.Event) {
// - cancelURL: custom URL to redirect when payment is canceled (empty for default)
//
// Returns the checkout session URL or an error if the session creation fails.
func genStripeLink(referenceId string, customerId string, email string, amount int64, successURL string, cancelURL string) (string, error) {
func genStripeLink(c *gin.Context, referenceId string, customerId string, email string, amount int64, successURL string, cancelURL string) (string, error) {
if !strings.HasPrefix(setting.StripeApiSecret, "sk_") && !strings.HasPrefix(setting.StripeApiSecret, "rk_") {
return "", fmt.Errorf("无效的Stripe API密钥")
}
Expand All @@ -348,10 +348,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 = system_setting.ServerAddress + common.GetThemeAwarePath(c, "/console/log")
}
if cancelURL == "" {
cancelURL = system_setting.ServerAddress + "/console/topup"
cancelURL = system_setting.ServerAddress + common.GetThemeAwarePath(c, "/console/topup")
}

params := &stripe.CheckoutSessionParams{
Expand Down
2 changes: 1 addition & 1 deletion controller/topup_waffo.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func RequestWaffoPay(c *gin.Context) {
if setting.WaffoNotifyUrl != "" {
notifyUrl = setting.WaffoNotifyUrl
}
returnUrl := system_setting.ServerAddress + "/console/topup?show_history=true"
returnUrl := system_setting.ServerAddress + common.GetThemeAwarePath(c, "/console/topup?show_history=true")
if setting.WaffoReturnUrl != "" {
returnUrl = setting.WaffoReturnUrl
}
Expand Down
6 changes: 3 additions & 3 deletions controller/topup_waffo_pancake.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ func getWaffoPancakeBuyerEmail(user *model.User) string {
return ""
}

func getWaffoPancakeReturnURL() string {
func getWaffoPancakeReturnURL(c *gin.Context) string {
if strings.TrimSpace(setting.WaffoPancakeReturnURL) != "" {
return setting.WaffoPancakeReturnURL
}
return strings.TrimRight(system_setting.ServerAddress, "/") + "/console/topup?show_history=true"
return strings.TrimRight(system_setting.ServerAddress, "/") + common.GetThemeAwarePath(c, "/console/topup?show_history=true")
}

func RequestWaffoPancakePay(c *gin.Context) {
Expand Down Expand Up @@ -186,7 +186,7 @@ func RequestWaffoPancakePay(c *gin.Context) {
TaxCategory: "saas",
},
BuyerEmail: getWaffoPancakeBuyerEmail(user),
SuccessURL: getWaffoPancakeReturnURL(),
SuccessURL: getWaffoPancakeReturnURL(c),
ExpiresInSeconds: &expiresInSeconds,
})
if err != nil {
Expand Down
38 changes: 1 addition & 37 deletions router/web-router.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package router
import (
"embed"
"net/http"
"net/url"
"strings"

"github.com/QuantumNous/new-api/common"
Expand Down Expand Up @@ -39,7 +38,7 @@ func SetWebRouter(router *gin.Engine, assets ThemeAssets) {
}

theme := resolveFrontendTheme(c)
if redirectPath := mapFrontendPath(theme, c.Request.URL.Path); redirectPath != "" && redirectPath != c.Request.URL.Path {
if redirectPath := common.MapFrontendPath(theme, c.Request.URL.Path); redirectPath != "" && redirectPath != c.Request.URL.Path {
if c.Request.URL.RawQuery != "" {
redirectPath = redirectPath + "?" + c.Request.URL.RawQuery
}
Expand Down Expand Up @@ -102,41 +101,6 @@ func resolveFrontendTheme(c *gin.Context) string {
return common.GetTheme()
}

func mapFrontendPath(theme string, path string) string {
normalizedPath := path
if normalizedPath == "" {
normalizedPath = "/"
}
unescapedPath, err := url.PathUnescape(normalizedPath)
if err == nil && unescapedPath != "" {
normalizedPath = unescapedPath
}
normalizedPath = strings.TrimSuffix(normalizedPath, "/")
if normalizedPath == "" {
normalizedPath = "/"
}

if theme == "classic" {
switch normalizedPath {
case "/dashboard":
return "/console"
case "/profile":
return "/console/personal"
}
}

if theme == "default" {
switch normalizedPath {
case "/console":
return "/dashboard"
case "/console/personal":
return "/profile"
}
}

return ""
}

func selectFrontendFS(theme string, defaultFS, classicFS static.ServeFileSystem) static.ServeFileSystem {
if theme == "classic" {
return classicFS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function Mermaid(props) {
const text = new XMLSerializer().serializeToString(svg);
const blob = new Blob([text], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
window.open(url, '_blank', 'noopener,noreferrer');
}

if (hasError) {
Expand Down
Loading
Loading