Merge pull request #1278 from QuantumNous/alpha

feat: conditionally set Gemini ThinkingBudget based on MaxOutputTokens
This commit is contained in:
Calcium-Ion
2025-06-21 18:27:05 +08:00
committed by GitHub
29 changed files with 935 additions and 289 deletions

View File

@@ -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

View File

@@ -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("尚未实现")
}

View File

@@ -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),
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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" />}

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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} />

View File

@@ -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;

View File

@@ -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>
</>
);

View File

@@ -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;

View File

@@ -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;
}
});

View File

@@ -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>

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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 = {

View File

@@ -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({

View File

@@ -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)}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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={() => {

View File

@@ -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);

View File

@@ -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)}
>