diff --git a/common/constants.go b/common/constants.go
index 40d77ce..681afcf 100644
--- a/common/constants.go
+++ b/common/constants.go
@@ -15,6 +15,14 @@ var Version = "v0.0.0" // this hard coding will be replaced automatic
var SystemName = "New API"
var Footer = ""
var Logo = ""
+var LogoFilePath = "" // LOGO_FILE_PATH 环境变量指定的本地 Logo 文件路径
+
+func GetEffectiveLogo() string {
+ if LogoFilePath != "" {
+ return "/logo.png"
+ }
+ return Logo
+}
var TopUpLink = ""
var DefaultLanguage = "" // admin-configured default language; empty = follow browser detection
diff --git a/controller/misc.go b/controller/misc.go
index 28e767f..9edd833 100644
--- a/controller/misc.go
+++ b/controller/misc.go
@@ -61,7 +61,7 @@ func GetStatus(c *gin.Context) {
"telegram_oauth": common.TelegramOAuthEnabled,
"telegram_bot_name": common.TelegramBotName,
"system_name": common.SystemName,
- "logo": common.Logo,
+ "logo": common.GetEffectiveLogo(),
"footer_html": common.Footer,
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
"wechat_login": common.WeChatAuthEnabled,
diff --git a/main.go b/main.go
index 4b58d71..3b5ef0e 100644
--- a/main.go
+++ b/main.go
@@ -137,6 +137,16 @@ func main() {
model.InitBatchUpdater()
}
+ logoFilePath := os.Getenv("LOGO_FILE_PATH")
+ if logoFilePath != "" {
+ if _, err := os.Stat(logoFilePath); err != nil {
+ common.SysLog("LOGO_FILE_PATH file not found: " + logoFilePath + ", falling back to default")
+ } else {
+ common.LogoFilePath = logoFilePath
+ common.SysLog("custom logo file: " + common.LogoFilePath)
+ }
+ }
+
if os.Getenv("ENABLE_PPROF") == "true" {
gopool.Go(func() {
log.Println(http.ListenAndServe("0.0.0.0:8005", nil))
diff --git a/model/channel_pricing.go b/model/channel_pricing.go
index e7932b1..7eacfd9 100644
--- a/model/channel_pricing.go
+++ b/model/channel_pricing.go
@@ -274,13 +274,8 @@ func GetChannelPricingByModelWithChannelInfo(modelName string) ([]*ChannelPricin
if !hasPrice {
globalModelPrice = 0
}
- // 获取全局扩展比率(用于 CASE WHEN 回退)
- globalCacheRatio, _ := ratio_setting.GetCacheRatio(modelName)
- globalCacheCreationRatio, _ := ratio_setting.GetCreateCacheRatio(modelName)
- globalImageRatio, _ := ratio_setting.GetImageRatio(modelName)
- globalAudioRatio := ratio_setting.GetAudioRatio(modelName)
- globalAudioCompletionRatio := ratio_setting.GetAudioCompletionRatio(modelName)
// 查询所有支持该模型的渠道,左连接渠道定价表
+ // 高级字段(cache/image/audio)不回退全局值,直接返回 0
err := DB.Table("abilities").
Select(`abilities.channel_id, channels.name as channel_name, channels.type as channel_type,
COALESCE(channel_pricings.quota_type, ?) as quota_type,
@@ -289,13 +284,13 @@ func GetChannelPricingByModelWithChannelInfo(modelName string) ([]*ChannelPricin
COALESCE(channel_pricings.model_price, ?) as model_price,
channel_pricings.id as id,
channel_pricings.tag_ids as tag_ids,
- CASE WHEN channel_pricings.cache_ratio > 0 THEN channel_pricings.cache_ratio ELSE ? END as cache_ratio,
- CASE WHEN channel_pricings.cache_creation_ratio > 0 THEN channel_pricings.cache_creation_ratio ELSE ? END as cache_creation_ratio,
- CASE WHEN channel_pricings.image_ratio > 0 THEN channel_pricings.image_ratio ELSE ? END as image_ratio,
- CASE WHEN channel_pricings.audio_ratio > 0 THEN channel_pricings.audio_ratio ELSE ? END as audio_ratio,
- CASE WHEN channel_pricings.audio_completion_ratio > 0 THEN channel_pricings.audio_completion_ratio ELSE ? END as audio_completion_ratio,
+ COALESCE(channel_pricings.cache_ratio, 0) as cache_ratio,
+ COALESCE(channel_pricings.cache_creation_ratio, 0) as cache_creation_ratio,
+ COALESCE(channel_pricings.image_ratio, 0) as image_ratio,
+ COALESCE(channel_pricings.audio_ratio, 0) as audio_ratio,
+ COALESCE(channel_pricings.audio_completion_ratio, 0) as audio_completion_ratio,
(channel_pricings.id IS NOT NULL) as has_custom_pricing`,
- defaultQuotaType, globalModelRatio, globalCompletionRatio, globalModelPrice, globalCacheRatio, globalCacheCreationRatio, globalImageRatio, globalAudioRatio, globalAudioCompletionRatio).
+ defaultQuotaType, globalModelRatio, globalCompletionRatio, globalModelPrice).
Joins("LEFT JOIN channels ON abilities.channel_id = channels.id").
Joins("LEFT JOIN channel_pricings ON abilities.channel_id = channel_pricings.channel_id AND channel_pricings.model_name = ? AND channel_pricings.deleted_at IS NULL", modelName).
Where("abilities.model = ?", modelName).
diff --git a/router/web-router.go b/router/web-router.go
index b053a3e..7246254 100644
--- a/router/web-router.go
+++ b/router/web-router.go
@@ -14,6 +14,14 @@ import (
)
func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
+ // LOGO_FILE_PATH 优先级最高:注册 /logo.png 路由提供磁盘文件
+ // 必须在 static.Serve("/") 之前注册,否则会被 embed 静态文件拦截
+ if common.LogoFilePath != "" {
+ router.GET("/logo.png", func(c *gin.Context) {
+ c.File(common.LogoFilePath)
+ })
+ }
+
router.Use(gzip.Gzip(gzip.DefaultCompression))
router.Use(middleware.GlobalWebRateLimit())
router.Use(middleware.Cache())
diff --git a/web/src/components/auth/LoginForm.jsx b/web/src/components/auth/LoginForm.jsx
index 666b9d4..82dba4e 100644
--- a/web/src/components/auth/LoginForm.jsx
+++ b/web/src/components/auth/LoginForm.jsx
@@ -505,7 +505,7 @@ const LoginForm = () => {
-

+
{systemName}
@@ -721,7 +721,7 @@ const LoginForm = () => {
-

+
{systemName}
@@ -885,7 +885,7 @@ const LoginForm = () => {
}}
>
-

+
diff --git a/web/src/components/auth/PasswordResetConfirm.jsx b/web/src/components/auth/PasswordResetConfirm.jsx
index 9bc37b3..936a362 100644
--- a/web/src/components/auth/PasswordResetConfirm.jsx
+++ b/web/src/components/auth/PasswordResetConfirm.jsx
@@ -118,7 +118,7 @@ const PasswordResetConfirm = () => {
-

+
{systemName}
diff --git a/web/src/components/auth/PasswordResetForm.jsx b/web/src/components/auth/PasswordResetForm.jsx
index 92afc2a..876dcc0 100644
--- a/web/src/components/auth/PasswordResetForm.jsx
+++ b/web/src/components/auth/PasswordResetForm.jsx
@@ -118,7 +118,7 @@ const PasswordResetForm = () => {
-

+
{systemName}
diff --git a/web/src/components/auth/RegisterForm.jsx b/web/src/components/auth/RegisterForm.jsx
index 5d757fd..de28a9b 100644
--- a/web/src/components/auth/RegisterForm.jsx
+++ b/web/src/components/auth/RegisterForm.jsx
@@ -396,7 +396,7 @@ const RegisterForm = () => {
-

+
{systemName}
@@ -559,7 +559,7 @@ const RegisterForm = () => {
-

+
{systemName}
@@ -745,7 +745,7 @@ const RegisterForm = () => {
}}
>
-

+
diff --git a/web/src/components/layout/Footer.jsx b/web/src/components/layout/Footer.jsx
index ab4c173..e685204 100644
--- a/web/src/components/layout/Footer.jsx
+++ b/web/src/components/layout/Footer.jsx
@@ -52,6 +52,8 @@ const FooterBar = () => {
diff --git a/web/src/components/layout/PageLayout.jsx b/web/src/components/layout/PageLayout.jsx
index b8978a3..16cac4c 100644
--- a/web/src/components/layout/PageLayout.jsx
+++ b/web/src/components/layout/PageLayout.jsx
@@ -91,8 +91,9 @@ const PageLayout = () => {
if (success) {
statusDispatch({ type: 'set', payload: data });
setStatusData(data);
- // Apply admin-configured default language
- if (data.default_language) {
+ // Apply admin-configured default language only if user has no preference
+ const savedLang = localStorage.getItem('i18nextLng');
+ if (data.default_language && !savedLang) {
i18n.changeLanguage(data.default_language);
}
} else {
diff --git a/web/src/components/layout/headerbar/HeaderLogo.jsx b/web/src/components/layout/headerbar/HeaderLogo.jsx
index 73be051..178249a 100644
--- a/web/src/components/layout/headerbar/HeaderLogo.jsx
+++ b/web/src/components/layout/headerbar/HeaderLogo.jsx
@@ -44,6 +44,8 @@ const HeaderLogo = ({
diff --git a/web/src/components/table/model-pricing/modal/components/ChannelPricingCard.jsx b/web/src/components/table/model-pricing/modal/components/ChannelPricingCard.jsx
index c953b58..45afe75 100644
--- a/web/src/components/table/model-pricing/modal/components/ChannelPricingCard.jsx
+++ b/web/src/components/table/model-pricing/modal/components/ChannelPricingCard.jsx
@@ -18,9 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useEffect, useMemo } from 'react';
-import { Card, Avatar, Typography, Table, Tag, Spin, Banner } from '@douyinfe/semi-ui';
-import { IconServer } from '@douyinfe/semi-icons';
-import { API } from '../../../../../helpers';
+import { Card, Avatar, Typography, Table, Tag, Spin, Banner, Tooltip } from '@douyinfe/semi-ui';
+import { IconServer, IconCopy } from '@douyinfe/semi-icons';
+import { API, copy, showSuccess } from '../../../../../helpers';
const { Text } = Typography;
@@ -128,9 +128,20 @@ const ChannelPricingCard = ({
dataIndex: 'channelName',
render: (text, record) => (
-
- ID: {record.channelId}
-
+
+ {
+ const ok = await copy(String(record.channelId));
+ if (ok) showSuccess(t('已复制到剪切板'));
+ }}
+ >
+ ID: {record.channelId}
+
+
{text}
@@ -213,23 +224,28 @@ const ChannelPricingCard = ({
);
const renderCachePrice = (v, record) => {
- if (record.quotaType !== 0 || v <= 0) return '-';
+ if (record.quotaType !== 0) return '-';
return (
-
- {formatPrice(record.modelRatio * v * 2)}
-
+ <>
+
+ {formatPrice(record.modelRatio * v * 2)}
+
+
+ / {tokenUnit === 'K' ? '1K' : '1M'} tokens
+
+ >
);
};
const advancedColumns = hasAdvancedPricing
? [
{
- title: t('缓存读取') + ` / ${tokenUnit === 'K' ? '1K' : '1M'} tokens`,
+ title: t('缓存读取'),
dataIndex: 'cacheRatio',
render: renderCachePrice,
},
{
- title: t('缓存创建') + ` / ${tokenUnit === 'K' ? '1K' : '1M'} tokens`,
+ title: t('缓存创建'),
dataIndex: 'cacheCreationRatio',
render: renderCachePrice,
},
diff --git a/web/src/context/User/index.jsx b/web/src/context/User/index.jsx
index 7ecdebf..2dd9100 100644
--- a/web/src/context/User/index.jsx
+++ b/web/src/context/User/index.jsx
@@ -20,7 +20,6 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { reducer, initialState } from './reducer';
-import { StatusContext } from '../Status';
export const UserContext = React.createContext({
state: initialState,
@@ -30,7 +29,6 @@ export const UserContext = React.createContext({
export const UserProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState);
const { i18n } = useTranslation();
- const [statusState] = React.useContext(StatusContext);
// Sync language preference when user data is loaded
useEffect(() => {
@@ -39,15 +37,12 @@ export const UserProvider = ({ children }) => {
const settings = JSON.parse(state.user.setting);
if (settings.language && settings.language !== i18n.language) {
i18n.changeLanguage(settings.language);
- } else if (!settings.language && statusState.status?.default_language) {
- // No personal preference — fall back to admin default
- i18n.changeLanguage(statusState.status.default_language);
}
} catch (e) {
// Ignore parse errors
}
}
- }, [state.user?.setting, statusState.status?.default_language, i18n]);
+ }, [state.user?.setting, i18n]);
return (
diff --git a/web/src/hooks/common/useHeaderBar.js b/web/src/hooks/common/useHeaderBar.js
index 02b005e..6eb26fa 100644
--- a/web/src/hooks/common/useHeaderBar.js
+++ b/web/src/hooks/common/useHeaderBar.js
@@ -158,20 +158,20 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
});
if (res.data.success) {
// Update user context with new setting
- if (userState?.user?.setting) {
- try {
- const settings = JSON.parse(userState.user.setting);
- settings.language = lang;
- userDispatch({
- type: 'login',
- payload: {
- ...userState.user,
- setting: JSON.stringify(settings),
- },
- });
- } catch (e) {
- // Ignore parse errors
- }
+ try {
+ const settings = userState?.user?.setting
+ ? JSON.parse(userState.user.setting)
+ : {};
+ settings.language = lang;
+ userDispatch({
+ type: 'login',
+ payload: {
+ ...userState.user,
+ setting: JSON.stringify(settings),
+ },
+ });
+ } catch (e) {
+ // Ignore parse errors
}
}
} catch (error) {
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index a6da31e..d184454 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -345,7 +345,7 @@
"令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制": "The quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account",
"令牌端点": "Token Endpoint",
"令牌管理": "Token Management",
- "以一套域名、密钥与风控策略连接全球大模型资源,保障可观测、可拓展、可控。": "Connect to global LLM resources with a single domain, key, and policy. Observable, scalable, and controllable.",
+ "以一套域名、密钥与风控策略连接顶级大模型资源,保障可观测、可拓展、可控。": "Connect to top-tier LLM resources with a single domain, key, and policy. Observable, scalable, and controllable.",
"以下上游数据可能不可信:": "The following upstream data may not be reliable: ",
"以下文件解析失败,已忽略:{{list}}": "The following files failed to parse and have been ignored: {{list}}",
"以单一域名和密钥连接全部大模型供应商,智能容灾切换确保业务不中断。": "Connect all LLM providers with a single domain and key. Smart failover ensures uninterrupted service.",
@@ -2229,6 +2229,8 @@
"统一入口,极速连通": "Unified Entry, Instant Connect",
"统一的": "The Unified",
"统一的大模型接口网关": "Unified LLM API Gateway",
+ "链接顶级 AI 能力": "Connect Top-Tier AI",
+ "点击复制渠道 ID": "Click to copy channel ID",
"统一监控": "Unified Monitoring",
"统计Tokens": "Statistical Tokens",
"统计已重置": "Statistics reset",
diff --git a/web/src/i18n/locales/zh-CN.json b/web/src/i18n/locales/zh-CN.json
index 2a478df..d02076e 100644
--- a/web/src/i18n/locales/zh-CN.json
+++ b/web/src/i18n/locales/zh-CN.json
@@ -337,7 +337,7 @@
"令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制": "令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制",
"令牌端点": "令牌端点",
"令牌管理": "令牌管理",
- "以一套域名、密钥与风控策略连接全球大模型资源,保障可观测、可拓展、可控。": "以一套域名、密钥与风控策略连接全球大模型资源,保障可观测、可拓展、可控。",
+ "以一套域名、密钥与风控策略连接顶级大模型资源,保障可观测、可拓展、可控。": "以一套域名、密钥与风控策略连接顶级大模型资源,保障可观测、可拓展、可控。",
"以下上游数据可能不可信:": "以下上游数据可能不可信:",
"以下文件解析失败,已忽略:{{list}}": "以下文件解析失败,已忽略:{{list}}",
"以单一域名和密钥连接全部大模型供应商,智能容灾切换确保业务不中断。": "以单一域名和密钥连接全部大模型供应商,智能容灾切换确保业务不中断。",
@@ -2211,6 +2211,8 @@
"统一入口,极速连通": "统一入口,极速连通",
"统一的": "统一的",
"统一的大模型接口网关": "统一的大模型接口网关",
+ "链接顶级 AI 能力": "链接顶级 AI 能力",
+ "点击复制渠道 ID": "点击复制渠道 ID",
"统一监控": "统一监控",
"统计Tokens": "统计Tokens",
"统计已重置": "统计已重置",
diff --git a/web/src/pages/Setting/Operation/SettingsRegionSync.jsx b/web/src/pages/Setting/Operation/SettingsRegionSync.jsx
index 69f2b41..08a8f51 100644
--- a/web/src/pages/Setting/Operation/SettingsRegionSync.jsx
+++ b/web/src/pages/Setting/Operation/SettingsRegionSync.jsx
@@ -323,6 +323,7 @@ export default function SettingsRegionSync(props) {
'region_sync.min_balance_threshold'
)}
min={0}
+ extraText={t('额度单位换算:500,000 额度 = $1 USD。例如设置为 100,000 即 $0.2')}
/>
diff --git a/web/src/pages/Setting/Ratio/ChannelPricingView.jsx b/web/src/pages/Setting/Ratio/ChannelPricingView.jsx
index cd09e3e..f35eb16 100644
--- a/web/src/pages/Setting/Ratio/ChannelPricingView.jsx
+++ b/web/src/pages/Setting/Ratio/ChannelPricingView.jsx
@@ -582,8 +582,8 @@ const ChannelPricingView = ({ channels, channelPricings, tags, onRefresh }) => {
-
-
+
+