From bc322ddac4bfbebc055bae600ceed156c86357a4 Mon Sep 17 00:00:00 2001
From: Peter Dave Hello
Date: Tue, 29 Apr 2025 22:54:43 +0800
Subject: [PATCH 01/18] refactor: optimize Dockerfile apk usage
---
Dockerfile | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 214ceaa3..3b42089b 100644
--- a/Dockerfile
+++ b/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
From 4a313a5f93d09698768f3eeaeee4998147682b6e Mon Sep 17 00:00:00 2001
From: skynono <6811626@qq.com>
Date: Thu, 5 Jun 2025 17:31:15 +0800
Subject: [PATCH 02/18] feat: add moonshot(kimi) update balance
---
controller/channel-billing.go | 38 +++++++++++++++++++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/controller/channel-billing.go b/controller/channel-billing.go
index 2bda0fd2..9bf5d1fe 100644
--- a/controller/channel-billing.go
+++ b/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("尚未实现")
}
From 1ec2bbd533f8f4d95c700c358aff2d613b291bac Mon Sep 17 00:00:00 2001
From: RedwindA
Date: Thu, 12 Jun 2025 18:20:58 +0800
Subject: [PATCH 03/18] update input range and description for thinking adapter
budget tokens
---
web/src/pages/Setting/Model/SettingGeminiModel.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/src/pages/Setting/Model/SettingGeminiModel.js b/web/src/pages/Setting/Model/SettingGeminiModel.js
index b802af1a..6a4d941a 100644
--- a/web/src/pages/Setting/Model/SettingGeminiModel.js
+++ b/web/src/pages/Setting/Model/SettingGeminiModel.js
@@ -208,8 +208,8 @@ export default function SettingGeminiModel(props) {
label={t('请求模型带-thinking后缀的BudgetTokens数(超出24576的部分将被忽略)')}
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({
From a39b2f5aa78de7d4a4e882c1767b661e62489fa0 Mon Sep 17 00:00:00 2001
From: RedwindA
Date: Wed, 18 Jun 2025 01:09:09 +0800
Subject: [PATCH 04/18] feat(ratio): add new Gemini model ratios and enhance
flash model handling
---
setting/operation_setting/model-ratio.go | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/setting/operation_setting/model-ratio.go b/setting/operation_setting/model-ratio.go
index 5155b2fc..175995ae 100644
--- a/setting/operation_setting/model-ratio.go
+++ b/setting/operation_setting/model-ratio.go
@@ -139,6 +139,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,
@@ -147,6 +148,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
@@ -487,12 +490,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
}
From f35784aa97e6f1dee9c140a0a026c3d8a66e0a79 Mon Sep 17 00:00:00 2001
From: RedwindA
Date: Wed, 18 Jun 2025 03:25:59 +0800
Subject: [PATCH 05/18] feat(gemini): update audio input pricing and adjust
model handling logic
---
relay/channel/gemini/relay-gemini.go | 3 +--
setting/operation_setting/tools.go | 9 ++++++---
2 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go
index 635041d7..d8d7db39 100644
--- a/relay/channel/gemini/relay-gemini.go
+++ b/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)
@@ -142,7 +141,7 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
}
}
} else if strings.HasSuffix(modelName, "-nothinking") {
- if !isNew25Pro && !is25FlashLite {
+ if !isNew25Pro {
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
ThinkingBudget: common.GetPointer(0),
}
diff --git a/setting/operation_setting/tools.go b/setting/operation_setting/tools.go
index 3e1af99e..daa15e5e 100644
--- a/setting/operation_setting/tools.go
+++ b/setting/operation_setting/tools.go
@@ -17,6 +17,7 @@ const (
const (
// Gemini Audio Input Price
Gemini25FlashPreviewInputAudioPrice = 1.00
+ Gemini25FlashProductionInputAudioPrice = 1.00 // for `gemini-2.5-flash`
Gemini25FlashNativeAudioInputAudioPrice = 3.00
Gemini20FlashInputAudioPrice = 0.70
)
@@ -64,10 +65,12 @@ 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") {
+ return Gemini25FlashPreviewInputAudioPrice
+ } else if strings.HasPrefix(modelName, "gemini-2.5-flash") {
+ return Gemini25FlashProductionInputAudioPrice
} else if strings.HasPrefix(modelName, "gemini-2.0-flash") {
return Gemini20FlashInputAudioPrice
}
From b95c5bb8f4fe4f674d4ae11d4c90ec3d9f38c84b Mon Sep 17 00:00:00 2001
From: RedwindA
Date: Wed, 18 Jun 2025 03:38:58 +0800
Subject: [PATCH 06/18] feat(gemini): add pricing for Gemini 2.5 Flash Lite
preview audio input
---
setting/operation_setting/tools.go | 3 +++
1 file changed, 3 insertions(+)
diff --git a/setting/operation_setting/tools.go b/setting/operation_setting/tools.go
index daa15e5e..a401b923 100644
--- a/setting/operation_setting/tools.go
+++ b/setting/operation_setting/tools.go
@@ -18,6 +18,7 @@ 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
)
@@ -67,6 +68,8 @@ func GetFileSearchPricePerThousand() float64 {
func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
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") {
From c192d07a04acdcb481b95ac8c22c2fe0b27320d3 Mon Sep 17 00:00:00 2001
From: RedwindA
Date: Thu, 19 Jun 2025 20:16:04 +0800
Subject: [PATCH 07/18] fix gizmo completion ratio
---
setting/ratio_setting/model_ratio.go | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go
index 266c1e07..99eba788 100644
--- a/setting/ratio_setting/model_ratio.go
+++ b/setting/ratio_setting/model_ratio.go
@@ -414,7 +414,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
@@ -432,12 +437,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" {
From 0b326e7af49d84cd235425f4f0a56d5fbd8d03ee Mon Sep 17 00:00:00 2001
From: "Apple\\Apple"
Date: Sat, 21 Jun 2025 02:00:58 +0800
Subject: [PATCH 08/18] =?UTF-8?q?=F0=9F=8C=90=20feat(i18n):=20add=20Englis?=
=?UTF-8?q?h=20translation=20for=20"=E6=9A=B4=E9=9C=B2=E5=80=8D=E7=8E=87?=
=?UTF-8?q?=E6=8E=A5=E5=8F=A3"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add translation "Expose ratio API" for Chinese text "暴露倍率接口"
- Update English locale file (en.json) with new translation entry
---
web/src/i18n/locales/en.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index ab793364..9a37dd26 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -1688,5 +1688,6 @@
"暂无差异化倍率显示": "No differential ratio display",
"请先选择同步渠道": "Please select the synchronization channel first",
"与本地相同": "Same as local",
- "未找到匹配的模型": "No matching model found"
+ "未找到匹配的模型": "No matching model found",
+ "暴露倍率接口": "Expose ratio API"
}
\ No newline at end of file
From 9c3a13cb238fa5ea5f5e591a6b219970b5dbfb86 Mon Sep 17 00:00:00 2001
From: "Apple\\Apple"
Date: Sat, 21 Jun 2025 02:09:08 +0800
Subject: [PATCH 09/18] =?UTF-8?q?=F0=9F=93=9D=20i18n:=20Update=20ratio=20s?=
=?UTF-8?q?ync=20message=20text?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Change message from "已与上游倍率完全一致,无需同步" to "未找到差异化倍率,无需同步"
- Update English translation to "No differential ratio found, no synchronization is required"
- Improve user experience clarity for upstream ratio synchronization status
---
web/src/i18n/locales/en.json | 2 +-
web/src/pages/Setting/Ratio/UpstreamRatioSync.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index 9a37dd26..b89207e5 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -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",
diff --git a/web/src/pages/Setting/Ratio/UpstreamRatioSync.js b/web/src/pages/Setting/Ratio/UpstreamRatioSync.js
index f83e0cdc..c246a3fe 100644
--- a/web/src/pages/Setting/Ratio/UpstreamRatioSync.js
+++ b/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);
From edaff1c689bf79ecb415854c191dd7f7bc1b58ff Mon Sep 17 00:00:00 2001
From: t0ng7u
Date: Sat, 21 Jun 2025 02:21:27 +0800
Subject: [PATCH 10/18] =?UTF-8?q?=F0=9F=8E=A8=20feat(UI):=20Add=20Lucide?=
=?UTF-8?q?=20icons=20to=20settings=20tabs=20for=20improved=20navigation?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add icons to each settings tab to enhance visual recognition
- Import necessary Lucide React icons (Settings, Calculator, Gauge, Shapes, etc.)
- Create consistent tab styling with icons aligned next to text
- Reorder tabs to place "Other Settings" as the last option
- Improve overall settings page UI with better visual hierarchy
---
web/src/pages/Setting/index.js | 68 ++++++++++++++++++++++++++++------
1 file changed, 56 insertions(+), 12 deletions(-)
diff --git a/web/src/pages/Setting/index.js b/web/src/pages/Setting/index.js
index 5572e540..f50586fe 100644
--- a/web/src/pages/Setting/index.js
+++ b/web/src/pages/Setting/index.js
@@ -2,6 +2,15 @@ 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
+} from 'lucide-react';
import SystemSetting from '../../components/settings/SystemSetting.js';
import { isRoot } from '../../helpers';
@@ -21,40 +30,75 @@ const Setting = () => {
if (isRoot()) {
panes.push({
- tab: t('运营设置'),
+ tab: (
+
+
+ {t('运营设置')}
+
+ ),
content: ,
itemKey: 'operation',
});
panes.push({
- tab: t('倍率设置'),
+ tab: (
+
+
+ {t('倍率设置')}
+
+ ),
content: ,
itemKey: 'ratio',
});
panes.push({
- tab: t('速率限制设置'),
+ tab: (
+
+
+ {t('速率限制设置')}
+
+ ),
content: ,
itemKey: 'ratelimit',
});
panes.push({
- tab: t('模型相关设置'),
+ tab: (
+
+
+ {t('模型相关设置')}
+
+ ),
content: ,
itemKey: 'models',
});
panes.push({
- tab: t('系统设置'),
+ tab: (
+
+
+ {t('系统设置')}
+
+ ),
content: ,
itemKey: 'system',
});
panes.push({
- tab: t('其他设置'),
- content: ,
- itemKey: 'other',
- });
- panes.push({
- tab: t('仪表盘设置'),
+ tab: (
+
+
+ {t('仪表盘设置')}
+
+ ),
content: ,
itemKey: 'dashboard',
});
+ panes.push({
+ tab: (
+
+
+ {t('其他设置')}
+
+ ),
+ content: ,
+ itemKey: 'other',
+ });
}
const onChangeTab = (key) => {
setTabActiveKey(key);
@@ -74,7 +118,7 @@ const Setting = () => {
onChangeTab(key)}
>
From 8ec18dd21bcaf966cd3c8e305715509ae30e2b9d Mon Sep 17 00:00:00 2001
From: "Apple\\Apple"
Date: Sat, 21 Jun 2025 02:36:09 +0800
Subject: [PATCH 11/18] =?UTF-8?q?=F0=9F=92=AC=20refactor:=20separate=20cha?=
=?UTF-8?q?t=20settings=20into=20dedicated=20tab?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Create new ChatsSetting component for managing chat configurations
- Add "Chat Settings" tab with MessageSquare icon in settings page
- Remove chat settings section from OperationSetting component
- Update import path to use Chat directory structure
---
web/src/components/settings/ChatsSetting.js | 63 +++++++++++++++++++
.../components/settings/OperationSetting.js | 8 ---
.../components/settings/RateLimitSetting.js | 19 +++---
web/src/i18n/locales/en.json | 2 +-
.../{Operation => Chat}/SettingsChats.js | 27 +-------
.../Setting/Operation/SettingsGeneral.js | 5 --
web/src/pages/Setting/index.js | 14 ++++-
7 files changed, 87 insertions(+), 51 deletions(-)
create mode 100644 web/src/components/settings/ChatsSetting.js
rename web/src/pages/Setting/{Operation => Chat}/SettingsChats.js (85%)
diff --git a/web/src/components/settings/ChatsSetting.js b/web/src/components/settings/ChatsSetting.js
new file mode 100644
index 00000000..6330808d
--- /dev/null
+++ b/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 (
+ <>
+
+ {/* 聊天设置 */}
+
+
+
+
+ >
+ );
+};
+
+export default ChatsSetting;
\ No newline at end of file
diff --git a/web/src/components/settings/OperationSetting.js b/web/src/components/settings/OperationSetting.js
index f6786f95..779d80a6 100644
--- a/web/src/components/settings/OperationSetting.js
+++ b/web/src/components/settings/OperationSetting.js
@@ -7,7 +7,6 @@ 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 = () => {
@@ -56,9 +55,6 @@ const OperationSetting = () => {
AutomaticDisableChannelEnabled: false,
AutomaticEnableChannelEnabled: false,
AutomaticDisableKeywords: '',
-
- /* 聊天设置 */
- Chats: '[]',
});
let [loading, setLoading] = useState(false);
@@ -131,10 +127,6 @@ const OperationSetting = () => {
- {/* 聊天设置 */}
-
-
-
>
);
diff --git a/web/src/components/settings/RateLimitSetting.js b/web/src/components/settings/RateLimitSetting.js
index 4d061363..21580d1a 100644
--- a/web/src/components/settings/RateLimitSetting.js
+++ b/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;
}
});
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index b89207e5..d9d5486e 100644
--- a/web/src/i18n/locales/en.json
+++ b/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",
diff --git a/web/src/pages/Setting/Operation/SettingsChats.js b/web/src/pages/Setting/Chat/SettingsChats.js
similarity index 85%
rename from web/src/pages/Setting/Operation/SettingsChats.js
rename to web/src/pages/Setting/Chat/SettingsChats.js
index 4322f558..76f3f9f2 100644
--- a/web/src/pages/Setting/Operation/SettingsChats.js
+++ b/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 }}
>
-
-
+
-
+ {/* 数据看板设置 */}
+
+
+
+
{/* API信息管理 */}
diff --git a/web/src/components/settings/OperationSetting.js b/web/src/components/settings/OperationSetting.js
index 9b0e772c..75a5c81a 100644
--- a/web/src/components/settings/OperationSetting.js
+++ b/web/src/components/settings/OperationSetting.js
@@ -3,7 +3,6 @@ import { Card, Spin } from '@douyinfe/semi-ui';
import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral.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 { API, showError } from '../../helpers';
@@ -35,11 +34,6 @@ const OperationSetting = () => {
/* 日志设置 */
LogConsumeEnabled: false,
- /* 数据看板 */
- DataExportEnabled: false,
- DataExportDefaultTime: 'hour',
- DataExportInterval: 5,
-
/* 监控设置 */
ChannelDisableThreshold: 0,
QuotaRemindThreshold: 0,
@@ -102,10 +96,6 @@ const OperationSetting = () => {
- {/* 数据看板 */}
-
-
-
{/* 监控设置 */}
diff --git a/web/src/pages/Setting/Operation/SettingsDataDashboard.js b/web/src/pages/Setting/Dashboard/SettingsDataDashboard.js
similarity index 100%
rename from web/src/pages/Setting/Operation/SettingsDataDashboard.js
rename to web/src/pages/Setting/Dashboard/SettingsDataDashboard.js
From 01ef1fe4e4984802ef344ac1dbed49cf66476a6f Mon Sep 17 00:00:00 2001
From: t0ng7u
Date: Sat, 21 Jun 2025 04:16:01 +0800
Subject: [PATCH 14/18] =?UTF-8?q?=F0=9F=93=9D=20refactor:=20reorganize=20p?=
=?UTF-8?q?ayment=20settings=20into=20dedicated=20tab?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Restructure payment settings into a separate tab for better organization and user experience. The changes include:
1. Create dedicated Payment components in the Setting directory structure
2. Move payment-related settings from SystemSetting to PaymentSetting
3. Add proper i18n support with useTranslation hook
4. Split payment settings into GeneralPayment and PaymentGateway components
5. Fix internationalization issues in placeholder text
6. Update navigation with CreditCard icon for payment tab
This refactoring improves code maintainability by following the established project pattern of having specialized setting components in their own directories.
---
web/src/components/settings/PaymentSetting.js | 88 +++++++
web/src/components/settings/SystemSetting.js | 150 ------------
web/src/i18n/locales/en.json | 13 +-
.../Setting/Payment/SettingsGeneralPayment.js | 74 ++++++
.../Setting/Payment/SettingsPaymentGateway.js | 218 ++++++++++++++++++
web/src/pages/Setting/index.js | 14 +-
6 files changed, 405 insertions(+), 152 deletions(-)
create mode 100644 web/src/components/settings/PaymentSetting.js
create mode 100644 web/src/pages/Setting/Payment/SettingsGeneralPayment.js
create mode 100644 web/src/pages/Setting/Payment/SettingsPaymentGateway.js
diff --git a/web/src/components/settings/PaymentSetting.js b/web/src/components/settings/PaymentSetting.js
new file mode 100644
index 00000000..91a40a2b
--- /dev/null
+++ b/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 (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default PaymentSetting;
\ No newline at end of file
diff --git a/web/src/components/settings/SystemSetting.js b/web/src/components/settings/SystemSetting.js
index 1236ef2e..b5829f31 100644
--- a/web/src/components/settings/SystemSetting.js
+++ b/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',
}}
>
-
-
-
-
-
-
@@ -604,80 +528,6 @@ const SystemSetting = () => {
-
-
-
- (当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{
+ 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 (
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/pages/Setting/Payment/SettingsPaymentGateway.js b/web/src/pages/Setting/Payment/SettingsPaymentGateway.js
new file mode 100644
index 00000000..0bb63b53
--- /dev/null
+++ b/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 (
+
+
+
+ {t('(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/pages/Setting/index.js b/web/src/pages/Setting/index.js
index 09b9acdf..d0ae763b 100644
--- a/web/src/pages/Setting/index.js
+++ b/web/src/pages/Setting/index.js
@@ -11,7 +11,8 @@ import {
MoreHorizontal,
LayoutDashboard,
MessageSquare,
- Palette
+ Palette,
+ CreditCard
} from 'lucide-react';
import SystemSetting from '../../components/settings/SystemSetting.js';
@@ -24,6 +25,7 @@ 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();
@@ -63,6 +65,16 @@ const Setting = () => {
content: ,
itemKey: 'drawing',
});
+ panes.push({
+ tab: (
+
+
+ {t('支付设置')}
+
+ ),
+ content: ,
+ itemKey: 'payment',
+ });
panes.push({
tab: (
From 2902d6c7c27e94ffe8483ab7f2c930c11620ff1b Mon Sep 17 00:00:00 2001
From: t0ng7u
Date: Sat, 21 Jun 2025 04:25:56 +0800
Subject: [PATCH 15/18] =?UTF-8?q?=F0=9F=8E=A8=20style:=20add=20`mt-2`=20st?=
=?UTF-8?q?yle=20in=20ratio=20section?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/src/components/settings/RatioSetting.js | 2 +-
web/src/pages/Setting/Ratio/ModelRationNotSetEditor.js | 2 +-
web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/web/src/components/settings/RatioSetting.js b/web/src/components/settings/RatioSetting.js
index 1d87c6de..99a6a3cf 100644
--- a/web/src/components/settings/RatioSetting.js
+++ b/web/src/components/settings/RatioSetting.js
@@ -82,7 +82,7 @@ const RatioSetting = () => {
{/* 模型倍率设置以及可视化编辑器 */}
-
+
diff --git a/web/src/pages/Setting/Ratio/ModelRationNotSetEditor.js b/web/src/pages/Setting/Ratio/ModelRationNotSetEditor.js
index d5d8d832..25c67eee 100644
--- a/web/src/pages/Setting/Ratio/ModelRationNotSetEditor.js
+++ b/web/src/pages/Setting/Ratio/ModelRationNotSetEditor.js
@@ -372,7 +372,7 @@ export default function ModelRatioNotSetEditor(props) {
return (
<>
-
+
} onClick={() => setVisible(true)}>
{t('添加模型')}
diff --git a/web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js b/web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js
index 85426a5e..983f3afe 100644
--- a/web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js
+++ b/web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js
@@ -404,7 +404,7 @@ export default function ModelSettingsVisualEditor(props) {
return (
<>
-
+
}
onClick={() => {
From 1ad2f63f85b876bd7de660a8f505be3715bba0ac Mon Sep 17 00:00:00 2001
From: "Apple\\Apple"
Date: Sat, 21 Jun 2025 06:06:21 +0800
Subject: [PATCH 16/18] =?UTF-8?q?=E2=9C=A8=20feat:=20Enhance=20announcemen?=
=?UTF-8?q?ts=20UX=20with=20unread=20badge,=20tabbed=20NoticeModal,=20and?=
=?UTF-8?q?=20shine=20animation?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
• HeaderBar
- Added dynamic unread badge; click now opens NoticeModal on “System Announcements” tab
- Passes `defaultTab` and `unreadKeys` props to NoticeModal for contextual behaviour
• NoticeModal
- Introduced Tabs inside the modal title with Lucide icons (Bell, Megaphone)
- Displays in-app notice (markdown) and system announcements separately
- Highlights unread announcements with “shine” text animation
- Accepts new props `defaultTab`, `unreadKeys` to control initial tab and highlight logic
• CSS (index.css)
- Implemented `sweep-shine` keyframes and `.shine-text` utility for left-to-right glow
- Added dark-mode variant for better contrast
- Ensured cross-browser support with standard `background-clip`
Overall, users now see an unread counter, are directed to new announcements automatically, and benefit from an eye-catching glow effect that works in both light and dark themes.
---
web/src/components/layout/HeaderBar.js | 92 +++++++++++++++++---
web/src/components/layout/NoticeModal.js | 104 +++++++++++++++++++++--
web/src/i18n/locales/en.json | 3 +-
web/src/index.css | 28 ++++++
4 files changed, 206 insertions(+), 21 deletions(-)
diff --git a/web/src/components/layout/HeaderBar.js b/web/src/components/layout/HeaderBar.js
index 6317c576..b7425645 100644
--- a/web/src/components/layout/HeaderBar.js
+++ b/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 (
setNoticeVisible(false)}
+ onClose={handleNoticeClose}
isMobile={styleState.isMobile}
+ defaultTab={unreadCount > 0 ? 'system' : 'inApp'}
+ unreadKeys={getUnreadKeys()}
/>
@@ -462,14 +517,27 @@ const HeaderBar = () => {
)}
-
}
- 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 ? (
+
+ }
+ 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"
+ />
+
+ ) : (
+
}
+ 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"
+ />
+ )}
:
}
diff --git a/web/src/components/layout/NoticeModal.js b/web/src/components/layout/NoticeModal.js
index 1bbaa554..456c012f 100644
--- a/web/src/components/layout/NoticeModal.js
+++ b/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
;
}
@@ -64,14 +92,74 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
return (
);
};
+ const renderAnnouncementTimeline = () => {
+ if (processedAnnouncements.length === 0) {
+ return (
+
+ }
+ darkModeImage={}
+ description={t('暂无系统公告')}
+ />
+
+ );
+ }
+
+ return (
+
+
+ {processedAnnouncements.map((item, idx) => (
+
+
+ {item.isUnread ? (
+
+ {item.content}
+
+ ) : (
+ item.content
+ )}
+ {item.extra &&
{item.extra}
}
+
+
+ ))}
+
+
+ );
+ };
+
+ const renderBody = () => {
+ if (activeTab === 'inApp') {
+ return renderMarkdownNotice();
+ }
+ return renderAnnouncementTimeline();
+ };
+
return (
+ {t('系统公告')}
+
+ {t('通知')}} itemKey='inApp' />
+ {t('系统公告')}} itemKey='system' />
+
+
+ }
visible={visible}
onCancel={onClose}
footer={(
@@ -82,7 +170,7 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
)}
size={isMobile ? 'full-width' : 'large'}
>
- {renderContent()}
+ {renderBody()}
);
};
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index 82a3d41a..cab7f8fb 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -1700,5 +1700,6 @@
"最低充值美元数量": "Minimum recharge dollar amount",
"充值分组倍率": "Recharge group ratio",
"充值方式设置": "Recharge method settings",
- "更新支付设置": "Update payment settings"
+ "更新支付设置": "Update payment settings",
+ "通知": "Notice"
}
\ No newline at end of file
diff --git a/web/src/index.css b/web/src/index.css
index ff7294ad..c95e6db4 100644
--- a/web/src/index.css
+++ b/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;
}
\ No newline at end of file
From d487be0029d3c0c926418dc65919e60687a63fc9 Mon Sep 17 00:00:00 2001
From: t0ng7u
Date: Sat, 21 Jun 2025 06:15:26 +0800
Subject: [PATCH 17/18] =?UTF-8?q?=E2=9C=A8=20feat(settings/announcements):?=
=?UTF-8?q?=20sort=20by=20publishDate=20desc?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add reverse-chronological sorting for the announcements list so that the newest
items appear first in the dashboard.
No API changes; this only affects front-end display and user notifications.
---
web/src/components/settings/DashboardSetting.js | 10 +++++-----
.../pages/Setting/Dashboard/SettingsAnnouncements.js | 10 ++++++++--
2 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/web/src/components/settings/DashboardSetting.js b/web/src/components/settings/DashboardSetting.js
index 86af88c0..4fa1ad10 100644
--- a/web/src/components/settings/DashboardSetting.js
+++ b/web/src/components/settings/DashboardSetting.js
@@ -121,16 +121,16 @@ const DashboardSetting = () => {
- {/* API信息管理 */}
-
-
-
-
{/* 系统公告管理 */}
+ {/* API信息管理 */}
+
+
+
+
{/* 常见问答管理 */}
diff --git a/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js b/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js
index f1b99f43..c15e2885 100644
--- a/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js
+++ b/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js
@@ -388,11 +388,17 @@ const SettingsAnnouncements = ({ options, refresh }) => {
);
- // 计算当前页显示的数据
+ // 计算当前页显示的数据(按发布时间倒序排序,最新优先显示)
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 = {
From 6bb552128c0d98eab9b37f15da122d79bcc3ce12 Mon Sep 17 00:00:00 2001
From: CaIon <1808837298@qq.com>
Date: Sat, 21 Jun 2025 17:51:13 +0800
Subject: [PATCH 18/18] =?UTF-8?q?=E2=9C=A8=20feat(relay-gemini):=20conditi?=
=?UTF-8?q?onally=20set=20ThinkingBudget=20based=20on=20MaxOutputTokens?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
relay/channel/gemini/relay-gemini.go | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go
index e5eeca1c..18edfd04 100644
--- a/relay/channel/gemini/relay-gemini.go
+++ b/relay/channel/gemini/relay-gemini.go
@@ -133,12 +133,14 @@ 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 {