From f48e8d51802ee58badf834736fbba5b53a2c9ca9 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Thu, 11 Sep 2025 10:34:51 +0800 Subject: [PATCH 01/33] feat: add thousand separators to token display in dashboard --- web/src/hooks/dashboard/useDashboardStats.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/hooks/dashboard/useDashboardStats.jsx b/web/src/hooks/dashboard/useDashboardStats.jsx index aa9677a5..dbf3b67e 100644 --- a/web/src/hooks/dashboard/useDashboardStats.jsx +++ b/web/src/hooks/dashboard/useDashboardStats.jsx @@ -102,7 +102,7 @@ export const useDashboardStats = ( }, { title: t('统计Tokens'), - value: isNaN(consumeTokens) ? 0 : consumeTokens, + value: isNaN(consumeTokens) ? 0 : consumeTokens.toLocaleString(), icon: , avatarColor: 'pink', trendData: trendData.tokens, From 2836ec2eb393f5890c3d3c2ea485baf8e14a82ef Mon Sep 17 00:00:00 2001 From: QuentinHsu Date: Mon, 15 Sep 2025 21:45:00 +0800 Subject: [PATCH 02/33] feat: add date range preset constants and use them in the log filter --- .../table/mj-logs/MjLogsFilters.jsx | 7 +++ .../table/task-logs/TaskLogsFilters.jsx | 7 +++ .../table/usage-logs/UsageLogsFilters.jsx | 7 +++ web/src/constants/console.constants.js | 49 +++++++++++++++++++ web/src/i18n/locales/en.json | 7 ++- 5 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 web/src/constants/console.constants.js diff --git a/web/src/components/table/mj-logs/MjLogsFilters.jsx b/web/src/components/table/mj-logs/MjLogsFilters.jsx index 44c6bcfc..6db96e79 100644 --- a/web/src/components/table/mj-logs/MjLogsFilters.jsx +++ b/web/src/components/table/mj-logs/MjLogsFilters.jsx @@ -21,6 +21,8 @@ import React from 'react'; import { Button, Form } from '@douyinfe/semi-ui'; import { IconSearch } from '@douyinfe/semi-icons'; +import { DATE_RANGE_PRESETS } from '../../../constants/console.constants'; + const MjLogsFilters = ({ formInitValues, setFormApi, @@ -54,6 +56,11 @@ const MjLogsFilters = ({ showClear pure size='small' + presets={DATE_RANGE_PRESETS.map(preset => ({ + text: t(preset.text), + start: preset.start(), + end: preset.end() + }))} /> diff --git a/web/src/components/table/task-logs/TaskLogsFilters.jsx b/web/src/components/table/task-logs/TaskLogsFilters.jsx index d5e081ab..e27cea86 100644 --- a/web/src/components/table/task-logs/TaskLogsFilters.jsx +++ b/web/src/components/table/task-logs/TaskLogsFilters.jsx @@ -21,6 +21,8 @@ import React from 'react'; import { Button, Form } from '@douyinfe/semi-ui'; import { IconSearch } from '@douyinfe/semi-icons'; +import { DATE_RANGE_PRESETS } from '../../../constants/console.constants'; + const TaskLogsFilters = ({ formInitValues, setFormApi, @@ -54,6 +56,11 @@ const TaskLogsFilters = ({ showClear pure size='small' + presets={DATE_RANGE_PRESETS.map(preset => ({ + text: t(preset.text), + start: preset.start(), + end: preset.end() + }))} /> diff --git a/web/src/components/table/usage-logs/UsageLogsFilters.jsx b/web/src/components/table/usage-logs/UsageLogsFilters.jsx index f76ec823..58e5a469 100644 --- a/web/src/components/table/usage-logs/UsageLogsFilters.jsx +++ b/web/src/components/table/usage-logs/UsageLogsFilters.jsx @@ -21,6 +21,8 @@ import React from 'react'; import { Button, Form } from '@douyinfe/semi-ui'; import { IconSearch } from '@douyinfe/semi-icons'; +import { DATE_RANGE_PRESETS } from '../../../constants/console.constants'; + const LogsFilters = ({ formInitValues, setFormApi, @@ -55,6 +57,11 @@ const LogsFilters = ({ showClear pure size='small' + presets={DATE_RANGE_PRESETS.map(preset => ({ + text: t(preset.text), + start: preset.start(), + end: preset.end() + }))} /> diff --git a/web/src/constants/console.constants.js b/web/src/constants/console.constants.js new file mode 100644 index 00000000..23ee1e17 --- /dev/null +++ b/web/src/constants/console.constants.js @@ -0,0 +1,49 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import dayjs from 'dayjs'; + +// ========== 日期预设常量 ========== +export const DATE_RANGE_PRESETS = [ + { + text: '今天', + start: () => dayjs().startOf('day').toDate(), + end: () => dayjs().endOf('day').toDate() + }, + { + text: '近 7 天', + start: () => dayjs().subtract(6, 'day').startOf('day').toDate(), + end: () => dayjs().endOf('day').toDate() + }, + { + text: '本周', + start: () => dayjs().startOf('week').toDate(), + end: () => dayjs().endOf('week').toDate() + }, + { + text: '近 30 天', + start: () => dayjs().subtract(29, 'day').startOf('day').toDate(), + end: () => dayjs().endOf('day').toDate() + }, + { + text: '本月', + start: () => dayjs().startOf('month').toDate(), + end: () => dayjs().endOf('month').toDate() + }, +]; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 73dfbebe..a527b91c 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -2084,5 +2084,10 @@ "原价": "Original price", "优惠": "Discount", "折": "% off", - "节省": "Save" + "节省": "Save", + "今天": "Today", + "近 7 天": "Last 7 Days", + "本周": "This Week", + "本月": "This Month", + "近 30 天": "Last 30 Days" } From fc4660f403771a2577521184d7abcf6c97022ad0 Mon Sep 17 00:00:00 2001 From: QuentinHsu Date: Mon, 15 Sep 2025 22:30:41 +0800 Subject: [PATCH 03/33] feat: add jsconfig.json and configure path aliases --- web/jsconfig.json | 9 +++++++++ web/vite.config.js | 6 ++++++ 2 files changed, 15 insertions(+) create mode 100644 web/jsconfig.json diff --git a/web/jsconfig.json b/web/jsconfig.json new file mode 100644 index 00000000..ced4d054 --- /dev/null +++ b/web/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*"] +} \ No newline at end of file diff --git a/web/vite.config.js b/web/vite.config.js index 3515dce7..d57fd9d9 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -20,10 +20,16 @@ For commercial licensing, please contact support@quantumnous.com import react from '@vitejs/plugin-react'; import { defineConfig, transformWithEsbuild } from 'vite'; import pkg from '@douyinfe/vite-plugin-semi'; +import path from 'path'; const { vitePluginSemi } = pkg; // https://vitejs.dev/config/ export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, plugins: [ { name: 'treat-js-files-as-jsx', From 1c13fc0e04b98495930639493816edb67d755a70 Mon Sep 17 00:00:00 2001 From: MyPrototypeWhat Date: Thu, 18 Sep 2025 12:01:35 +0800 Subject: [PATCH 04/33] refactor: Enhance UserArea dropdown positioning with useRef - Added useRef to manage dropdown positioning in UserArea component. - Wrapped Dropdown in a div with a ref to ensure correct popup container. - Minor adjustments to maintain existing functionality and styling. --- .../components/layout/headerbar/UserArea.jsx | 170 +++++++++--------- 1 file changed, 87 insertions(+), 83 deletions(-) diff --git a/web/src/components/layout/headerbar/UserArea.jsx b/web/src/components/layout/headerbar/UserArea.jsx index 8ea70f47..9fc011da 100644 --- a/web/src/components/layout/headerbar/UserArea.jsx +++ b/web/src/components/layout/headerbar/UserArea.jsx @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React from 'react'; +import React, { useRef } from 'react'; import { Link } from 'react-router-dom'; import { Avatar, Button, Dropdown, Typography } from '@douyinfe/semi-ui'; import { ChevronDown } from 'lucide-react'; @@ -39,6 +39,7 @@ const UserArea = ({ navigate, t, }) => { + const dropdownRef = useRef(null); if (isLoading) { return ( - { - navigate('/console/personal'); - }} - className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white' - > -
- - {t('个人设置')} -
-
- { - navigate('/console/token'); - }} - className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white' - > -
- - {t('令牌管理')} -
-
- { - navigate('/console/topup'); - }} - className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white' - > -
- - {t('钱包管理')} -
-
- -
- - {t('退出')} -
-
- - } - > - - + + {userState.user.username[0].toUpperCase()} + + + + {userState.user.username} + + + + + + ); } else { const showRegisterButton = !isSelfUseMode; From 241b92b28edec5455851adbe802f157893cf7019 Mon Sep 17 00:00:00 2001 From: joesonshaw Date: Fri, 19 Sep 2025 10:49:47 +0800 Subject: [PATCH 05/33] =?UTF-8?q?fix(relay-xunfei):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=AE=AF=E9=A3=9E=E6=B8=A0=E9=81=93=E6=97=A0=E6=B3=95=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E9=97=AE=E9=A2=98=20#1740?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将连接延迟关闭逻辑调整到协程中执行,防止在完全接收到所有数据前提前关闭 --- relay/channel/xunfei/relay-xunfei.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/relay/channel/xunfei/relay-xunfei.go b/relay/channel/xunfei/relay-xunfei.go index 9d5c190f..9503d5d3 100644 --- a/relay/channel/xunfei/relay-xunfei.go +++ b/relay/channel/xunfei/relay-xunfei.go @@ -207,10 +207,6 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap return nil, nil, err } - defer func() { - conn.Close() - }() - data := requestOpenAI2Xunfei(textRequest, appId, domain) err = conn.WriteJSON(data) if err != nil { @@ -220,6 +216,9 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap dataChan := make(chan XunfeiChatResponse) stopChan := make(chan bool) go func() { + defer func() { + conn.Close() + }() for { _, msg, err := conn.ReadMessage() if err != nil { From 354e866a5b4779aa2d5437377749b57f90a0bf05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E3=80=82?= Date: Mon, 1 Sep 2025 09:52:52 +0800 Subject: [PATCH 06/33] =?UTF-8?q?=E4=BF=AE=E5=A4=8D"=E8=BE=B9=E6=A0=8F"?= =?UTF-8?q?=E9=9A=90=E8=97=8F=E5=90=8E=E6=97=A0=E6=B3=95=E5=8D=B3=E6=97=B6?= =?UTF-8?q?=E7=94=9F=E6=95=88=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../personal/cards/NotificationSettings.jsx | 9 ++++++- web/src/hooks/common/useSidebar.js | 27 ++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index 0b097eaf..aad612d2 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -44,6 +44,7 @@ import CodeViewer from '../../../playground/CodeViewer'; import { StatusContext } from '../../../../context/Status'; import { UserContext } from '../../../../context/User'; import { useUserPermissions } from '../../../../hooks/common/useUserPermissions'; +import { useSidebar } from '../../../../hooks/common/useSidebar'; const NotificationSettings = ({ t, @@ -97,6 +98,9 @@ const NotificationSettings = ({ isSidebarModuleAllowed, } = useUserPermissions(); + // 使用useSidebar钩子获取刷新方法 + const { refreshUserConfig } = useSidebar(); + // 左侧边栏设置处理函数 const handleSectionChange = (sectionKey) => { return (checked) => { @@ -132,6 +136,9 @@ const NotificationSettings = ({ }); if (res.data.success) { showSuccess(t('侧边栏设置保存成功')); + + // 刷新useSidebar钩子中的用户配置,实现实时更新 + await refreshUserConfig(); } else { showError(res.data.message); } @@ -334,7 +341,7 @@ const NotificationSettings = ({ loading={sidebarLoading} className='!rounded-lg' > - {t('保存边栏设置')} + {t('保存设置')} ) : ( diff --git a/web/src/hooks/common/useSidebar.js b/web/src/hooks/common/useSidebar.js index 5dce44f9..e964855e 100644 --- a/web/src/hooks/common/useSidebar.js +++ b/web/src/hooks/common/useSidebar.js @@ -21,6 +21,10 @@ import { useState, useEffect, useMemo, useContext } from 'react'; import { StatusContext } from '../../context/Status'; import { API } from '../../helpers'; +// 创建一个全局事件系统来同步所有useSidebar实例 +const sidebarEventTarget = new EventTarget(); +const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh'; + export const useSidebar = () => { const [statusState] = useContext(StatusContext); const [userConfig, setUserConfig] = useState(null); @@ -124,9 +128,11 @@ export const useSidebar = () => { // 刷新用户配置的方法(供外部调用) const refreshUserConfig = async () => { - if (Object.keys(adminConfig).length > 0) { - await loadUserConfig(); - } + // 移除adminConfig的条件限制,直接刷新用户配置 + await loadUserConfig(); + + // 触发全局刷新事件,通知所有useSidebar实例更新 + sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT)); }; // 加载用户配置 @@ -137,6 +143,21 @@ export const useSidebar = () => { } }, [adminConfig]); + // 监听全局刷新事件 + useEffect(() => { + const handleRefresh = () => { + if (Object.keys(adminConfig).length > 0) { + loadUserConfig(); + } + }; + + sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh); + + return () => { + sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh); + }; + }, [adminConfig]); + // 计算最终的显示配置 const finalConfig = useMemo(() => { const result = {}; From b79fe6cff08842906d8710c4cf4a5d81bc08942f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E3=80=82?= Date: Mon, 1 Sep 2025 10:20:15 +0800 Subject: [PATCH 07/33] =?UTF-8?q?=E4=BF=AE=E5=A4=8D"=E8=BE=B9=E6=A0=8F"?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/hooks/common/useSidebar.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/hooks/common/useSidebar.js b/web/src/hooks/common/useSidebar.js index e964855e..13d76fd8 100644 --- a/web/src/hooks/common/useSidebar.js +++ b/web/src/hooks/common/useSidebar.js @@ -128,8 +128,9 @@ export const useSidebar = () => { // 刷新用户配置的方法(供外部调用) const refreshUserConfig = async () => { - // 移除adminConfig的条件限制,直接刷新用户配置 - await loadUserConfig(); + if (Object.keys(adminConfig).length > 0) { + await loadUserConfig(); + } // 触发全局刷新事件,通知所有useSidebar实例更新 sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT)); From 647ed1be834d32d6e6b12b9bc25dd8e39aebfe25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E3=80=82?= Date: Tue, 2 Sep 2025 18:10:08 +0800 Subject: [PATCH 08/33] =?UTF-8?q?=E6=94=B9=E8=BF=9B"=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F"=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/auth/ModuleRoute.jsx | 200 ++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 web/src/components/auth/ModuleRoute.jsx diff --git a/web/src/components/auth/ModuleRoute.jsx b/web/src/components/auth/ModuleRoute.jsx new file mode 100644 index 00000000..3f208c7f --- /dev/null +++ b/web/src/components/auth/ModuleRoute.jsx @@ -0,0 +1,200 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useState, useEffect, useContext } from 'react'; +import { Navigate } from 'react-router-dom'; +import { StatusContext } from '../../context/Status'; +import Loading from '../common/ui/Loading'; +import { API } from '../../helpers'; + +/** + * ModuleRoute - 基于功能模块权限的路由保护组件 + * + * @param {Object} props + * @param {React.ReactNode} props.children - 要保护的子组件 + * @param {string} props.modulePath - 模块权限路径,如 "admin.channel", "console.token" + * @param {React.ReactNode} props.fallback - 无权限时显示的组件,默认跳转到 /forbidden + * @returns {React.ReactNode} + */ +const ModuleRoute = ({ children, modulePath, fallback = }) => { + const [hasPermission, setHasPermission] = useState(null); + const [statusState] = useContext(StatusContext); + + useEffect(() => { + checkModulePermission(); + }, [modulePath, statusState?.status]); // 只在status数据变化时重新检查 + + const checkModulePermission = async () => { + try { + // 检查用户是否已登录 + const user = localStorage.getItem('user'); + if (!user) { + setHasPermission(false); + return; + } + + const userData = JSON.parse(user); + const userRole = userData.role; + + // 超级管理员始终有权限 + if (userRole >= 100) { + setHasPermission(true); + return; + } + + // 检查模块权限 + const permission = await checkModulePermissionAPI(modulePath); + + // 如果返回null,表示status数据还未加载完成,保持loading状态 + if (permission === null) { + setHasPermission(null); + return; + } + + setHasPermission(permission); + } catch (error) { + console.error('检查模块权限失败:', error); + // 出错时采用安全优先策略,拒绝访问 + setHasPermission(false); + } + }; + + const checkModulePermissionAPI = async (modulePath) => { + try { + // 数据看板始终允许访问,不受控制台区域开关影响 + if (modulePath === 'console.detail') { + return true; + } + + // 从StatusContext中获取配置信息 + // 如果status数据还未加载完成,返回null表示需要等待 + if (!statusState?.status) { + return null; + } + + const user = JSON.parse(localStorage.getItem('user')); + const userRole = user.role; + + // 解析模块路径 + const pathParts = modulePath.split('.'); + if (pathParts.length < 2) { + return false; + } + + // 普通用户权限检查 + if (userRole < 10) { + return await isUserModuleAllowed(modulePath); + } + + // 超级管理员权限检查 - 不受系统配置限制 + if (userRole >= 100) { + return true; + } + + // 管理员权限检查 - 受系统配置限制 + if (userRole >= 10 && userRole < 100) { + // 从/api/user/self获取系统权限配置 + try { + const userRes = await API.get('/api/user/self'); + if (userRes.data.success && userRes.data.data.sidebar_config) { + const sidebarConfigData = userRes.data.data.sidebar_config; + // 管理员权限检查基于系统配置,不受用户偏好影响 + const systemConfig = sidebarConfigData.system || sidebarConfigData; + return checkModulePermissionInConfig(systemConfig, modulePath); + } else { + // 没有配置时,除了系统设置外都允许访问 + return modulePath !== 'admin.setting'; + } + } catch (error) { + console.error('获取侧边栏配置失败:', error); + return false; + } + } + + return false; + } catch (error) { + console.error('API权限检查失败:', error); + return false; + } + }; + + const isUserModuleAllowed = async (modulePath) => { + // 数据看板始终允许访问,不受控制台区域开关影响 + if (modulePath === 'console.detail') { + return true; + } + + // 普通用户的权限基于最终计算的配置 + try { + const userRes = await API.get('/api/user/self'); + if (userRes.data.success && userRes.data.data.sidebar_config) { + const sidebarConfigData = userRes.data.data.sidebar_config; + // 使用最终计算的配置进行权限检查 + const finalConfig = sidebarConfigData.final || sidebarConfigData; + return checkModulePermissionInConfig(finalConfig, modulePath); + } + return false; + } catch (error) { + console.error('获取用户权限配置失败:', error); + return false; + } + }; + + // 检查新的sidebar_config结构中的模块权限 + const checkModulePermissionInConfig = (sidebarConfig, modulePath) => { + const parts = modulePath.split('.'); + if (parts.length !== 2) { + return false; + } + + const [sectionKey, moduleKey] = parts; + const section = sidebarConfig[sectionKey]; + + // 检查区域是否存在且启用 + if (!section || !section.enabled) { + return false; + } + + // 检查模块是否启用 + const moduleValue = section[moduleKey]; + // 处理布尔值和嵌套对象两种情况 + if (typeof moduleValue === 'boolean') { + return moduleValue === true; + } else if (typeof moduleValue === 'object' && moduleValue !== null) { + // 对于嵌套对象,检查其enabled状态 + return moduleValue.enabled === true; + } + return false; + }; + + // 权限检查中 + if (hasPermission === null) { + return ; + } + + // 无权限 + if (!hasPermission) { + return fallback; + } + + // 有权限,渲染子组件 + return children; +}; + +export default ModuleRoute; From 09374778bd96f0fd7d521ed457d08cf700c18c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E3=80=82?= Date: Tue, 2 Sep 2025 19:26:30 +0800 Subject: [PATCH 09/33] =?UTF-8?q?=E6=94=B9=E8=BF=9B"=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F"=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6-1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d798db5953906aa5ff76cf6f2b641eb204d279b0. --- web/src/components/auth/ModuleRoute.jsx | 200 ------------------------ 1 file changed, 200 deletions(-) delete mode 100644 web/src/components/auth/ModuleRoute.jsx diff --git a/web/src/components/auth/ModuleRoute.jsx b/web/src/components/auth/ModuleRoute.jsx deleted file mode 100644 index 3f208c7f..00000000 --- a/web/src/components/auth/ModuleRoute.jsx +++ /dev/null @@ -1,200 +0,0 @@ -/* -Copyright (C) 2025 QuantumNous - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact support@quantumnous.com -*/ - -import React, { useState, useEffect, useContext } from 'react'; -import { Navigate } from 'react-router-dom'; -import { StatusContext } from '../../context/Status'; -import Loading from '../common/ui/Loading'; -import { API } from '../../helpers'; - -/** - * ModuleRoute - 基于功能模块权限的路由保护组件 - * - * @param {Object} props - * @param {React.ReactNode} props.children - 要保护的子组件 - * @param {string} props.modulePath - 模块权限路径,如 "admin.channel", "console.token" - * @param {React.ReactNode} props.fallback - 无权限时显示的组件,默认跳转到 /forbidden - * @returns {React.ReactNode} - */ -const ModuleRoute = ({ children, modulePath, fallback = }) => { - const [hasPermission, setHasPermission] = useState(null); - const [statusState] = useContext(StatusContext); - - useEffect(() => { - checkModulePermission(); - }, [modulePath, statusState?.status]); // 只在status数据变化时重新检查 - - const checkModulePermission = async () => { - try { - // 检查用户是否已登录 - const user = localStorage.getItem('user'); - if (!user) { - setHasPermission(false); - return; - } - - const userData = JSON.parse(user); - const userRole = userData.role; - - // 超级管理员始终有权限 - if (userRole >= 100) { - setHasPermission(true); - return; - } - - // 检查模块权限 - const permission = await checkModulePermissionAPI(modulePath); - - // 如果返回null,表示status数据还未加载完成,保持loading状态 - if (permission === null) { - setHasPermission(null); - return; - } - - setHasPermission(permission); - } catch (error) { - console.error('检查模块权限失败:', error); - // 出错时采用安全优先策略,拒绝访问 - setHasPermission(false); - } - }; - - const checkModulePermissionAPI = async (modulePath) => { - try { - // 数据看板始终允许访问,不受控制台区域开关影响 - if (modulePath === 'console.detail') { - return true; - } - - // 从StatusContext中获取配置信息 - // 如果status数据还未加载完成,返回null表示需要等待 - if (!statusState?.status) { - return null; - } - - const user = JSON.parse(localStorage.getItem('user')); - const userRole = user.role; - - // 解析模块路径 - const pathParts = modulePath.split('.'); - if (pathParts.length < 2) { - return false; - } - - // 普通用户权限检查 - if (userRole < 10) { - return await isUserModuleAllowed(modulePath); - } - - // 超级管理员权限检查 - 不受系统配置限制 - if (userRole >= 100) { - return true; - } - - // 管理员权限检查 - 受系统配置限制 - if (userRole >= 10 && userRole < 100) { - // 从/api/user/self获取系统权限配置 - try { - const userRes = await API.get('/api/user/self'); - if (userRes.data.success && userRes.data.data.sidebar_config) { - const sidebarConfigData = userRes.data.data.sidebar_config; - // 管理员权限检查基于系统配置,不受用户偏好影响 - const systemConfig = sidebarConfigData.system || sidebarConfigData; - return checkModulePermissionInConfig(systemConfig, modulePath); - } else { - // 没有配置时,除了系统设置外都允许访问 - return modulePath !== 'admin.setting'; - } - } catch (error) { - console.error('获取侧边栏配置失败:', error); - return false; - } - } - - return false; - } catch (error) { - console.error('API权限检查失败:', error); - return false; - } - }; - - const isUserModuleAllowed = async (modulePath) => { - // 数据看板始终允许访问,不受控制台区域开关影响 - if (modulePath === 'console.detail') { - return true; - } - - // 普通用户的权限基于最终计算的配置 - try { - const userRes = await API.get('/api/user/self'); - if (userRes.data.success && userRes.data.data.sidebar_config) { - const sidebarConfigData = userRes.data.data.sidebar_config; - // 使用最终计算的配置进行权限检查 - const finalConfig = sidebarConfigData.final || sidebarConfigData; - return checkModulePermissionInConfig(finalConfig, modulePath); - } - return false; - } catch (error) { - console.error('获取用户权限配置失败:', error); - return false; - } - }; - - // 检查新的sidebar_config结构中的模块权限 - const checkModulePermissionInConfig = (sidebarConfig, modulePath) => { - const parts = modulePath.split('.'); - if (parts.length !== 2) { - return false; - } - - const [sectionKey, moduleKey] = parts; - const section = sidebarConfig[sectionKey]; - - // 检查区域是否存在且启用 - if (!section || !section.enabled) { - return false; - } - - // 检查模块是否启用 - const moduleValue = section[moduleKey]; - // 处理布尔值和嵌套对象两种情况 - if (typeof moduleValue === 'boolean') { - return moduleValue === true; - } else if (typeof moduleValue === 'object' && moduleValue !== null) { - // 对于嵌套对象,检查其enabled状态 - return moduleValue.enabled === true; - } - return false; - }; - - // 权限检查中 - if (hasPermission === null) { - return ; - } - - // 无权限 - if (!hasPermission) { - return fallback; - } - - // 有权限,渲染子组件 - return children; -}; - -export default ModuleRoute; From 22db389facfd6c79086aa4a8e6a8a48b08120457 Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Tue, 16 Sep 2025 12:30:22 +0800 Subject: [PATCH 10/33] feat: vidu video support multi images --- relay/channel/task/vidu/adaptor.go | 10 ++-------- relay/common/relay_utils.go | 28 ++-------------------------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/relay/channel/task/vidu/adaptor.go b/relay/channel/task/vidu/adaptor.go index a1140d1e..8974c614 100644 --- a/relay/channel/task/vidu/adaptor.go +++ b/relay/channel/task/vidu/adaptor.go @@ -80,8 +80,7 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { } func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError { - // Use the unified validation method for TaskSubmitReq with image-based action determination - return relaycommon.ValidateTaskRequestWithImageBinding(c, info) + return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate) } func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, _ *relaycommon.RelayInfo) (io.Reader, error) { @@ -187,14 +186,9 @@ func (a *TaskAdaptor) GetChannelName() string { // ============================ func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*requestPayload, error) { - var images []string - if req.Image != "" { - images = []string{req.Image} - } - r := requestPayload{ Model: defaultString(req.Model, "viduq1"), - Images: images, + Images: req.Images, Prompt: req.Prompt, Duration: defaultInt(req.Duration, 5), Resolution: defaultString(req.Size, "1080p"), diff --git a/relay/common/relay_utils.go b/relay/common/relay_utils.go index cf6d08dd..96d1370b 100644 --- a/relay/common/relay_utils.go +++ b/relay/common/relay_utils.go @@ -79,34 +79,10 @@ func ValidateBasicTaskRequest(c *gin.Context, info *RelayInfo, action string) *d req.Images = []string{req.Image} } - storeTaskRequest(c, info, action, req) - return nil -} - -func ValidateTaskRequestWithImage(c *gin.Context, info *RelayInfo, requestObj interface{}) *dto.TaskError { - hasPrompt, ok := requestObj.(HasPrompt) - if !ok { - return createTaskError(fmt.Errorf("request must have prompt"), "invalid_request", http.StatusBadRequest, true) - } - - if taskErr := validatePrompt(hasPrompt.GetPrompt()); taskErr != nil { - return taskErr - } - - action := constant.TaskActionTextGenerate - if hasImage, ok := requestObj.(HasImage); ok && hasImage.HasImage() { + if req.HasImage() { action = constant.TaskActionGenerate } - storeTaskRequest(c, info, action, requestObj) + storeTaskRequest(c, info, action, req) return nil } - -func ValidateTaskRequestWithImageBinding(c *gin.Context, info *RelayInfo) *dto.TaskError { - var req TaskSubmitReq - if err := c.ShouldBindJSON(&req); err != nil { - return createTaskError(err, "invalid_request_body", http.StatusBadRequest, false) - } - - return ValidateTaskRequestWithImage(c, info, req) -} From b183f2f6633d5544382d2d6df4ca882f737ac0fb Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Fri, 19 Sep 2025 17:44:58 +0800 Subject: [PATCH 11/33] feat: vidu video add starEnd and reference gen video --- constant/task.go | 6 ++++-- relay/channel/task/vidu/adaptor.go | 4 ++++ relay/common/relay_utils.go | 8 ++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/constant/task.go b/constant/task.go index 21790145..e174fd60 100644 --- a/constant/task.go +++ b/constant/task.go @@ -11,8 +11,10 @@ const ( SunoActionMusic = "MUSIC" SunoActionLyrics = "LYRICS" - TaskActionGenerate = "generate" - TaskActionTextGenerate = "textGenerate" + TaskActionGenerate = "generate" + TaskActionTextGenerate = "textGenerate" + TaskActionFirstTailGenerate = "firstTailGenerate" + TaskActionReferenceGenerate = "referenceGenerate" ) var SunoModel2Action = map[string]string{ diff --git a/relay/channel/task/vidu/adaptor.go b/relay/channel/task/vidu/adaptor.go index 8974c614..358aef58 100644 --- a/relay/channel/task/vidu/adaptor.go +++ b/relay/channel/task/vidu/adaptor.go @@ -111,6 +111,10 @@ func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, erro switch info.Action { case constant.TaskActionGenerate: path = "/img2video" + case constant.TaskActionFirstTailGenerate: + path = "/start-end2video" + case constant.TaskActionReferenceGenerate: + path = "/reference2video" default: path = "/text2video" } diff --git a/relay/common/relay_utils.go b/relay/common/relay_utils.go index 96d1370b..3a721b47 100644 --- a/relay/common/relay_utils.go +++ b/relay/common/relay_utils.go @@ -81,6 +81,14 @@ func ValidateBasicTaskRequest(c *gin.Context, info *RelayInfo, action string) *d if req.HasImage() { action = constant.TaskActionGenerate + if info.ChannelType == constant.ChannelTypeVidu { + // vidu 增加 首尾帧生视频和参考图生视频 + if len(req.Images) == 2 { + action = constant.TaskActionFirstTailGenerate + } else if len(req.Images) > 2 { + action = constant.TaskActionReferenceGenerate + } + } } storeTaskRequest(c, info, action, req) From d29fbd378d387ebaf604905cafe4305415b3415e Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Fri, 19 Sep 2025 18:36:44 +0800 Subject: [PATCH 12/33] feat: vidu video add starEnd and reference gen video show type --- .../table/task-logs/TaskLogsColumnDefs.jsx | 21 ++++++++++++++++--- web/src/constants/common.constant.js | 2 ++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/web/src/components/table/task-logs/TaskLogsColumnDefs.jsx b/web/src/components/table/task-logs/TaskLogsColumnDefs.jsx index 766c1715..b63c7dd4 100644 --- a/web/src/components/table/task-logs/TaskLogsColumnDefs.jsx +++ b/web/src/components/table/task-logs/TaskLogsColumnDefs.jsx @@ -35,8 +35,9 @@ import { Sparkles, } from 'lucide-react'; import { - TASK_ACTION_GENERATE, - TASK_ACTION_TEXT_GENERATE, + TASK_ACTION_FIRST_TAIL_GENERATE, + TASK_ACTION_GENERATE, TASK_ACTION_REFERENCE_GENERATE, + TASK_ACTION_TEXT_GENERATE } from '../../../constants/common.constant'; import { CHANNEL_OPTIONS } from '../../../constants/channel.constants'; @@ -111,6 +112,18 @@ const renderType = (type, t) => { {t('文生视频')} ); + case TASK_ACTION_FIRST_TAIL_GENERATE: + return ( + }> + {t('首尾生视频')} + + ); + case TASK_ACTION_REFERENCE_GENERATE: + return ( + }> + {t('参照生视频')} + + ); default: return ( }> @@ -343,7 +356,9 @@ export const getTaskLogsColumns = ({ // 仅当为视频生成任务且成功,且 fail_reason 是 URL 时显示可点击链接 const isVideoTask = record.action === TASK_ACTION_GENERATE || - record.action === TASK_ACTION_TEXT_GENERATE; + record.action === TASK_ACTION_TEXT_GENERATE || + record.action === TASK_ACTION_FIRST_TAIL_GENERATE || + record.action === TASK_ACTION_REFERENCE_GENERATE; const isSuccess = record.status === 'SUCCESS'; const isUrl = typeof text === 'string' && /^https?:\/\//.test(text); if (isSuccess && isVideoTask && isUrl) { diff --git a/web/src/constants/common.constant.js b/web/src/constants/common.constant.js index 277bb9a5..57fbbbde 100644 --- a/web/src/constants/common.constant.js +++ b/web/src/constants/common.constant.js @@ -40,3 +40,5 @@ export const API_ENDPOINTS = [ export const TASK_ACTION_GENERATE = 'generate'; export const TASK_ACTION_TEXT_GENERATE = 'textGenerate'; +export const TASK_ACTION_FIRST_TAIL_GENERATE = 'firstTailGenerate'; +export const TASK_ACTION_REFERENCE_GENERATE = 'referenceGenerate'; From 7384b0925ebc59837bf3da52bc7872bee57f65ba Mon Sep 17 00:00:00 2001 From: Seefs Date: Sat, 20 Sep 2025 00:22:54 +0800 Subject: [PATCH 13/33] fix: claude system prompt overwrite --- relay/claude_handler.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/relay/claude_handler.go b/relay/claude_handler.go index dbdc6ee1..3c9272b6 100644 --- a/relay/claude_handler.go +++ b/relay/claude_handler.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "one-api/common" + "one-api/constant" "one-api/dto" relaycommon "one-api/relay/common" "one-api/relay/helper" @@ -69,6 +70,31 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ info.UpstreamModelName = request.Model } + if info.ChannelSetting.SystemPrompt != "" && info.ChannelSetting.SystemPromptOverride { + if request.System == nil { + request.SetStringSystem(info.ChannelSetting.SystemPrompt) + } else if info.ChannelSetting.SystemPromptOverride { + common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true) + if request.IsStringSystem() { + existing := strings.TrimSpace(request.GetStringSystem()) + if existing == "" { + request.SetStringSystem(info.ChannelSetting.SystemPrompt) + } else { + request.SetStringSystem(info.ChannelSetting.SystemPrompt + "\n" + existing) + } + } else { + systemContents := request.ParseSystem() + newSystem := dto.ClaudeMediaMessage{Type: dto.ContentTypeText} + newSystem.SetText(info.ChannelSetting.SystemPrompt) + if len(systemContents) == 0 { + request.System = []dto.ClaudeMediaMessage{newSystem} + } else { + request.System = append([]dto.ClaudeMediaMessage{newSystem}, systemContents...) + } + } + } + } + var requestBody io.Reader if model_setting.GetGlobalSettings().PassThroughRequestEnabled || info.ChannelSetting.PassThroughBodyEnabled { body, err := common.GetRequestBody(c) From 6f5dd3487cd04f4f8f4f585ea0786c2560c7f316 Mon Sep 17 00:00:00 2001 From: Zhaokun Zhang Date: Sat, 20 Sep 2025 11:09:28 +0800 Subject: [PATCH 14/33] fix: address copy functionality and code logic issues for #1828 - utils.jsx: Replace input with textarea in copy function to preserve line breaks in multi-line content, preventing formatting loss mentioned in #1828 - api.js: Fix duplicate 'group' property in buildApiPayload to resolve syntax issues - MarkdownRenderer.jsx: Refactor code text extraction using textContent for accurate copying Closes #1828 Signed-off-by: Zhaokun Zhang --- .../common/markdown/MarkdownRenderer.jsx | 4 ++-- web/src/helpers/api.js | 15 ++++++++------- web/src/helpers/utils.jsx | 18 +++++++++++------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/web/src/components/common/markdown/MarkdownRenderer.jsx b/web/src/components/common/markdown/MarkdownRenderer.jsx index f1283a64..05419f8c 100644 --- a/web/src/components/common/markdown/MarkdownRenderer.jsx +++ b/web/src/components/common/markdown/MarkdownRenderer.jsx @@ -181,8 +181,8 @@ export function PreCode(props) { e.preventDefault(); e.stopPropagation(); if (ref.current) { - const code = - ref.current.querySelector('code')?.innerText ?? ''; + const codeElement = ref.current.querySelector('code'); + const code = codeElement?.textContent ?? ''; copy(code).then((success) => { if (success) { Toast.success(t('代码已复制到剪贴板')); diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js index b7092fe7..bc389b2e 100644 --- a/web/src/helpers/api.js +++ b/web/src/helpers/api.js @@ -118,7 +118,6 @@ export const buildApiPayload = ( model: inputs.model, group: inputs.group, messages: processedMessages, - group: inputs.group, stream: inputs.stream, }; @@ -132,13 +131,15 @@ export const buildApiPayload = ( seed: 'seed', }; + Object.entries(parameterMappings).forEach(([key, param]) => { - if ( - parameterEnabled[key] && - inputs[param] !== undefined && - inputs[param] !== null - ) { - payload[param] = inputs[param]; + const enabled = parameterEnabled[key]; + const value = inputs[param]; + const hasValue = value !== undefined && value !== null; + + + if (enabled && hasValue) { + payload[param] = value; } }); diff --git a/web/src/helpers/utils.jsx b/web/src/helpers/utils.jsx index e446ea69..bcd13230 100644 --- a/web/src/helpers/utils.jsx +++ b/web/src/helpers/utils.jsx @@ -75,13 +75,17 @@ export async function copy(text) { await navigator.clipboard.writeText(text); } catch (e) { try { - // 构建input 执行 复制命令 - var _input = window.document.createElement('input'); - _input.value = text; - window.document.body.appendChild(_input); - _input.select(); - window.document.execCommand('Copy'); - window.document.body.removeChild(_input); + // 构建 textarea 执行复制命令,保留多行文本格式 + const textarea = window.document.createElement('textarea'); + textarea.value = text; + textarea.setAttribute('readonly', ''); + textarea.style.position = 'fixed'; + textarea.style.left = '-9999px'; + textarea.style.top = '-9999px'; + window.document.body.appendChild(textarea); + textarea.select(); + window.document.execCommand('copy'); + window.document.body.removeChild(textarea); } catch (e) { okay = false; console.error(e); From c0574a0e53369e5715aa31f139b30d4133f2268f Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 20 Sep 2025 13:27:32 +0800 Subject: [PATCH 15/33] feat: add PromptCacheKey field to openai_request struct --- dto/openai_request.go | 1 + 1 file changed, 1 insertion(+) diff --git a/dto/openai_request.go b/dto/openai_request.go index cd05a63c..53adb7f3 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -777,6 +777,7 @@ type OpenAIResponsesRequest struct { Reasoning *Reasoning `json:"reasoning,omitempty"` ServiceTier string `json:"service_tier,omitempty"` Store bool `json:"store,omitempty"` + PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"` Stream bool `json:"stream,omitempty"` Temperature float64 `json:"temperature,omitempty"` Text json.RawMessage `json:"text,omitempty"` From eeac99731b05f04fc02fcc2813ea4f4a0991daf7 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 20 Sep 2025 13:28:33 +0800 Subject: [PATCH 16/33] feat: change ParallelToolCalls and Store fields to json.RawMessage type --- dto/openai_request.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dto/openai_request.go b/dto/openai_request.go index 53adb7f3..191fa638 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -772,11 +772,11 @@ type OpenAIResponsesRequest struct { Instructions json.RawMessage `json:"instructions,omitempty"` MaxOutputTokens uint `json:"max_output_tokens,omitempty"` Metadata json.RawMessage `json:"metadata,omitempty"` - ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"` + ParallelToolCalls json.RawMessage `json:"parallel_tool_calls,omitempty"` PreviousResponseID string `json:"previous_response_id,omitempty"` Reasoning *Reasoning `json:"reasoning,omitempty"` ServiceTier string `json:"service_tier,omitempty"` - Store bool `json:"store,omitempty"` + Store json.RawMessage `json:"store,omitempty"` PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"` Stream bool `json:"stream,omitempty"` Temperature float64 `json:"temperature,omitempty"` From 3638bf149c5b67f7531223613010016643b7b837 Mon Sep 17 00:00:00 2001 From: Seefs Date: Sat, 20 Sep 2025 13:38:44 +0800 Subject: [PATCH 17/33] fix: gemini system prompt overwrite --- relay/claude_handler.go | 2 +- relay/gemini_handler.go | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/relay/claude_handler.go b/relay/claude_handler.go index 3c9272b6..59d12abe 100644 --- a/relay/claude_handler.go +++ b/relay/claude_handler.go @@ -70,7 +70,7 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ info.UpstreamModelName = request.Model } - if info.ChannelSetting.SystemPrompt != "" && info.ChannelSetting.SystemPromptOverride { + if info.ChannelSetting.SystemPrompt != "" { if request.System == nil { request.SetStringSystem(info.ChannelSetting.SystemPrompt) } else if info.ChannelSetting.SystemPromptOverride { diff --git a/relay/gemini_handler.go b/relay/gemini_handler.go index 0252d657..1410da60 100644 --- a/relay/gemini_handler.go +++ b/relay/gemini_handler.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "one-api/common" + "one-api/constant" "one-api/dto" "one-api/logger" "one-api/relay/channel/gemini" @@ -94,6 +95,32 @@ func GeminiHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ adaptor.Init(info) + if info.ChannelSetting.SystemPrompt != "" { + if request.SystemInstructions == nil { + request.SystemInstructions = &dto.GeminiChatContent{ + Parts: []dto.GeminiPart{ + {Text: info.ChannelSetting.SystemPrompt}, + }, + } + } else if len(request.SystemInstructions.Parts) == 0 { + request.SystemInstructions.Parts = []dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}} + } else if info.ChannelSetting.SystemPromptOverride { + common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true) + merged := false + for i := range request.SystemInstructions.Parts { + if request.SystemInstructions.Parts[i].Text == "" { + continue + } + request.SystemInstructions.Parts[i].Text = info.ChannelSetting.SystemPrompt + "\n" + request.SystemInstructions.Parts[i].Text + merged = true + break + } + if !merged { + request.SystemInstructions.Parts = append([]dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}, request.SystemInstructions.Parts...) + } + } + } + // Clean up empty system instruction if request.SystemInstructions != nil { hasContent := false From 5545e70a426953bfd97f244473f295e4bc5b459f Mon Sep 17 00:00:00 2001 From: huanghejian Date: Fri, 26 Sep 2025 15:32:59 +0800 Subject: [PATCH 18/33] feat: amazon nova model --- relay/channel/aws/constants.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/relay/channel/aws/constants.go b/relay/channel/aws/constants.go index 72d0f989..3a28c95c 100644 --- a/relay/channel/aws/constants.go +++ b/relay/channel/aws/constants.go @@ -21,6 +21,10 @@ var awsModelIDMap = map[string]string{ "nova-lite-v1:0": "amazon.nova-lite-v1:0", "nova-pro-v1:0": "amazon.nova-pro-v1:0", "nova-premier-v1:0": "amazon.nova-premier-v1:0", + "nova-canvas-v1:0": "amazon.nova-canvas-v1:0", + "nova-reel-v1:0": "amazon.nova-reel-v1:0", + "nova-reel-v1:1": "amazon.nova-reel-v1:1", + "nova-sonic-v1:0": "amazon.nova-sonic-v1:0", } var awsModelCanCrossRegionMap = map[string]map[string]bool{ From a069d03ef716a2679a62ae06e6ad8e4a9784fb0a Mon Sep 17 00:00:00 2001 From: huanghejian Date: Fri, 26 Sep 2025 15:55:00 +0800 Subject: [PATCH 19/33] feat: amazon nova model --- relay/channel/aws/constants.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/relay/channel/aws/constants.go b/relay/channel/aws/constants.go index 3a28c95c..5ac7ce99 100644 --- a/relay/channel/aws/constants.go +++ b/relay/channel/aws/constants.go @@ -86,10 +86,27 @@ var awsModelCanCrossRegionMap = map[string]map[string]bool{ "apac": true, }, "amazon.nova-premier-v1:0": { + "us": true, + }, + "amazon.nova-canvas-v1:0": { "us": true, "eu": true, "apac": true, - }} + }, + "amazon.nova-reel-v1:0": { + "us": true, + "eu": true, + "apac": true, + }, + "amazon.nova-reel-v1:1": { + "us": true, + }, + "amazon.nova-sonic-v1:0": { + "us": true, + "eu": true, + "apac": true, + }, +} var awsRegionCrossModelPrefixMap = map[string]string{ "us": "us", From b3b6ec0375d604f1dbb786c69d182d558572a706 Mon Sep 17 00:00:00 2001 From: Seefs Date: Sat, 27 Sep 2025 00:15:28 +0800 Subject: [PATCH 20/33] fix: add missing fields to Gemini request --- dto/gemini.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/dto/gemini.go b/dto/gemini.go index 5df67ba0..ad2ddb8b 100644 --- a/dto/gemini.go +++ b/dto/gemini.go @@ -14,7 +14,30 @@ type GeminiChatRequest struct { SafetySettings []GeminiChatSafetySettings `json:"safetySettings,omitempty"` GenerationConfig GeminiChatGenerationConfig `json:"generationConfig,omitempty"` Tools json.RawMessage `json:"tools,omitempty"` + ToolConfig *ToolConfig `json:"toolConfig,omitempty"` SystemInstructions *GeminiChatContent `json:"systemInstruction,omitempty"` + CachedContent string `json:"cachedContent,omitempty"` +} + +type ToolConfig struct { + FunctionCallingConfig *FunctionCallingConfig `json:"functionCallingConfig,omitempty"` + RetrievalConfig *RetrievalConfig `json:"retrievalConfig,omitempty"` +} + +type FunctionCallingConfig struct { + Mode FunctionCallingConfigMode `json:"mode,omitempty"` + AllowedFunctionNames []string `json:"allowedFunctionNames,omitempty"` +} +type FunctionCallingConfigMode string + +type RetrievalConfig struct { + LatLng *LatLng `json:"latLng,omitempty"` + LanguageCode string `json:"languageCode,omitempty"` +} + +type LatLng struct { + Latitude *float64 `json:"latitude,omitempty"` + Longitude *float64 `json:"longitude,omitempty"` } func (r *GeminiChatRequest) GetTokenCountMeta() *types.TokenCountMeta { @@ -239,12 +262,20 @@ type GeminiChatGenerationConfig struct { StopSequences []string `json:"stopSequences,omitempty"` ResponseMimeType string `json:"responseMimeType,omitempty"` ResponseSchema any `json:"responseSchema,omitempty"` + ResponseJsonSchema any `json:"responseJsonSchema,omitempty"` + PresencePenalty *float32 `json:"presencePenalty,omitempty"` + FrequencyPenalty *float32 `json:"frequencyPenalty,omitempty"` + ResponseLogprobs bool `json:"responseLogprobs,omitempty"` + Logprobs *int32 `json:"logprobs,omitempty"` + MediaResolution MediaResolution `json:"mediaResolution,omitempty"` 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 MediaResolution string + type GeminiChatCandidate struct { Content GeminiChatContent `json:"content"` FinishReason *string `json:"finishReason"` From b66316fe6f593921de0a0685880a396969bdfeca Mon Sep 17 00:00:00 2001 From: Seefs Date: Sat, 27 Sep 2025 00:24:29 +0800 Subject: [PATCH 21/33] fix: jsonRaw --- dto/gemini.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dto/gemini.go b/dto/gemini.go index ad2ddb8b..b1f7b9a4 100644 --- a/dto/gemini.go +++ b/dto/gemini.go @@ -261,8 +261,8 @@ type GeminiChatGenerationConfig struct { CandidateCount int `json:"candidateCount,omitempty"` StopSequences []string `json:"stopSequences,omitempty"` ResponseMimeType string `json:"responseMimeType,omitempty"` - ResponseSchema any `json:"responseSchema,omitempty"` - ResponseJsonSchema any `json:"responseJsonSchema,omitempty"` + ResponseSchema json.RawMessage `json:"responseSchema,omitempty"` + ResponseJsonSchema json.RawMessage `json:"responseJsonSchema,omitempty"` PresencePenalty *float32 `json:"presencePenalty,omitempty"` FrequencyPenalty *float32 `json:"frequencyPenalty,omitempty"` ResponseLogprobs bool `json:"responseLogprobs,omitempty"` From 53513cbe1d0c9690d601f7b7050b205a0770df27 Mon Sep 17 00:00:00 2001 From: Seefs Date: Sat, 27 Sep 2025 00:33:05 +0800 Subject: [PATCH 22/33] fix: jsonRaw --- dto/gemini.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dto/gemini.go b/dto/gemini.go index b1f7b9a4..bc05c6aa 100644 --- a/dto/gemini.go +++ b/dto/gemini.go @@ -261,7 +261,7 @@ type GeminiChatGenerationConfig struct { CandidateCount int `json:"candidateCount,omitempty"` StopSequences []string `json:"stopSequences,omitempty"` ResponseMimeType string `json:"responseMimeType,omitempty"` - ResponseSchema json.RawMessage `json:"responseSchema,omitempty"` + ResponseSchema any `json:"responseSchema,omitempty"` ResponseJsonSchema json.RawMessage `json:"responseJsonSchema,omitempty"` PresencePenalty *float32 `json:"presencePenalty,omitempty"` FrequencyPenalty *float32 `json:"frequencyPenalty,omitempty"` From ee6e5ff8828e6c9ef77015d5a43a910e04cdeacf Mon Sep 17 00:00:00 2001 From: RedwindA Date: Sat, 27 Sep 2025 01:19:09 +0800 Subject: [PATCH 23/33] =?UTF-8?q?feat:=20=E4=BB=85=E4=B8=BA=E9=80=82?= =?UTF-8?q?=E5=BD=93=E7=9A=84=E6=B8=A0=E9=81=93=E6=B8=B2=E6=9F=93=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E6=A8=A1=E5=9E=8B=E5=88=97=E8=A1=A8=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../channels/modals/EditChannelModal.jsx | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index c0a21624..967bf88a 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -85,6 +85,26 @@ const REGION_EXAMPLE = { 'claude-3-5-sonnet-20240620': 'europe-west1', }; +// 支持并且已适配通过接口获取模型列表的渠道类型 +const MODEL_FETCHABLE_TYPES = new Set([ + 1, + 4, + 14, + 34, + 17, + 26, + 24, + 47, + 25, + 20, + 23, + 31, + 35, + 40, + 42, + 48, +]); + function type2secretPrompt(type) { // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥') switch (type) { @@ -1872,13 +1892,15 @@ const EditChannelModal = (props) => { > {t('填入所有模型')} - + {MODEL_FETCHABLE_TYPES.has(inputs.type) && ( + + )} )} + handleDeleteKey(record.index)} + okType={'danger'} + position={'topRight'} + > + + ), }, diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index bceb5f08..b0469895 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1889,6 +1889,10 @@ "确定要删除所有已自动禁用的密钥吗?": "Are you sure you want to delete all automatically disabled keys?", "此操作不可撤销,将永久删除已自动禁用的密钥": "This operation cannot be undone, and all automatically disabled keys will be permanently deleted.", "删除自动禁用密钥": "Delete auto disabled keys", + "确定要删除此密钥吗?": "Are you sure you want to delete this key?", + "此操作不可撤销,将永久删除该密钥": "This operation cannot be undone, and the key will be permanently deleted.", + "密钥已删除": "Key has been deleted", + "删除密钥失败": "Failed to delete key", "图标": "Icon", "模型图标": "Model icon", "请输入图标名称": "Please enter the icon name", From f50eb9dde7caa2c6f5d41bbef5ea077a18b93aa2 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 27 Sep 2025 15:04:06 +0800 Subject: [PATCH 27/33] feat: rename output binaries to new-api for consistency across platforms --- .github/workflows/linux-release.yml | 4 ++-- .github/workflows/macos-release.yml | 2 +- .github/workflows/windows-release.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index c87fcfce..953845ff 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -38,13 +38,13 @@ jobs: - name: Build Backend (amd64) run: | go mod download - go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o one-api + go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api - name: Build Backend (arm64) run: | sudo apt-get update DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu - CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o one-api-arm64 + CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api-arm64 - name: Release uses: softprops/action-gh-release@v1 diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index 1bc786ac..efaaa107 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -39,7 +39,7 @@ jobs: - name: Build Backend run: | go mod download - go build -ldflags "-X 'one-api/common.Version=$(git describe --tags)'" -o one-api-macos + go build -ldflags "-X 'one-api/common.Version=$(git describe --tags)'" -o new-api-macos - name: Release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index de3d83d5..1f4f63c8 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -41,7 +41,7 @@ jobs: - name: Build Backend run: | go mod download - go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)'" -o one-api.exe + go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)'" -o new-api.exe - name: Release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') From d1f590aa7b319307fdec3d0e201892ca3a97698a Mon Sep 17 00:00:00 2001 From: huanghejian Date: Sat, 27 Sep 2025 15:19:54 +0800 Subject: [PATCH 28/33] =?UTF-8?q?pref:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/channel/volcengine/adaptor.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/relay/channel/volcengine/adaptor.go b/relay/channel/volcengine/adaptor.go index 5bea7d46..21d6e170 100644 --- a/relay/channel/volcengine/adaptor.go +++ b/relay/channel/volcengine/adaptor.go @@ -9,6 +9,7 @@ import ( "mime/multipart" "net/http" "net/textproto" + channelconstant "one-api/constant" "one-api/dto" "one-api/relay/channel" "one-api/relay/channel/openai" @@ -191,7 +192,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { // 支持自定义域名,如果未设置则使用默认域名 baseUrl := info.ChannelBaseUrl if baseUrl == "" { - baseUrl = "https://ark.cn-beijing.volces.com" + baseUrl = channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine] } switch info.RelayMode { From 5f34c4a97d8a5477c418cdce2faea86c8090fc21 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 27 Sep 2025 15:24:40 +0800 Subject: [PATCH 29/33] feat: update release configuration to use new-api binaries for consistency --- .github/workflows/linux-release.yml | 4 ++-- .github/workflows/macos-release.yml | 2 +- .github/workflows/windows-release.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index 953845ff..3e3ddc53 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -51,8 +51,8 @@ jobs: if: startsWith(github.ref, 'refs/tags/') with: files: | - one-api - one-api-arm64 + new-api + new-api-arm64 draft: true generate_release_notes: true env: diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index efaaa107..8eaf2d67 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -44,7 +44,7 @@ jobs: uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: - files: one-api-macos + files: new-api-macos draft: true generate_release_notes: true env: diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index 1f4f63c8..30e864f3 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -46,7 +46,7 @@ jobs: uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: - files: one-api.exe + files: new-api.exe draft: true generate_release_notes: true env: From b836bce81c4742576a097a12a9b154c11567e340 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 27 Sep 2025 16:19:58 +0800 Subject: [PATCH 30/33] feat: add startup logging with network IPs and container detection --- common/sys_log.go | 35 ++++++++++++++++++++++- common/utils.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++ main.go | 8 +++++- 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/common/sys_log.go b/common/sys_log.go index 478015f0..c3e736da 100644 --- a/common/sys_log.go +++ b/common/sys_log.go @@ -2,9 +2,10 @@ package common import ( "fmt" - "github.com/gin-gonic/gin" "os" "time" + + "github.com/gin-gonic/gin" ) func SysLog(s string) { @@ -22,3 +23,35 @@ func FatalLog(v ...any) { _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v) os.Exit(1) } + +func LogStartupSuccess(startTime time.Time, port string) { + + duration := time.Since(startTime) + durationMs := duration.Milliseconds() + + // Get network IPs + networkIps := GetNetworkIps() + + // Print blank line for spacing + fmt.Fprintf(gin.DefaultWriter, "\n") + + // Print the main success message + fmt.Fprintf(gin.DefaultWriter, " \033[32m%s %s\033[0m ready in %d ms\n", SystemName, Version, durationMs) + fmt.Fprintf(gin.DefaultWriter, "\n") + + // Skip fancy startup message in container environments + if IsRunningInContainer() { + return + } + + // Print local URL + fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mLocal:\033[0m http://localhost:%s/\n", port) + + // Print network URLs + for _, ip := range networkIps { + fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mNetwork:\033[0m http://%s:%s/\n", ip, port) + } + + // Print blank line for spacing + fmt.Fprintf(gin.DefaultWriter, "\n") +} diff --git a/common/utils.go b/common/utils.go index 883abfd1..21f72ec6 100644 --- a/common/utils.go +++ b/common/utils.go @@ -68,6 +68,78 @@ func GetIp() (ip string) { return } +func GetNetworkIps() []string { + var networkIps []string + ips, err := net.InterfaceAddrs() + if err != nil { + log.Println(err) + return networkIps + } + + for _, a := range ips { + if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() { + if ipNet.IP.To4() != nil { + ip := ipNet.IP.String() + // Include common private network ranges + if strings.HasPrefix(ip, "10.") || + strings.HasPrefix(ip, "172.") || + strings.HasPrefix(ip, "192.168.") { + networkIps = append(networkIps, ip) + } + } + } + } + return networkIps +} + +// IsRunningInContainer detects if the application is running inside a container +func IsRunningInContainer() bool { + // Method 1: Check for .dockerenv file (Docker containers) + if _, err := os.Stat("/.dockerenv"); err == nil { + return true + } + + // Method 2: Check cgroup for container indicators + if data, err := os.ReadFile("/proc/1/cgroup"); err == nil { + content := string(data) + if strings.Contains(content, "docker") || + strings.Contains(content, "containerd") || + strings.Contains(content, "kubepods") || + strings.Contains(content, "/lxc/") { + return true + } + } + + // Method 3: Check environment variables commonly set by container runtimes + containerEnvVars := []string{ + "KUBERNETES_SERVICE_HOST", + "DOCKER_CONTAINER", + "container", + } + + for _, envVar := range containerEnvVars { + if os.Getenv(envVar) != "" { + return true + } + } + + // Method 4: Check if init process is not the traditional init + if data, err := os.ReadFile("/proc/1/comm"); err == nil { + comm := strings.TrimSpace(string(data)) + // In containers, process 1 is often not "init" or "systemd" + if comm != "init" && comm != "systemd" { + // Additional check: if it's a common container entrypoint + if strings.Contains(comm, "docker") || + strings.Contains(comm, "containerd") || + strings.Contains(comm, "runc") { + return true + } + } + } + + return false +} + var sizeKB = 1024 var sizeMB = sizeKB * 1024 var sizeGB = sizeMB * 1024 diff --git a/main.go b/main.go index 0caf5361..b1421f9e 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "one-api/setting/ratio_setting" "os" "strconv" + "time" "github.com/bytedance/gopkg/util/gopool" "github.com/gin-contrib/sessions" @@ -33,6 +34,7 @@ var buildFS embed.FS var indexPage []byte func main() { + startTime := time.Now() err := InitResources() if err != nil { @@ -150,6 +152,10 @@ func main() { if port == "" { port = strconv.Itoa(*common.Port) } + + // Log startup success message + common.LogStartupSuccess(startTime, port) + err = server.Run(":" + port) if err != nil { common.FatalLog("failed to start HTTP server: " + err.Error()) @@ -204,4 +210,4 @@ func InitResources() error { return err } return nil -} \ No newline at end of file +} From da88e746ef8ab696e923f7f77811ffe768a13a28 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 27 Sep 2025 16:30:24 +0800 Subject: [PATCH 31/33] feat: add startup logging with network IPs and container detection --- common/sys_log.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/common/sys_log.go b/common/sys_log.go index c3e736da..b29adc3e 100644 --- a/common/sys_log.go +++ b/common/sys_log.go @@ -40,13 +40,11 @@ func LogStartupSuccess(startTime time.Time, port string) { fmt.Fprintf(gin.DefaultWriter, "\n") // Skip fancy startup message in container environments - if IsRunningInContainer() { - return + if !IsRunningInContainer() { + // Print local URL + fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mLocal:\033[0m http://localhost:%s/\n", port) } - // Print local URL - fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mLocal:\033[0m http://localhost:%s/\n", port) - // Print network URLs for _, ip := range networkIps { fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mNetwork:\033[0m http://%s:%s/\n", ip, port) From 7dbdd91b304877739414f561426317a67ad8f02d Mon Sep 17 00:00:00 2001 From: huanghejian Date: Sat, 27 Sep 2025 16:40:18 +0800 Subject: [PATCH 32/33] =?UTF-8?q?pref:=20=E9=98=B2=E5=91=86=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E9=9C=80=E8=A6=81=EF=BC=8C=E4=B8=8D=E5=85=81=E8=AE=B8?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=EF=BC=8CAPI=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=BA=E4=B8=8B=E6=8B=89=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../channels/modals/EditChannelModal.jsx | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 9d7e7f37..6ceb1ebf 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -1736,7 +1736,8 @@ const EditChannelModal = (props) => { {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && - inputs.type !== 36 && ( + inputs.type !== 36 && + inputs.type !== 45 && (
{ />
)} + + {inputs.type === 45 && ( +
+ + handleInputChange('base_url', value) + } + optionList={[ + { + value: 'https://ark.cn-beijing.volces.com', + label: 'https://ark.cn-beijing.volces.com' + }, + { + value: 'https://ark.ap-southeast.bytepluses.com', + label: 'https://ark.ap-southeast.bytepluses.com' + } + ]} + defaultValue='https://ark.cn-beijing.volces.com' + /> +
+ )} )} From 183ae15a57e3b12d8f18d563b61835cda0be1d68 Mon Sep 17 00:00:00 2001 From: huanghejian Date: Sat, 27 Sep 2025 17:03:34 +0800 Subject: [PATCH 33/33] =?UTF-8?q?fix:=20=E9=BB=98=E8=AE=A4=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=9B=BD=E5=86=85=E5=9C=B0=E5=9D=80=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E8=AE=BE=E7=BD=AEbase=20url=E5=BC=B9=E7=AA=97=E6=8F=90?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/table/channels/modals/EditChannelModal.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 6ceb1ebf..ecc6ced6 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -322,6 +322,10 @@ const EditChannelModal = (props) => { case 36: localModels = ['suno_music', 'suno_lyrics']; break; + case 45: + localModels = getChannelModels(value); + setInputs((prevInputs) => ({ ...prevInputs, base_url: 'https://ark.cn-beijing.volces.com' })); + break; default: localModels = getChannelModels(value); break; @@ -822,6 +826,10 @@ const EditChannelModal = (props) => { showInfo(t('请至少选择一个模型!')); return; } + if (localInputs.type === 45 && (!localInputs.base_url || localInputs.base_url.trim() === '')) { + showInfo(t('请输入API地址!')); + return; + } if ( localInputs.model_mapping && localInputs.model_mapping !== '' &&