From 87fc681df3f1152e472f701d5335b54ee75c870e Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Wed, 18 Jun 2025 01:29:35 +0800 Subject: [PATCH 01/14] =?UTF-8?q?=F0=9F=9A=80=20feat(ui):=20isolate=20rati?= =?UTF-8?q?o=20configurations=20into=20dedicated=20=E2=80=9CRatio=E2=80=9D?= =?UTF-8?q?=20tab=20and=20refactor=20settings=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary • Added new Ratio tab in Settings for managing all ratio-related configurations (group & model multipliers). • Created `RatioSetting` component to host GroupRatio, ModelRatio, Visual Editor and Unset-Models panels. • Moved ratio components to `web/src/pages/Setting/Ratio/` directory: – `GroupRatioSettings.js` – `ModelRatioSettings.js` – `ModelSettingsVisualEditor.js` – `ModelRationNotSetEditor.js` • Updated imports in `RatioSetting.js` to use the new path. • Updated main Settings router (`web/src/pages/Setting/index.js`) to include the new “Ratio Settings” tab. • Pruned `OperationSetting.js`: – Removed ratio-specific cards, tabs and unused imports. – Reduced state to only the keys required by its child components. – Deleted obsolete fields (`StreamCacheQueueLength`, `CheckSensitiveOnCompletionEnabled`, `StopOnSensitiveEnabled`). • Added boolean handling simplification in `OperationSetting.js`. • Adjusted helper import list and removed unused translation hook. Why Separating ratio-related settings improves UX clarity, reduces cognitive load in the Operation Settings panel and keeps the codebase modular and easier to maintain. BREAKING CHANGE The file paths for ratio components have changed. Any external imports referencing the old `Operation` directory must update to the new `Ratio` path. --- .../components/settings/OperationSetting.js | 104 ++++++----------- web/src/components/settings/RatioSetting.js | 109 ++++++++++++++++++ web/src/i18n/locales/en.json | 2 +- .../GroupRatioSettings.js | 6 +- .../ModelRatioSettings.js | 0 .../ModelRationNotSetEditor.js | 0 .../ModelSettingsVisualEditor.js | 0 web/src/pages/Setting/index.js | 8 +- 8 files changed, 154 insertions(+), 75 deletions(-) create mode 100644 web/src/components/settings/RatioSetting.js rename web/src/pages/Setting/{Operation => Ratio}/GroupRatioSettings.js (99%) rename web/src/pages/Setting/{Operation => Ratio}/ModelRatioSettings.js (100%) rename web/src/pages/Setting/{Operation => Ratio}/ModelRationNotSetEditor.js (100%) rename web/src/pages/Setting/{Operation => Ratio}/ModelSettingsVisualEditor.js (100%) diff --git a/web/src/components/settings/OperationSetting.js b/web/src/components/settings/OperationSetting.js index 7bd9bf62..f6786f95 100644 --- a/web/src/components/settings/OperationSetting.js +++ b/web/src/components/settings/OperationSetting.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Card, Spin, Tabs } from '@douyinfe/semi-ui'; +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'; @@ -7,63 +7,58 @@ 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 ModelSettingsVisualEditor from '../../pages/Setting/Operation/ModelSettingsVisualEditor.js'; -import GroupRatioSettings from '../../pages/Setting/Operation/GroupRatioSettings.js'; -import ModelRatioSettings from '../../pages/Setting/Operation/ModelRatioSettings.js'; - -import { API, showError, showSuccess } from '../../helpers'; import SettingsChats from '../../pages/Setting/Operation/SettingsChats.js'; -import { useTranslation } from 'react-i18next'; -import ModelRatioNotSetEditor from '../../pages/Setting/Operation/ModelRationNotSetEditor.js'; +import { API, showError } from '../../helpers'; const OperationSetting = () => { - const { t } = useTranslation(); let [inputs, setInputs] = useState({ + /* 额度相关 */ QuotaForNewUser: 0, + PreConsumedQuota: 0, QuotaForInviter: 0, QuotaForInvitee: 0, - QuotaRemindThreshold: 0, - PreConsumedQuota: 0, - StreamCacheQueueLength: 0, - ModelRatio: '', - CacheRatio: '', - CompletionRatio: '', - ModelPrice: '', - GroupRatio: '', - GroupGroupRatio: '', - AutoGroups: '', - DefaultUseAutoGroup: false, - UserUsableGroups: '', + + /* 通用设置 */ TopUpLink: '', 'general_setting.docs_link': '', - // ChatLink2: '', // 添加的新状态变量 QuotaPerUnit: 0, - AutomaticDisableChannelEnabled: false, - AutomaticEnableChannelEnabled: false, - ChannelDisableThreshold: 0, - LogConsumeEnabled: false, + RetryTimes: 0, DisplayInCurrencyEnabled: false, DisplayTokenStatEnabled: false, - CheckSensitiveEnabled: false, - CheckSensitiveOnPromptEnabled: false, - CheckSensitiveOnCompletionEnabled: '', - StopOnSensitiveEnabled: '', - SensitiveWords: '', + DefaultCollapseSidebar: false, + DemoSiteEnabled: false, + SelfUseModeEnabled: false, + + /* 绘图设置 */ + DrawingEnabled: false, MjNotifyEnabled: false, MjAccountFilterEnabled: false, - MjModeClearEnabled: false, MjForwardUrlEnabled: false, + MjModeClearEnabled: false, MjActionCheckSuccessEnabled: false, - DrawingEnabled: false, + + /* 敏感词设置 */ + CheckSensitiveEnabled: false, + CheckSensitiveOnPromptEnabled: false, + SensitiveWords: '', + + /* 日志设置 */ + LogConsumeEnabled: false, + + /* 数据看板 */ DataExportEnabled: false, DataExportDefaultTime: 'hour', DataExportInterval: 5, - DefaultCollapseSidebar: false, // 默认折叠侧边栏 - RetryTimes: 0, - Chats: '[]', - DemoSiteEnabled: false, - SelfUseModeEnabled: false, + + /* 监控设置 */ + ChannelDisableThreshold: 0, + QuotaRemindThreshold: 0, + AutomaticDisableChannelEnabled: false, + AutomaticEnableChannelEnabled: false, AutomaticDisableKeywords: '', + + /* 聊天设置 */ + Chats: '[]', }); let [loading, setLoading] = useState(false); @@ -74,22 +69,9 @@ const OperationSetting = () => { if (success) { let newInputs = {}; data.forEach((item) => { - if ( - item.key === 'ModelRatio' || - item.key === 'GroupRatio' || - item.key === 'GroupGroupRatio' || - item.key === 'AutoGroups' || - item.key === 'UserUsableGroups' || - item.key === 'CompletionRatio' || - item.key === 'ModelPrice' || - item.key === 'CacheRatio' - ) { - item.value = JSON.stringify(JSON.parse(item.value), null, 2); - } if ( item.key.endsWith('Enabled') || - ['DefaultCollapseSidebar'].includes(item.key) || - ['DefaultUseAutoGroup'].includes(item.key) + ['DefaultCollapseSidebar'].includes(item.key) ) { newInputs[item.key] = item.value === 'true' ? true : false; } else { @@ -153,24 +135,6 @@ const OperationSetting = () => { - {/* 分组倍率设置 */} - - - - {/* 合并模型倍率设置和可视化倍率设置 */} - - - - - - - - - - - - - ); diff --git a/web/src/components/settings/RatioSetting.js b/web/src/components/settings/RatioSetting.js new file mode 100644 index 00000000..bf97282c --- /dev/null +++ b/web/src/components/settings/RatioSetting.js @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from 'react'; +import { Card, Spin, Tabs } from '@douyinfe/semi-ui'; +import { useTranslation } from 'react-i18next'; + +import GroupRatioSettings from '../../pages/Setting/Ratio/GroupRatioSettings.js'; +import ModelRatioSettings from '../../pages/Setting/Ratio/ModelRatioSettings.js'; +import ModelSettingsVisualEditor from '../../pages/Setting/Ratio/ModelSettingsVisualEditor.js'; +import ModelRatioNotSetEditor from '../../pages/Setting/Ratio/ModelRationNotSetEditor.js'; + +import { API, showError } from '../../helpers'; + +const RatioSetting = () => { + const { t } = useTranslation(); + + let [inputs, setInputs] = useState({ + ModelPrice: '', + ModelRatio: '', + CacheRatio: '', + CompletionRatio: '', + GroupRatio: '', + GroupGroupRatio: '', + AutoGroups: '', + DefaultUseAutoGroup: false, + UserUsableGroups: '', + }); + + const [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 === 'ModelRatio' || + item.key === 'GroupRatio' || + item.key === 'GroupGroupRatio' || + item.key === 'AutoGroups' || + item.key === 'UserUsableGroups' || + item.key === 'CompletionRatio' || + item.key === 'ModelPrice' || + item.key === 'CacheRatio' + ) { + try { + item.value = JSON.stringify(JSON.parse(item.value), null, 2); + } catch (e) { + // 如果后端返回的不是合法 JSON,直接展示 + } + } + if (['DefaultUseAutoGroup'].includes(item.key)) { + newInputs[item.key] = item.value === 'true' ? true : false; + } else { + newInputs[item.key] = item.value; + } + }); + setInputs(newInputs); + } else { + showError(message); + } + }; + + const onRefresh = async () => { + try { + setLoading(true); + await getOptions(); + } catch (error) { + showError('刷新失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + onRefresh(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {/* 分组倍率设置 */} + + + + {/* 模型倍率设置以及可视化编辑器 */} + + + + + + + + + + + + + + + ); +}; + +export default RatioSetting; \ No newline at end of file diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index b456fff5..fc80f9c1 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1588,7 +1588,7 @@ "性能指标": "Performance Indicators", "模型数据分析": "Model Data Analysis", "搜索无结果": "No results found", - "仪表盘配置": "Dashboard Configuration", + "仪表盘设置": "Dashboard Settings", "API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)": "API information management, you can configure multiple API addresses for status display and load balancing (maximum 50)", "线路描述": "Route description", "颜色": "Color", diff --git a/web/src/pages/Setting/Operation/GroupRatioSettings.js b/web/src/pages/Setting/Ratio/GroupRatioSettings.js similarity index 99% rename from web/src/pages/Setting/Operation/GroupRatioSettings.js rename to web/src/pages/Setting/Ratio/GroupRatioSettings.js index 4a51a98c..3c7c754b 100644 --- a/web/src/pages/Setting/Operation/GroupRatioSettings.js +++ b/web/src/pages/Setting/Ratio/GroupRatioSettings.js @@ -184,16 +184,16 @@ export default function GroupRatioSettings(props) { if (!value || value.trim() === '') { return true; // Allow empty values } - + // First check if it's valid JSON try { const parsed = JSON.parse(value); - + // Check if it's an array if (!Array.isArray(parsed)) { return false; } - + // Check if every element is a string return parsed.every(item => typeof item === 'string'); } catch (error) { diff --git a/web/src/pages/Setting/Operation/ModelRatioSettings.js b/web/src/pages/Setting/Ratio/ModelRatioSettings.js similarity index 100% rename from web/src/pages/Setting/Operation/ModelRatioSettings.js rename to web/src/pages/Setting/Ratio/ModelRatioSettings.js diff --git a/web/src/pages/Setting/Operation/ModelRationNotSetEditor.js b/web/src/pages/Setting/Ratio/ModelRationNotSetEditor.js similarity index 100% rename from web/src/pages/Setting/Operation/ModelRationNotSetEditor.js rename to web/src/pages/Setting/Ratio/ModelRationNotSetEditor.js diff --git a/web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js b/web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js similarity index 100% rename from web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js rename to web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js diff --git a/web/src/pages/Setting/index.js b/web/src/pages/Setting/index.js index dc48c8dc..5572e540 100644 --- a/web/src/pages/Setting/index.js +++ b/web/src/pages/Setting/index.js @@ -10,6 +10,7 @@ import OperationSetting from '../../components/settings/OperationSetting.js'; 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'; const Setting = () => { const { t } = useTranslation(); @@ -24,6 +25,11 @@ const Setting = () => { content: , itemKey: 'operation', }); + panes.push({ + tab: t('倍率设置'), + content: , + itemKey: 'ratio', + }); panes.push({ tab: t('速率限制设置'), content: , @@ -45,7 +51,7 @@ const Setting = () => { itemKey: 'other', }); panes.push({ - tab: t('仪表盘配置'), + tab: t('仪表盘设置'), content: , itemKey: 'dashboard', }); From 547da2da60a6f63f021122e1f3d31280d428115b Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Wed, 18 Jun 2025 02:33:18 +0800 Subject: [PATCH 02/14] =?UTF-8?q?=F0=9F=9A=80=20feat(Channels):=20Enhance?= =?UTF-8?q?=20Channel=20Filtering=20&=20Performance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(api): • Add optional `type` query param to `/api/channel` endpoint for type-specific pagination • Return `type_counts` map with counts for each channel type • Implement `GetChannelsByType`, `CountChannelsByType`, `CountChannelsGroupByType` in `model/channel.go` feat(frontend): • Introduce type Tabs in `ChannelsTable` to switch between channel types • Tabs show dynamic counts using backend `type_counts`; “All” is computed from sum • Persist active type, reload data on tab change (with proper query params) perf(frontend): • Use a request counter (`useRef`) to discard stale responses when tabs switch quickly • Move all `useMemo` hooks to top level to satisfy React Hook rules • Remove redundant local type counting fallback when backend data present ui: • Remove icons from response-time tags for cleaner look • Use Semi-UI native arrow controls for Tabs; custom arrow code deleted chore: • Minor refactor & comments for clarity • Ensure ESLint Hook rules pass Result: Channel list now supports fast, accurate type filtering with correct counts, improved concurrency safety, and cleaner UI. --- controller/channel.go | 34 ++++-- model/channel.go | 36 ++++++ web/src/components/table/ChannelsTable.js | 136 ++++++++++++++++++---- 3 files changed, 177 insertions(+), 29 deletions(-) diff --git a/controller/channel.go b/controller/channel.go index 1cfb7906..acaf2977 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -52,6 +52,14 @@ func GetAllChannels(c *gin.Context) { channelData := make([]*model.Channel, 0) idSort, _ := strconv.ParseBool(c.Query("id_sort")) enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode")) + // type filter + typeStr := c.Query("type") + typeFilter := -1 + if typeStr != "" { + if t, err := strconv.Atoi(typeStr); err == nil { + typeFilter = t + } + } var total int64 @@ -72,6 +80,14 @@ func GetAllChannels(c *gin.Context) { } // 计算 tag 总数用于分页 total, _ = model.CountAllTags() + } else if typeFilter >= 0 { + channels, err := model.GetChannelsByType((p-1)*pageSize, pageSize, idSort, typeFilter) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()}) + return + } + channelData = channels + total, _ = model.CountChannelsByType(typeFilter) } else { channels, err := model.GetAllChannels((p-1)*pageSize, pageSize, false, idSort) if err != nil { @@ -82,14 +98,18 @@ func GetAllChannels(c *gin.Context) { total, _ = model.CountAllChannels() } + // calculate type counts + typeCounts, _ := model.CountChannelsGroupByType() + c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "items": channelData, - "total": total, - "page": p, - "page_size": pageSize, + "success": true, + "message": "", + "data": gin.H{ + "items": channelData, + "total": total, + "page": p, + "page_size": pageSize, + "type_counts": typeCounts, }, }) return diff --git a/model/channel.go b/model/channel.go index b5503eee..6cbd8adc 100644 --- a/model/channel.go +++ b/model/channel.go @@ -597,3 +597,39 @@ func CountAllTags() (int64, error) { err := DB.Model(&Channel{}).Where("tag is not null AND tag != ''").Distinct("tag").Count(&total).Error return total, err } + +// Get channels of specified type with pagination +func GetChannelsByType(startIdx int, num int, idSort bool, channelType int) ([]*Channel, error) { + var channels []*Channel + order := "priority desc" + if idSort { + order = "id desc" + } + err := DB.Where("type = ?", channelType).Order(order).Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error + return channels, err +} + +// Count channels of specific type +func CountChannelsByType(channelType int) (int64, error) { + var count int64 + err := DB.Model(&Channel{}).Where("type = ?", channelType).Count(&count).Error + return count, err +} + +// Return map[type]count for all channels +func CountChannelsGroupByType() (map[int64]int64, error) { + type result struct { + Type int64 `gorm:"column:type"` + Count int64 `gorm:"column:count"` + } + var results []result + err := DB.Model(&Channel{}).Select("type, count(*) as count").Group("type").Find(&results).Error + if err != nil { + return nil, err + } + counts := make(map[int64]int64) + for _, r := range results { + counts[r.Type] = r.Count + } + return counts, nil +} diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index f5a78490..a18920ab 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo, useRef } from 'react'; import { API, showError, @@ -16,11 +16,6 @@ import { XCircle, AlertCircle, HelpCircle, - TestTube, - Zap, - Timer, - Clock, - AlertTriangle, Coins, Tags } from 'lucide-react'; @@ -43,7 +38,9 @@ import { Typography, Checkbox, Card, - Form + Form, + Tabs, + TabPane } from '@douyinfe/semi-ui'; import { IllustrationNoResult, @@ -141,31 +138,31 @@ const ChannelsTable = () => { time = time.toFixed(2) + t(' 秒'); if (responseTime === 0) { return ( - }> + {t('未测试')} ); } else if (responseTime <= 1000) { return ( - }> + {time} ); } else if (responseTime <= 3000) { return ( - }> + {time} ); } else if (responseTime <= 5000) { return ( - }> + {time} ); } else { return ( - }> + {time} ); @@ -682,11 +679,10 @@ const ChannelsTable = () => { const [isBatchTesting, setIsBatchTesting] = useState(false); const [testQueue, setTestQueue] = useState([]); const [isProcessingQueue, setIsProcessingQueue] = useState(false); - - // Form API 引用 + const [activeTypeKey, setActiveTypeKey] = useState('all'); + const [typeCounts, setTypeCounts] = useState({}); + const requestCounter = useRef(0); const [formApi, setFormApi] = useState(null); - - // Form 初始值 const formInitValues = { searchKeyword: '', searchGroup: '', @@ -868,17 +864,23 @@ const ChannelsTable = () => { setChannels(channelDates); }; - const loadChannels = async (page, pageSize, idSort, enableTagMode) => { + const loadChannels = async (page, pageSize, idSort, enableTagMode, typeKey = activeTypeKey) => { + const reqId = ++requestCounter.current; // 记录当前请求序号 setLoading(true); + const typeParam = typeKey === 'all' ? '' : `&type=${typeKey}`; const res = await API.get( - `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}`, + `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}`, ); - if (res === undefined) { + if (res === undefined || reqId !== requestCounter.current) { return; } const { success, message, data } = res.data; if (success) { - const { items, total } = data; + const { items, total, type_counts } = data; + if (type_counts) { + const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0); + setTypeCounts({ ...type_counts, all: sumAll }); + } setChannelFormat(items, enableTagMode); setChannelCount(total); } else { @@ -1044,12 +1046,16 @@ const ChannelsTable = () => { return; } + const typeParam = activeTypeKey === 'all' ? '' : `&type=${activeTypeKey}`; const res = await API.get( - `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}`, + `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}`, ); const { success, message, data } = res.data; if (success) { - setChannelFormat(data, enableTagMode); + const { items = [], type_counts = {} } = data; + const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0); + setTypeCounts({ ...type_counts, all: sumAll }); + setChannelFormat(items, enableTagMode); setActivePage(1); } else { showError(message); @@ -1179,7 +1185,92 @@ const ChannelsTable = () => { } }; + const channelTypeCounts = useMemo(() => { + if (Object.keys(typeCounts).length > 0) return typeCounts; + // fallback 本地计算 + const counts = { all: channels.length }; + channels.forEach((channel) => { + const collect = (ch) => { + const type = ch.type; + counts[type] = (counts[type] || 0) + 1; + }; + if (channel.children !== undefined) { + channel.children.forEach(collect); + } else { + collect(channel); + } + }); + return counts; + }, [typeCounts, channels]); + + const availableTypeKeys = useMemo(() => { + const keys = ['all']; + Object.entries(channelTypeCounts).forEach(([k, v]) => { + if (k !== 'all' && v > 0) keys.push(String(k)); + }); + return keys; + }, [channelTypeCounts]); + + const renderTypeTabs = () => { + return ( + { + setActiveTypeKey(key); + setActivePage(1); + loadChannels(1, pageSize, idSort, enableTagMode, key); + }} + className="mb-4" + > + + {t('全部')} + + {channelTypeCounts['all'] || 0} + + + } + /> + + {CHANNEL_OPTIONS.filter((opt) => availableTypeKeys.includes(String(opt.value))).map((option) => { + const key = String(option.value); + const count = channelTypeCounts[option.value] || 0; + return ( + + {getChannelIcon(option.value)} + {option.label} + + {count} + + + } + /> + ); + })} + + ); + }; + let pageData = channels; + if (activeTypeKey !== 'all') { + const typeVal = parseInt(activeTypeKey); + if (!isNaN(typeVal)) { + pageData = pageData.filter((ch) => { + if (ch.children !== undefined) { + return ch.children.some((c) => c.type === typeVal); + } + return ch.type === typeVal; + }); + } + } const handlePageChange = (page) => { setActivePage(page); @@ -1371,6 +1462,7 @@ const ChannelsTable = () => { const renderHeader = () => (
+ {renderTypeTabs()}
@@ -1388,6 +1389,7 @@ const Detail = (props) => { ) : ( Date: Wed, 18 Jun 2025 05:10:32 +0800 Subject: [PATCH 06/14] =?UTF-8?q?=F0=9F=90=9B=20fix(detail):=20explicitly?= =?UTF-8?q?=20set=20`preventScroll=3D{true}`=20on=20Tabs=20to=20stop=20pag?= =?UTF-8?q?e=20jump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem Semi UI’s Tabs calls `focus()` on the active tab during mount, causing the browser to scroll the page to that element. Using the bare `preventScroll` shorthand was not picked up reliably, so the page still jumped to the Tabs’ position on first render. Changes • Updated both Tabs instances in `web/src/pages/Detail/index.js` to `preventScroll={true}` instead of the shorthand prop. • Ensures the prop is explicitly interpreted as boolean `true`, converting the internal call to `focus({ preventScroll: true })`. Result The `Detail` page now stays at its original scroll position after load, eliminating the unexpected auto-scroll behavior. --- web/src/pages/Detail/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/Detail/index.js b/web/src/pages/Detail/index.js index b4bd9b7f..15c02abf 100644 --- a/web/src/pages/Detail/index.js +++ b/web/src/pages/Detail/index.js @@ -1112,7 +1112,7 @@ const Detail = (props) => {
@@ -1389,7 +1389,7 @@ const Detail = (props) => { ) : ( Date: Wed, 18 Jun 2025 18:00:49 +0800 Subject: [PATCH 07/14] =?UTF-8?q?=F0=9F=9A=9A=20Refactor(ratio=5Fsetting):?= =?UTF-8?q?=20refactor=20ratio=20management=20into=20standalone=20`ratio?= =?UTF-8?q?=5Fsetting`=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary • Migrated all ratio-related sources into `setting/ratio_setting/` – `model_ratio.go` (renamed from model-ratio.go) – `cache_ratio.go` – `group_ratio.go` • Changed package name to `ratio_setting` and relocated initialization (`ratio_setting.InitRatioSettings()` in main). • Updated every import & call site: – Model / cache / completion / image ratio helpers – Group ratio helpers (`GetGroupRatio*`, `ContainsGroupRatio`, `CheckGroupRatio`, etc.) – JSON-serialization & update helpers (`*Ratio2JSONString`, `Update*RatioByJSONString`) • Adjusted controllers, middleware, relay helpers, services and models to reference the new package. • Removed obsolete `setting` / `operation_setting` imports; added missing `ratio_setting` imports. • Adopted idiomatic map iteration (`for key := range m`) where value is unused. • Ran static checks to ensure clean build. This commit centralises all ratio configuration (model, cache and group) in one cohesive module, simplifying future maintenance and improving code clarity. --- controller/group.go | 5 ++-- controller/option.go | 3 +- controller/pricing.go | 12 ++++---- main.go | 4 +-- middleware/distributor.go | 3 +- model/option.go | 25 +++++++++-------- model/pricing.go | 8 +++--- relay/helper/price.go | 23 ++++++++------- relay/relay-mj.go | 14 +++++----- relay/relay_task.go | 9 +++--- service/quota.go | 28 +++++++++---------- .../cache_ratio.go | 2 +- setting/{ => ratio_setting}/group_ratio.go | 2 +- .../model_ratio.go} | 5 ++-- 14 files changed, 73 insertions(+), 70 deletions(-) rename setting/{operation_setting => ratio_setting}/cache_ratio.go (99%) rename setting/{ => ratio_setting}/group_ratio.go (99%) rename setting/{operation_setting/model-ratio.go => ratio_setting/model_ratio.go} (99%) diff --git a/controller/group.go b/controller/group.go index 632b6cd5..2565b6ea 100644 --- a/controller/group.go +++ b/controller/group.go @@ -4,13 +4,14 @@ import ( "net/http" "one-api/model" "one-api/setting" + "one-api/setting/ratio_setting" "github.com/gin-gonic/gin" ) func GetGroups(c *gin.Context) { groupNames := make([]string, 0) - for groupName, _ := range setting.GetGroupRatioCopy() { + for groupName := range ratio_setting.GetGroupRatioCopy() { groupNames = append(groupNames, groupName) } c.JSON(http.StatusOK, gin.H{ @@ -25,7 +26,7 @@ func GetUserGroups(c *gin.Context) { userGroup := "" userId := c.GetInt("id") userGroup, _ = model.GetUserGroup(userId, false) - for groupName, ratio := range setting.GetGroupRatioCopy() { + for groupName, ratio := range ratio_setting.GetGroupRatioCopy() { // UserUsableGroups contains the groups that the user can use userUsableGroups := setting.GetUserUsableGroups(userGroup) if desc, ok := userUsableGroups[groupName]; ok { diff --git a/controller/option.go b/controller/option.go index 79ba2ffe..97bb6a5a 100644 --- a/controller/option.go +++ b/controller/option.go @@ -7,6 +7,7 @@ import ( "one-api/model" "one-api/setting" "one-api/setting/console_setting" + "one-api/setting/ratio_setting" "one-api/setting/system_setting" "strings" @@ -103,7 +104,7 @@ func UpdateOption(c *gin.Context) { return } case "GroupRatio": - err = setting.CheckGroupRatio(option.Value) + err = ratio_setting.CheckGroupRatio(option.Value) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, diff --git a/controller/pricing.go b/controller/pricing.go index e6a3e57f..f27336b7 100644 --- a/controller/pricing.go +++ b/controller/pricing.go @@ -3,7 +3,7 @@ package controller import ( "one-api/model" "one-api/setting" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "github.com/gin-gonic/gin" ) @@ -13,7 +13,7 @@ func GetPricing(c *gin.Context) { userId, exists := c.Get("id") usableGroup := map[string]string{} groupRatio := map[string]float64{} - for s, f := range setting.GetGroupRatioCopy() { + for s, f := range ratio_setting.GetGroupRatioCopy() { groupRatio[s] = f } var group string @@ -22,7 +22,7 @@ func GetPricing(c *gin.Context) { if err == nil { group = user.Group for g := range groupRatio { - ratio, ok := setting.GetGroupGroupRatio(group, g) + ratio, ok := ratio_setting.GetGroupGroupRatio(group, g) if ok { groupRatio[g] = ratio } @@ -32,7 +32,7 @@ func GetPricing(c *gin.Context) { usableGroup = setting.GetUserUsableGroups(group) // check groupRatio contains usableGroup - for group := range setting.GetGroupRatioCopy() { + for group := range ratio_setting.GetGroupRatioCopy() { if _, ok := usableGroup[group]; !ok { delete(groupRatio, group) } @@ -47,7 +47,7 @@ func GetPricing(c *gin.Context) { } func ResetModelRatio(c *gin.Context) { - defaultStr := operation_setting.DefaultModelRatio2JSONString() + defaultStr := ratio_setting.DefaultModelRatio2JSONString() err := model.UpdateOption("ModelRatio", defaultStr) if err != nil { c.JSON(200, gin.H{ @@ -56,7 +56,7 @@ func ResetModelRatio(c *gin.Context) { }) return } - err = operation_setting.UpdateModelRatioByJSONString(defaultStr) + err = ratio_setting.UpdateModelRatioByJSONString(defaultStr) if err != nil { c.JSON(200, gin.H{ "success": false, diff --git a/main.go b/main.go index 30ba8092..cf593b57 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( "one-api/model" "one-api/router" "one-api/service" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "os" "strconv" @@ -74,7 +74,7 @@ func main() { } // Initialize model settings - operation_setting.InitRatioSettings() + ratio_setting.InitRatioSettings() // Initialize constants constant.InitEnv() // Initialize options diff --git a/middleware/distributor.go b/middleware/distributor.go index 5d1c3641..84eb182e 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -11,6 +11,7 @@ import ( relayconstant "one-api/relay/constant" "one-api/service" "one-api/setting" + "one-api/setting/ratio_setting" "strconv" "strings" "time" @@ -48,7 +49,7 @@ func Distribute() func(c *gin.Context) { return } // check group in common.GroupRatio - if !setting.ContainsGroupRatio(tokenGroup) { + if !ratio_setting.ContainsGroupRatio(tokenGroup) { if tokenGroup != "auto" { abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup)) return diff --git a/model/option.go b/model/option.go index 1391b203..43c0a644 100644 --- a/model/option.go +++ b/model/option.go @@ -5,6 +5,7 @@ import ( "one-api/setting" "one-api/setting/config" "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "strconv" "strings" "time" @@ -96,13 +97,13 @@ func InitOptionMap() { common.OptionMap["ModelRequestRateLimitDurationMinutes"] = strconv.Itoa(setting.ModelRequestRateLimitDurationMinutes) common.OptionMap["ModelRequestRateLimitSuccessCount"] = strconv.Itoa(setting.ModelRequestRateLimitSuccessCount) common.OptionMap["ModelRequestRateLimitGroup"] = setting.ModelRequestRateLimitGroup2JSONString() - common.OptionMap["ModelRatio"] = operation_setting.ModelRatio2JSONString() - common.OptionMap["ModelPrice"] = operation_setting.ModelPrice2JSONString() - common.OptionMap["CacheRatio"] = operation_setting.CacheRatio2JSONString() - common.OptionMap["GroupRatio"] = setting.GroupRatio2JSONString() - common.OptionMap["GroupGroupRatio"] = setting.GroupGroupRatio2JSONString() + common.OptionMap["ModelRatio"] = ratio_setting.ModelRatio2JSONString() + common.OptionMap["ModelPrice"] = ratio_setting.ModelPrice2JSONString() + common.OptionMap["CacheRatio"] = ratio_setting.CacheRatio2JSONString() + common.OptionMap["GroupRatio"] = ratio_setting.GroupRatio2JSONString() + common.OptionMap["GroupGroupRatio"] = ratio_setting.GroupGroupRatio2JSONString() common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString() - common.OptionMap["CompletionRatio"] = operation_setting.CompletionRatio2JSONString() + common.OptionMap["CompletionRatio"] = ratio_setting.CompletionRatio2JSONString() common.OptionMap["TopUpLink"] = common.TopUpLink //common.OptionMap["ChatLink"] = common.ChatLink //common.OptionMap["ChatLink2"] = common.ChatLink2 @@ -358,19 +359,19 @@ func updateOptionMap(key string, value string) (err error) { case "DataExportDefaultTime": common.DataExportDefaultTime = value case "ModelRatio": - err = operation_setting.UpdateModelRatioByJSONString(value) + err = ratio_setting.UpdateModelRatioByJSONString(value) case "GroupRatio": - err = setting.UpdateGroupRatioByJSONString(value) + err = ratio_setting.UpdateGroupRatioByJSONString(value) case "GroupGroupRatio": - err = setting.UpdateGroupGroupRatioByJSONString(value) + err = ratio_setting.UpdateGroupGroupRatioByJSONString(value) case "UserUsableGroups": err = setting.UpdateUserUsableGroupsByJSONString(value) case "CompletionRatio": - err = operation_setting.UpdateCompletionRatioByJSONString(value) + err = ratio_setting.UpdateCompletionRatioByJSONString(value) case "ModelPrice": - err = operation_setting.UpdateModelPriceByJSONString(value) + err = ratio_setting.UpdateModelPriceByJSONString(value) case "CacheRatio": - err = operation_setting.UpdateCacheRatioByJSONString(value) + err = ratio_setting.UpdateCacheRatioByJSONString(value) case "TopUpLink": common.TopUpLink = value //case "ChatLink": diff --git a/model/pricing.go b/model/pricing.go index ba1815e2..74a25f2d 100644 --- a/model/pricing.go +++ b/model/pricing.go @@ -2,7 +2,7 @@ package model import ( "one-api/common" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "sync" "time" ) @@ -65,14 +65,14 @@ func updatePricing() { ModelName: model, EnableGroup: groups, } - modelPrice, findPrice := operation_setting.GetModelPrice(model, false) + modelPrice, findPrice := ratio_setting.GetModelPrice(model, false) if findPrice { pricing.ModelPrice = modelPrice pricing.QuotaType = 1 } else { - modelRatio, _ := operation_setting.GetModelRatio(model) + modelRatio, _ := ratio_setting.GetModelRatio(model) pricing.ModelRatio = modelRatio - pricing.CompletionRatio = operation_setting.GetCompletionRatio(model) + pricing.CompletionRatio = ratio_setting.GetCompletionRatio(model) pricing.QuotaType = 0 } pricingMap = append(pricingMap, pricing) diff --git a/relay/helper/price.go b/relay/helper/price.go index 326790b4..0d32c61a 100644 --- a/relay/helper/price.go +++ b/relay/helper/price.go @@ -5,8 +5,7 @@ import ( "one-api/common" constant2 "one-api/constant" relaycommon "one-api/relay/common" - "one-api/setting" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "github.com/gin-gonic/gin" ) @@ -49,21 +48,21 @@ func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) GroupR } // check user group special ratio - userGroupRatio, ok := setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group) + userGroupRatio, ok := ratio_setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group) if ok { // user group special ratio groupRatioInfo.GroupSpecialRatio = userGroupRatio groupRatioInfo.GroupRatio = userGroupRatio } else { // normal group ratio - groupRatioInfo.GroupRatio = setting.GetGroupRatio(relayInfo.Group) + groupRatioInfo.GroupRatio = ratio_setting.GetGroupRatio(relayInfo.Group) } return groupRatioInfo } func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, maxTokens int) (PriceData, error) { - modelPrice, usePrice := operation_setting.GetModelPrice(info.OriginModelName, false) + modelPrice, usePrice := ratio_setting.GetModelPrice(info.OriginModelName, false) groupRatioInfo := HandleGroupRatio(c, info) @@ -79,7 +78,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens preConsumedTokens = promptTokens + maxTokens } var success bool - modelRatio, success = operation_setting.GetModelRatio(info.OriginModelName) + modelRatio, success = ratio_setting.GetModelRatio(info.OriginModelName) if !success { acceptUnsetRatio := false if accept, ok := info.UserSetting[constant2.UserAcceptUnsetRatioModel]; ok { @@ -92,10 +91,10 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请联系管理员设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName) } } - completionRatio = operation_setting.GetCompletionRatio(info.OriginModelName) - cacheRatio, _ = operation_setting.GetCacheRatio(info.OriginModelName) - cacheCreationRatio, _ = operation_setting.GetCreateCacheRatio(info.OriginModelName) - imageRatio, _ = operation_setting.GetImageRatio(info.OriginModelName) + completionRatio = ratio_setting.GetCompletionRatio(info.OriginModelName) + cacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName) + cacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName) + imageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName) ratio := modelRatio * groupRatioInfo.GroupRatio preConsumedQuota = int(float64(preConsumedTokens) * ratio) } else { @@ -122,11 +121,11 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens } func ContainPriceOrRatio(modelName string) bool { - _, ok := operation_setting.GetModelPrice(modelName, false) + _, ok := ratio_setting.GetModelPrice(modelName, false) if ok { return true } - _, ok = operation_setting.GetModelRatio(modelName) + _, ok = ratio_setting.GetModelRatio(modelName) if ok { return true } diff --git a/relay/relay-mj.go b/relay/relay-mj.go index 9d0a2077..ce4346b6 100644 --- a/relay/relay-mj.go +++ b/relay/relay-mj.go @@ -15,7 +15,7 @@ import ( relayconstant "one-api/relay/constant" "one-api/service" "one-api/setting" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "strconv" "strings" "time" @@ -174,17 +174,17 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse { return service.MidjourneyErrorWrapper(constant.MjRequestError, "sour_base64_and_target_base64_is_required") } modelName := service.CoverActionToModelName(constant.MjActionSwapFace) - modelPrice, success := operation_setting.GetModelPrice(modelName, true) + modelPrice, success := ratio_setting.GetModelPrice(modelName, true) // 如果没有配置价格,则使用默认价格 if !success { - defaultPrice, ok := operation_setting.GetDefaultModelRatioMap()[modelName] + defaultPrice, ok := ratio_setting.GetDefaultModelRatioMap()[modelName] if !ok { modelPrice = 0.1 } else { modelPrice = defaultPrice } } - groupRatio := setting.GetGroupRatio(group) + groupRatio := ratio_setting.GetGroupRatio(group) ratio := modelPrice * groupRatio userQuota, err := model.GetUserQuota(userId, false) if err != nil { @@ -480,17 +480,17 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL) modelName := service.CoverActionToModelName(midjRequest.Action) - modelPrice, success := operation_setting.GetModelPrice(modelName, true) + modelPrice, success := ratio_setting.GetModelPrice(modelName, true) // 如果没有配置价格,则使用默认价格 if !success { - defaultPrice, ok := operation_setting.GetDefaultModelRatioMap()[modelName] + defaultPrice, ok := ratio_setting.GetDefaultModelRatioMap()[modelName] if !ok { modelPrice = 0.1 } else { modelPrice = defaultPrice } } - groupRatio := setting.GetGroupRatio(group) + groupRatio := ratio_setting.GetGroupRatio(group) ratio := modelPrice * groupRatio userQuota, err := model.GetUserQuota(userId, false) if err != nil { diff --git a/relay/relay_task.go b/relay/relay_task.go index 26874ba6..3da9a20f 100644 --- a/relay/relay_task.go +++ b/relay/relay_task.go @@ -15,8 +15,7 @@ import ( relaycommon "one-api/relay/common" relayconstant "one-api/relay/constant" "one-api/service" - "one-api/setting" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" ) /* @@ -38,9 +37,9 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) { } modelName := service.CoverTaskActionToModelName(platform, relayInfo.Action) - modelPrice, success := operation_setting.GetModelPrice(modelName, true) + modelPrice, success := ratio_setting.GetModelPrice(modelName, true) if !success { - defaultPrice, ok := operation_setting.GetDefaultModelRatioMap()[modelName] + defaultPrice, ok := ratio_setting.GetDefaultModelRatioMap()[modelName] if !ok { modelPrice = 0.1 } else { @@ -49,7 +48,7 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) { } // 预扣 - groupRatio := setting.GetGroupRatio(relayInfo.Group) + groupRatio := ratio_setting.GetGroupRatio(relayInfo.Group) ratio := modelPrice * groupRatio userQuota, err := model.GetUserQuota(relayInfo.UserId, false) if err != nil { diff --git a/service/quota.go b/service/quota.go index 0fb9e67c..973deba7 100644 --- a/service/quota.go +++ b/service/quota.go @@ -11,7 +11,7 @@ import ( relaycommon "one-api/relay/common" "one-api/relay/helper" "one-api/setting" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "strings" "time" @@ -46,9 +46,9 @@ func calculateAudioQuota(info QuotaInfo) int { return int(quota.IntPart()) } - completionRatio := decimal.NewFromFloat(operation_setting.GetCompletionRatio(info.ModelName)) - audioRatio := decimal.NewFromFloat(operation_setting.GetAudioRatio(info.ModelName)) - audioCompletionRatio := decimal.NewFromFloat(operation_setting.GetAudioCompletionRatio(info.ModelName)) + completionRatio := decimal.NewFromFloat(ratio_setting.GetCompletionRatio(info.ModelName)) + audioRatio := decimal.NewFromFloat(ratio_setting.GetAudioRatio(info.ModelName)) + audioCompletionRatio := decimal.NewFromFloat(ratio_setting.GetAudioCompletionRatio(info.ModelName)) groupRatio := decimal.NewFromFloat(info.GroupRatio) modelRatio := decimal.NewFromFloat(info.ModelRatio) @@ -94,18 +94,18 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag textOutTokens := usage.OutputTokenDetails.TextTokens audioInputTokens := usage.InputTokenDetails.AudioTokens audioOutTokens := usage.OutputTokenDetails.AudioTokens - groupRatio := setting.GetGroupRatio(relayInfo.Group) - modelRatio, _ := operation_setting.GetModelRatio(modelName) + groupRatio := ratio_setting.GetGroupRatio(relayInfo.Group) + modelRatio, _ := ratio_setting.GetModelRatio(modelName) autoGroup, exists := ctx.Get("auto_group") if exists { - groupRatio = setting.GetGroupRatio(autoGroup.(string)) + groupRatio = ratio_setting.GetGroupRatio(autoGroup.(string)) log.Printf("final group ratio: %f", groupRatio) relayInfo.Group = autoGroup.(string) } actualGroupRatio := groupRatio - userGroupRatio, ok := setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group) + userGroupRatio, ok := ratio_setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group) if ok { actualGroupRatio = userGroupRatio } @@ -154,9 +154,9 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod audioOutTokens := usage.OutputTokenDetails.AudioTokens tokenName := ctx.GetString("token_name") - completionRatio := decimal.NewFromFloat(operation_setting.GetCompletionRatio(modelName)) - audioRatio := decimal.NewFromFloat(operation_setting.GetAudioRatio(relayInfo.OriginModelName)) - audioCompletionRatio := decimal.NewFromFloat(operation_setting.GetAudioCompletionRatio(modelName)) + completionRatio := decimal.NewFromFloat(ratio_setting.GetCompletionRatio(modelName)) + audioRatio := decimal.NewFromFloat(ratio_setting.GetAudioRatio(relayInfo.OriginModelName)) + audioCompletionRatio := decimal.NewFromFloat(ratio_setting.GetAudioCompletionRatio(modelName)) modelRatio := priceData.ModelRatio groupRatio := priceData.GroupRatioInfo.GroupRatio @@ -289,9 +289,9 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, audioOutTokens := usage.CompletionTokenDetails.AudioTokens tokenName := ctx.GetString("token_name") - completionRatio := decimal.NewFromFloat(operation_setting.GetCompletionRatio(relayInfo.OriginModelName)) - audioRatio := decimal.NewFromFloat(operation_setting.GetAudioRatio(relayInfo.OriginModelName)) - audioCompletionRatio := decimal.NewFromFloat(operation_setting.GetAudioCompletionRatio(relayInfo.OriginModelName)) + completionRatio := decimal.NewFromFloat(ratio_setting.GetCompletionRatio(relayInfo.OriginModelName)) + audioRatio := decimal.NewFromFloat(ratio_setting.GetAudioRatio(relayInfo.OriginModelName)) + audioCompletionRatio := decimal.NewFromFloat(ratio_setting.GetAudioCompletionRatio(relayInfo.OriginModelName)) modelRatio := priceData.ModelRatio groupRatio := priceData.GroupRatioInfo.GroupRatio diff --git a/setting/operation_setting/cache_ratio.go b/setting/ratio_setting/cache_ratio.go similarity index 99% rename from setting/operation_setting/cache_ratio.go rename to setting/ratio_setting/cache_ratio.go index ec0c766d..aa934b22 100644 --- a/setting/operation_setting/cache_ratio.go +++ b/setting/ratio_setting/cache_ratio.go @@ -1,4 +1,4 @@ -package operation_setting +package ratio_setting import ( "encoding/json" diff --git a/setting/group_ratio.go b/setting/ratio_setting/group_ratio.go similarity index 99% rename from setting/group_ratio.go rename to setting/ratio_setting/group_ratio.go index 28dbd167..f600a7b5 100644 --- a/setting/group_ratio.go +++ b/setting/ratio_setting/group_ratio.go @@ -1,4 +1,4 @@ -package setting +package ratio_setting import ( "encoding/json" diff --git a/setting/operation_setting/model-ratio.go b/setting/ratio_setting/model_ratio.go similarity index 99% rename from setting/operation_setting/model-ratio.go rename to setting/ratio_setting/model_ratio.go index 5155b2fc..3102dfe9 100644 --- a/setting/operation_setting/model-ratio.go +++ b/setting/ratio_setting/model_ratio.go @@ -1,8 +1,9 @@ -package operation_setting +package ratio_setting import ( "encoding/json" "one-api/common" + "one-api/setting/operation_setting" "strings" "sync" ) @@ -366,7 +367,7 @@ func GetModelRatio(name string) (float64, bool) { } ratio, ok := modelRatioMap[name] if !ok { - return 37.5, SelfUseModeEnabled + return 37.5, operation_setting.SelfUseModeEnabled } return ratio, true } From 7572e791f6c72ac231b7a5af558b22f1a5ea0be9 Mon Sep 17 00:00:00 2001 From: CaIon <1808837298@qq.com> Date: Wed, 18 Jun 2025 20:50:13 +0800 Subject: [PATCH 08/14] feat(relay): add debug logging for Gemini request body and introduce flexible speech configuration --- relay/channel/gemini/dto.go | 1 + relay/relay-gemini.go | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/relay/channel/gemini/dto.go b/relay/channel/gemini/dto.go index fa9108df..b22e092a 100644 --- a/relay/channel/gemini/dto.go +++ b/relay/channel/gemini/dto.go @@ -140,6 +140,7 @@ type GeminiChatGenerationConfig struct { Seed int64 `json:"seed,omitempty"` ResponseModalities []string `json:"responseModalities,omitempty"` ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"` + SpeechConfig json.RawMessage `json:"speechConfig,omitempty"` // RawMessage to allow flexible speech config } type GeminiChatCandidate struct { diff --git a/relay/relay-gemini.go b/relay/relay-gemini.go index 21cf5e12..9edbe5c2 100644 --- a/relay/relay-gemini.go +++ b/relay/relay-gemini.go @@ -155,6 +155,10 @@ func GeminiHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) { return service.OpenAIErrorWrapperLocal(err, "marshal_text_request_failed", http.StatusInternalServerError) } + if common.DebugEnabled { + println("Gemini request body: %s", string(requestBody)) + } + resp, err := adaptor.DoRequest(c, relayInfo, bytes.NewReader(requestBody)) if err != nil { common.LogError(c, "Do gemini request failed: "+err.Error()) From 9708d645d3cec60bcbc82ae36de548dafa5e4d2e Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Wed, 18 Jun 2025 21:23:06 +0800 Subject: [PATCH 09/14] =?UTF-8?q?feat:=20=E5=85=85=E5=80=BC=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/misc.go | 1 + model/option.go | 3 + setting/payment.go | 37 ++ web/src/components/settings/SystemSetting.js | 18 +- web/src/helpers/data.js | 1 + web/src/pages/TopUp/index.js | 466 ++++++++++++------- 6 files changed, 362 insertions(+), 164 deletions(-) diff --git a/controller/misc.go b/controller/misc.go index 1caaf640..4ffe86f4 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -76,6 +76,7 @@ func GetStatus(c *gin.Context) { "demo_site_enabled": operation_setting.DemoSiteEnabled, "self_use_mode_enabled": operation_setting.SelfUseModeEnabled, "default_use_auto_group": setting.DefaultUseAutoGroup, + "pay_methods": setting.PayMethods, // 面板启用开关 "api_info_enabled": cs.ApiInfoEnabled, diff --git a/model/option.go b/model/option.go index 43c0a644..ec386b29 100644 --- a/model/option.go +++ b/model/option.go @@ -79,6 +79,7 @@ func InitOptionMap() { common.OptionMap["Chats"] = setting.Chats2JsonString() common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString() common.OptionMap["DefaultUseAutoGroup"] = strconv.FormatBool(setting.DefaultUseAutoGroup) + common.OptionMap["PayMethods"] = setting.PayMethods2JsonString() common.OptionMap["GitHubClientId"] = "" common.OptionMap["GitHubClientSecret"] = "" common.OptionMap["TelegramBotToken"] = "" @@ -388,6 +389,8 @@ func updateOptionMap(key string, value string) (err error) { operation_setting.AutomaticDisableKeywordsFromString(value) case "StreamCacheQueueLength": setting.StreamCacheQueueLength, _ = strconv.Atoi(value) + case "PayMethods": + err = setting.UpdatePayMethodsByJsonString(value) } return err } diff --git a/setting/payment.go b/setting/payment.go index f50723c3..4ffa4381 100644 --- a/setting/payment.go +++ b/setting/payment.go @@ -1,8 +1,45 @@ package setting +import "encoding/json" + var PayAddress = "" var CustomCallbackAddress = "" var EpayId = "" var EpayKey = "" var Price = 7.3 var MinTopUp = 1 + +var PayMethods = []map[string]string{ + { + "name": "支付宝", + "color": "rgba(var(--semi-blue-5), 1)", + "type": "zfb", + }, + { + "name": "微信", + "color": "rgba(var(--semi-green-5), 1)", + "type": "wx", + }, +} + +func UpdatePayMethodsByJsonString(jsonString string) error { + PayMethods = make([]map[string]string, 0) + return json.Unmarshal([]byte(jsonString), &PayMethods) +} + +func PayMethods2JsonString() string { + jsonBytes, err := json.Marshal(PayMethods) + if err != nil { + return "[]" + } + return string(jsonBytes) +} + +func ContainsPayMethod(method string) bool { + for _, payMethod := range PayMethods { + if payMethod["type"] == method { + return true + } + } + return false +} diff --git a/web/src/components/settings/SystemSetting.js b/web/src/components/settings/SystemSetting.js index 8219159b..1236ef2e 100644 --- a/web/src/components/settings/SystemSetting.js +++ b/web/src/components/settings/SystemSetting.js @@ -17,7 +17,7 @@ import { removeTrailingSlash, showError, showSuccess, - verifyJSON + verifyJSON, } from '../../helpers'; import axios from 'axios'; @@ -73,6 +73,7 @@ const SystemSetting = () => { LinuxDOOAuthEnabled: '', LinuxDOClientId: '', LinuxDOClientSecret: '', + PayMethods: '', }); const [originInputs, setOriginInputs] = useState({}); @@ -230,6 +231,12 @@ const SystemSetting = () => { return; } } + if (originInputs['PayMethods'] !== inputs.PayMethods) { + if (!verifyJSON(inputs.PayMethods)) { + showError('充值方式设置不是合法的 JSON 字符串'); + return; + } + } const options = [ { key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) }, @@ -256,6 +263,9 @@ const SystemSetting = () => { 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); }; @@ -658,6 +668,12 @@ const SystemSetting = () => { placeholder='为一个 JSON 文本,键为组名称,值为倍率' autosize /> + diff --git a/web/src/helpers/data.js b/web/src/helpers/data.js index bc1d28aa..afc29384 100644 --- a/web/src/helpers/data.js +++ b/web/src/helpers/data.js @@ -9,6 +9,7 @@ export function setStatusData(data) { localStorage.setItem('enable_task', data.enable_task); localStorage.setItem('enable_data_export', data.enable_data_export); localStorage.setItem('chats', JSON.stringify(data.chats)); + localStorage.setItem('pay_methods', JSON.stringify(data.pay_methods)); localStorage.setItem( 'data_export_default_time', data.data_export_default_time, diff --git a/web/src/pages/TopUp/index.js b/web/src/pages/TopUp/index.js index cb32bca2..e327178d 100644 --- a/web/src/pages/TopUp/index.js +++ b/web/src/pages/TopUp/index.js @@ -7,7 +7,7 @@ import { renderQuota, renderQuotaWithAmount, copy, - getQuotaPerUnit + getQuotaPerUnit, } from '../../helpers'; import { Avatar, @@ -34,7 +34,7 @@ import { Copy, Users, User, - Coins + Coins, } from 'lucide-react'; const { Text, Title } = Typography; @@ -49,9 +49,15 @@ const TopUp = () => { const [topUpCode, setTopUpCode] = useState(''); const [amount, setAmount] = useState(0.0); const [minTopUp, setMinTopUp] = useState(statusState?.status?.min_topup || 1); - const [topUpCount, setTopUpCount] = useState(statusState?.status?.min_topup || 1); - const [topUpLink, setTopUpLink] = useState(statusState?.status?.top_up_link || ''); - const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(statusState?.status?.enable_online_topup || false); + const [topUpCount, setTopUpCount] = useState( + statusState?.status?.min_topup || 1, + ); + const [topUpLink, setTopUpLink] = useState( + statusState?.status?.top_up_link || '', + ); + const [enableOnlineTopUp, setEnableOnlineTopUp] = useState( + statusState?.status?.enable_online_topup || false, + ); const [priceRatio, setPriceRatio] = useState(statusState?.status?.price || 1); const [userQuota, setUserQuota] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); @@ -61,6 +67,7 @@ const TopUp = () => { const [amountLoading, setAmountLoading] = useState(false); const [paymentLoading, setPaymentLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false); + const [payMethods, setPayMethods] = useState([]); // 邀请相关状态 const [affLink, setAffLink] = useState(''); @@ -76,7 +83,7 @@ const TopUp = () => { { value: 100 }, { value: 300 }, { value: 500 }, - { value: 1000 } + { value: 1000 }, ]); const [selectedPreset, setSelectedPreset] = useState(null); @@ -126,7 +133,7 @@ const TopUp = () => { if (userState.user) { const updatedUser = { ...userState.user, - quota: userState.user.quota + data + quota: userState.user.quota + data, }; userDispatch({ type: 'login', payload: updatedUser }); } @@ -283,6 +290,34 @@ const TopUp = () => { } getAffLink().then(); setTransferAmount(getQuotaPerUnit()); + + let payMethods = localStorage.getItem('pay_methods'); + try { + payMethods = JSON.parse(payMethods); + if (payMethods && payMethods.length > 0) { + // 检查name和type是否为空 + payMethods = payMethods.filter((method) => { + return method.name && method.type; + }); + // 如果没有color,则设置默认颜色 + payMethods = payMethods.map((method) => { + if (!method.color) { + if (method.type === 'zfb') { + method.color = 'rgba(var(--semi-blue-5), 1)'; + } else if (method.type === 'wx') { + method.color = 'rgba(var(--semi-green-5), 1)'; + } else { + method.color = 'rgba(var(--semi-primary-5), 1)'; + } + } + return method; + }); + setPayMethods(payMethods); + } + } catch (e) { + console.log(e); + showError(t('支付方式配置错误, 请联系管理员')); + } }, []); useEffect(() => { @@ -347,12 +382,12 @@ const TopUp = () => { }; return ( -
+
{/* 划转模态框 */} - +
+ {t('划转邀请额度')}
} @@ -360,22 +395,22 @@ const TopUp = () => { onOk={transfer} onCancel={handleTransferCancel} maskClosable={false} - size="small" + size='small' centered > -
+
- + {t('可用邀请额度')}
- + {t('划转额度')} ({t('最低') + renderQuota(getQuotaPerUnit())}) { max={userState?.user?.aff_quota || 0} value={transferAmount} onChange={(value) => setTransferAmount(value)} - size="large" - className="w-full" + size='large' + className='w-full' />
@@ -393,8 +428,8 @@ const TopUp = () => { {/* 充值确认模态框 */} - +
+ {t('充值确认')}
} @@ -402,57 +437,80 @@ const TopUp = () => { onOk={onlineTopUp} onCancel={handleCancel} maskClosable={false} - size="small" + size='small' centered confirmLoading={confirmLoading} > -
-
+
+
{t('充值数量')}: {renderQuotaWithAmount(topUpCount)}
-
+
{t('实付金额')}: {amountLoading ? ( ) : ( - {renderAmount()} + + {renderAmount()} + )}
-
+
{t('支付方式')}: - {payWay === 'zfb' ? ( -
- - {t('支付宝')} -
- ) : ( -
- - {t('微信')} -
- )} + {(() => { + const payMethod = payMethods.find( + (method) => method.type === payWay, + ); + if (payMethod) { + return ( +
+ {payMethod.type === 'zfb' ? ( + + ) : payMethod.type === 'wx' ? ( + + ) : ( + + )} + {payMethod.name} +
+ ); + } else { + // 默认充值方式 + return payWay === 'zfb' ? ( +
+ + {t('支付宝')} +
+ ) : ( +
+ + {t('微信')} +
+ ); + } + })()}
-
+
{/* 左侧充值区域 */} -
+
{/* 在线充值卡片 */} -
-
+
+
+
@@ -460,21 +518,23 @@ const TopUp = () => { {t('在线充值')} - + {t('快速方便的充值方式')}
-
+
{userDataLoading ? ( ) : ( - -
- - {getUsername()} ({getUserRole()}) - {getUsername()} + +
+ + + {getUsername()} ({getUserRole()}) + + {getUsername()}
)} @@ -483,29 +543,33 @@ const TopUp = () => {
} > -
+
{/* 账户余额信息 */} -
- - +
+ + {t('当前余额')} {userDataLoading ? ( - + ) : ( -
+
{renderQuota(userState?.user?.quota || userQuota)}
)} - - + + {t('历史消耗')} {userDataLoading ? ( - + ) : ( -
+
{renderQuota(userState?.user?.used_quota || 0)}
)} @@ -516,47 +580,59 @@ const TopUp = () => { <> {/* 预设充值额度卡片网格 */}
- {t('选择充值额度')} -
+ + {t('选择充值额度')} + +
{presetAmounts.map((preset, index) => ( selectPresetAmount(preset)} - className={`cursor-pointer !rounded-2xl transition-all hover:shadow-md ${selectedPreset === preset.value - ? 'border-blue-500' - : 'border-gray-200 hover:border-gray-300' - }`} + className={`cursor-pointer !rounded-2xl transition-all hover:shadow-md ${ + selectedPreset === preset.value + ? 'border-blue-500' + : 'border-gray-200 hover:border-gray-300' + }`} bodyStyle={{ textAlign: 'center' }} > -
- +
+ {formatLargeNumber(preset.value)}
-
- {t('实付')} ¥{(preset.value * priceRatio).toFixed(2)} +
+ {t('实付')} ¥ + {(preset.value * priceRatio).toFixed(2)}
))}
{/* 桌面端显示的自定义金额和支付按钮 */} -
+
- {t('或输入自定义金额')} + + {t('或输入自定义金额')} +
-
+
{t('充值数量')} {amountLoading ? ( - + ) : ( - {t('实付金额:') + renderAmount()} + + {t('实付金额:') + renderAmount()} + )}
{ getAmount(1); } }} - size="large" - className="w-full" - formatter={(value) => value ? `${value}` : ''} - parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0} + size='large' + className='w-full' + formatter={(value) => (value ? `${value}` : '')} + parser={(value) => + value ? parseInt(value.replace(/[^\d]/g, '')) : 0 + } />
-
- + {t('微信')} + */} + {payMethods.map((payMethod) => ( + + ))}
@@ -613,39 +716,41 @@ const TopUp = () => { {!enableOnlineTopUp && ( )} - {t('兑换码充值')} + {t('兑换码充值')} - -
- + +
+ {t('使用兑换码快速充值')}
-
+
setRedemptionCode(value)} - size="large" + size='large' />
-
+
{topUpLink && ( )}
{/* 右侧邀请信息卡片 */} -
+
-
-
+
+
+
@@ -689,7 +794,7 @@ const TopUp = () => { {t('邀请奖励')} - + {t('邀请好友获得额外奖励')}
@@ -698,53 +803,56 @@ const TopUp = () => {
} > -
-
- -
- {t('待使用收益')} +
+
+ +
+ {t('待使用收益')}
-
+
{renderQuota(userState?.user?.aff_quota || 0)}
-
- - {t('总收益')} -
+
+ + {t('总收益')} +
{renderQuota(userState?.user?.aff_history_quota || 0)}
- - {t('邀请人数')} -
- + + {t('邀请人数')} +
+ {userState?.user?.aff_count || 0}
-
+
{t('邀请链接')} } > @@ -753,24 +861,24 @@ const TopUp = () => { } /> -
- -
-
-
- +
+ +
+
+
+ {t('邀请好友注册,好友充值后您可获得相应奖励')}
-
-
- +
+
+ {t('通过划转功能将奖励额度转入到您的账户余额中')}
-
-
- +
+
+ {t('邀请的好友越多,获得的奖励越多')}
@@ -785,20 +893,27 @@ const TopUp = () => { {/* 移动端底部固定的自定义金额和支付区域 */} {enableOnlineTopUp && ( -
-
+
+
-
+
{t('充值数量')} {amountLoading ? ( ) : ( - {t('实付金额:') + renderAmount()} + + {t('实付金额:') + renderAmount()} + )}
{ getAmount(1); } }} - className="w-full" - formatter={(value) => value ? `${value}` : ''} - parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0} + className='w-full' + formatter={(value) => (value ? `${value}` : '')} + parser={(value) => + value ? parseInt(value.replace(/[^\d]/g, '')) : 0 + } />
-
- + {t('微信')} + */} + {payMethods.map((payMethod) => ( + + ))}
From 133d8c9f77ebda5d3d8740bc8297485afbb255a9 Mon Sep 17 00:00:00 2001 From: skynono Date: Wed, 18 Jun 2025 21:55:42 +0800 Subject: [PATCH 10/14] fix: task cost time --- web/src/components/table/TaskLogsTable.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/web/src/components/table/TaskLogsTable.js b/web/src/components/table/TaskLogsTable.js index 449b3d55..b3d0ab7b 100644 --- a/web/src/components/table/TaskLogsTable.js +++ b/web/src/components/table/TaskLogsTable.js @@ -96,20 +96,8 @@ const renderTimestamp = (timestampInSeconds) => { }; function renderDuration(submit_time, finishTime) { - // 确保startTime和finishTime都是有效的时间戳 if (!submit_time || !finishTime) return 'N/A'; - - // 将时间戳转换为Date对象 - const start = new Date(submit_time); - const finish = new Date(finishTime); - - // 计算时间差(毫秒) - const durationMs = finish - start; - - // 将时间差转换为秒,并保留一位小数 - const durationSec = (durationMs / 1000).toFixed(1); - - // 设置颜色:大于60秒则为红色,小于等于60秒则为绿色 + const durationSec = finishTime - submit_time; const color = durationSec > 60 ? 'red' : 'green'; // 返回带有样式的颜色标签 From a87d4271d39980861622d6635b3762d4010463ca Mon Sep 17 00:00:00 2001 From: tbphp Date: Thu, 19 Jun 2025 11:25:59 +0800 Subject: [PATCH 11/14] fix: Gemini & Vertex empty content error --- relay/channel/gemini/relay-gemini.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index 635041d7..d4b7c209 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -374,7 +374,9 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon if content.Role == "assistant" { content.Role = "model" } - geminiRequest.Contents = append(geminiRequest.Contents, content) + if len(content.Parts) > 0 { + geminiRequest.Contents = append(geminiRequest.Contents, content) + } } if len(system_content) > 0 { From d5a3eb7d0467f072eb1f21f900838de3a7601ab8 Mon Sep 17 00:00:00 2001 From: KamiPasi <92498495+KamiPasi@users.noreply.github.com> Date: Thu, 19 Jun 2025 12:06:42 +0800 Subject: [PATCH 12/14] =?UTF-8?q?=E9=80=8F=E4=BC=A0thinking=E5=8F=82?= =?UTF-8?q?=E6=95=B0,=20=E8=B1=86=E5=8C=85=E6=A8=A1=E5=9E=8B=E7=94=A8?= =?UTF-8?q?=E6=9D=A5=E6=8E=A7=E5=88=B6=E6=98=AF=E5=90=A6=E6=80=9D=E8=80=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dto/openai_request.go | 1 + 1 file changed, 1 insertion(+) diff --git a/dto/openai_request.go b/dto/openai_request.go index 299171ba..42c290ca 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -53,6 +53,7 @@ type GeneralOpenAIRequest struct { Modalities json.RawMessage `json:"modalities,omitempty"` Audio json.RawMessage `json:"audio,omitempty"` EnableThinking any `json:"enable_thinking,omitempty"` // ali + THINKING json.RawMessage `json:"thinking,omitempty"` // doubao ExtraBody json.RawMessage `json:"extra_body,omitempty"` WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"` // OpenRouter Params From 070e7b69118741dd96506d90bb446216c775f616 Mon Sep 17 00:00:00 2001 From: wans10 <51012039+wans10@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:34:11 +0800 Subject: [PATCH 13/14] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B8=A0=E9=81=93?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E6=A8=A1=E5=9E=8B=E9=80=89=E6=8B=A9=E4=B8=8B?= =?UTF-8?q?=E6=8B=89=E6=A1=86=E6=A8=A1=E5=9E=8B=E9=87=8D=E5=A4=8D=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pages/Channel/EditChannel.js | 33 ++++++++++++++++++---------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 8bfe5812..09b5ecae 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -298,18 +298,27 @@ const EditChannel = (props) => { } }; - useEffect(() => { - let localModelOptions = [...originModelOptions]; - inputs.models.forEach((model) => { - if (!localModelOptions.find((option) => option.label === model)) { - localModelOptions.push({ - label: model, - value: model, - }); - } - }); - setModelOptions(localModelOptions); - }, [originModelOptions, inputs.models]); +useEffect(() => { + // 使用 Map 来避免重复,以 value 为键 + const modelMap = new Map(); + + // 先添加原始模型选项 + originModelOptions.forEach(option => { + modelMap.set(option.value, option); + }); + + // 再添加当前选中的模型(如果不存在) + inputs.models.forEach(model => { + if (!modelMap.has(model)) { + modelMap.set(model, { + label: model, + value: model, + }); + } + }); + + setModelOptions(Array.from(modelMap.values())); +}, [originModelOptions, inputs.models]); useEffect(() => { fetchModels().then(); From 8e9dae7b5fecbfeb37270c3bfde4ae1c0990f419 Mon Sep 17 00:00:00 2001 From: CaIon <1808837298@qq.com> Date: Thu, 19 Jun 2025 15:36:06 +0800 Subject: [PATCH 14/14] fix: ratio render --- relay/helper/price.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relay/helper/price.go b/relay/helper/price.go index 0d32c61a..1ee2767e 100644 --- a/relay/helper/price.go +++ b/relay/helper/price.go @@ -35,7 +35,7 @@ func (p PriceData) ToSetting() string { func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) GroupRatioInfo { groupRatioInfo := GroupRatioInfo{ GroupRatio: 1.0, // default ratio - GroupSpecialRatio: 1.0, // default user group ratio + GroupSpecialRatio: -1, } // check auto group