diff --git a/.gitignore b/.gitignore index 00ab1253369..0efb6886dff 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,7 @@ token_estimator_test.go skills-lock.json .trae/ -private/ \ No newline at end of file +private/ + +fixdoc.md +hero-rep.html \ No newline at end of file diff --git a/common/frontend_theme.go b/common/frontend_theme.go index af1220b8007..dcbf93114c3 100644 --- a/common/frontend_theme.go +++ b/common/frontend_theme.go @@ -1,6 +1,7 @@ package common import ( + "net/url" "strings" "github.com/gin-gonic/gin" @@ -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", + "/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/"}, + {"/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 +} diff --git a/controller/misc.go b/controller/misc.go index a91efec7bce..27743840ef7 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -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" @@ -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 { diff --git a/controller/subscription_payment_epay.go b/controller/subscription_payment_epay.go index 2567654ff47..2154070ef9e 100644 --- a/controller/subscription_payment_epay.go +++ b/controller/subscription_payment_epay.go @@ -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 { @@ -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")) } diff --git a/controller/subscription_payment_stripe.go b/controller/subscription_payment_stripe.go index a5ce4685b4c..02fa417f620 100644 --- a/controller/subscription_payment_stripe.go +++ b/controller/subscription_payment_stripe.go @@ -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": "拉起支付失败"}) @@ -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), diff --git a/controller/telegram.go b/controller/telegram.go index f16cdd66c54..19d1860cd44 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.GetThemeAwarePath(c, "/console/personal")) } func TelegramLogin(c *gin.Context) { diff --git a/controller/topup.go b/controller/topup.go index a6445b40d68..592fedcf333 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -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) diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go index ceee8ecdd66..99e514322eb 100644 --- a/controller/topup_stripe.go +++ b/controller/topup_stripe.go @@ -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": "拉起支付失败"}) @@ -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密钥") } @@ -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{ diff --git a/controller/topup_waffo.go b/controller/topup_waffo.go index 1885c1ded9e..4cf4aaef09e 100644 --- a/controller/topup_waffo.go +++ b/controller/topup_waffo.go @@ -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 } diff --git a/controller/topup_waffo_pancake.go b/controller/topup_waffo_pancake.go index 09f1516304b..1837071c243 100644 --- a/controller/topup_waffo_pancake.go +++ b/controller/topup_waffo_pancake.go @@ -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) { @@ -186,7 +186,7 @@ func RequestWaffoPancakePay(c *gin.Context) { TaxCategory: "saas", }, BuyerEmail: getWaffoPancakeBuyerEmail(user), - SuccessURL: getWaffoPancakeReturnURL(), + SuccessURL: getWaffoPancakeReturnURL(c), ExpiresInSeconds: &expiresInSeconds, }) if err != nil { diff --git a/router/web-router.go b/router/web-router.go index 03f3f2e3699..17027316f77 100644 --- a/router/web-router.go +++ b/router/web-router.go @@ -3,7 +3,6 @@ package router import ( "embed" "net/http" - "net/url" "strings" "github.com/QuantumNous/new-api/common" @@ -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 } @@ -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 diff --git a/web/classic/src/components/common/markdown/MarkdownRenderer.jsx b/web/classic/src/components/common/markdown/MarkdownRenderer.jsx index 6a71c695f84..bb61efe3834 100644 --- a/web/classic/src/components/common/markdown/MarkdownRenderer.jsx +++ b/web/classic/src/components/common/markdown/MarkdownRenderer.jsx @@ -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) { diff --git a/web/classic/src/components/layout/PageLayout.jsx b/web/classic/src/components/layout/PageLayout.jsx index 971497503c9..b50ff961583 100644 --- a/web/classic/src/components/layout/PageLayout.jsx +++ b/web/classic/src/components/layout/PageLayout.jsx @@ -39,17 +39,7 @@ import { UserContext } from '../../context/User'; import { StatusContext } from '../../context/Status'; import { useLocation } from 'react-router-dom'; import { normalizeLanguage } from '../../i18n/language'; -const FRONTEND_THEME_COOKIE_NAME = 'frontend_theme'; -const FRONTEND_THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; -const normalizeFrontendTheme = (value) => { - return value === 'classic' ? 'classic' : 'default'; -}; - -const setFrontendTheme = (theme) => { - if (typeof document === 'undefined') return; - document.cookie = `${FRONTEND_THEME_COOKIE_NAME}=${theme}; path=/; max-age=${FRONTEND_THEME_COOKIE_MAX_AGE}`; -}; const { Sider, Content, Header } = Layout; const PageLayout = () => { @@ -135,9 +125,6 @@ const PageLayout = () => { try { const settings = JSON.parse(userState.user.setting); preferredLang = normalizeLanguage(settings.language); - if (settings.frontend_theme) { - setFrontendTheme(normalizeFrontendTheme(settings.frontend_theme)); - } } catch (e) { // Ignore parse errors } diff --git a/web/classic/src/components/playground/MessageContent.jsx b/web/classic/src/components/playground/MessageContent.jsx index 94f494bb31f..6d103fab2f7 100644 --- a/web/classic/src/components/playground/MessageContent.jsx +++ b/web/classic/src/components/playground/MessageContent.jsx @@ -93,7 +93,7 @@ const MessageContent = ({ theme='light' type='warning' icon={} - onClick={() => window.open('/console/setting?tab=ratio', '_blank')} + onClick={() => window.open('/console/setting?tab=ratio', '_blank', 'noopener,noreferrer')} > {t('前往设置')} diff --git a/web/classic/src/components/settings/personal/cards/PreferencesSettings.jsx b/web/classic/src/components/settings/personal/cards/PreferencesSettings.jsx index 8f18844c86f..2a8bec2c155 100644 --- a/web/classic/src/components/settings/personal/cards/PreferencesSettings.jsx +++ b/web/classic/src/components/settings/personal/cards/PreferencesSettings.jsx @@ -84,7 +84,7 @@ const updateFrontendThemePreference = async (theme, userId) => { const PreferencesSettings = ({ t }) => { const { i18n } = useTranslation(); - const [userState, userDispatch] = useContext(UserContext); + const [userState, userDispatch, startThemeNavigation] = useContext(UserContext); const [currentLanguage, setCurrentLanguage] = useState( normalizeLanguage(i18n.language) || 'zh-CN', ); @@ -198,6 +198,7 @@ const PreferencesSettings = ({ t }) => { setFrontendTheme(theme); setCurrentFrontendTheme(theme); showSuccess(t('界面风格已切换,正在跳转')); + startThemeNavigation(); setTimeout(() => { window.location.assign(getFrontendThemeSettingsPath(theme)); }, 300); diff --git a/web/classic/src/components/table/channels/modals/ModelTestModal.jsx b/web/classic/src/components/table/channels/modals/ModelTestModal.jsx index e7f57453229..f46439422f9 100644 --- a/web/classic/src/components/table/channels/modals/ModelTestModal.jsx +++ b/web/classic/src/components/table/channels/modals/ModelTestModal.jsx @@ -199,7 +199,7 @@ const ModelTestModal = ({ theme='light' type='warning' icon={} - onClick={() => window.open('/console/setting?tab=ratio', '_blank')} + onClick={() => window.open('/console/setting?tab=ratio', '_blank', 'noopener,noreferrer')} style={{ width: 'fit-content' }} > {t('前往设置')} diff --git a/web/classic/src/components/table/model-deployments/modals/ViewLogsModal.jsx b/web/classic/src/components/table/model-deployments/modals/ViewLogsModal.jsx index 3d0446aeab9..e5041401fe8 100644 --- a/web/classic/src/components/table/model-deployments/modals/ViewLogsModal.jsx +++ b/web/classic/src/components/table/model-deployments/modals/ViewLogsModal.jsx @@ -573,7 +573,7 @@ const ViewLogsModal = ({ visible, onCancel, deployment, t }) => { size='small' theme='borderless' onClick={() => - window.open(containerDetails.public_url, '_blank') + window.open(containerDetails.public_url, '_blank', 'noopener,noreferrer') } /> diff --git a/web/classic/src/components/table/task-logs/modals/AudioPreviewModal.jsx b/web/classic/src/components/table/task-logs/modals/AudioPreviewModal.jsx index 0b2cada1988..afd260a5462 100644 --- a/web/classic/src/components/table/task-logs/modals/AudioPreviewModal.jsx +++ b/web/classic/src/components/table/task-logs/modals/AudioPreviewModal.jsx @@ -119,7 +119,7 @@ const AudioClipCard = ({ clip }) => { diff --git a/web/classic/src/components/table/task-logs/modals/ContentModal.jsx b/web/classic/src/components/table/task-logs/modals/ContentModal.jsx index 3527fd96d61..88a5289111b 100644 --- a/web/classic/src/components/table/task-logs/modals/ContentModal.jsx +++ b/web/classic/src/components/table/task-logs/modals/ContentModal.jsx @@ -55,7 +55,7 @@ const ContentModal = ({ }; const handleOpenInNewTab = () => { - window.open(modalContent, '_blank'); + window.open(modalContent, '_blank', 'noopener,noreferrer'); }; const renderVideoContent = () => { diff --git a/web/classic/src/components/table/tokens/modals/CCSwitchModal.jsx b/web/classic/src/components/table/tokens/modals/CCSwitchModal.jsx index 6cf817330a5..69bb737a868 100644 --- a/web/classic/src/components/table/tokens/modals/CCSwitchModal.jsx +++ b/web/classic/src/components/table/tokens/modals/CCSwitchModal.jsx @@ -117,7 +117,7 @@ export default function CCSwitchModal({ return; } const url = buildCCSwitchURL(app, name, models, 'sk-' + tokenKey); - window.open(url, '_blank'); + window.open(url, '_blank', 'noopener,noreferrer'); onClose(); }; diff --git a/web/classic/src/components/topup/SubscriptionPlansCard.jsx b/web/classic/src/components/topup/SubscriptionPlansCard.jsx index 9c50828372b..8e91861b8df 100644 --- a/web/classic/src/components/topup/SubscriptionPlansCard.jsx +++ b/web/classic/src/components/topup/SubscriptionPlansCard.jsx @@ -124,7 +124,7 @@ const SubscriptionPlansCard = ({ plan_id: selectedPlan.plan.id, }); if (res.data?.message === 'success') { - window.open(res.data.data?.pay_link, '_blank'); + window.open(res.data.data?.pay_link, '_blank', 'noopener,noreferrer'); showSuccess(t('已打开支付页面')); closeBuy(); } else { @@ -152,7 +152,7 @@ const SubscriptionPlansCard = ({ plan_id: selectedPlan.plan.id, }); if (res.data?.message === 'success') { - window.open(res.data.data?.checkout_url, '_blank'); + window.open(res.data.data?.checkout_url, '_blank', 'noopener,noreferrer'); showSuccess(t('已打开支付页面')); closeBuy(); } else { diff --git a/web/classic/src/components/topup/index.jsx b/web/classic/src/components/topup/index.jsx index 1c23ca928c1..df2c590c082 100644 --- a/web/classic/src/components/topup/index.jsx +++ b/web/classic/src/components/topup/index.jsx @@ -188,7 +188,7 @@ const TopUp = () => { showError(t('超级管理员未设置充值链接!')); return; } - window.open(topUpLink, '_blank'); + window.open(topUpLink, '_blank', 'noopener,noreferrer'); }; const preTopUp = async (payment) => { @@ -294,7 +294,7 @@ const TopUp = () => { if (message === 'success') { if (payWay === 'stripe') { // Stripe 支付回调处理 - window.open(data.pay_link, '_blank'); + window.open(data.pay_link, '_blank', 'noopener,noreferrer'); } else { // 普通支付表单提交 let params = data; @@ -397,7 +397,7 @@ const TopUp = () => { if (res !== undefined) { const { message, data } = res.data; if (message === 'success' && data?.payment_url) { - window.open(data.payment_url, '_blank'); + window.open(data.payment_url, '_blank', 'noopener,noreferrer'); } else { showError(data || t('支付请求失败')); } @@ -455,7 +455,7 @@ const TopUp = () => { if (message === 'success') { const checkoutUrl = data?.checkout_url || ''; if (checkoutUrl) { - window.open(checkoutUrl, '_blank'); + window.open(checkoutUrl, '_blank', 'noopener,noreferrer'); } else { showError(t('支付请求失败')); } @@ -503,7 +503,7 @@ const TopUp = () => { const processCreemCallback = (data) => { // 与 Stripe 保持一致的实现方式 - window.open(data.checkout_url, '_blank'); + window.open(data.checkout_url, '_blank', 'noopener,noreferrer'); }; const getUserQuota = async () => { diff --git a/web/classic/src/context/User/index.jsx b/web/classic/src/context/User/index.jsx index 04511648e9c..47c056c87cc 100644 --- a/web/classic/src/context/User/index.jsx +++ b/web/classic/src/context/User/index.jsx @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { reducer, initialState } from './reducer'; import { normalizeLanguage } from '../../i18n/language'; @@ -37,13 +37,19 @@ const setFrontendTheme = (theme) => { export const UserContext = React.createContext({ state: initialState, dispatch: () => null, + startThemeNavigation: () => {}, }); export const UserProvider = ({ children }) => { const [state, dispatch] = React.useReducer(reducer, initialState); const { i18n } = useTranslation(); + const themeRedirectAttemptedRef = useRef(false); + const themeNavigationPendingRef = useRef(false); + + const startThemeNavigation = useCallback(() => { + themeNavigationPendingRef.current = true; + }, []); - // Sync language preference when user data is loaded useEffect(() => { if (state.user?.setting) { try { @@ -56,7 +62,12 @@ export const UserProvider = ({ children }) => { localStorage.setItem('i18nextLng', normalizedLanguage); } if (settings.frontend_theme) { - setFrontendTheme(normalizeFrontendTheme(settings.frontend_theme)); + const normalizedTheme = normalizeFrontendTheme(settings.frontend_theme); + setFrontendTheme(normalizedTheme); + if (normalizedTheme === 'default' && !themeRedirectAttemptedRef.current && !themeNavigationPendingRef.current) { + themeRedirectAttemptedRef.current = true; + window.location.replace('/dashboard'); + } } } catch (e) { // Ignore parse errors @@ -65,7 +76,7 @@ export const UserProvider = ({ children }) => { }, [state.user?.setting, i18n]); return ( - + {children} ); diff --git a/web/classic/src/helpers/api.js b/web/classic/src/helpers/api.js index 88122a564cf..87c680d5dda 100644 --- a/web/classic/src/helpers/api.js +++ b/web/classic/src/helpers/api.js @@ -42,7 +42,7 @@ function redirectToOAuthUrl(url, options = {}) { const targetUrl = typeof url === 'string' ? url : url.toString(); if (openInNewTab) { - window.open(targetUrl, '_blank'); + window.open(targetUrl, '_blank', 'noopener,noreferrer'); return; } diff --git a/web/classic/src/hooks/tokens/useTokensData.jsx b/web/classic/src/hooks/tokens/useTokensData.jsx index abee82b3977..55bdd411f0b 100644 --- a/web/classic/src/hooks/tokens/useTokensData.jsx +++ b/web/classic/src/hooks/tokens/useTokensData.jsx @@ -257,7 +257,7 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => { url = url.replaceAll('{key}', `sk-${fullKey}`); } - window.open(url, '_blank'); + window.open(url, '_blank', 'noopener,noreferrer'); }; // Manage token function (delete, enable, disable) diff --git a/web/classic/src/pages/Home/index.jsx b/web/classic/src/pages/Home/index.jsx index c153c1b3da9..81c7d677f1a 100644 --- a/web/classic/src/pages/Home/index.jsx +++ b/web/classic/src/pages/Home/index.jsx @@ -233,6 +233,7 @@ const Home = () => { window.open( 'https://github.com/QuantumNous/new-api', '_blank', + 'noopener,noreferrer', ) } > @@ -244,7 +245,7 @@ const Home = () => { size={isMobile ? 'default' : 'large'} className='flex items-center !rounded-3xl px-6 py-2' icon={} - onClick={() => window.open(docsLink, '_blank')} + onClick={() => window.open(docsLink, '_blank', 'noopener,noreferrer')} > {t('文档')} diff --git a/web/classic/src/pages/Setting/Model/SettingModelDeployment.jsx b/web/classic/src/pages/Setting/Model/SettingModelDeployment.jsx index fdfbb448e36..3858e215d5b 100644 --- a/web/classic/src/pages/Setting/Model/SettingModelDeployment.jsx +++ b/web/classic/src/pages/Setting/Model/SettingModelDeployment.jsx @@ -310,7 +310,7 @@ export default function SettingModelDeployment(props) { theme='solid' style={{ width: '100%' }} onClick={() => - window.open('https://ai.io.net/ai/api-keys', '_blank') + window.open('https://ai.io.net/ai/api-keys', '_blank', 'noopener,noreferrer') } > {t('前往 io.net API Keys')} diff --git a/web/default/src/components/langfuse-trace-notice.tsx b/web/default/src/components/langfuse-trace-notice.tsx new file mode 100644 index 00000000000..12419175538 --- /dev/null +++ b/web/default/src/components/langfuse-trace-notice.tsx @@ -0,0 +1,22 @@ +import { ShieldAlert } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { useStatus } from '@/hooks/use-status' + +export function LangfuseTraceNotice() { + const { t } = useTranslation() + const { status } = useStatus() + + if (!status?.langfuse_trace_content) return null + + return ( + + + + {t( + 'This site uses Langfuse to record user prompts and other data. Please be aware of security risks.' + )} + + + ) +} diff --git a/web/default/src/features/auth/hooks/use-auth-redirect.ts b/web/default/src/features/auth/hooks/use-auth-redirect.ts index 49fc8f69ebf..843d424fef5 100644 --- a/web/default/src/features/auth/hooks/use-auth-redirect.ts +++ b/web/default/src/features/auth/hooks/use-auth-redirect.ts @@ -3,7 +3,6 @@ import i18n from 'i18next' import { useAuthStore } from '@/stores/auth-store' import { getSelf } from '@/lib/api' import { - getFrontendThemeSettingsPath, normalizeFrontendTheme, setFrontendTheme, } from '@/lib/frontend-theme' @@ -90,8 +89,8 @@ export function useAuthRedirect() { const savedTheme = getSavedFrontendTheme(user) if (savedTheme) { setFrontendTheme(savedTheme) - if (!redirectTo) { - window.location.assign(getFrontendThemeSettingsPath(savedTheme)) + if (savedTheme === 'classic') { + window.location.replace('/console') return } } diff --git a/web/default/src/features/channels/components/channels-columns.tsx b/web/default/src/features/channels/components/channels-columns.tsx index 047ecf63c65..6a0444c04ef 100644 --- a/web/default/src/features/channels/components/channels-columns.tsx +++ b/web/default/src/features/channels/components/channels-columns.tsx @@ -661,7 +661,7 @@ export function useChannelsColumns(): ColumnDef[] { onClick={(e) => { e.stopPropagation() if (!deploymentId) return - const targetUrl = `/console/deployment?deployment_id=${deploymentId}` + const targetUrl = `/models/deployments?deployment_id=${deploymentId}` window.open(targetUrl, '_blank', 'noopener') }} > diff --git a/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx b/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx index 9d2990f22de..a7d47ce6ca1 100644 --- a/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx @@ -352,7 +352,7 @@ export function ChannelTestDialog({ size='sm' className='w-fit' onClick={() => - window.open('/console/setting?tab=ratio', '_blank') + window.open('/system-settings?tab=ratio', '_blank', 'noopener') } > diff --git a/web/default/src/features/keys/components/dialogs/cc-switch-dialog.tsx b/web/default/src/features/keys/components/dialogs/cc-switch-dialog.tsx index f99beafd29d..eab2e99a609 100644 --- a/web/default/src/features/keys/components/dialogs/cc-switch-dialog.tsx +++ b/web/default/src/features/keys/components/dialogs/cc-switch-dialog.tsx @@ -128,7 +128,7 @@ export function CCSwitchDialog(props: Props) { ? props.tokenKey : `sk-${props.tokenKey}` const url = buildCCSwitchURL(app, name, models, key) - window.open(url, '_blank') + window.open(url, '_blank', 'noopener,noreferrer') props.onOpenChange(false) } diff --git a/web/default/src/features/keys/index.tsx b/web/default/src/features/keys/index.tsx index c7bb51fcaca..ea09cb11edb 100644 --- a/web/default/src/features/keys/index.tsx +++ b/web/default/src/features/keys/index.tsx @@ -1,4 +1,5 @@ import { useTranslation } from 'react-i18next' +import { LangfuseTraceNotice } from '@/components/langfuse-trace-notice' import { SectionPageLayout } from '@/components/layout' import { ApiKeysDialogs } from './components/api-keys-dialogs' import { ApiKeysProvider } from './components/api-keys-provider' @@ -14,7 +15,10 @@ export function ApiKeys() { {t('Manage your API keys for accessing the service')} - +
+ + +
diff --git a/web/default/src/features/models/components/dialogs/view-details-dialog.tsx b/web/default/src/features/models/components/dialogs/view-details-dialog.tsx index ae2a9d33344..27966c46a06 100644 --- a/web/default/src/features/models/components/dialogs/view-details-dialog.tsx +++ b/web/default/src/features/models/components/dialogs/view-details-dialog.tsx @@ -224,7 +224,7 @@ export function ViewDetailsDialog({