Просмотр исходного кода

Merge pull request #1278 from QuantumNous/alpha

feat: conditionally set Gemini ThinkingBudget based on MaxOutputTokens
Calcium-Ion 6 месяцев назад
Родитель
Сommit
cf4700a35c
29 измененных файлов с 935 добавлено и 289 удалено
  1. 1 2
      Dockerfile
  2. 38 0
      controller/channel-billing.go
  3. 6 5
      relay/channel/gemini/relay-gemini.go
  4. 9 3
      setting/operation_setting/tools.go
  5. 19 12
      setting/ratio_setting/model_ratio.go
  6. 80 12
      web/src/components/layout/HeaderBar.js
  7. 96 8
      web/src/components/layout/NoticeModal.js
  8. 63 0
      web/src/components/settings/ChatsSetting.js
  9. 17 2
      web/src/components/settings/DashboardSetting.js
  10. 65 0
      web/src/components/settings/DrawingSetting.js
  11. 0 31
      web/src/components/settings/OperationSetting.js
  12. 88 0
      web/src/components/settings/PaymentSetting.js
  13. 9 10
      web/src/components/settings/RateLimitSetting.js
  14. 1 1
      web/src/components/settings/RatioSetting.js
  15. 0 150
      web/src/components/settings/SystemSetting.js
  16. 16 3
      web/src/i18n/locales/en.json
  17. 28 0
      web/src/index.css
  18. 1 26
      web/src/pages/Setting/Chat/SettingsChats.js
  19. 8 2
      web/src/pages/Setting/Dashboard/SettingsAnnouncements.js
  20. 0 0
      web/src/pages/Setting/Dashboard/SettingsDataDashboard.js
  21. 0 0
      web/src/pages/Setting/Drawing/SettingsDrawing.js
  22. 2 2
      web/src/pages/Setting/Model/SettingGeminiModel.js
  23. 0 5
      web/src/pages/Setting/Operation/SettingsGeneral.js
  24. 74 0
      web/src/pages/Setting/Payment/SettingsGeneralPayment.js
  25. 218 0
      web/src/pages/Setting/Payment/SettingsPaymentGateway.js
  26. 1 1
      web/src/pages/Setting/Ratio/ModelRationNotSetEditor.js
  27. 1 1
      web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js
  28. 1 1
      web/src/pages/Setting/Ratio/UpstreamRatioSync.js
  29. 93 12
      web/src/pages/Setting/index.js

+ 1 - 2
Dockerfile

@@ -24,8 +24,7 @@ RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)'" -o one-
 
 FROM alpine
 
-RUN apk update \
-    && apk upgrade \
+RUN apk upgrade --no-cache \
     && apk add --no-cache ca-certificates tzdata ffmpeg \
     && update-ca-certificates
 

+ 38 - 0
controller/channel-billing.go

@@ -4,11 +4,13 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/shopspring/decimal"
 	"io"
 	"net/http"
 	"one-api/common"
 	"one-api/model"
 	"one-api/service"
+	"one-api/setting"
 	"strconv"
 	"time"
 
@@ -304,6 +306,40 @@ func updateChannelOpenRouterBalance(channel *model.Channel) (float64, error) {
 	return balance, nil
 }
 
+func updateChannelMoonshotBalance(channel *model.Channel) (float64, error) {
+	url := "https://api.moonshot.cn/v1/users/me/balance"
+	body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
+	if err != nil {
+		return 0, err
+	}
+
+	type MoonshotBalanceData struct {
+		AvailableBalance float64 `json:"available_balance"`
+		VoucherBalance   float64 `json:"voucher_balance"`
+		CashBalance      float64 `json:"cash_balance"`
+	}
+
+	type MoonshotBalanceResponse struct {
+		Code   int                 `json:"code"`
+		Data   MoonshotBalanceData `json:"data"`
+		Scode  string              `json:"scode"`
+		Status bool                `json:"status"`
+	}
+
+	response := MoonshotBalanceResponse{}
+	err = json.Unmarshal(body, &response)
+	if err != nil {
+		return 0, err
+	}
+	if !response.Status || response.Code != 0 {
+		return 0, fmt.Errorf("failed to update moonshot balance, status: %v, code: %d, scode: %s", response.Status, response.Code, response.Scode)
+	}
+	availableBalanceCny := response.Data.AvailableBalance
+	availableBalanceUsd := decimal.NewFromFloat(availableBalanceCny).Div(decimal.NewFromFloat(setting.Price)).InexactFloat64()
+	channel.UpdateBalance(availableBalanceUsd)
+	return availableBalanceUsd, nil
+}
+
 func updateChannelBalance(channel *model.Channel) (float64, error) {
 	baseURL := common.ChannelBaseURLs[channel.Type]
 	if channel.GetBaseURL() == "" {
@@ -332,6 +368,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) {
 		return updateChannelDeepSeekBalance(channel)
 	case common.ChannelTypeOpenRouter:
 		return updateChannelOpenRouterBalance(channel)
+	case common.ChannelTypeMoonshot:
+		return updateChannelMoonshotBalance(channel)
 	default:
 		return 0, errors.New("尚未实现")
 	}

+ 6 - 5
relay/channel/gemini/relay-gemini.go

@@ -103,7 +103,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
 		isNew25Pro := strings.HasPrefix(modelName, "gemini-2.5-pro") &&
 			!strings.HasPrefix(modelName, "gemini-2.5-pro-preview-05-06") &&
 			!strings.HasPrefix(modelName, "gemini-2.5-pro-preview-03-25")
-		is25FlashLite := strings.HasPrefix(modelName, "gemini-2.5-flash-lite")
 
 		if strings.Contains(modelName, "-thinking-") {
 			parts := strings.SplitN(modelName, "-thinking-", 2)
@@ -134,15 +133,17 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
 					IncludeThoughts: true,
 				}
 			} else {
-				budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(geminiRequest.GenerationConfig.MaxOutputTokens)
-				clampedBudget := clampThinkingBudget(modelName, int(budgetTokens))
 				geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
-					ThinkingBudget:  common.GetPointer(clampedBudget),
 					IncludeThoughts: true,
 				}
+				if geminiRequest.GenerationConfig.MaxOutputTokens > 0 {
+					budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(geminiRequest.GenerationConfig.MaxOutputTokens)
+					clampedBudget := clampThinkingBudget(modelName, int(budgetTokens))
+					geminiRequest.GenerationConfig.ThinkingConfig.ThinkingBudget = common.GetPointer(clampedBudget)
+				}
 			}
 		} else if strings.HasSuffix(modelName, "-nothinking") {
-			if !isNew25Pro && !is25FlashLite {
+			if !isNew25Pro {
 				geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
 					ThinkingBudget: common.GetPointer(0),
 				}

+ 9 - 3
setting/operation_setting/tools.go

@@ -17,6 +17,8 @@ const (
 const (
 	// Gemini Audio Input Price
 	Gemini25FlashPreviewInputAudioPrice     = 1.00
+	Gemini25FlashProductionInputAudioPrice  = 1.00 // for `gemini-2.5-flash`
+	Gemini25FlashLitePreviewInputAudioPrice = 0.50
 	Gemini25FlashNativeAudioInputAudioPrice = 3.00
 	Gemini20FlashInputAudioPrice            = 0.70
 )
@@ -64,10 +66,14 @@ func GetFileSearchPricePerThousand() float64 {
 }
 
 func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
-	if strings.HasPrefix(modelName, "gemini-2.5-flash-preview") {
-		return Gemini25FlashPreviewInputAudioPrice
-	} else if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-native-audio") {
+	if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-native-audio") {
 		return Gemini25FlashNativeAudioInputAudioPrice
+	} else if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-lite") {
+		return Gemini25FlashLitePreviewInputAudioPrice
+	} else if strings.HasPrefix(modelName, "gemini-2.5-flash-preview") {
+		return Gemini25FlashPreviewInputAudioPrice
+	} else if strings.HasPrefix(modelName, "gemini-2.5-flash") {
+		return Gemini25FlashProductionInputAudioPrice
 	} else if strings.HasPrefix(modelName, "gemini-2.0-flash") {
 		return Gemini20FlashInputAudioPrice
 	}

+ 19 - 12
setting/ratio_setting/model_ratio.go

@@ -140,6 +140,7 @@ var defaultModelRatio = map[string]float64{
 	"gemini-2.0-flash":                          0.05,
 	"gemini-2.5-pro-exp-03-25":                  0.625,
 	"gemini-2.5-pro-preview-03-25":              0.625,
+	"gemini-2.5-pro":                            0.625,
 	"gemini-2.5-flash-preview-04-17":            0.075,
 	"gemini-2.5-flash-preview-04-17-thinking":   0.075,
 	"gemini-2.5-flash-preview-04-17-nothinking": 0.075,
@@ -148,6 +149,8 @@ var defaultModelRatio = map[string]float64{
 	"gemini-2.5-flash-preview-05-20-nothinking": 0.075,
 	"gemini-2.5-flash-thinking-*":               0.075, // 用于为后续所有2.5 flash thinking budget 模型设置默认倍率
 	"gemini-2.5-pro-thinking-*":                 0.625, // 用于为后续所有2.5 pro thinking budget 模型设置默认倍率
+	"gemini-2.5-flash-lite-preview-06-17":       0.05,
+	"gemini-2.5-flash":                          0.15,
 	"text-embedding-004":                        0.001,
 	"chatglm_turbo":                             0.3572,     // ¥0.005 / 1k tokens
 	"chatglm_pro":                               0.7143,     // ¥0.01 / 1k tokens
@@ -423,7 +426,12 @@ func UpdateCompletionRatioByJSONString(jsonStr string) error {
 func GetCompletionRatio(name string) float64 {
 	CompletionRatioMutex.RLock()
 	defer CompletionRatioMutex.RUnlock()
-
+	if strings.HasPrefix(name, "gpt-4-gizmo") {
+		name = "gpt-4-gizmo-*"
+	}
+	if strings.HasPrefix(name, "gpt-4o-gizmo") {
+		name = "gpt-4o-gizmo-*"
+	}
 	if strings.Contains(name, "/") {
 		if ratio, ok := CompletionRatio[name]; ok {
 			return ratio
@@ -441,12 +449,6 @@ func GetCompletionRatio(name string) float64 {
 
 func getHardcodedCompletionModelRatio(name string) (float64, bool) {
 	lowercaseName := strings.ToLower(name)
-	if strings.HasPrefix(name, "gpt-4-gizmo") {
-		name = "gpt-4-gizmo-*"
-	}
-	if strings.HasPrefix(name, "gpt-4o-gizmo") {
-		name = "gpt-4o-gizmo-*"
-	}
 	if strings.HasPrefix(name, "gpt-4") && !strings.HasSuffix(name, "-all") && !strings.HasSuffix(name, "-gizmo-*") {
 		if strings.HasPrefix(name, "gpt-4o") {
 			if name == "gpt-4o-2024-05-13" {
@@ -500,12 +502,17 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) {
 			return 4, true
 		} else if strings.HasPrefix(name, "gemini-2.5-pro") { // 移除preview来增加兼容性,这里假设正式版的倍率和preview一致
 			return 8, true
-		} else if strings.HasPrefix(name, "gemini-2.5-flash") { // 同上
-			if strings.HasSuffix(name, "-nothinking") {
-				return 4, false
-			} else {
-				return 3.5 / 0.6, false
+		} else if strings.HasPrefix(name, "gemini-2.5-flash") { // 处理不同的flash模型倍率
+			if strings.HasPrefix(name, "gemini-2.5-flash-preview") {
+				if strings.HasSuffix(name, "-nothinking") {
+					return 4, true
+				}
+				return 3.5 / 0.15, true
+			}
+			if strings.HasPrefix(name, "gemini-2.5-flash-lite-preview") {
+				return 4, true
 			}
+			return 2.5 / 0.3, true
 		}
 		return 4, false
 	}

+ 80 - 12
web/src/components/layout/HeaderBar.js

@@ -28,6 +28,7 @@ import {
   Tag,
   Typography,
   Skeleton,
+  Badge,
 } from '@douyinfe/semi-ui';
 import { StatusContext } from '../../context/Status/index.js';
 import { useStyle, styleActions } from '../../context/Style/index.js';
@@ -43,6 +44,7 @@ const HeaderBar = () => {
   const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
   const location = useLocation();
   const [noticeVisible, setNoticeVisible] = useState(false);
+  const [unreadCount, setUnreadCount] = useState(0);
 
   const systemName = getSystemName();
   const logo = getLogo();
@@ -53,9 +55,44 @@ const HeaderBar = () => {
   const docsLink = statusState?.status?.docs_link || '';
   const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
 
+  const isConsoleRoute = location.pathname.startsWith('/console');
+
   const theme = useTheme();
   const setTheme = useSetTheme();
 
+  const announcements = statusState?.status?.announcements || [];
+
+  const getAnnouncementKey = (a) => `${a?.publishDate || ''}-${(a?.content || '').slice(0, 30)}`;
+
+  const calculateUnreadCount = () => {
+    if (!announcements.length) return 0;
+    let readKeys = [];
+    try {
+      readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || [];
+    } catch (_) {
+      readKeys = [];
+    }
+    const readSet = new Set(readKeys);
+    return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).length;
+  };
+
+  const getUnreadKeys = () => {
+    if (!announcements.length) return [];
+    let readKeys = [];
+    try {
+      readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || [];
+    } catch (_) {
+      readKeys = [];
+    }
+    const readSet = new Set(readKeys);
+    return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).map(getAnnouncementKey);
+  };
+
+  useEffect(() => {
+    setUnreadCount(calculateUnreadCount());
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [announcements]);
+
   const mainNavLinks = [
     {
       text: t('首页'),
@@ -106,6 +143,25 @@ const HeaderBar = () => {
     }, 3000);
   };
 
+  const handleNoticeOpen = () => {
+    setNoticeVisible(true);
+  };
+
+  const handleNoticeClose = () => {
+    setNoticeVisible(false);
+    if (announcements.length) {
+      let readKeys = [];
+      try {
+        readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || [];
+      } catch (_) {
+        readKeys = [];
+      }
+      const mergedKeys = Array.from(new Set([...readKeys, ...announcements.map(getAnnouncementKey)]));
+      localStorage.setItem('notice_read_keys', JSON.stringify(mergedKeys));
+    }
+    setUnreadCount(0);
+  };
+
   useEffect(() => {
     if (theme === 'dark') {
       document.body.setAttribute('theme-mode', 'dark');
@@ -353,15 +409,14 @@ const HeaderBar = () => {
     }
   };
 
-  // 检查当前路由是否以/console开头
-  const isConsoleRoute = location.pathname.startsWith('/console');
-
   return (
     <header className="text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg">
       <NoticeModal
         visible={noticeVisible}
-        onClose={() => setNoticeVisible(false)}
+        onClose={handleNoticeClose}
         isMobile={styleState.isMobile}
+        defaultTab={unreadCount > 0 ? 'system' : 'inApp'}
+        unreadKeys={getUnreadKeys()}
       />
       <div className="w-full px-2">
         <div className="flex items-center justify-between h-16">
@@ -462,14 +517,27 @@ const HeaderBar = () => {
               </Dropdown>
             )}
 
-            <Button
-              icon={<IconBell className="text-lg" />}
-              aria-label={t('系统公告')}
-              onClick={() => setNoticeVisible(true)}
-              theme="borderless"
-              type="tertiary"
-              className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
-            />
+            {unreadCount > 0 ? (
+              <Badge count={unreadCount} type="danger" overflowCount={99}>
+                <Button
+                  icon={<IconBell className="text-lg" />}
+                  aria-label={t('系统公告')}
+                  onClick={handleNoticeOpen}
+                  theme="borderless"
+                  type="tertiary"
+                  className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
+                />
+              </Badge>
+            ) : (
+              <Button
+                icon={<IconBell className="text-lg" />}
+                aria-label={t('系统公告')}
+                onClick={handleNoticeOpen}
+                theme="borderless"
+                type="tertiary"
+                className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
+              />
+            )}
 
             <Button
               icon={theme === 'dark' ? <IconSun size="large" className="text-yellow-500" /> : <IconMoon size="large" className="text-gray-300" />}

+ 96 - 8
web/src/components/layout/NoticeModal.js

@@ -1,14 +1,36 @@
-import React, { useEffect, useState } from 'react';
-import { Button, Modal, Empty } from '@douyinfe/semi-ui';
+import React, { useEffect, useState, useContext, useMemo } from 'react';
+import { Button, Modal, Empty, Tabs, TabPane, Timeline } from '@douyinfe/semi-ui';
 import { useTranslation } from 'react-i18next';
-import { API, showError } from '../../helpers';
+import { API, showError, getRelativeTime } from '../../helpers';
 import { marked } from 'marked';
 import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
+import { StatusContext } from '../../context/Status/index.js';
+import { Bell, Megaphone } from 'lucide-react';
 
-const NoticeModal = ({ visible, onClose, isMobile }) => {
+const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadKeys = [] }) => {
   const { t } = useTranslation();
   const [noticeContent, setNoticeContent] = useState('');
   const [loading, setLoading] = useState(false);
+  const [activeTab, setActiveTab] = useState(defaultTab);
+
+  const [statusState] = useContext(StatusContext);
+
+  const announcements = statusState?.status?.announcements || [];
+
+  const unreadSet = useMemo(() => new Set(unreadKeys), [unreadKeys]);
+
+  const getKeyForItem = (item) => `${item?.publishDate || ''}-${(item?.content || '').slice(0, 30)}`;
+
+  const processedAnnouncements = useMemo(() => {
+    return (announcements || []).slice(0, 20).map(item => ({
+      key: getKeyForItem(item),
+      type: item.type || 'default',
+      time: getRelativeTime(item.publishDate),
+      content: item.content,
+      extra: item.extra,
+      isUnread: unreadSet.has(getKeyForItem(item))
+    }));
+  }, [announcements, unreadSet]);
 
   const handleCloseTodayNotice = () => {
     const today = new Date().toDateString();
@@ -44,7 +66,13 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
     }
   }, [visible]);
 
-  const renderContent = () => {
+  useEffect(() => {
+    if (visible) {
+      setActiveTab(defaultTab);
+    }
+  }, [defaultTab, visible]);
+
+  const renderMarkdownNotice = () => {
     if (loading) {
       return <div className="py-12"><Empty description={t('加载中...')} /></div>;
     }
@@ -64,14 +92,74 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
     return (
       <div
         dangerouslySetInnerHTML={{ __html: noticeContent }}
-        className="notice-content-scroll max-h-[60vh] overflow-y-auto pr-2"
+        className="notice-content-scroll max-h-[55vh] overflow-y-auto pr-2"
       />
     );
   };
 
+  const renderAnnouncementTimeline = () => {
+    if (processedAnnouncements.length === 0) {
+      return (
+        <div className="py-12">
+          <Empty
+            image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
+            darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
+            description={t('暂无系统公告')}
+          />
+        </div>
+      );
+    }
+
+    return (
+      <div className="max-h-[55vh] overflow-y-auto pr-2 card-content-scroll">
+        <Timeline mode="alternate">
+          {processedAnnouncements.map((item, idx) => (
+            <Timeline.Item
+              key={idx}
+              type={item.type}
+              time={item.time}
+              className={item.isUnread ? '' : ''}
+            >
+              <div>
+                {item.isUnread ? (
+                  <span className="shine-text">
+                    {item.content}
+                  </span>
+                ) : (
+                  item.content
+                )}
+                {item.extra && <div className="text-xs text-gray-500">{item.extra}</div>}
+              </div>
+            </Timeline.Item>
+          ))}
+        </Timeline>
+      </div>
+    );
+  };
+
+  const renderBody = () => {
+    if (activeTab === 'inApp') {
+      return renderMarkdownNotice();
+    }
+    return renderAnnouncementTimeline();
+  };
+
   return (
     <Modal
-      title={t('系统公告')}
+      title={
+        <div className="flex items-center justify-between w-full">
+          <span>{t('系统公告')}</span>
+          <Tabs
+            activeKey={activeTab}
+            onChange={setActiveTab}
+            type='card'
+            size='small'
+          >
+            <TabPane tab={<span className="flex items-center gap-1"><Bell size={14} /> {t('通知')}</span>} itemKey='inApp' />
+            <TabPane tab={<span className="flex items-center gap-1"><Megaphone size={14} /> {t('系统公告')}</span>} itemKey='system' />
+          </Tabs>
+        </div>
+      }
       visible={visible}
       onCancel={onClose}
       footer={(
@@ -82,7 +170,7 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
       )}
       size={isMobile ? 'full-width' : 'large'}
     >
-      {renderContent()}
+      {renderBody()}
     </Modal>
   );
 };

+ 63 - 0
web/src/components/settings/ChatsSetting.js

@@ -0,0 +1,63 @@
+import React, { useEffect, useState } from 'react';
+import { Card, Spin } from '@douyinfe/semi-ui';
+import SettingsChats from '../../pages/Setting/Chat/SettingsChats.js';
+import { API, showError } from '../../helpers';
+
+const ChatsSetting = () => {
+  let [inputs, setInputs] = useState({
+    /* 聊天设置 */
+    Chats: '[]',
+  });
+
+  let [loading, setLoading] = useState(false);
+
+  const getOptions = async () => {
+    const res = await API.get('/api/option/');
+    const { success, message, data } = res.data;
+    if (success) {
+      let newInputs = {};
+      data.forEach((item) => {
+        if (
+          item.key.endsWith('Enabled') ||
+          ['DefaultCollapseSidebar'].includes(item.key)
+        ) {
+          newInputs[item.key] = item.value === 'true' ? true : false;
+        } else {
+          newInputs[item.key] = item.value;
+        }
+      });
+
+      setInputs(newInputs);
+    } else {
+      showError(message);
+    }
+  };
+
+  async function onRefresh() {
+    try {
+      setLoading(true);
+      await getOptions();
+    } catch (error) {
+      showError('刷新失败');
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  useEffect(() => {
+    onRefresh();
+  }, []);
+
+  return (
+    <>
+      <Spin spinning={loading} size='large'>
+        {/* 聊天设置 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsChats options={inputs} refresh={onRefresh} />
+        </Card>
+      </Spin>
+    </>
+  );
+};
+
+export default ChatsSetting; 

+ 17 - 2
web/src/components/settings/DashboardSetting.js

@@ -5,6 +5,7 @@ import SettingsAPIInfo from '../../pages/Setting/Dashboard/SettingsAPIInfo.js';
 import SettingsAnnouncements from '../../pages/Setting/Dashboard/SettingsAnnouncements.js';
 import SettingsFAQ from '../../pages/Setting/Dashboard/SettingsFAQ.js';
 import SettingsUptimeKuma from '../../pages/Setting/Dashboard/SettingsUptimeKuma.js';
+import SettingsDataDashboard from '../../pages/Setting/Dashboard/SettingsDataDashboard.js';
 
 const DashboardSetting = () => {
   let [inputs, setInputs] = useState({
@@ -23,6 +24,11 @@ const DashboardSetting = () => {
     FAQ: '',
     UptimeKumaUrl: '',
     UptimeKumaSlug: '',
+
+    /* 数据看板 */
+    DataExportEnabled: false,
+    DataExportDefaultTime: 'hour',
+    DataExportInterval: 5,
   });
 
   let [loading, setLoading] = useState(false);
@@ -37,6 +43,10 @@ const DashboardSetting = () => {
         if (item.key in inputs) {
           newInputs[item.key] = item.value;
         }
+        if (item.key.endsWith('Enabled') &&
+          (item.key === 'DataExportEnabled')) {
+          newInputs[item.key] = item.value === 'true' ? true : false;
+        }
       });
       setInputs(newInputs);
     } else {
@@ -106,9 +116,9 @@ const DashboardSetting = () => {
           </p>
         </Modal>
 
-        {/* API信息管理 */}
+        {/* 数据看板设置 */}
         <Card style={{ marginTop: '10px' }}>
-          <SettingsAPIInfo options={inputs} refresh={onRefresh} />
+          <SettingsDataDashboard options={inputs} refresh={onRefresh} />
         </Card>
 
         {/* 系统公告管理 */}
@@ -116,6 +126,11 @@ const DashboardSetting = () => {
           <SettingsAnnouncements options={inputs} refresh={onRefresh} />
         </Card>
 
+        {/* API信息管理 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsAPIInfo options={inputs} refresh={onRefresh} />
+        </Card>
+
         {/* 常见问答管理 */}
         <Card style={{ marginTop: '10px' }}>
           <SettingsFAQ options={inputs} refresh={onRefresh} />

+ 65 - 0
web/src/components/settings/DrawingSetting.js

@@ -0,0 +1,65 @@
+import React, { useEffect, useState } from 'react';
+import { Card, Spin } from '@douyinfe/semi-ui';
+import SettingsDrawing from '../../pages/Setting/Drawing/SettingsDrawing.js';
+import { API, showError } from '../../helpers';
+
+const DrawingSetting = () => {
+  let [inputs, setInputs] = useState({
+    /* 绘图设置 */
+    DrawingEnabled: false,
+    MjNotifyEnabled: false,
+    MjAccountFilterEnabled: false,
+    MjForwardUrlEnabled: false,
+    MjModeClearEnabled: false,
+    MjActionCheckSuccessEnabled: false,
+  });
+
+  let [loading, setLoading] = useState(false);
+
+  const getOptions = async () => {
+    const res = await API.get('/api/option/');
+    const { success, message, data } = res.data;
+    if (success) {
+      let newInputs = {};
+      data.forEach((item) => {
+        if (item.key.endsWith('Enabled')) {
+          newInputs[item.key] = item.value === 'true' ? true : false;
+        } else {
+          newInputs[item.key] = item.value;
+        }
+      });
+
+      setInputs(newInputs);
+    } else {
+      showError(message);
+    }
+  };
+
+  async function onRefresh() {
+    try {
+      setLoading(true);
+      await getOptions();
+    } catch (error) {
+      showError('刷新失败');
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  useEffect(() => {
+    onRefresh();
+  }, []);
+
+  return (
+    <>
+      <Spin spinning={loading} size='large'>
+        {/* 绘图设置 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsDrawing options={inputs} refresh={onRefresh} />
+        </Card>
+      </Spin>
+    </>
+  );
+};
+
+export default DrawingSetting; 

+ 0 - 31
web/src/components/settings/OperationSetting.js

@@ -1,13 +1,10 @@
 import React, { useEffect, useState } from 'react';
 import { Card, Spin } from '@douyinfe/semi-ui';
 import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral.js';
-import SettingsDrawing from '../../pages/Setting/Operation/SettingsDrawing.js';
 import SettingsSensitiveWords from '../../pages/Setting/Operation/SettingsSensitiveWords.js';
 import SettingsLog from '../../pages/Setting/Operation/SettingsLog.js';
-import SettingsDataDashboard from '../../pages/Setting/Operation/SettingsDataDashboard.js';
 import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring.js';
 import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit.js';
-import SettingsChats from '../../pages/Setting/Operation/SettingsChats.js';
 import { API, showError } from '../../helpers';
 
 const OperationSetting = () => {
@@ -29,14 +26,6 @@ const OperationSetting = () => {
     DemoSiteEnabled: false,
     SelfUseModeEnabled: false,
 
-    /* 绘图设置 */
-    DrawingEnabled: false,
-    MjNotifyEnabled: false,
-    MjAccountFilterEnabled: false,
-    MjForwardUrlEnabled: false,
-    MjModeClearEnabled: false,
-    MjActionCheckSuccessEnabled: false,
-
     /* 敏感词设置 */
     CheckSensitiveEnabled: false,
     CheckSensitiveOnPromptEnabled: false,
@@ -45,20 +34,12 @@ const OperationSetting = () => {
     /* 日志设置 */
     LogConsumeEnabled: false,
 
-    /* 数据看板 */
-    DataExportEnabled: false,
-    DataExportDefaultTime: 'hour',
-    DataExportInterval: 5,
-
     /* 监控设置 */
     ChannelDisableThreshold: 0,
     QuotaRemindThreshold: 0,
     AutomaticDisableChannelEnabled: false,
     AutomaticEnableChannelEnabled: false,
     AutomaticDisableKeywords: '',
-
-    /* 聊天设置 */
-    Chats: '[]',
   });
 
   let [loading, setLoading] = useState(false);
@@ -107,10 +88,6 @@ const OperationSetting = () => {
         <Card style={{ marginTop: '10px' }}>
           <SettingsGeneral options={inputs} refresh={onRefresh} />
         </Card>
-        {/* 绘图设置 */}
-        <Card style={{ marginTop: '10px' }}>
-          <SettingsDrawing options={inputs} refresh={onRefresh} />
-        </Card>
         {/* 屏蔽词过滤设置 */}
         <Card style={{ marginTop: '10px' }}>
           <SettingsSensitiveWords options={inputs} refresh={onRefresh} />
@@ -119,10 +96,6 @@ const OperationSetting = () => {
         <Card style={{ marginTop: '10px' }}>
           <SettingsLog options={inputs} refresh={onRefresh} />
         </Card>
-        {/* 数据看板 */}
-        <Card style={{ marginTop: '10px' }}>
-          <SettingsDataDashboard options={inputs} refresh={onRefresh} />
-        </Card>
         {/* 监控设置 */}
         <Card style={{ marginTop: '10px' }}>
           <SettingsMonitoring options={inputs} refresh={onRefresh} />
@@ -131,10 +104,6 @@ const OperationSetting = () => {
         <Card style={{ marginTop: '10px' }}>
           <SettingsCreditLimit options={inputs} refresh={onRefresh} />
         </Card>
-        {/* 聊天设置 */}
-        <Card style={{ marginTop: '10px' }}>
-          <SettingsChats options={inputs} refresh={onRefresh} />
-        </Card>
       </Spin>
     </>
   );

+ 88 - 0
web/src/components/settings/PaymentSetting.js

@@ -0,0 +1,88 @@
+import React, { useEffect, useState } from 'react';
+import { Card, Spin } from '@douyinfe/semi-ui';
+import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralPayment.js';
+import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway.js';
+import { API, showError } from '../../helpers';
+import { useTranslation } from 'react-i18next';
+
+const PaymentSetting = () => {
+  const { t } = useTranslation();
+  let [inputs, setInputs] = useState({
+    ServerAddress: '',
+    PayAddress: '',
+    EpayId: '',
+    EpayKey: '',
+    Price: 7.3,
+    MinTopUp: 1,
+    TopupGroupRatio: '',
+    CustomCallbackAddress: '',
+    PayMethods: '',
+  });
+
+  let [loading, setLoading] = useState(false);
+
+  const getOptions = async () => {
+    const res = await API.get('/api/option/');
+    const { success, message, data } = res.data;
+    if (success) {
+      let newInputs = {};
+      data.forEach((item) => {
+        switch (item.key) {
+          case 'TopupGroupRatio':
+            try {
+              newInputs[item.key] = JSON.stringify(JSON.parse(item.value), null, 2);
+            } catch (error) {
+              console.error('解析TopupGroupRatio出错:', error);
+              newInputs[item.key] = item.value;
+            }
+            break;
+          case 'Price':
+          case 'MinTopUp':
+            newInputs[item.key] = parseFloat(item.value);
+            break;
+          default:
+            if (item.key.endsWith('Enabled')) {
+              newInputs[item.key] = item.value === 'true' ? true : false;
+            } else {
+              newInputs[item.key] = item.value;
+            }
+            break;
+        }
+      });
+
+      setInputs(newInputs);
+    } else {
+      showError(t(message));
+    }
+  };
+
+  async function onRefresh() {
+    try {
+      setLoading(true);
+      await getOptions();
+    } catch (error) {
+      showError(t('刷新失败'));
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  useEffect(() => {
+    onRefresh();
+  }, []);
+
+  return (
+    <>
+      <Spin spinning={loading} size='large'>
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsGeneralPayment options={inputs} refresh={onRefresh} />
+        </Card>
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsPaymentGateway options={inputs} refresh={onRefresh} />
+        </Card>
+      </Spin>
+    </>
+  );
+};
+
+export default PaymentSetting; 

+ 9 - 10
web/src/components/settings/RateLimitSetting.js

@@ -1,8 +1,7 @@
 import React, { useEffect, useState } from 'react';
-import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
+import { Card, Spin } from '@douyinfe/semi-ui';
 
-import { API, showError, showSuccess } from '../../helpers/index.js';
-import SettingsChats from '../../pages/Setting/Operation/SettingsChats.js';
+import { API, showError } from '../../helpers/index.js';
 import { useTranslation } from 'react-i18next';
 import RequestRateLimit from '../../pages/Setting/RateLimit/SettingsRequestRateLimit.js';
 
@@ -24,14 +23,14 @@ const RateLimitSetting = () => {
     if (success) {
       let newInputs = {};
       data.forEach((item) => {
-      if (item.key === 'ModelRequestRateLimitGroup') {
-        item.value = JSON.stringify(JSON.parse(item.value), null, 2);
-      }
+        if (item.key === 'ModelRequestRateLimitGroup') {
+          item.value = JSON.stringify(JSON.parse(item.value), null, 2);
+        }
 
-      if (item.key.endsWith('Enabled')) {
-        newInputs[item.key] = item.value === 'true' ? true : false;
-      } else {
-        newInputs[item.key] = item.value;
+        if (item.key.endsWith('Enabled')) {
+          newInputs[item.key] = item.value === 'true' ? true : false;
+        } else {
+          newInputs[item.key] = item.value;
         }
       });
 

+ 1 - 1
web/src/components/settings/RatioSetting.js

@@ -82,7 +82,7 @@ const RatioSetting = () => {
     <Spin spinning={loading} size='large'>
       {/* 模型倍率设置以及可视化编辑器 */}
       <Card style={{ marginTop: '10px' }}>
-        <Tabs type='line'>
+        <Tabs type='card'>
           <Tabs.TabPane tab={t('模型倍率设置')} itemKey='model'>
             <ModelRatioSettings options={inputs} refresh={onRefresh} />
           </Tabs.TabPane>

+ 0 - 150
web/src/components/settings/SystemSetting.js

@@ -17,7 +17,6 @@ import {
   removeTrailingSlash,
   showError,
   showSuccess,
-  verifyJSON,
 } from '../../helpers';
 import axios from 'axios';
 
@@ -42,17 +41,9 @@ const SystemSetting = () => {
     SMTPAccount: '',
     SMTPFrom: '',
     SMTPToken: '',
-    ServerAddress: '',
     WorkerUrl: '',
     WorkerValidKey: '',
     WorkerAllowHttpImageRequestEnabled: '',
-    EpayId: '',
-    EpayKey: '',
-    Price: 7.3,
-    MinTopUp: 1,
-    TopupGroupRatio: '',
-    PayAddress: '',
-    CustomCallbackAddress: '',
     Footer: '',
     WeChatAuthEnabled: '',
     WeChatServerAddress: '',
@@ -73,7 +64,6 @@ const SystemSetting = () => {
     LinuxDOOAuthEnabled: '',
     LinuxDOClientId: '',
     LinuxDOClientSecret: '',
-    PayMethods: '',
   });
 
   const [originInputs, setOriginInputs] = useState({});
@@ -200,11 +190,6 @@ const SystemSetting = () => {
     setInputs(values);
   };
 
-  const submitServerAddress = async () => {
-    let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
-    await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]);
-  };
-
   const submitWorker = async () => {
     let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
     const options = [
@@ -220,56 +205,6 @@ const SystemSetting = () => {
     await updateOptions(options);
   };
 
-  const submitPayAddress = async () => {
-    if (inputs.ServerAddress === '') {
-      showError('请先填写服务器地址');
-      return;
-    }
-    if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
-      if (!verifyJSON(inputs.TopupGroupRatio)) {
-        showError('充值分组倍率不是合法的 JSON 字符串');
-        return;
-      }
-    }
-    if (originInputs['PayMethods'] !== inputs.PayMethods) {
-      if (!verifyJSON(inputs.PayMethods)) {
-        showError('充值方式设置不是合法的 JSON 字符串');
-        return;
-      }
-    }
-
-    const options = [
-      { key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) },
-    ];
-
-    if (inputs.EpayId !== '') {
-      options.push({ key: 'EpayId', value: inputs.EpayId });
-    }
-    if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') {
-      options.push({ key: 'EpayKey', value: inputs.EpayKey });
-    }
-    if (inputs.Price !== '') {
-      options.push({ key: 'Price', value: inputs.Price.toString() });
-    }
-    if (inputs.MinTopUp !== '') {
-      options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() });
-    }
-    if (inputs.CustomCallbackAddress !== '') {
-      options.push({
-        key: 'CustomCallbackAddress',
-        value: inputs.CustomCallbackAddress,
-      });
-    }
-    if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
-      options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio });
-    }
-    if (originInputs['PayMethods'] !== inputs.PayMethods) {
-      options.push({ key: 'PayMethods', value: inputs.PayMethods });
-    }
-
-    await updateOptions(options);
-  };
-
   const submitSMTP = async () => {
     const options = [];
 
@@ -551,17 +486,6 @@ const SystemSetting = () => {
                 marginTop: '10px',
               }}
             >
-              <Card>
-                <Form.Section text='通用设置'>
-                  <Form.Input
-                    field='ServerAddress'
-                    label='服务器地址'
-                    placeholder='例如:https://yourdomain.com'
-                    style={{ width: '100%' }}
-                  />
-                  <Button onClick={submitServerAddress}>更新服务器地址</Button>
-                </Form.Section>
-              </Card>
               <Card>
                 <Form.Section text='代理设置'>
                   <Text>
@@ -604,80 +528,6 @@ const SystemSetting = () => {
                 </Form.Section>
               </Card>
 
-              <Card>
-                <Form.Section text='支付设置'>
-                  <Text>
-                    (当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
-                  </Text>
-                  <Row
-                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
-                  >
-                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.Input
-                        field='PayAddress'
-                        label='支付地址'
-                        placeholder='例如:https://yourdomain.com'
-                      />
-                    </Col>
-                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.Input
-                        field='EpayId'
-                        label='易支付商户ID'
-                        placeholder='例如:0001'
-                      />
-                    </Col>
-                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.Input
-                        field='EpayKey'
-                        label='易支付商户密钥'
-                        placeholder='敏感信息不会发送到前端显示'
-                        type='password'
-                      />
-                    </Col>
-                  </Row>
-                  <Row
-                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
-                    style={{ marginTop: 16 }}
-                  >
-                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.Input
-                        field='CustomCallbackAddress'
-                        label='回调地址'
-                        placeholder='例如:https://yourdomain.com'
-                      />
-                    </Col>
-                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.InputNumber
-                        field='Price'
-                        precision={2}
-                        label='充值价格(x元/美金)'
-                        placeholder='例如:7,就是7元/美金'
-                      />
-                    </Col>
-                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.InputNumber
-                        field='MinTopUp'
-                        label='最低充值美元数量'
-                        placeholder='例如:2,就是最低充值2$'
-                      />
-                    </Col>
-                  </Row>
-                  <Form.TextArea
-                    field='TopupGroupRatio'
-                    label='充值分组倍率'
-                    placeholder='为一个 JSON 文本,键为组名称,值为倍率'
-                    autosize
-                  />
-                  <Form.TextArea
-                    field='PayMethods'
-                    label='充值方式设置'
-                    placeholder='为一个 JSON 文本'
-                    autosize
-                  />
-                  <Button onClick={submitPayAddress}>更新支付设置</Button>
-                </Form.Section>
-              </Card>
-
               <Card>
                 <Form.Section text='配置登录注册'>
                   <Row

+ 16 - 3
web/src/i18n/locales/en.json

@@ -1206,7 +1206,7 @@
   "默认折叠侧边栏": "Default collapse sidebar",
   "聊天链接功能已经弃用,请使用下方聊天设置功能": "Chat link function has been deprecated, please use the chat settings below",
   "你似乎并没有修改什么": "You seem to have not modified anything",
-  "令牌聊天设置": "Chat settings",
+  "聊天设置": "Chat settings",
   "必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能": "Must set all chat links above to empty to use the chat settings below",
   "链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "The {key} in the link will be automatically replaced with sk-xxxx, the {address} will be automatically replaced with the server address in system settings, and the end will not have / and /v1",
   "聊天配置": "Chat configuration",
@@ -1672,7 +1672,7 @@
   "获取倍率失败:": "Failed to get ratios: ",
   "后端请求失败": "Backend request failed",
   "部分渠道测试失败:": "Some channels failed to test: ",
-  "已与上游倍率完全一致,无需同步": "The upstream ratio is completely consistent, no synchronization is required",
+  "未找到差异化倍率,无需同步": "No differential ratio found, no synchronization is required",
   "请求后端接口失败:": "Failed to request the backend interface: ",
   "同步成功": "Synchronization successful",
   "部分保存失败": "Some settings failed to save",
@@ -1688,5 +1688,18 @@
   "暂无差异化倍率显示": "No differential ratio display",
   "请先选择同步渠道": "Please select the synchronization channel first",
   "与本地相同": "Same as local",
-  "未找到匹配的模型": "No matching model found"
+  "未找到匹配的模型": "No matching model found",
+  "暴露倍率接口": "Expose ratio API",
+  "支付设置": "Payment Settings",
+  "(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Currently only supports Epay interface, the default callback address is the server address above!)",
+  "支付地址": "Payment address",
+  "易支付商户ID": "Epay merchant ID",
+  "易支付商户密钥": "Epay merchant key",
+  "回调地址": "Callback address",
+  "充值价格(x元/美金)": "Recharge price (x yuan/dollar)",
+  "最低充值美元数量": "Minimum recharge dollar amount",
+  "充值分组倍率": "Recharge group ratio",
+  "充值方式设置": "Recharge method settings",
+  "更新支付设置": "Update payment settings",
+  "通知": "Notice"
 }

+ 28 - 0
web/src/index.css

@@ -500,4 +500,32 @@ code {
 
 .components-transfer-selected-item .semi-icon-close:hover {
   color: var(--semi-color-text-0);
+}
+
+/* ==================== 未读通知闪光效果 ==================== */
+@keyframes sweep-shine {
+  0% {
+    background-position: 200% 0;
+  }
+
+  100% {
+    background-position: -200% 0;
+  }
+}
+
+.shine-text {
+  background: linear-gradient(90deg, currentColor 0%, currentColor 40%, rgba(255, 255, 255, 0.9) 50%, currentColor 60%, currentColor 100%);
+  background-size: 200% 100%;
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+  animation: sweep-shine 4s linear infinite;
+}
+
+.dark .shine-text {
+  background: linear-gradient(90deg, currentColor 0%, currentColor 40%, #facc15 50%, currentColor 60%, currentColor 100%);
+  background-size: 200% 100%;
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
 }

+ 1 - 26
web/src/pages/Setting/Operation/SettingsChats.js → web/src/pages/Setting/Chat/SettingsChats.js

@@ -2,10 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
 import {
   Banner,
   Button,
-  Col,
   Form,
-  Popconfirm,
-  Row,
   Space,
   Spin,
 } from '@douyinfe/semi-ui';
@@ -16,7 +13,6 @@ import {
   showSuccess,
   showWarning,
   verifyJSON,
-  verifyJSONPromise,
 } from '../../../helpers';
 import { useTranslation } from 'react-i18next';
 
@@ -80,21 +76,6 @@ export default function SettingsChats(props) {
     }
   }
 
-  async function resetModelRatio() {
-    try {
-      let res = await API.post(`/api/option/rest_model_ratio`);
-      // return {success, message}
-      if (res.data.success) {
-        showSuccess(res.data.message);
-        props.refresh();
-      } else {
-        showError(res.data.message);
-      }
-    } catch (error) {
-      showError(error);
-    }
-  }
-
   useEffect(() => {
     const currentInputs = {};
     for (let key in props.options) {
@@ -119,13 +100,7 @@ export default function SettingsChats(props) {
         getFormApi={(formAPI) => (refForm.current = formAPI)}
         style={{ marginBottom: 15 }}
       >
-        <Form.Section text={t('令牌聊天设置')}>
-          <Banner
-            type='warning'
-            description={t(
-              '必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能',
-            )}
-          />
+        <Form.Section text={t('聊天设置')}>
           <Banner
             type='info'
             description={t(

+ 8 - 2
web/src/pages/Setting/Dashboard/SettingsAnnouncements.js

@@ -388,11 +388,17 @@ const SettingsAnnouncements = ({ options, refresh }) => {
     </div>
   );
 
-  // 计算当前页显示的数据
+  // 计算当前页显示的数据(按发布时间倒序排序,最新优先显示)
   const getCurrentPageData = () => {
+    const sortedList = [...announcementsList].sort((a, b) => {
+      const dateA = new Date(a.publishDate).getTime();
+      const dateB = new Date(b.publishDate).getTime();
+      return dateB - dateA; // 倒序,最新的排在前面
+    });
+
     const startIndex = (currentPage - 1) * pageSize;
     const endIndex = startIndex + pageSize;
-    return announcementsList.slice(startIndex, endIndex);
+    return sortedList.slice(startIndex, endIndex);
   };
 
   const rowSelection = {

+ 0 - 0
web/src/pages/Setting/Operation/SettingsDataDashboard.js → web/src/pages/Setting/Dashboard/SettingsDataDashboard.js


+ 0 - 0
web/src/pages/Setting/Operation/SettingsDrawing.js → web/src/pages/Setting/Drawing/SettingsDrawing.js


+ 2 - 2
web/src/pages/Setting/Model/SettingGeminiModel.js

@@ -209,8 +209,8 @@ export default function SettingGeminiModel(props) {
                   label={t('思考预算占比')}
                   field={'gemini.thinking_adapter_budget_tokens_percentage'}
                   initValue={''}
-                  extraText={t('0.1-1之间的小数')}
-                  min={0.1}
+                  extraText={t('0.002-1之间的小数')} 
+                  min={0.002}
                   max={1}
                   onChange={(value) =>
                     setInputs({

+ 0 - 5
web/src/pages/Setting/Operation/SettingsGeneral.js

@@ -6,7 +6,6 @@ import {
   Form,
   Row,
   Spin,
-  Collapse,
   Modal,
 } from '@douyinfe/semi-ui';
 import {
@@ -92,10 +91,6 @@ export default function GeneralSettings(props) {
   return (
     <>
       <Spin spinning={loading}>
-        <Banner
-          type='warning'
-          description={t('聊天链接功能已经弃用,请使用下方聊天设置功能')}
-        />
         <Form
           values={inputs}
           getFormApi={(formAPI) => (refForm.current = formAPI)}

+ 74 - 0
web/src/pages/Setting/Payment/SettingsGeneralPayment.js

@@ -0,0 +1,74 @@
+import React, { useEffect, useState, useRef } from 'react';
+import {
+  Button,
+  Form,
+  Spin,
+} from '@douyinfe/semi-ui';
+import {
+  API,
+  removeTrailingSlash,
+  showError,
+  showSuccess,
+} from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+export default function SettingsGeneralPayment(props) {
+  const { t } = useTranslation();
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    ServerAddress: '',
+  });
+  const formApiRef = useRef(null);
+
+  useEffect(() => {
+    if (props.options && formApiRef.current) {
+      const currentInputs = { ServerAddress: props.options.ServerAddress || '' };
+      setInputs(currentInputs);
+      formApiRef.current.setValues(currentInputs);
+    }
+  }, [props.options]);
+
+  const handleFormChange = (values) => {
+    setInputs(values);
+  };
+
+  const submitServerAddress = async () => {
+    setLoading(true);
+    try {
+      let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
+      const res = await API.put('/api/option/', {
+        key: 'ServerAddress',
+        value: ServerAddress,
+      });
+      if (res.data.success) {
+        showSuccess(t('更新成功'));
+        props.refresh && props.refresh();
+      } else {
+        showError(res.data.message);
+      }
+    } catch (error) {
+      showError(t('更新失败'));
+    }
+    setLoading(false);
+  };
+
+  return (
+    <Spin spinning={loading}>
+      <Form
+        initValues={inputs}
+        onValueChange={handleFormChange}
+        getFormApi={(api) => (formApiRef.current = api)}
+      >
+        <Form.Section text={t('通用设置')}>
+          <Form.Input
+            field='ServerAddress'
+            label={t('服务器地址')}
+            placeholder={'https://yourdomain.com'}
+            style={{ width: '100%' }}
+          />
+          <Button onClick={submitServerAddress}>{t('更新服务器地址')}</Button>
+        </Form.Section>
+      </Form>
+    </Spin>
+  );
+} 

+ 218 - 0
web/src/pages/Setting/Payment/SettingsPaymentGateway.js

@@ -0,0 +1,218 @@
+import React, { useEffect, useState, useRef } from 'react';
+import {
+  Button,
+  Form,
+  Row,
+  Col,
+  Typography,
+  Spin,
+} from '@douyinfe/semi-ui';
+const { Text } = Typography;
+import {
+  API,
+  removeTrailingSlash,
+  showError,
+  showSuccess,
+  verifyJSON,
+} from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+export default function SettingsPaymentGateway(props) {
+  const { t } = useTranslation();
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    PayAddress: '',
+    EpayId: '',
+    EpayKey: '',
+    Price: 7.3,
+    MinTopUp: 1,
+    TopupGroupRatio: '',
+    CustomCallbackAddress: '',
+    PayMethods: '',
+  });
+  const [originInputs, setOriginInputs] = useState({});
+  const formApiRef = useRef(null);
+
+  useEffect(() => {
+    if (props.options && formApiRef.current) {
+      const currentInputs = {
+        PayAddress: props.options.PayAddress || '',
+        EpayId: props.options.EpayId || '',
+        EpayKey: props.options.EpayKey || '',
+        Price: props.options.Price !== undefined ? parseFloat(props.options.Price) : 7.3,
+        MinTopUp: props.options.MinTopUp !== undefined ? parseFloat(props.options.MinTopUp) : 1,
+        TopupGroupRatio: props.options.TopupGroupRatio || '',
+        CustomCallbackAddress: props.options.CustomCallbackAddress || '',
+        PayMethods: props.options.PayMethods || '',
+      };
+      setInputs(currentInputs);
+      setOriginInputs({ ...currentInputs });
+      formApiRef.current.setValues(currentInputs);
+    }
+  }, [props.options]);
+
+  const handleFormChange = (values) => {
+    setInputs(values);
+  };
+
+  const submitPayAddress = async () => {
+    if (props.options.ServerAddress === '') {
+      showError(t('请先填写服务器地址'));
+      return;
+    }
+
+    if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
+      if (!verifyJSON(inputs.TopupGroupRatio)) {
+        showError(t('充值分组倍率不是合法的 JSON 字符串'));
+        return;
+      }
+    }
+
+    if (originInputs['PayMethods'] !== inputs.PayMethods) {
+      if (!verifyJSON(inputs.PayMethods)) {
+        showError(t('充值方式设置不是合法的 JSON 字符串'));
+        return;
+      }
+    }
+
+    setLoading(true);
+    try {
+      const options = [
+        { key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) },
+      ];
+
+      if (inputs.EpayId !== '') {
+        options.push({ key: 'EpayId', value: inputs.EpayId });
+      }
+      if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') {
+        options.push({ key: 'EpayKey', value: inputs.EpayKey });
+      }
+      if (inputs.Price !== '') {
+        options.push({ key: 'Price', value: inputs.Price.toString() });
+      }
+      if (inputs.MinTopUp !== '') {
+        options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() });
+      }
+      if (inputs.CustomCallbackAddress !== '') {
+        options.push({
+          key: 'CustomCallbackAddress',
+          value: inputs.CustomCallbackAddress,
+        });
+      }
+      if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
+        options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio });
+      }
+      if (originInputs['PayMethods'] !== inputs.PayMethods) {
+        options.push({ key: 'PayMethods', value: inputs.PayMethods });
+      }
+
+      // 发送请求
+      const requestQueue = options.map(opt =>
+        API.put('/api/option/', {
+          key: opt.key,
+          value: opt.value,
+        })
+      );
+
+      const results = await Promise.all(requestQueue);
+
+      // 检查所有请求是否成功
+      const errorResults = results.filter(res => !res.data.success);
+      if (errorResults.length > 0) {
+        errorResults.forEach(res => {
+          showError(res.data.message);
+        });
+      } else {
+        showSuccess(t('更新成功'));
+        // 更新本地存储的原始值
+        setOriginInputs({ ...inputs });
+        props.refresh && props.refresh();
+      }
+    } catch (error) {
+      showError(t('更新失败'));
+    }
+    setLoading(false);
+  };
+
+  return (
+    <Spin spinning={loading}>
+      <Form
+        initValues={inputs}
+        onValueChange={handleFormChange}
+        getFormApi={(api) => (formApiRef.current = api)}
+      >
+        <Form.Section text={t('支付设置')}>
+          <Text>
+            {t('(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)')}
+          </Text>
+          <Row
+            gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+          >
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.Input
+                field='PayAddress'
+                label={t('支付地址')}
+                placeholder={t('例如:https://yourdomain.com')}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.Input
+                field='EpayId'
+                label={t('易支付商户ID')}
+                placeholder={t('例如:0001')}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.Input
+                field='EpayKey'
+                label={t('易支付商户密钥')}
+                placeholder={t('敏感信息不会发送到前端显示')}
+                type='password'
+              />
+            </Col>
+          </Row>
+          <Row
+            gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+            style={{ marginTop: 16 }}
+          >
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.Input
+                field='CustomCallbackAddress'
+                label={t('回调地址')}
+                placeholder={t('例如:https://yourdomain.com')}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.InputNumber
+                field='Price'
+                precision={2}
+                label={t('充值价格(x元/美金)')}
+                placeholder={t('例如:7,就是7元/美金')}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.InputNumber
+                field='MinTopUp'
+                label={t('最低充值美元数量')}
+                placeholder={t('例如:2,就是最低充值2$')}
+              />
+            </Col>
+          </Row>
+          <Form.TextArea
+            field='TopupGroupRatio'
+            label={t('充值分组倍率')}
+            placeholder={t('为一个 JSON 文本,键为组名称,值为倍率')}
+            autosize
+          />
+          <Form.TextArea
+            field='PayMethods'
+            label={t('充值方式设置')}
+            placeholder={t('为一个 JSON 文本')}
+            autosize
+          />
+          <Button onClick={submitPayAddress}>{t('更新支付设置')}</Button>
+        </Form.Section>
+      </Form>
+    </Spin>
+  );
+} 

+ 1 - 1
web/src/pages/Setting/Ratio/ModelRationNotSetEditor.js

@@ -372,7 +372,7 @@ export default function ModelRatioNotSetEditor(props) {
   return (
     <>
       <Space vertical align='start' style={{ width: '100%' }}>
-        <Space>
+        <Space className='mt-2'>
           <Button icon={<IconPlus />} onClick={() => setVisible(true)}>
             {t('添加模型')}
           </Button>

+ 1 - 1
web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js

@@ -404,7 +404,7 @@ export default function ModelSettingsVisualEditor(props) {
   return (
     <>
       <Space vertical align='start' style={{ width: '100%' }}>
-        <Space>
+        <Space className='mt-2'>
           <Button
             icon={<IconPlus />}
             onClick={() => {

+ 1 - 1
web/src/pages/Setting/Ratio/UpstreamRatioSync.js

@@ -125,7 +125,7 @@ export default function UpstreamRatioSync(props) {
       setHasSynced(true);
 
       if (Object.keys(differences).length === 0) {
-        showSuccess(t('已与上游倍率完全一致,无需同步'));
+        showSuccess(t('未找到差异化倍率,无需同步'));
       }
     } catch (e) {
       showError(t('请求后端接口失败:') + e.message);

+ 93 - 12
web/src/pages/Setting/index.js

@@ -2,6 +2,18 @@ import React, { useEffect, useState } from 'react';
 import { Layout, TabPane, Tabs } from '@douyinfe/semi-ui';
 import { useNavigate, useLocation } from 'react-router-dom';
 import { useTranslation } from 'react-i18next';
+import {
+  Settings,
+  Calculator,
+  Gauge,
+  Shapes,
+  Cog,
+  MoreHorizontal,
+  LayoutDashboard,
+  MessageSquare,
+  Palette,
+  CreditCard
+} from 'lucide-react';
 
 import SystemSetting from '../../components/settings/SystemSetting.js';
 import { isRoot } from '../../helpers';
@@ -11,6 +23,9 @@ import RateLimitSetting from '../../components/settings/RateLimitSetting.js';
 import ModelSetting from '../../components/settings/ModelSetting.js';
 import DashboardSetting from '../../components/settings/DashboardSetting.js';
 import RatioSetting from '../../components/settings/RatioSetting.js';
+import ChatsSetting from '../../components/settings/ChatsSetting.js';
+import DrawingSetting from '../../components/settings/DrawingSetting.js';
+import PaymentSetting from '../../components/settings/PaymentSetting.js';
 
 const Setting = () => {
   const { t } = useTranslation();
@@ -21,40 +36,105 @@ const Setting = () => {
 
   if (isRoot()) {
     panes.push({
-      tab: t('运营设置'),
+      tab: (
+        <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
+          <Settings size={18} />
+          {t('运营设置')}
+        </span>
+      ),
       content: <OperationSetting />,
       itemKey: 'operation',
     });
     panes.push({
-      tab: t('倍率设置'),
+      tab: (
+        <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
+          <MessageSquare size={18} />
+          {t('聊天设置')}
+        </span>
+      ),
+      content: <ChatsSetting />,
+      itemKey: 'chats',
+    });
+    panes.push({
+      tab: (
+        <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
+          <Palette size={18} />
+          {t('绘图设置')}
+        </span>
+      ),
+      content: <DrawingSetting />,
+      itemKey: 'drawing',
+    });
+    panes.push({
+      tab: (
+        <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
+          <CreditCard size={18} />
+          {t('支付设置')}
+        </span>
+      ),
+      content: <PaymentSetting />,
+      itemKey: 'payment',
+    });
+    panes.push({
+      tab: (
+        <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
+          <Calculator size={18} />
+          {t('倍率设置')}
+        </span>
+      ),
       content: <RatioSetting />,
       itemKey: 'ratio',
     });
     panes.push({
-      tab: t('速率限制设置'),
+      tab: (
+        <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
+          <Gauge size={18} />
+          {t('速率限制设置')}
+        </span>
+      ),
       content: <RateLimitSetting />,
       itemKey: 'ratelimit',
     });
     panes.push({
-      tab: t('模型相关设置'),
+      tab: (
+        <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
+          <Shapes size={18} />
+          {t('模型相关设置')}
+        </span>
+      ),
       content: <ModelSetting />,
       itemKey: 'models',
     });
     panes.push({
-      tab: t('系统设置'),
+      tab: (
+        <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
+          <Cog size={18} />
+          {t('系统设置')}
+        </span>
+      ),
       content: <SystemSetting />,
       itemKey: 'system',
     });
     panes.push({
-      tab: t('其他设置'),
-      content: <OtherSetting />,
-      itemKey: 'other',
-    });
-    panes.push({
-      tab: t('仪表盘设置'),
+      tab: (
+        <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
+          <LayoutDashboard size={18} />
+          {t('仪表盘设置')}
+        </span>
+      ),
       content: <DashboardSetting />,
       itemKey: 'dashboard',
     });
+    panes.push({
+      tab: (
+        <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
+          <MoreHorizontal size={18} />
+          {t('其他设置')}
+        </span>
+      ),
+      content: <OtherSetting />,
+      itemKey: 'other',
+    });
   }
   const onChangeTab = (key) => {
     setTabActiveKey(key);
@@ -74,7 +154,8 @@ const Setting = () => {
       <Layout>
         <Layout.Content>
           <Tabs
-            type='line'
+            type='card'
+            collapsible
             activeKey={tabActiveKey}
             onChange={(key) => onChangeTab(key)}
           >